Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.67% covered (warning)
86.67%
26 / 30
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileContentsHasher
86.67% covered (warning)
86.67%
26 / 30
25.00% covered (danger)
25.00%
1 / 4
13.40
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 singleton
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getFileContentsHashInternal
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
3.03
 getFileContentsHash
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2/**
3 * Generate hash digests of file contents to help with cache invalidation.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22class FileContentsHasher {
23    private const ALGO = 'md4';
24
25    /** @var BagOStuff */
26    protected $cache;
27
28    /** @var FileContentsHasher */
29    private static $instance;
30
31    public function __construct() {
32        $this->cache = function_exists( 'apcu_fetch' ) ? new APCUBagOStuff() : new EmptyBagOStuff();
33    }
34
35    /**
36     * Get the singleton instance of this class.
37     *
38     * @return FileContentsHasher
39     */
40    public static function singleton() {
41        if ( !self::$instance ) {
42            self::$instance = new self;
43        }
44
45        return self::$instance;
46    }
47
48    /**
49     * Get a hash of a file's contents, either by retrieving a previously-
50     * computed hash from the cache, or by computing a hash from the file.
51     *
52     * @param string $filePath Full path to the file.
53     * @return string|bool Hash of file contents, or false if the file could not be read.
54     */
55    private function getFileContentsHashInternal( $filePath ) {
56        // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
57        $mtime = @filemtime( $filePath );
58        if ( $mtime === false ) {
59            return false;
60        }
61
62        $cacheKey = $this->cache->makeGlobalKey( __CLASS__, $filePath, $mtime, self::ALGO );
63        return $this->cache->getWithSetCallback(
64            $cacheKey,
65            $this->cache::TTL_DAY,
66            static function () use ( $filePath ) {
67                // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
68                $contents = @file_get_contents( $filePath );
69                if ( $contents === false ) {
70                    // Don't cache false
71                    return false;
72                }
73
74                return hash( self::ALGO, $contents );
75            }
76        );
77    }
78
79    /**
80     * Get a hash of the combined contents of one or more files, either by
81     * retrieving a previously-computed hash from the cache, or by computing
82     * a hash from the files.
83     *
84     * @param string|string[] $filePaths One or more file paths.
85     * @return string|bool Hash of files' contents, or false if no file could not be read.
86     */
87    public static function getFileContentsHash( $filePaths ) {
88        $instance = self::singleton();
89
90        if ( !is_array( $filePaths ) ) {
91            $filePaths = (array)$filePaths;
92        }
93
94        if ( count( $filePaths ) === 1 ) {
95            $hash = $instance->getFileContentsHashInternal( $filePaths[0] );
96            return $hash;
97        }
98
99        sort( $filePaths );
100        $hashes = [];
101        foreach ( $filePaths as $filePath ) {
102            $hashes[] = $instance->getFileContentsHashInternal( $filePath ) ?: '';
103        }
104
105        $hashes = implode( '', $hashes );
106        return $hashes ? hash( self::ALGO, $hashes ) : false;
107    }
108}