Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
File_Paths
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
9 / 9
15
100.00% covered (success)
100.00%
1 / 1
 set
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 jsonSerialize
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 get_paths
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 jsonUnserialize
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 is_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_issued
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 find_issued
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 generate_cache_id
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 cache_prefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Automattic\Jetpack_Boost\Lib\Minify;
4
5use Automattic\Jetpack\Boost_Core\Lib\Cacheable;
6use Automattic\Jetpack\Boost_Core\Lib\Transient;
7
8/**
9 * Store the hash of the path string so that the concatenated file can be constructed
10 * on-demand.
11 *
12 * We are setting the transient for a long time because, if the content of the hash is not accessible, the concatenated file
13 * will not be able to construct itself. Because of page caching, there may be situations where this function will not
14 * get called for a long time, and we still want to serve the concatenated file in those cases. Having the value in
15 * a Boost Transient ensures that the value is cleaned up when Boost is uninstalled.
16 *
17 * Only lists stored here are built.
18 */
19final class File_Paths extends Cacheable {
20    private $paths;
21    private $mtime;
22    private $cache_buster;
23
24    protected const DEFAULT_EXPIRY = MONTH_IN_SECONDS;
25
26    public function set( $paths, $mtime, $cache_buster ) {
27        $this->paths        = $paths;
28        $this->mtime        = $mtime;
29        $this->cache_buster = $cache_buster;
30    }
31
32    public function jsonSerialize(): array {
33        return array(
34            'paths'        => $this->paths,
35            'mtime'        => $this->mtime,
36            'cache_buster' => $this->cache_buster,
37        );
38    }
39
40    /**
41     * Get an array of file paths that belong to the same hash.
42     *
43     * @return array Array of file paths relative to the webroot.
44     */
45    public function get_paths() {
46        return $this->paths;
47    }
48
49    public static function jsonUnserialize( $data ) {
50        $instance = new self();
51        $instance->set( $data['paths'], $data['mtime'], $data['cache_buster'] );
52        return $instance;
53    }
54
55    /**
56     * Whether a string has the form of the IDs generate_cache_id() produces.
57     *
58     * @param string $id The string to check.
59     *
60     * @return bool
61     */
62    public static function is_id( $id ) {
63        return 1 === preg_match( '/^[0-9a-f]{10}$/D', $id );
64    }
65
66    /**
67     * Get a stored list by its ID.
68     *
69     * @param string $id The list ID.
70     *
71     * @return self|null
72     */
73    public static function get_issued( $id ) {
74        $issued = self::get( $id );
75
76        if ( ! $issued ) {
77            // WordPress notes every missing option in one shared cache entry, which made-up IDs would keep growing.
78            $missing_options = wp_cache_get( 'notoptions', 'options' );
79            $option_name     = Transient::key( self::cache_prefix() . $id );
80            if ( is_array( $missing_options ) && isset( $missing_options[ $option_name ] ) ) {
81                unset( $missing_options[ $option_name ] );
82                wp_cache_set( 'notoptions', $missing_options, 'options' );
83            }
84        }
85
86        return $issued;
87    }
88
89    /**
90     * Find the stored list holding exactly these paths, by the ID built from them, their newest modification time and the current cache buster.
91     *
92     * @param string[] $paths        File paths in list order.
93     * @param int      $mtime        Newest modification time of the files.
94     * @param int      $cache_buster Current cache buster.
95     *
96     * @return self|null
97     */
98    public static function find_issued( $paths, $mtime, $cache_buster ) {
99        $candidate = new self();
100        $candidate->set( $paths, $mtime, $cache_buster );
101
102        $issued = self::get_issued( $candidate->generate_cache_id() );
103
104        // IDs are short enough for different lists to share one, so compare the lists as well.
105        // Stored stylesheet lists are keyed by handle, so only the values and their order count.
106        return $issued && is_array( $issued->get_paths() ) && array_values( $issued->get_paths() ) === $paths ? $issued : null;
107    }
108
109    protected function generate_cache_id() {
110        $hash = md5( implode( ',', $this->paths ) . '_' . $this->mtime . '_' . $this->cache_buster );
111
112        // Keep cache id small as option keys have a character limit.
113        return substr( $hash, 0, 10 );
114    }
115
116    protected static function cache_prefix() {
117        return 'concat_paths_';
118    }
119}