100, 'retention_days' => 90, 'seo_canonical' => 1, 'seo_mode' => 'canonical', 'exclude_logged_in' => 0, 'delete_on_uninstall' => 0, ); } /** * Get settings merged with defaults. * * @return array */ public static function get() { $saved = get_option( self::OPTION, array() ); if ( ! is_array( $saved ) ) { $saved = array(); } $merged = wp_parse_args( $saved, self::defaults() ); // Back-compat: installs saved before seo_mode existed derive it from // the old on/off canonical toggle, so an explicit "off" stays off. if ( ! isset( $saved['seo_mode'] ) ) { $merged['seo_mode'] = empty( $merged['seo_canonical'] ) ? 'off' : 'canonical'; } return $merged; } /** * Settings as the admin screen may see them: only the keys this plugin * knows (defaults()), so nothing else the option holds reaches the * browser. * * @return array */ public static function for_admin() { $settings = array_intersect_key( self::get(), self::defaults() ); return $settings; } /** * A whole number from a submitted value, held between $min and $max, or * null when the value is not a number (the caller keeps what is stored). * * Only a number or a numeric string counts: '', 'abc', '12abc', true and * a list used to be cast, so a script's typo set the field to its * minimum (or to 12). A negative is held at $min, never turned positive. * A number is held between the bounds before it becomes an int: casting * one past PHP's int range gave 0 (or wrapped round) and PHP 8.5 warns. * * @param mixed $value Submitted value. * @param int $min Smallest stored value. * @param int $max Largest stored value. * @return int|null */ public static function number( $value, $min, $max ) { if ( is_bool( $value ) || ! is_scalar( $value ) ) { return null; } $value = trim( (string) $value ); if ( ! is_numeric( $value ) ) { return null; } return (int) floor( max( (float) $min, min( (float) $max, (float) $value ) ) ); } /** * An on/off switch from a submitted value: 1, 0, or null when the value * means neither (the caller keeps what is stored). * * true, 1, '1', 'true', 'on' and 'yes' are on; false, 0, '0', '', 'false', * 'off' and 'no' are off, in any case and with spaces around. Anything * non-empty used to be on, so a REST client's "false" switched the data * wipe on. * * @param mixed $value Submitted value. * @return int|null */ public static function flag( $value ) { if ( ! is_scalar( $value ) ) { return null; } $on = filter_var( is_bool( $value ) ? $value : trim( (string) $value ), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); return null === $on ? null : (int) $on; } /** * The roles besides administrator that hold the manage-tests capability * now, however they came by it: what the Settings screen lists, each with * a way to take it back. * * @return array[] Each { slug, name }. */ public static function access_roles() { $out = array(); foreach ( wp_roles()->role_objects as $slug => $role ) { if ( 'administrator' !== $slug && $role->has_cap( IMPROVEAB_CAP ) ) { $out[] = array( 'slug' => (string) $slug, 'name' => isset( wp_roles()->role_names[ $slug ] ) ? translate_user_role( wp_roles()->role_names[ $slug ] ) : (string) $slug, ); } } return $out; } /** * The roles a `revoke_access` value names that may lose the manage-tests * capability: existing roles besides administrator, which always keeps * it. * * @param mixed $value Submitted value: a list of role slugs. * @return string[] */ public static function revoked_roles( $value ) { $out = array(); foreach ( is_array( $value ) ? $value : array() as $slug ) { if ( ! is_scalar( $slug ) ) { continue; } $slug = sanitize_key( (string) $slug ); if ( '' !== $slug && 'administrator' !== $slug && get_role( $slug ) && ! in_array( $slug, $out, true ) ) { $out[] = $slug; } } return $out; } /** * Store the settings this plugin owns. Whatever else the option holds is * kept as it is, and so is a setting * sent with a value that is not one (a word for a number, a list for a * switch). * * `revoke_access` (a list of role slugs) takes the manage-tests * capability back from those roles, with nothing else to allow first: * access given to a role can always be taken away. Giving it is not * done here. Nothing of it is stored in the option. * * @param array $data Submitted values (a partial set is fine). * @return array|WP_Error The stored settings, or why they could not be stored. */ public static function update( $data ) { $data = is_array( $data ) ? $data : array(); $out = self::get(); $before = $out; $revoked = isset( $data['revoke_access'] ) ? self::revoked_roles( $data['revoke_access'] ) : array(); foreach ( array( 'min_visitors' => 1000000, 'retention_days' => 3650 ) as $key => $max ) { $num = isset( $data[ $key ] ) ? self::number( $data[ $key ], 1, $max ) : null; if ( null !== $num ) { $out[ $key ] = $num; } } foreach ( array( 'seo_canonical', 'exclude_logged_in', 'delete_on_uninstall' ) as $key ) { $on = isset( $data[ $key ] ) ? self::flag( $data[ $key ] ) : null; if ( null !== $on ) { $out[ $key ] = $on; } } if ( isset( $data['seo_mode'] ) && in_array( $data['seo_mode'], array( 'canonical', 'noindex', 'off' ), true ) ) { $out['seo_mode'] = $data['seo_mode']; // Keep the legacy flag in step for anything still reading it. $out['seo_canonical'] = ( 'canonical' === $data['seo_mode'] ) ? 1 : 0; } update_option( self::OPTION, $out ); // update_option() answers false for an unchanged value as well as for // a row the database refused, so what is stored is what decides: a // value WordPress cannot write (half a character at a cut, say) used // to drop the whole save while the route answered 200. if ( maybe_serialize( get_option( self::OPTION ) ) !== maybe_serialize( $out ) ) { return new WP_Error( 'improveab_db', __( 'Could not save the settings.', 'improveab-ab-testing' ), array( 'status' => 500 ) ); } foreach ( $revoked as $slug ) { $role = get_role( $slug ); if ( $role && $role->has_cap( IMPROVEAB_CAP ) ) { $role->remove_cap( IMPROVEAB_CAP ); } } // These two change what every page prints; cached HTML must follow. foreach ( array( 'seo_mode', 'exclude_logged_in' ) as $key ) { if ( $before[ $key ] !== $out[ $key ] && class_exists( 'ImproveAB_Cache' ) ) { ImproveAB_Cache::purge_pages( 'settings' ); break; } } return $out; } }