, errors: array} Valid feeds, and an error code per rejected row index. */ public static function sanitize( $raw, Url_Validator $validator ): array { $feeds = array(); $errors = array(); $seen = array(); foreach ( is_array( $raw ) ? array_values( $raw ) : array() as $index => $row ) { $url = is_array( $row ) && is_string( $row['url'] ?? null ) ? trim( $row['url'] ) : ''; $label = is_array( $row ) && is_string( $row['label'] ?? null ) ? Feed_Normalizer::plain_text( $row['label'], self::MAX_LABEL ) : ''; $color = is_array( $row ) ? self::sanitize_color( $row['color'] ?? '' ) : ''; if ( '' === $url && '' === $label ) { continue; } $error = '' === $url ? Url_Validator::INVALID_URL : $validator->check_feed_url( $url ); if ( null !== $error ) { $errors[ $index ] = $error; continue; } if ( isset( $seen[ $url ] ) ) { continue; } $seen[ $url ] = true; $feeds[] = self::feed( $url, $label, $color ); }//end foreach return array( 'feeds' => $feeds, 'errors' => $errors, ); } /** * Validates an optional source colour: `#rgb` or `#rrggbb`, returned as lower-case `#rrggbb`. * Anything else, including CSS functions and named colours, becomes an empty string, which * means "assign automatically". * * @param mixed $value Submitted value. */ public static function sanitize_color( $value ): string { if ( ! is_string( $value ) ) { return ''; } $value = strtolower( trim( $value ) ); if ( 1 === preg_match( '/^#([0-9a-f])([0-9a-f])([0-9a-f])\z/', $value, $m ) ) { return '#' . $m[1] . $m[1] . $m[2] . $m[2] . $m[3] . $m[3]; } return 1 === preg_match( '/^#[0-9a-f]{6}\z/', $value ) ? $value : ''; } /** * Builds a feed row. The colour key is only present when a colour is set. * * @param string $url URL. * @param string $label Label. * @param string $color Validated colour or an empty string. * @return array{url:string,label:string,color?:string} */ public static function feed( string $url, string $label, string $color ): array { $feed = array( 'url' => $url, 'label' => $label, ); if ( '' !== $color ) { $feed['color'] = $color; } return $feed; } /** * Reads feeds that were stored earlier. Structure only: no URL checks and no DNS, because * the fetcher validates every URL again before each request. * * @param mixed $stored Stored value. * @return array */ public static function from_storage( $stored ): array { $feeds = array(); foreach ( is_array( $stored ) ? $stored : array() as $row ) { if ( is_array( $row ) && is_string( $row['url'] ?? null ) && '' !== $row['url'] ) { $feeds[] = self::feed( $row['url'], is_string( $row['label'] ?? null ) ? $row['label'] : '', self::sanitize_color( $row['color'] ?? '' ) ); } } return $feeds; } }