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