Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.27% covered (warning)
52.27%
23 / 44
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
BadFileLookup
53.49% covered (warning)
53.49%
23 / 43
66.67% covered (warning)
66.67%
2 / 3
37.64
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 isBadFile
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
6
 buildBadFilesList
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2
3namespace MediaWiki\Page\File;
4
5use MediaWiki\HookContainer\HookContainer;
6use MediaWiki\HookContainer\HookRunner;
7use MediaWiki\Linker\LinkTarget;
8use MediaWiki\Title\MalformedTitleException;
9use MediaWiki\Title\TitleParser;
10use RepoGroup;
11use Wikimedia\ObjectCache\BagOStuff;
12
13class BadFileLookup {
14    /** @var callable Returns contents of bad file list (see comment for isBadFile()) */
15    private $listCallback;
16
17    private BagOStuff $cache;
18    private RepoGroup $repoGroup;
19    private TitleParser $titleParser;
20    private HookRunner $hookRunner;
21
22    /** @var array<string,array<int,array<string,true>>>|null Parsed bad file list */
23    private $badFiles;
24
25    /**
26     * Do not call directly. Use MediaWikiServices.
27     *
28     * @param callable $listCallback Callback that returns wikitext of a bad file list
29     * @param BagOStuff $cache For caching parsed versions of the bad file list
30     * @param RepoGroup $repoGroup
31     * @param TitleParser $titleParser
32     * @param HookContainer $hookContainer
33     */
34    public function __construct(
35        callable $listCallback,
36        BagOStuff $cache,
37        RepoGroup $repoGroup,
38        TitleParser $titleParser,
39        HookContainer $hookContainer
40    ) {
41        $this->listCallback = $listCallback;
42        $this->cache = $cache;
43        $this->repoGroup = $repoGroup;
44        $this->titleParser = $titleParser;
45        $this->hookRunner = new HookRunner( $hookContainer );
46    }
47
48    /**
49     * Determine if a file exists on the 'bad image list'.
50     *
51     * The format of MediaWiki:Bad_image_list is as follows:
52     *    * Only list items (lines starting with "*") are considered
53     *    * The first link on a line must be a link to a bad file
54     *    * Any subsequent links on the same line are considered to be exceptions,
55     *      i.e. articles where the file may occur inline.
56     *
57     * @param string $name The file name to check
58     * @param LinkTarget|null $contextTitle The page on which the file occurs, if known
59     * @return bool
60     */
61    public function isBadFile( $name, ?LinkTarget $contextTitle = null ) {
62        // Handle redirects; callers almost always hit RepoGroup::findFile() anyway,
63        // so just use that method because it has a fast process cache.
64        $file = $this->repoGroup->findFile( $name );
65        // XXX If we don't find the file we also don't replace spaces by underscores or otherwise
66        // validate or normalize the title, is this right?
67        if ( $file ) {
68            $name = $file->getTitle()->getDBkey();
69        }
70
71        // Run the extension hook
72        $bad = false;
73        if ( !$this->hookRunner->onBadImage( $name, $bad ) ) {
74            return (bool)$bad;
75        }
76
77        if ( $this->badFiles === null ) {
78            $list = ( $this->listCallback )();
79            $key = $this->cache->makeKey( 'bad-image-list', sha1( $list ) );
80            $this->badFiles = $this->cache->getWithSetCallback(
81                $key,
82                BagOStuff::TTL_DAY,
83                function () use ( $list ) {
84                    return $this->buildBadFilesList( $list );
85                }
86            );
87        }
88
89        return isset( $this->badFiles[$name] ) && ( !$contextTitle ||
90            !isset( $this->badFiles[$name][$contextTitle->getNamespace()][$contextTitle->getDBkey()] ) );
91    }
92
93    /**
94     * @param string $list
95     * @return array<string,array<int,array<string,true>>>
96     */
97    private function buildBadFilesList( string $list ): array {
98        $ret = [];
99        $lines = explode( "\n", $list );
100        foreach ( $lines as $line ) {
101            // List items only
102            if ( substr( $line, 0, 1 ) !== '*' ) {
103                continue;
104            }
105
106            // Find all links
107            $m = [];
108            // XXX What is the ':?' doing in the regex? Why not let the TitleParser strip it?
109            if ( !preg_match_all( '/\[\[:?(.*?)\]\]/', $line, $m ) ) {
110                continue;
111            }
112
113            $fileDBkey = null;
114            $exceptions = [];
115            foreach ( $m[1] as $i => $titleText ) {
116                try {
117                    $title = $this->titleParser->parseTitle( $titleText );
118                } catch ( MalformedTitleException $e ) {
119                    continue;
120                }
121                if ( $i == 0 ) {
122                    $fileDBkey = $title->getDBkey();
123                } else {
124                    $exceptions[$title->getNamespace()][$title->getDBkey()] = true;
125                }
126            }
127
128            if ( $fileDBkey !== null ) {
129                $ret[$fileDBkey] = $exceptions;
130            }
131        }
132        return $ret;
133    }
134}
135
136/** @deprecated class alias since 1.40 */
137class_alias( BadFileLookup::class, 'MediaWiki\\BadFileLookup' );