Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.13% covered (success)
99.13%
114 / 115
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
Settings
99.13% covered (success)
99.13%
114 / 115
66.67% covered (warning)
66.67%
2 / 3
30
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
100.00% covered (success)
100.00%
94 / 94
100.00% covered (success)
100.00%
1 / 1
23
 update_reader_views
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
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`, and saves the WordPress.com Reader setting.
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                array( 'status' => 400 )
67            );
68        }
69
70        $provided = array_intersect_key( $values, array_flip( $keys ) );
71        if ( empty( $provided ) ) {
72            return new WP_Error(
73                self::ERROR_PREFIX . 'missing_setting_field',
74                sprintf(
75                    /* translators: %s: comma-separated list of writable field names. */
76                    __( 'Provide at least one of: %s.', 'jetpack-stats-pkg' ),
77                    implode( ', ', $keys )
78                ),
79                array( 'status' => 400 )
80            );
81        }
82
83        $defaults    = Options::get_defaults();
84        $before      = self::get( $keys );
85        $known_roles = null;
86        foreach ( $keys as $role_field ) {
87            if ( ! array_key_exists( $role_field, $provided ) ) {
88                continue;
89            }
90            if ( ! is_array( $defaults[ $role_field ] ?? null ) ) {
91                continue;
92            }
93            if ( null === $known_roles ) {
94                $known_roles = array_keys( wp_roles()->roles );
95            }
96            if ( ! is_array( $provided[ $role_field ] ) ) {
97                return new WP_Error(
98                    self::ERROR_PREFIX . 'invalid_' . $role_field,
99                    sprintf(
100                        /* translators: %s: the offending field name. */
101                        __( 'Field `%s` must be an array of role slugs.', 'jetpack-stats-pkg' ),
102                        $role_field
103                    ),
104                    array( 'status' => 400 )
105                );
106            }
107            // Direct PHP callers skip the REST schema's `minItems` check.
108            if ( 'roles' === $role_field && empty( $provided[ $role_field ] ) ) {
109                return new WP_Error(
110                    self::ERROR_PREFIX . 'invalid_roles',
111                    __( 'Field `roles` must be a non-empty array of role slugs.', 'jetpack-stats-pkg' ),
112                    array( 'status' => 400 )
113                );
114            }
115            $sanitized = array();
116            foreach ( $provided[ $role_field ] as $role ) {
117                if ( ! is_string( $role ) || '' === $role ) {
118                    return new WP_Error(
119                        self::ERROR_PREFIX . 'invalid_role',
120                        sprintf(
121                            /* translators: 1: field name, 2: comma-separated list of valid role slugs. */
122                            __( 'Role slugs in `%1$s` must be non-empty strings. Known roles: %2$s.', 'jetpack-stats-pkg' ),
123                            $role_field,
124                            implode( ', ', $known_roles )
125                        ),
126                        array( 'status' => 400 )
127                    );
128                }
129                if ( ! in_array( $role, $known_roles, true ) && ! in_array( $role, $before[ $role_field ], true ) ) {
130                    return new WP_Error(
131                        self::ERROR_PREFIX . 'invalid_role',
132                        sprintf(
133                            /* translators: 1: unknown role slug, 2: field name, 3: comma-separated list of valid role slugs. */
134                            __( 'Unknown role `%1$s` in `%2$s`. Known roles: %3$s.', 'jetpack-stats-pkg' ),
135                            $role,
136                            $role_field,
137                            implode( ', ', $known_roles )
138                        ),
139                        array( 'status' => 400 )
140                    );
141                }
142                $sanitized[] = $role;
143            }
144            // Administrators keep `view_stats`, because the screen that changes this list needs it.
145            if ( 'roles' === $role_field && ! in_array( 'administrator', $sanitized, true ) ) {
146                array_unshift( $sanitized, 'administrator' );
147            }
148            $provided[ $role_field ] = array_values( array_unique( $sanitized ) );
149        }
150
151        $changes = array();
152        foreach ( $provided as $key => $value ) {
153            if ( is_bool( $defaults[ $key ] ?? null ) ) {
154                $value = (bool) $value;
155            }
156            $current = $before[ $key ] ?? null;
157            if ( $current === $value ) {
158                continue;
159            }
160            $changes[ $key ] = $value;
161        }
162
163        if ( ! empty( $changes ) ) {
164            Options::set_options( $changes );
165        }
166
167        $after = self::get( $keys );
168        foreach ( $changes as $key => $value ) {
169            if ( $after[ $key ] !== $value ) {
170                return new WP_Error(
171                    self::ERROR_PREFIX . 'save_failed',
172                    __( 'The Stats settings could not be saved.', 'jetpack-stats-pkg' ),
173                    array( 'status' => 500 )
174                );
175            }
176        }
177
178        return array(
179            'changed'  => $after !== $before,
180            'settings' => $after,
181        );
182    }
183
184    /**
185     * Save whether the WordPress.com Reader shows post views for this site.
186     *
187     * @since 0.23.0
188     *
189     * @param bool $enabled Whether the Reader shows post views.
190     * @return true|WP_Error True once stored, or an error when the site kept another value.
191     */
192    public static function update_reader_views( bool $enabled ) {
193        $value = (int) $enabled;
194        update_option( 'wpcom_reader_views_enabled', $value );
195        // update_option() also returns false for an unchanged value, so read the option back.
196        if ( (int) get_option( 'wpcom_reader_views_enabled', 1 ) !== $value ) {
197            return new WP_Error(
198                self::ERROR_PREFIX . 'save_failed',
199                __( 'The Stats settings could not be saved.', 'jetpack-stats-pkg' ),
200                array( 'status' => 500 )
201            );
202        }
203
204        return true;
205    }
206}