|null */ private $sizes = null; /** * Byte sizes of every autoloaded option, by name. * * @since 1.0.0 * @return array */ public function sizes(): array { if ( null === $this->sizes ) { $this->sizes = array(); foreach ( (array) wp_load_alloptions() as $name => $value ) { $this->sizes[ (string) $name ] = self::size( $value ); } } return $this->sizes; } /** * Bytes a value takes as stored. Values are raw strings; anything else (only through filters on * `alloptions`) is measured on a safe copy, so no object code runs. * * @since 1.0.0 * @param mixed $value Value from wp_load_alloptions(). * @return int */ public static function size( $value ): int { if ( is_string( $value ) ) { return strlen( $value ); } if ( is_scalar( $value ) || null === $value ) { return strlen( (string) $value ); } return strlen( serialize( SafeDump::to_array( $value ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- size of a plain copy (no objects), as Site Health measures it. } /** * The total autoload size WordPress's Site Health warns about (800 KB by default). * * @since 1.0.0 * @return int Bytes. */ public static function total_limit(): int { /** This filter is documented in wp-admin/includes/class-wp-site-health.php */ $limit = apply_filters( 'site_status_autoloaded_options_size_limit', 800000 ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress's own limit, read the way Site Health reads it. return is_numeric( $limit ) ? max( 1, (int) $limit ) : 800000; } /** * Largest value WordPress autoloads by its own choice (150 KB by default). * * @since 1.0.0 * @param string $name Option name. * @return int Bytes. */ public static function option_limit( string $name ): int { /** This filter is documented in wp-includes/option.php */ $limit = apply_filters( 'wp_max_autoloaded_option_size', 150000, $name ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress's own limit, read the way add_option() reads it. return is_numeric( $limit ) ? max( 1, (int) $limit ) : 150000; } /** * Autoload values that mean "loads on every request". * * @since 1.0.0 * @return string[] */ public static function autoload_values(): array { return function_exists( 'wp_autoload_values_to_autoload' ) ? (array) wp_autoload_values_to_autoload() : array( 'yes', 'on', 'auto-on', 'auto' ); } /** * Totals. * * @since 1.0.0 * @return array{count: int, bytes: int, limit: int, over: bool, large: int} */ public function summary(): array { $sizes = $this->sizes(); $bytes = array_sum( $sizes ); $large = 0; foreach ( $sizes as $name => $size ) { if ( $size > self::option_limit( (string) $name ) ) { ++$large; } } $limit = self::total_limit(); return array( 'count' => count( $sizes ), 'bytes' => (int) $bytes, 'limit' => $limit, 'over' => $bytes > $limit, 'large' => $large, ); } /** * The largest autoloaded options matching a filter. * * @since 1.0.0 * @param array $filter `s` (text in the name) and `owner` (one of OWNER_FILTERS or ''). * @return array{matched: int, rows: array>} */ public function rows( array $filter ): array { $search = isset( $filter['s'] ) && is_string( $filter['s'] ) ? trim( $filter['s'] ) : ''; $owner = isset( $filter['owner'] ) && in_array( $filter['owner'], self::OWNER_FILTERS, true ) ? $filter['owner'] : ''; $sizes = $this->sizes(); if ( '' !== $search ) { $sizes = array_filter( $sizes, static fn( $name ) => false !== stripos( (string) $name, $search ), ARRAY_FILTER_USE_KEY ); } $owners = array(); if ( '' !== $owner ) { foreach ( array_keys( $sizes ) as $name ) { $guess = Policy::owner( (string) $name ); if ( self::owner_matches( $guess, $owner ) ) { $owners[ (string) $name ] = $guess; } else { unset( $sizes[ $name ] ); } } } uksort( $sizes, static function ( $a, $b ) use ( $sizes ) { $by_size = $sizes[ $b ] <=> $sizes[ $a ]; return 0 !== $by_size ? $by_size : strcmp( (string) $a, (string) $b ); } ); $matched = count( $sizes ); $top = array_slice( $sizes, 0, self::LIMIT, true ); $stored = $this->stored_autoload( array_map( 'strval', array_keys( $top ) ) ); $rows = array(); foreach ( $top as $name => $size ) { $name = (string) $name; $guess = $owners[ $name ] ?? Policy::owner( $name ); $autoload = $stored[ strtolower( $name ) ] ?? ''; $large = $size > self::option_limit( $name ); $rows[] = array( 'name' => $name, 'size' => (int) $size, 'autoload' => $autoload, 'large' => $large, 'forced' => $large && in_array( $autoload, array( 'on', 'yes' ), true ), 'owner' => $guess, 'toggle_block' => Policy::toggle_block( $name ), 'stop_note' => Policy::stop_note( $guess ), 'delete_block' => Policy::delete_block( $name, $guess ), ); } return array( 'matched' => $matched, 'rows' => $rows, ); } /** * Whether an owner guess fits a list filter. * * @since 1.0.0 * @param array $guess Owner guess. * @param string $filter Filter. * @return bool */ public static function owner_matches( array $guess, string $filter ): bool { if ( 'inactive' === $filter ) { return in_array( $guess['type'] ?? '', array( 'plugin', 'theme' ), true ) && in_array( $guess['state'] ?? '', array( 'inactive', 'not-installed' ), true ); } return ( $guess['type'] ?? '' ) === $filter; } /** * Stored autoload flags, in one query. * * @since 1.0.0 * @param string[] $names Option names. * @return array Lowercased name => autoload value. */ public function stored_autoload( array $names ): array { global $wpdb; $names = array_values( array_unique( array_filter( $names, static fn( $n ) => is_string( $n ) && '' !== $n ) ) ); if ( ! $names ) { return array(); } $marks = implode( ',', array_fill( 0, count( $names ), '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $marks is only %s placeholders; flags only, no values. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT option_name, autoload FROM %i WHERE option_name IN ($marks)", array_merge( array( $wpdb->options ), $names ) ), ARRAY_A ); $out = array(); foreach ( (array) $rows as $row ) { $out[ strtolower( (string) $row['option_name'] ) ] = (string) $row['autoload']; } return $out; } /** * One option as stored: the name as the database has it, the raw value (never unserialized) * and its autoload flag. * * @since 1.0.0 * @param string $name Option name. * @return array{name: string, value: string, autoload: string}|null Null when there's no such option. */ public function raw( string $name ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- the raw stored value on purpose: get_option() would unserialize it. $row = $wpdb->get_row( $wpdb->prepare( 'SELECT option_name, option_value, autoload FROM %i WHERE option_name = %s LIMIT 1', $wpdb->options, $name ), ARRAY_A ); if ( ! is_array( $row ) ) { return null; } return array( 'name' => (string) $row['option_name'], 'value' => (string) $row['option_value'], 'autoload' => (string) $row['autoload'], ); } /** * Whether an autoload flag means "loads on every request". * * @since 1.0.0 * @param string $autoload Stored flag. * @return bool */ public static function is_autoloaded( string $autoload ): bool { return in_array( $autoload, self::autoload_values(), true ); } }