Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
27.59% |
24 / 87 |
|
16.67% |
1 / 6 |
CRAP | |
0.00% |
0 / 1 |
HookHandler | |
27.59% |
24 / 87 |
|
16.67% |
1 / 6 |
205.78 | |
0.00% |
0 / 1 |
onGetExtendedMetadata | |
95.65% |
22 / 23 |
|
0.00% |
0 / 1 |
5 | |||
onValidateExtendedMetadataCache | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onContentAlterParserOutput | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
90 | |||
getDataCollector | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
onSkinAfterBottomScripts | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
20 | |||
doSkinAfterBottomScripts | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace CommonsMetadata; |
4 | |
5 | use CommonsMetadata\Hooks\SkinAfterBottomScriptsHandler; |
6 | use File; |
7 | use FormatMetadata; |
8 | use LocalRepo; |
9 | use MediaWiki\Content\Content; |
10 | use MediaWiki\Content\Hook\ContentAlterParserOutputHook; |
11 | use MediaWiki\Context\DerivativeContext; |
12 | use MediaWiki\Context\IContextSource; |
13 | use MediaWiki\Hook\GetExtendedMetadataHook; |
14 | use MediaWiki\Hook\SkinAfterBottomScriptsHook; |
15 | use MediaWiki\Hook\ValidateExtendedMetadataCacheHook; |
16 | use MediaWiki\MediaWikiServices; |
17 | use MediaWiki\Parser\ParserOutput; |
18 | use MediaWiki\Title\Title; |
19 | use Skin; |
20 | use Wikimedia\Bcp47Code\Bcp47Code; |
21 | |
22 | /** |
23 | * Hook handler |
24 | */ |
25 | class HookHandler implements |
26 | GetExtendedMetadataHook, |
27 | ValidateExtendedMetadataCacheHook, |
28 | ContentAlterParserOutputHook, |
29 | SkinAfterBottomScriptsHook |
30 | { |
31 | /** |
32 | * Metadata version. When getting metadata of a remote file via the API, sometimes |
33 | * we get the data generated by a CommonsMetadata extension installed at the remote, |
34 | * as well. We use this version number to keep track of whether that data is different |
35 | * from what would be generated here. |
36 | * @var float |
37 | */ |
38 | private const VERSION = 1.2; |
39 | |
40 | /** |
41 | * Hook handler for extended metadata |
42 | * |
43 | * @param array &$combinedMeta Metadata so far |
44 | * @param File $file The file object in question |
45 | * @param IContextSource $context Context. Used to select language |
46 | * @param bool $singleLang Get only target language, or all translations |
47 | * @param int &$maxCache How many seconds to cache the result |
48 | */ |
49 | public function onGetExtendedMetadata( |
50 | &$combinedMeta, $file, $context, $singleLang, &$maxCache |
51 | ) { |
52 | global $wgCommonsMetadataForceRecalculate; |
53 | |
54 | if ( |
55 | isset( $combinedMeta['CommonsMetadataExtension']['value'] ) |
56 | && $combinedMeta['CommonsMetadataExtension']['value'] == self::VERSION |
57 | && !$wgCommonsMetadataForceRecalculate |
58 | ) { |
59 | // This is a file from a remote API repo, and CommonsMetadata is installed on |
60 | // the remote as well, and generates the same metadata format. We have nothing to do. |
61 | return; |
62 | } else { |
63 | $combinedMeta['CommonsMetadataExtension'] = [ |
64 | 'value' => self::VERSION, |
65 | 'source' => 'extension', |
66 | ]; |
67 | } |
68 | |
69 | $lang = $context->getLanguage(); |
70 | |
71 | $templateParser = new TemplateParser(); |
72 | $templateParser->setMultiLanguage( !$singleLang ); |
73 | $fallbacks = MediaWikiServices::getInstance()->getLanguageFallback()->getAll( $lang->getCode() ); |
74 | array_unshift( $fallbacks, $lang->getCode() ); |
75 | $templateParser->setPriorityLanguages( $fallbacks ); |
76 | $templateParser->setArtistCreditSeparator( $context->msg( 'commonsmetadata-artistcredit-separator' )->text() ); |
77 | |
78 | $dataCollector = new DataCollector(); |
79 | $dataCollector->setLanguage( $lang ); |
80 | $dataCollector->setMultiLang( !$singleLang ); |
81 | $dataCollector->setTemplateParser( $templateParser ); |
82 | $dataCollector->setLicenseParser( new LicenseParser() ); |
83 | |
84 | $dataCollector->collect( $combinedMeta, $file ); |
85 | |
86 | if ( !$file->getDescriptionTouched() ) { |
87 | // Not all files provide the last update time of the description. |
88 | // If that's the case, just cache blindly for a shorter period. |
89 | $maxCache = 60 * 60 * 12; |
90 | } |
91 | } |
92 | |
93 | /** |
94 | * Hook to check if cache is stale |
95 | * |
96 | * @param string $timestamp Timestamp of when cache taken |
97 | * @param File $file The file metadata is for |
98 | * @return bool Is metadata still valid |
99 | */ |
100 | public function onValidateExtendedMetadataCache( $timestamp, $file ) { |
101 | return // use cached value if... |
102 | // we don't know when the file was last updated |
103 | !$file->getDescriptionTouched() |
104 | // or last update was before we cached it |
105 | || wfTimestamp( TS_UNIX, $file->getDescriptionTouched() ) |
106 | <= wfTimestamp( TS_UNIX, $timestamp ); |
107 | } |
108 | |
109 | /** |
110 | * Check HTML output of a file page to see if it has all the basic metadata, and |
111 | * add tracking categories if it does not. |
112 | * @param Content $content |
113 | * @param Title $title |
114 | * @param ParserOutput $parserOutput |
115 | */ |
116 | public function onContentAlterParserOutput( |
117 | $content, $title, $parserOutput |
118 | ) { |
119 | global $wgCommonsMetadataSetTrackingCategories; |
120 | |
121 | if ( |
122 | !$wgCommonsMetadataSetTrackingCategories |
123 | || !$title->inNamespace( NS_FILE ) |
124 | || !$parserOutput->hasText() |
125 | || $content->getModel() !== CONTENT_MODEL_WIKITEXT |
126 | ) { |
127 | return; |
128 | } |
129 | |
130 | /* |
131 | * We also need to check if the file can be found. This is pretty straightforward, except |
132 | * for when a file gets moved: the old & new file details are cached, and cache is purged |
133 | * later on, in a DeferredUpdate. |
134 | * We could just `$repo->findFile( $title, [ 'ignoreRedirect' => true, 'latest' => true ] )` |
135 | * to force it to always check the database, but apart from file moves, the data in cache |
136 | * (if any) is usually just fine. |
137 | * Instead, we'll: |
138 | * * first test if `$title->isRedirect()`, to weed out the old (now renamed) title |
139 | * * attempt to fetch from cache, which should usually be fine |
140 | * * then fallback to DB, for files that have just been renamed |
141 | */ |
142 | $services = MediaWikiServices::getInstance(); |
143 | $trackingCategories = $services->getTrackingCategories(); |
144 | $repo = $services->getRepoGroup()->getLocalRepo(); |
145 | if ( $title->isRedirect() ) { |
146 | return; |
147 | } |
148 | $file = $repo->findFile( $title, [ 'ignoreRedirect' => true ] ); |
149 | if ( $file === false ) { |
150 | $file = $repo->findFile( $title, [ 'ignoreRedirect' => true, 'latest' => true ] ); |
151 | if ( $file === false ) { |
152 | return; |
153 | } |
154 | } |
155 | |
156 | $langCode = $parserOutput->getLanguage() ?? $services->getContentLanguage(); |
157 | $dataCollector = self::getDataCollector( $langCode, true ); |
158 | |
159 | $categoryKeys = $dataCollector->verifyAttributionMetadata( $parserOutput, $file ); |
160 | foreach ( $categoryKeys as $key ) { |
161 | $trackingCategories->addTrackingCategory( |
162 | $parserOutput, |
163 | 'commonsmetadata-trackingcategory-' . $key, |
164 | $title |
165 | ); |
166 | } |
167 | } |
168 | |
169 | /** |
170 | * @param Bcp47Code $langCode |
171 | * @param bool $singleLang |
172 | * @return DataCollector |
173 | */ |
174 | private static function getDataCollector( Bcp47Code $langCode, $singleLang ) { |
175 | $templateParser = new TemplateParser(); |
176 | $templateParser->setMultiLanguage( !$singleLang ); |
177 | $fallbacks = MediaWikiServices::getInstance()->getLanguageFallback()->getAll( $langCode->toBcp47Code() ); |
178 | array_unshift( $fallbacks, $langCode->toBcp47Code() ); |
179 | $templateParser->setPriorityLanguages( $fallbacks ); |
180 | $templateParser->setArtistCreditSeparator( wfMessage( 'commonsmetadata-artistcredit-separator' )->text() ); |
181 | |
182 | $lang = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $langCode ); |
183 | |
184 | $dataCollector = new DataCollector(); |
185 | $dataCollector->setLanguage( $lang ); |
186 | $dataCollector->setMultiLang( !$singleLang ); |
187 | $dataCollector->setTemplateParser( $templateParser ); |
188 | $dataCollector->setLicenseParser( new LicenseParser() ); |
189 | |
190 | return $dataCollector; |
191 | } |
192 | |
193 | /** |
194 | * Injects an inline JSON-LD script schema with image license info. |
195 | * |
196 | * See https://phabricator.wikimedia.org/T254039. This only adds the script |
197 | * to File pages. |
198 | * |
199 | * @param Skin $skin |
200 | * @param string &$html |
201 | */ |
202 | public function onSkinAfterBottomScripts( $skin, &$html ) { |
203 | $title = $skin->getOutput()->getTitle(); |
204 | $isFilePage = $title->inNamespace( NS_FILE ); |
205 | |
206 | if ( |
207 | !$title || |
208 | !$title->exists() || |
209 | !$isFilePage |
210 | ) { |
211 | return; |
212 | } |
213 | |
214 | $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo(); |
215 | |
216 | // Get and prepare FormatMetadata object. |
217 | $format = new FormatMetadata; |
218 | $context = new DerivativeContext( $format->getContext() ); |
219 | // Language doesn't matter so just use en to improve performance. |
220 | $format->setSingleLanguage( true ); |
221 | $context->setLanguage( 'en' ); |
222 | $format->setContext( $context ); |
223 | |
224 | // Get URL for public domain page from config. |
225 | $config = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'CommonsMetadata' ); |
226 | $publicDomainPageUrl = $config->get( 'CommonsMetadataPublicDomainPageUrl' ); |
227 | |
228 | $handler = new SkinAfterBottomScriptsHandler( $format, $publicDomainPageUrl ); |
229 | |
230 | $html .= $this->doSkinAfterBottomScripts( |
231 | $localRepo, |
232 | $handler, |
233 | $title |
234 | ); |
235 | } |
236 | |
237 | /** |
238 | * Get schema script html (or empty string). |
239 | * |
240 | * @param LocalRepo $localRepo |
241 | * @param SkinAfterBottomScriptsHandler $handler |
242 | * @param Title $title |
243 | * @return string |
244 | */ |
245 | public function doSkinAfterBottomScripts( |
246 | LocalRepo $localRepo, |
247 | SkinAfterBottomScriptsHandler $handler, |
248 | Title $title |
249 | ) { |
250 | $file = $localRepo->newFile( $title ); |
251 | return $handler->getSchemaElement( $title, $file ); |
252 | } |
253 | } |