, descriptors: array, unknown: array, total_bytes:int, total_count:int} */ public function analyze( int $top_rows = 10 ): array { global $wpdb; $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT option_name, LENGTH(option_value) AS bytes FROM %i WHERE autoload IN (%s, %s, %s, %s) ORDER BY bytes DESC', $wpdb->options, 'yes', 'on', 'auto-on', 'auto' ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- analysis query. if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; // Lists installed but inactive plugins as owners too. } $overrides = (array) get_option( 'bloatwright_overrides', array() ); $owner = new Owner( $overrides ); $plugins = array(); $descriptors = array(); foreach ( $owner->all() as $key => $d ) { if ( in_array( $d['type'], array( 'plugin', 'inactive', 'theme' ), true ) ) { $plugins[ Aggregator::slug( $d ) ] = $key; } } $prefixes = self::prefix_overrides( $overrides, array_keys( $owner->all() ) ); $owners = array(); $total = 0; $count = 0; $unknown = array(); foreach ( (array) $rows as $r ) { $name = (string) $r['option_name']; $bytes = (int) $r['bytes']; $total += $bytes; ++$count; $key = $this->owner_for_option( $name, $plugins, $prefixes ); if ( Owner::UNKNOWN === $key ) { if ( count( $unknown ) < 20 ) { $unknown[] = array( 'name' => $name, 'bytes' => $bytes, ); } } if ( ! isset( $owners[ $key ] ) ) { $owners[ $key ] = array( 'bytes' => 0, 'count' => 0, 'rows' => array(), ); $descriptors[ $key ] = $owner->describe( $key ); } $owners[ $key ]['bytes'] += $bytes; ++$owners[ $key ]['count']; if ( count( $owners[ $key ]['rows'] ) < $top_rows ) { $owners[ $key ]['rows'][] = array( 'name' => $name, 'bytes' => $bytes, ); } } uasort( $owners, static function ( $a, $b ) { return $b['bytes'] <=> $a['bytes']; } ); return array( 'owners' => $owners, 'descriptors' => $descriptors, 'unknown' => $unknown, 'total_bytes' => $total, 'total_count' => $count, ); } /** * Attribution overrides whose key is an option-name prefix rather than a directory (no slash or * backslash, not a Windows drive path), longest first: prefix => owner key. * * @param array $overrides Stored overrides (directory or option prefix => owner key). * @param string[] $known Owner keys that exist. * @return array */ public static function prefix_overrides( array $overrides, array $known ): array { $out = array(); foreach ( $overrides as $prefix => $key ) { $prefix = trim( (string) $prefix ); if ( '' === $prefix || preg_match( '#[/\\\\]|^[A-Za-z]:#', $prefix ) || ! in_array( (string) $key, $known, true ) ) { continue; } $out[ $prefix ] = (string) $key; } uksort( $out, static function ( $a, $b ) { return strlen( (string) $b ) <=> strlen( (string) $a ); } ); return $out; } /** * Owner key for an option name. Option-prefix overrides from Settings win over everything else. * * @param string $name Option. * @param array $plugins slug => owner key (installed plugins/themes). * @param array $prefixes Option-prefix overrides (prefix => owner key), longest first. */ public function owner_for_option( string $name, array $plugins, array $prefixes = array() ): string { $bare = $name; foreach ( array( '_transient_timeout_', '_transient_', '_site_transient_timeout_', '_site_transient_' ) as $t ) { if ( 0 === strpos( $bare, $t ) ) { $bare = substr( $bare, strlen( $t ) ); break; } } foreach ( $prefixes as $prefix => $key ) { if ( 0 === strpos( $bare, (string) $prefix ) || 0 === strpos( $name, (string) $prefix ) ) { return $key; } } if ( in_array( $bare, self::CORE, true ) || 0 === strpos( $bare, 'theme_mods_' ) || 0 === strpos( $bare, 'widget_' ) || 0 === strpos( $bare, '_wp_' ) || ( 0 === strpos( $bare, 'wp_' ) && ! isset( $plugins['wp'] ) && $this->is_core_wp_option( $bare ) ) ) { if ( 0 === strpos( $bare, 'theme_mods_' ) ) { $sheet = substr( $bare, strlen( 'theme_mods_' ) ); if ( isset( $plugins[ $sheet ] ) ) { return $plugins[ $sheet ]; } } return Owner::CORE; } $slug = PrefixRegistry::match( $name ); if ( '' !== $slug ) { if ( 'core' === $slug ) { return Owner::CORE; } if ( isset( $plugins[ $slug ] ) ) { return $plugins[ $slug ]; } return 'plugin:' . $slug; // Known plugin, currently inactive/uninstalled. } // Fuzzy: compare the option's leading tokens with each installed slug's tokens. The longest // match wins; on a tie a slug matched in full beats one matched only by its first word, so // example_reviews_cache belongs to example-reviews, not to example-slider. Two slugs that // match equally well, and neither in full, make the option ambiguous: it stays unknown. $tokens = array_values( array_filter( (array) preg_split( '/[_\-]+/', strtolower( $bare ) ) ) ); if ( $tokens ) { $best = ''; $best_len = 0; $best_full = false; $tied = false; foreach ( $plugins as $slug => $key ) { list( $len, $full ) = self::slug_match( $tokens, (string) $slug ); if ( $len <= 0 ) { continue; } if ( $len > $best_len || ( $len === $best_len && $full && ! $best_full ) ) { $best = $key; $best_len = $len; $best_full = $full; $tied = false; } elseif ( $len === $best_len && $full === $best_full && $key !== $best ) { $tied = true; } } if ( '' !== $best && ! ( $tied && ! $best_full ) ) { return $best; } } return Owner::UNKNOWN; } /** * How well an option name's tokens match a plugin or theme slug. * * Returns the number of matched characters and whether the whole slug was matched: the option's * leading tokens repeat the slug's tokens (heavy_init_x for heavy-init; only heavy_x matches it * partly), its first token is the slug without separators (heavyinit_x) or the start of it * (woocom_x for woocommerce, at least four characters, partly), or the slug's initials * (acf_x for advanced-custom-fields). The option's first token needs at least three characters. * * @param string[] $tokens Lower-case tokens of the option name. * @param string $slug Plugin or theme directory slug. * @return array{0:int,1:bool} */ private static function slug_match( array $tokens, string $slug ): array { $slug_tokens = array_values( array_filter( (array) preg_split( '/[_\-]+/', strtolower( $slug ) ) ) ); $first = (string) ( $tokens[0] ?? '' ); if ( ! $slug_tokens || strlen( $first ) < 3 ) { return array( 0, false ); } $len = 0; $i = 0; while ( isset( $tokens[ $i ], $slug_tokens[ $i ] ) && $tokens[ $i ] === $slug_tokens[ $i ] ) { $len += strlen( (string) $tokens[ $i ] ); ++$i; } if ( $len > 0 ) { return array( $len, count( $slug_tokens ) === $i ); } $compact = implode( '', $slug_tokens ); if ( $first === $compact ) { return array( strlen( $first ), true ); } if ( strlen( $first ) >= 4 && 0 === strpos( $compact, $first ) ) { return array( strlen( $first ), false ); } if ( count( $slug_tokens ) > 1 ) { $initials = ''; foreach ( $slug_tokens as $t ) { $initials .= $t[0]; } if ( $first === $initials ) { return array( strlen( $first ), true ); } } return array( 0, false ); } /** * Core options that start with wp_ (the rest of that prefix is up for grabs by plugins). * * @param string $name Option. */ private function is_core_wp_option( string $name ): bool { return in_array( $name, array( 'wp_user_roles', 'wp_page_for_privacy_policy', 'wp_force_deactivated_plugins', 'wp_attachment_pages_enabled', 'wp_calendar_block_has_published_posts' ), true ); } }