settings = $settings; } /** * Sanitize a submitted settings array. * * @param mixed $input Raw submitted value. * @return array */ public function sanitize( $input ): array { $input = is_array( $input ) ? $input : array(); $current = $this->settings->load(); $clean = array(); // A form drawn in simple mode leaves the advanced fields out, and an absent checkbox otherwise // means "off". Only a form that says it was simple keeps them as they were; a write from // anywhere else (WP-CLI, another plugin) is still taken as the whole truth. $kept = isset( $input[ self::MODE_FIELD ] ) && 'simple' === $input[ self::MODE_FIELD ] ? Settings::advanced_only() : array(); foreach ( Settings::defaults() as $key => $default ) { if ( in_array( $key, $kept, true ) ) { $clean[ $key ] = $current->get( $key ); continue; } if ( is_bool( $default ) ) { // An unchecked checkbox is absent from the POST body rather than false, so absence is the // only way "off" can be expressed. $clean[ $key ] = ! empty( $input[ $key ] ); continue; } if ( is_array( $default ) ) { $clean[ $key ] = $this->sanitize_list( $key, isset( $input[ $key ] ) ? $input[ $key ] : array() ); continue; } $choices = Settings::choices( $key ); if ( array() !== $choices ) { $submitted = isset( $input[ $key ] ) && is_scalar( $input[ $key ] ) ? (string) $input[ $key ] : ''; $clean[ $key ] = in_array( $submitted, $choices, true ) ? $submitted : $choices[0]; continue; } $raw = isset( $input[ $key ] ) && is_scalar( $input[ $key ] ) ? (string) $input[ $key ] : ''; $clean[ $key ] = $this->sanitize_field( $key, $raw, $current ); } return $clean; } /** * Sanitize a list-valued setting. * * @param string $key Field name. * @param mixed $raw Submitted value. * @return string[] */ private function sanitize_list( string $key, $raw ): array { $values = is_array( $raw ) ? $raw : array(); $values = array_map( 'sanitize_key', array_filter( $values, 'is_scalar' ) ); if ( 'exclude_roles' === $key ) { // Only roles that exist on this site. A stored slug for a role a plugin has since removed // would silently exclude nobody while still appearing to be set. $known = array_keys( (array) wp_roles()->get_names() ); $values = array_intersect( $values, $known ); } return array_values( array_unique( $values ) ); } /** * Mask a secret for display, showing just enough for the site owner to recognise which key it is. * * @param string $secret Stored secret. * @return string */ public static function mask( string $secret ): string { if ( '' === $secret ) { return ''; } // Never the first characters: the prefix identifies the key type and is the same on every key, // so it would confirm nothing while the last four confirm exactly which key this is. return str_repeat( self::MASK_CHAR, 12 ) . substr( $secret, -4 ); } /** * Sanitize one string field. * * @param string $key Field name. * @param string $raw Submitted value. * @param Settings $current Values being replaced. * @return string */ private function sanitize_field( string $key, string $raw, Settings $current ): string { $raw = trim( $raw ); switch ( $key ) { case 'server_key': case 'api_token': // Both are shown masked, so the value posted back by someone who did not touch the field // is the mask itself. Storing that would silently disconnect the site. if ( '' !== $raw && false !== strpos( $raw, self::MASK_CHAR ) ) { return $current->str( $key ); } return sanitize_text_field( $raw ); case 'exclude_post_ids': $ids = preg_split( '/[\s,]+/', $raw, -1, PREG_SPLIT_NO_EMPTY ); $ids = array_filter( array_map( 'absint', is_array( $ids ) ? $ids : array() ) ); return implode( ', ', array_unique( $ids ) ); case 'custom_tag_code': case 'custom_code': return $this->sanitize_code( $key, $raw, $current ); case 'default_cc': $digits = preg_replace( '/[^0-9]/', '', $raw ); $digits = is_string( $digits ) ? $digits : ''; return '' !== $digits ? '+' . $digits : ''; default: return sanitize_text_field( $raw ); } } /** * Accept a block of site-supplied code, but only from someone trusted with code. * * These two settings are printed to the page as written — that is what they are for. WordPress * already has a name for the privilege of publishing unfiltered markup, and this is the same * privilege, so it uses the same check rather than inventing a weaker one. On multisite that * means network administrators only, which is the correct answer there. * * @param string $key Field name. * @param string $raw Submitted value. * @param Settings $current Values being replaced. * @return string */ private function sanitize_code( string $key, string $raw, Settings $current ): string { if ( $raw === $current->str( $key ) ) { return $raw; } if ( ! current_user_can( 'unfiltered_html' ) ) { add_settings_error( Settings::OPTION, 'adpix_analytics_custom_code', __( 'Custom code can only be changed by a user who is allowed to publish unfiltered HTML. Your other changes were saved.', 'adpix-analytics' ), 'error' ); return $current->str( $key ); } return $raw; } }