Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.33% covered (warning)
83.33%
25 / 30
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileContentsHasher
86.21% covered (warning)
86.21%
25 / 29
25.00% covered (danger)
25.00%
1 / 4
13.44
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%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3namespace MediaWiki\Utils;
4
5/**
6 * @license GPL-2.0-or-later
7 */
8
9use Wikimedia\ObjectCache\APCUBagOStuff;
10use Wikimedia\ObjectCache\BagOStuff;
11use Wikimedia\ObjectCache\EmptyBagOStuff;
12
13/**
14 * Generate hash digests of file contents to help with cache invalidation.
15 */
16class FileContentsHasher {
17    private const ALGO = 'md4';
18
19    /** @var BagOStuff */
20    protected $cache;
21
22    /** @var FileContentsHasher */
23    private static $instance;
24
25    public function __construct() {
26        $this->cache = function_exists( 'apcu_fetch' ) ? new APCUBagOStuff() : new EmptyBagOStuff();
27    }
28
29    /**
30     * Get the singleton instance of this class.
31     *
32     * @return FileContentsHasher
33     */
34    public static function singleton() {
35        if ( !self::$instance ) {
36            self::$instance = new self;
37        }
38
39        return self::$instance;
40    }
41
42    /**
43     * Get a hash of a file's contents, either by retrieving a previously-
44     * computed hash from the cache, or by computing a hash from the file.
45     *
46     * @param string $filePath Full path to the file.
47     * @return string|bool Hash of file contents, or false if the file could not be read.
48     */
49    private function getFileContentsHashInternal( $filePath ) {
50        // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
51        $mtime = @filemtime( $filePath );
52        if ( $mtime === false ) {
53            return false;
54        }
55
56        $cacheKey = $this->cache->makeGlobalKey( __CLASS__, $filePath, $mtime, self::ALGO );
57        return $this->cache->getWithSetCallback(
58            $cacheKey,
59            $this->cache::TTL_DAY,
60            static function () use ( $filePath ) {
61                // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
62                $contents = @file_get_contents( $filePath );
63                if ( $contents === false ) {
64                    // Don't cache false
65                    return false;
66                }
67
68                return hash( self::ALGO, $contents );
69            }
70        );
71    }
72
73    /**
74     * Get a hash of the combined contents of one or more files, either by
75     * retrieving a previously-computed hash from the cache, or by computing
76     * a hash from the files.
77     *
78     * @param string|string[] $filePaths One or more file paths.
79     * @return string|bool Hash of files' contents, or false if no file could not be read.
80     */
81    public static function getFileContentsHash( $filePaths ) {
82        $instance = self::singleton();
83
84        if ( !is_array( $filePaths ) ) {
85            $filePaths = (array)$filePaths;
86        }
87
88        if ( count( $filePaths ) === 1 ) {
89            return $instance->getFileContentsHashInternal( $filePaths[0] );
90        }
91
92        $hashes = [];
93        foreach ( $filePaths as $filePath ) {
94            $hashes[] = $instance->getFileContentsHashInternal( $filePath ) ?: '';
95        }
96
97        // T425356: Ensure stable sort regardless of absolute file path, which may vary over time
98        sort( $hashes );
99        $hashes = implode( '', $hashes );
100        return $hashes ? hash( self::ALGO, $hashes ) : false;
101    }
102}
103
104/** @deprecated class alias since 1.46 */
105class_alias( FileContentsHasher::class, 'FileContentsHasher' );