Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
97.33% |
73 / 75 |
|
84.62% |
11 / 13 |
CRAP | |
0.00% |
0 / 1 |
| Speed_Score_History | |
97.33% |
73 / 75 |
|
84.62% |
11 / 13 |
32 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
6 | |||
| maybe_migrate_legacy_records | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
| migrate_legacy_record | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
7 | |||
| get_latest_timestamp | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| get_option_name | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| count | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| latest | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| latest_scores | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| get_stale_timestamp | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| mark_stale | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| is_stale | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
| push | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| touch_latest | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Speed score history after Jetpack Boost |
| 4 | * |
| 5 | * @package automattic/jetpack-boost-speed-score |
| 6 | */ |
| 7 | |
| 8 | namespace Automattic\Jetpack\Boost_Speed_Score; |
| 9 | |
| 10 | use Automattic\Jetpack\Boost_Core\Lib\Transient; |
| 11 | |
| 12 | /** |
| 13 | * Class Speed_Score_History |
| 14 | * |
| 15 | * @package Automattic\Jetpack\Boost_Speed_Score\Lib |
| 16 | */ |
| 17 | class Speed_Score_History { |
| 18 | const OPTION_PREFIX = 'jetpack_boost_speed_score_history_'; |
| 19 | const STALE_TRANSIENT_KEY = 'speed_score_stale_marker'; |
| 20 | |
| 21 | /** |
| 22 | * Mark the raw home and site URL migration as complete. Autoloaded, so checking it costs no query. |
| 23 | */ |
| 24 | const LEGACY_MIGRATED_OPTION = 'jetpack_boost_speed_score_legacy_migrated'; |
| 25 | |
| 26 | /** |
| 27 | * Limit the number of recent records to keep. |
| 28 | */ |
| 29 | const LIMIT = 20; |
| 30 | |
| 31 | /** |
| 32 | * The list of history entries. |
| 33 | * |
| 34 | * @var array |
| 35 | */ |
| 36 | private $entries; |
| 37 | |
| 38 | /** |
| 39 | * URL of the site the speed score belongs to. |
| 40 | * |
| 41 | * @var string |
| 42 | */ |
| 43 | private $url; |
| 44 | |
| 45 | /** |
| 46 | * Speed_Score_History constructor. |
| 47 | * |
| 48 | * @param string $url URL of the site the speed scores belong to. |
| 49 | */ |
| 50 | public function __construct( $url ) { |
| 51 | self::maybe_migrate_legacy_records(); |
| 52 | |
| 53 | $this->url = $url; |
| 54 | $this->entries = get_option( $this->get_option_name(), array() ); |
| 55 | |
| 56 | if ( empty( $this->entries ) ) { |
| 57 | $migrated_option = $this->get_option_name() . '_legacy_migrated'; |
| 58 | if ( get_option( $migrated_option ) ) { |
| 59 | return; |
| 60 | } |
| 61 | |
| 62 | $path_length = strcspn( $url, '?#' ); |
| 63 | $base_url = rtrim( substr( $url, 0, $path_length ), '/' ); |
| 64 | $suffix = substr( $url, $path_length ); |
| 65 | $legacy_urls = array( $base_url . '/' . $suffix ); |
| 66 | |
| 67 | if ( $base_url === rtrim( home_url(), '/' ) ) { |
| 68 | $site_url = rtrim( get_site_url(), '/' ); |
| 69 | if ( $site_url !== $base_url && set_url_scheme( $site_url, 'https' ) !== rtrim( set_url_scheme( get_option( 'home' ), 'https' ), '/' ) ) { |
| 70 | $legacy_urls[] = $site_url . $suffix; |
| 71 | $legacy_urls[] = $site_url . '/' . $suffix; |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | self::migrate_legacy_record( $url, $legacy_urls ); |
| 76 | $this->entries = get_option( $this->get_option_name(), array() ); |
| 77 | update_option( $migrated_option, 1, false ); |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | /** |
| 82 | * Migrate older spellings of the raw home and site URLs once per site. |
| 83 | * |
| 84 | * The marker is set last, so a request arriving midway repeats the work rather than finding no |
| 85 | * scores and starting a new test. |
| 86 | */ |
| 87 | private static function maybe_migrate_legacy_records() { |
| 88 | if ( get_option( self::LEGACY_MIGRATED_OPTION ) ) { |
| 89 | return; |
| 90 | } |
| 91 | |
| 92 | // The raw options, because a translation plugin's home_url filter would migrate only that language's record. |
| 93 | // The first build can run under cron, where the home URL can have another scheme than in the admin. |
| 94 | foreach ( array( 'http', 'https' ) as $scheme ) { |
| 95 | $home_url = rtrim( set_url_scheme( get_option( 'home' ), $scheme ), '/' ); |
| 96 | |
| 97 | // My Jetpack saved the home page's scores under the site URL, which differs when WordPress lives in a subdirectory. |
| 98 | // Match schemes first: under cron or CLI the two can differ by scheme alone, which isn't a subdirectory install. |
| 99 | $site_url = rtrim( set_url_scheme( get_option( 'siteurl' ), $scheme ), '/' ); |
| 100 | |
| 101 | foreach ( array( '', '?jb-disable-modules=all' ) as $query ) { |
| 102 | $legacy_urls = array( $home_url . '/' . $query ); |
| 103 | if ( $site_url !== $home_url ) { |
| 104 | $legacy_urls[] = $site_url . $query; |
| 105 | $legacy_urls[] = $site_url . '/' . $query; |
| 106 | } |
| 107 | |
| 108 | self::migrate_legacy_record( $home_url . $query, $legacy_urls ); |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | update_option( self::LEGACY_MIGRATED_OPTION, 1, true ); |
| 113 | } |
| 114 | |
| 115 | /** |
| 116 | * Move the newest of the older records to the given URL's key, unless its own record is newer. |
| 117 | * |
| 118 | * The canonical spelling is the one My Jetpack used to write to, so it can hold scores older |
| 119 | * than the ones the Boost dashboard showed. |
| 120 | * |
| 121 | * @param string $url The URL as it is keyed now. |
| 122 | * @param string[] $legacy_urls The spellings its scores may have been saved under. |
| 123 | */ |
| 124 | private static function migrate_legacy_record( $url, $legacy_urls ) { |
| 125 | $newest_entries = array(); |
| 126 | $legacy_option_names = array(); |
| 127 | |
| 128 | foreach ( $legacy_urls as $legacy_url ) { |
| 129 | $option_name = static::OPTION_PREFIX . hash( Speed_Score_Request::CACHE_KEY_HASH_ALGO, $legacy_url ); |
| 130 | $entries = get_option( $option_name, array() ); |
| 131 | |
| 132 | if ( empty( $entries ) ) { |
| 133 | continue; |
| 134 | } |
| 135 | |
| 136 | if ( self::get_latest_timestamp( $entries ) > self::get_latest_timestamp( $newest_entries ) ) { |
| 137 | $newest_entries = $entries; |
| 138 | } |
| 139 | $legacy_option_names[] = $option_name; |
| 140 | } |
| 141 | |
| 142 | if ( empty( $legacy_option_names ) ) { |
| 143 | return; |
| 144 | } |
| 145 | |
| 146 | $option_name = static::OPTION_PREFIX . Speed_Score_Request::generate_cache_id_from_url( $url ); |
| 147 | if ( self::get_latest_timestamp( $newest_entries ) > self::get_latest_timestamp( get_option( $option_name, array() ) ) ) { |
| 148 | update_option( $option_name, $newest_entries, false ); |
| 149 | } |
| 150 | |
| 151 | // Do not reorder: a request that finds these gone must also find the record they moved to. |
| 152 | foreach ( $legacy_option_names as $legacy_option_name ) { |
| 153 | delete_option( $legacy_option_name ); |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | /** |
| 158 | * Get the timestamp of the last of the given history entries. |
| 159 | * |
| 160 | * @param array $entries History entries, oldest first. |
| 161 | * |
| 162 | * @return int |
| 163 | */ |
| 164 | private static function get_latest_timestamp( $entries ) { |
| 165 | $latest = end( $entries ); |
| 166 | |
| 167 | return (int) ( $latest['timestamp'] ?? 0 ); |
| 168 | } |
| 169 | |
| 170 | /** |
| 171 | * Determine the option_name used to store the speed score data in options table. |
| 172 | * |
| 173 | * @return string |
| 174 | */ |
| 175 | private function get_option_name() { |
| 176 | return static::OPTION_PREFIX . Speed_Score_Request::generate_cache_id_from_url( $this->url ); |
| 177 | } |
| 178 | |
| 179 | /** |
| 180 | * Get the number of history records currently stored. |
| 181 | * |
| 182 | * @return int |
| 183 | */ |
| 184 | public function count() { |
| 185 | return count( $this->entries ); |
| 186 | } |
| 187 | |
| 188 | /** |
| 189 | * Find the latest available speed score history record. |
| 190 | * |
| 191 | * @param int $offset Instead of receiving the last one, you can use offset to receive a slightly older speed score. |
| 192 | * |
| 193 | * @return array|null |
| 194 | */ |
| 195 | public function latest( $offset = 0 ) { |
| 196 | $index = $this->count() - ( $offset + 1 ); |
| 197 | |
| 198 | if ( $index >= 0 ) { |
| 199 | return $this->entries[ $index ]; |
| 200 | } |
| 201 | |
| 202 | return null; |
| 203 | } |
| 204 | |
| 205 | /** |
| 206 | * Find the latest available speed scores. |
| 207 | * |
| 208 | * @param int $offset Instead of receiving the last one, you can use offset to receive a slightly older speed score. |
| 209 | * |
| 210 | * @return array|null |
| 211 | */ |
| 212 | public function latest_scores( $offset = 0 ) { |
| 213 | $index = $this->count() - ( $offset + 1 ); |
| 214 | |
| 215 | if ( $index >= 0 ) { |
| 216 | return $this->entries[ $index ]['scores']; |
| 217 | } |
| 218 | |
| 219 | return null; |
| 220 | } |
| 221 | |
| 222 | /** |
| 223 | * Get a timestamp that marks previous history stale. |
| 224 | * |
| 225 | * All speed score before this timestamp are considered stale. |
| 226 | * |
| 227 | * @return int |
| 228 | */ |
| 229 | public static function get_stale_timestamp() { |
| 230 | $last_stale_marker = Transient::get( static::STALE_TRANSIENT_KEY, 0 ); |
| 231 | |
| 232 | // Any score that is older than 24 hours or before the last stale marker is considered stale. |
| 233 | return max( $last_stale_marker, time() - DAY_IN_SECONDS ); |
| 234 | } |
| 235 | |
| 236 | /** |
| 237 | * Mark previous speed score as stale. |
| 238 | * |
| 239 | * If a there were significant changes to the site, we want to mark prior speed scores as stale. |
| 240 | */ |
| 241 | public static function mark_stale() { |
| 242 | Transient::set( static::STALE_TRANSIENT_KEY, time() ); |
| 243 | } |
| 244 | |
| 245 | /** |
| 246 | * Check if the last item in history is stale. |
| 247 | */ |
| 248 | public function is_stale() { |
| 249 | $last_entry = $this->latest(); |
| 250 | |
| 251 | if ( ! $last_entry ) { |
| 252 | return true; |
| 253 | } |
| 254 | |
| 255 | return $last_entry['timestamp'] < self::get_stale_timestamp(); |
| 256 | } |
| 257 | |
| 258 | /** |
| 259 | * Record a new history entry for speed scores. |
| 260 | * |
| 261 | * @param array $entry The new entry to save. |
| 262 | */ |
| 263 | public function push( $entry ) { |
| 264 | $this->entries[] = $entry; |
| 265 | $this->entries = array_slice( $this->entries, - static::LIMIT ); |
| 266 | update_option( $this->get_option_name(), $this->entries, false ); |
| 267 | } |
| 268 | |
| 269 | /** |
| 270 | * Move the latest entry's timestamp to now without changing its scores. |
| 271 | * |
| 272 | * Used when a run completes with the same scores as before: no new entry is pushed, but |
| 273 | * staleness is measured by the timestamp, so it has to move or repeating scores look stale |
| 274 | * forever. |
| 275 | * |
| 276 | * @since 0.6.0 |
| 277 | */ |
| 278 | public function touch_latest() { |
| 279 | $index = $this->count() - 1; |
| 280 | |
| 281 | if ( $index < 0 ) { |
| 282 | return; |
| 283 | } |
| 284 | |
| 285 | $this->entries[ $index ]['timestamp'] = time(); |
| 286 | update_option( $this->get_option_name(), $this->entries, false ); |
| 287 | } |
| 288 | } |