Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
88.01% |
257 / 292 |
|
41.67% |
5 / 12 |
CRAP | |
0.00% |
0 / 1 |
SearchOptions | |
88.01% |
257 / 292 |
|
41.67% |
5 / 12 |
46.18 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getInstanceFromContext | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
getSearchOptions | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getOptions | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
getImageSizes | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
3 | |||
getMimeTypes | |
100.00% |
104 / 104 |
|
100.00% |
1 / 1 |
8 | |||
getAssessments | |
93.10% |
27 / 29 |
|
0.00% |
0 / 1 |
8.02 | |||
getSorts | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
getLicenseGroups | |
92.31% |
24 / 26 |
|
0.00% |
0 / 1 |
7.02 | |||
getNamespaces | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
3 | |||
getNamespaceGroups | |
97.37% |
37 / 38 |
|
0.00% |
0 / 1 |
2 | |||
getNamespaceIdsFromInput | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\MediaSearch; |
4 | |
5 | use InvalidArgumentException; |
6 | use MediaWiki\Config\Config; |
7 | use MediaWiki\Config\ConfigException; |
8 | use MediaWiki\MediaWikiServices; |
9 | use MessageLocalizer; |
10 | use Wikibase\Search\Elastic\Query\HasLicenseFeature; |
11 | |
12 | /** |
13 | * @license GPL-2.0-or-later |
14 | */ |
15 | class SearchOptions { |
16 | |
17 | public const TYPE_IMAGE = 'image'; |
18 | public const TYPE_AUDIO = 'audio'; |
19 | public const TYPE_VIDEO = 'video'; |
20 | public const TYPE_PAGE = 'page'; |
21 | public const TYPE_OTHER = 'other'; |
22 | |
23 | public const ALL_TYPES = [ |
24 | self::TYPE_IMAGE, |
25 | self::TYPE_AUDIO, |
26 | self::TYPE_VIDEO, |
27 | self::TYPE_PAGE, |
28 | self::TYPE_OTHER, |
29 | ]; |
30 | |
31 | public const FILTER_MIME = 'filemime'; |
32 | public const FILTER_SIZE = 'fileres'; |
33 | public const FILTER_ASSESSMENT = 'assessment'; |
34 | public const FILTER_LICENSE = 'haslicense'; |
35 | public const FILTER_SORT = 'sort'; |
36 | public const FILTER_NAMESPACE = 'namespace'; |
37 | |
38 | public const ALL_FILTERS = [ |
39 | self::FILTER_MIME, |
40 | self::FILTER_SIZE, |
41 | self::FILTER_ASSESSMENT, |
42 | self::FILTER_LICENSE, |
43 | self::FILTER_SORT, |
44 | self::FILTER_NAMESPACE, |
45 | ]; |
46 | |
47 | public const NAMESPACES_ALL = 'all'; |
48 | public const NAMESPACES_ALL_INCL_FILE = 'all_incl_file'; |
49 | public const NAMESPACES_DISCUSSION = 'discussion'; |
50 | public const NAMESPACES_HELP = 'help'; |
51 | public const NAMESPACES_CUSTOM = 'custom'; |
52 | |
53 | public const NAMESPACE_GROUPS = [ |
54 | self::NAMESPACES_ALL, |
55 | self::NAMESPACES_ALL_INCL_FILE, |
56 | self::NAMESPACES_DISCUSSION, |
57 | self::NAMESPACES_HELP, |
58 | self::NAMESPACES_CUSTOM |
59 | ]; |
60 | |
61 | /** @var MessageLocalizer */ |
62 | private $context; |
63 | |
64 | /** @var Config|null */ |
65 | private $mainConfig; |
66 | |
67 | /** @var Config|null */ |
68 | private $searchConfig; |
69 | |
70 | /** |
71 | * @param MessageLocalizer $context |
72 | * @param Config|null $mainConfig |
73 | * @param Config|null $searchConfig |
74 | */ |
75 | public function __construct( |
76 | MessageLocalizer $context, |
77 | Config $mainConfig = null, |
78 | Config $searchConfig = null |
79 | ) { |
80 | $this->context = $context; |
81 | $this->mainConfig = $mainConfig; |
82 | $this->searchConfig = $searchConfig; |
83 | } |
84 | |
85 | /** |
86 | * @param MessageLocalizer $context |
87 | * @return SearchOptions |
88 | */ |
89 | public static function getInstanceFromContext( MessageLocalizer $context ) { |
90 | $configFactory = MediaWikiServices::getInstance()->getConfigFactory(); |
91 | |
92 | try { |
93 | $mainConfig = $configFactory->makeConfig( 'main' ); |
94 | } catch ( ConfigException $e ) { |
95 | $mainConfig = null; |
96 | } |
97 | |
98 | try { |
99 | $searchConfig = $configFactory->makeConfig( 'WikibaseCirrusSearch' ); |
100 | } catch ( ConfigException $e ) { |
101 | $searchConfig = null; |
102 | } |
103 | |
104 | return new static( $context, $mainConfig, $searchConfig ); |
105 | } |
106 | |
107 | /** |
108 | * @param MessageLocalizer $context |
109 | * @return array |
110 | */ |
111 | public static function getSearchOptions( MessageLocalizer $context ): array { |
112 | $instance = static::getInstanceFromContext( $context ); |
113 | return $instance->getOptions(); |
114 | } |
115 | |
116 | /** |
117 | * Generate an associative array that combines all options for filter, |
118 | * sort, and licenses for all media types, with labels in the appropriate |
119 | * language. |
120 | * |
121 | * @return array |
122 | */ |
123 | public function getOptions(): array { |
124 | $searchOptions = []; |
125 | // Some options are only present for certain media types. |
126 | // The methods which generate type-specific options take a mediatype |
127 | // argument and will return false if the given type does not support the |
128 | // options in question. |
129 | |
130 | // The $context object must be passed down to the various helper methods |
131 | // because getSearchOptions can be called both as a ResourceLoader callback |
132 | // as well as during SpecialMediaSearch->execute(); we need to make sure |
133 | // the messages can be internationalized in the same way regardless |
134 | foreach ( static::ALL_TYPES as $type ) { |
135 | $searchOptions[ $type ] = array_filter( [ |
136 | static::FILTER_LICENSE => $this->getLicenseGroups( $type ), |
137 | static::FILTER_MIME => $this->getMimeTypes( $type ), |
138 | static::FILTER_SIZE => $this->getImageSizes( $type ), |
139 | static::FILTER_ASSESSMENT => $this->getAssessments( $type ), |
140 | static::FILTER_NAMESPACE => $this->getNamespaces( $type ), |
141 | static::FILTER_SORT => $this->getSorts( $type ) |
142 | ] ); |
143 | } |
144 | |
145 | return $searchOptions; |
146 | } |
147 | |
148 | /** |
149 | * Get the size options. Only supported by "image" type. |
150 | * |
151 | * @param string $type |
152 | * @return array |
153 | * @throws InvalidArgumentException |
154 | */ |
155 | public function getImageSizes( string $type ): array { |
156 | if ( !in_array( $type, static::ALL_TYPES, true ) ) { |
157 | throw new InvalidArgumentException( "$type is not a valid type" ); |
158 | } |
159 | |
160 | if ( $type === static::TYPE_IMAGE ) { |
161 | return [ 'items' => [ |
162 | [ |
163 | 'label' => $this->context->msg( 'mediasearch-filter-size-unset' )->text(), |
164 | 'value' => '' |
165 | ], |
166 | [ |
167 | 'label' => $this->context->msg( 'mediasearch-filter-size-small' )->text(), |
168 | 'value' => '<500' |
169 | ], |
170 | [ |
171 | // phpcs:ignore Generic.Files.LineLength.TooLong |
172 | 'label' => $this->context->msg( 'mediasearch-filter-size-medium' )->text(), |
173 | 'value' => '500,1000' |
174 | ], |
175 | [ |
176 | 'label' => $this->context->msg( 'mediasearch-filter-size-large' )->text(), |
177 | 'value' => '>1000' |
178 | ], |
179 | ] ]; |
180 | } else { |
181 | return []; |
182 | } |
183 | } |
184 | |
185 | /** |
186 | * Get the mimetype options for a given mediatype. All types except "page" |
187 | * support this option. |
188 | * |
189 | * @param string $type |
190 | * @return array |
191 | */ |
192 | public function getMimeTypes( string $type ): array { |
193 | if ( !in_array( $type, static::ALL_TYPES, true ) ) { |
194 | throw new InvalidArgumentException( "$type is not a valid type" ); |
195 | } |
196 | |
197 | switch ( $type ) { |
198 | case static::TYPE_IMAGE: |
199 | return [ 'items' => [ |
200 | [ |
201 | // phpcs:ignore Generic.Files.LineLength.TooLong |
202 | 'label' => $this->context->msg( 'mediasearch-filter-file-type-unset' )->text(), |
203 | 'value' => '' |
204 | ], |
205 | [ |
206 | 'label' => 'tiff', |
207 | 'value' => 'tiff' |
208 | ], |
209 | [ |
210 | 'label' => 'png', |
211 | 'value' => 'png' |
212 | ], |
213 | [ |
214 | 'label' => 'gif', |
215 | 'value' => 'gif' |
216 | ], |
217 | [ |
218 | 'label' => 'jpg', |
219 | 'value' => 'jpeg' |
220 | ], |
221 | [ |
222 | 'label' => 'webp', |
223 | 'value' => 'webp' |
224 | ], |
225 | [ |
226 | 'label' => 'xcf', |
227 | 'value' => 'xcf' |
228 | ], |
229 | [ |
230 | 'label' => 'svg', |
231 | 'value' => 'svg' |
232 | ] |
233 | ] ]; |
234 | case static::TYPE_AUDIO: |
235 | return [ 'items' => [ |
236 | [ |
237 | // phpcs:ignore Generic.Files.LineLength.TooLong |
238 | 'label' => $this->context->msg( 'mediasearch-filter-file-type-unset' )->text(), |
239 | 'value' => '' |
240 | ], |
241 | [ |
242 | 'label' => 'mid', |
243 | 'value' => 'midi' |
244 | ], |
245 | [ |
246 | 'label' => 'flac', |
247 | 'value' => 'flac' |
248 | ], |
249 | [ |
250 | 'label' => 'wav', |
251 | 'value' => 'wav' |
252 | ], |
253 | [ |
254 | 'label' => 'mp3', |
255 | 'value' => 'mpeg' |
256 | ], |
257 | [ |
258 | 'label' => 'ogg', |
259 | 'value' => 'ogg' |
260 | ] |
261 | ] ]; |
262 | case static::TYPE_VIDEO: |
263 | return [ 'items' => [ |
264 | [ |
265 | // phpcs:ignore Generic.Files.LineLength.TooLong |
266 | 'label' => $this->context->msg( 'mediasearch-filter-file-type-unset' )->text(), |
267 | 'value' => '' |
268 | ], |
269 | [ |
270 | 'label' => 'webm', |
271 | 'value' => 'webm' |
272 | ], |
273 | [ |
274 | 'label' => 'mpg', |
275 | 'value' => 'mpeg' |
276 | ], |
277 | [ |
278 | 'label' => 'ogg', |
279 | 'value' => 'ogg' |
280 | ] |
281 | ] ]; |
282 | case static::TYPE_OTHER: |
283 | return [ 'items' => [ |
284 | [ |
285 | // phpcs:ignore Generic.Files.LineLength.TooLong |
286 | 'label' => $this->context->msg( 'mediasearch-filter-file-type-unset' )->text(), |
287 | 'value' => '' |
288 | ], |
289 | [ |
290 | 'label' => 'pdf', |
291 | 'value' => 'pdf' |
292 | ], |
293 | [ |
294 | 'label' => 'djvu', |
295 | 'value' => 'djvu' |
296 | ], |
297 | [ |
298 | 'label' => 'stl', |
299 | 'value' => 'sla' |
300 | ] |
301 | ] ]; |
302 | case static::TYPE_PAGE: |
303 | default: |
304 | return []; |
305 | } |
306 | } |
307 | |
308 | /** |
309 | * Get the assessment options (only applicable for image and video types) based on configuration |
310 | * |
311 | * @param string $type |
312 | * @return array [ 'items' => [], 'data' => [] ] |
313 | */ |
314 | public function getAssessments( string $type ): array { |
315 | $assessmentOptions = []; |
316 | $assessmentData = []; |
317 | |
318 | if ( !in_array( $type, static::ALL_TYPES, true ) ) { |
319 | throw new InvalidArgumentException( "$type is not a valid type" ); |
320 | } |
321 | |
322 | // Bail early and return empty if we can't access config vars for some reason; |
323 | // Feature will simply not be enabled in this case |
324 | if ( $this->mainConfig ) { |
325 | $assessmentConfig = $this->mainConfig->get( 'MediaSearchAssessmentFilters' ); |
326 | } else { |
327 | return []; |
328 | } |
329 | |
330 | // If we have the appropriate config data and we are on an image or video tab, |
331 | // build a data structure for the pre-definied assessment types and |
332 | // labels, along with their corresponding wikidata statements |
333 | if ( $assessmentConfig && ( $type === static::TYPE_IMAGE || $type === self::TYPE_VIDEO ) ) { |
334 | // Start with the default label |
335 | $assessmentOptions[] = [ |
336 | 'label' => $this->context->msg( 'mediasearch-filter-assessment-unset' )->text(), |
337 | 'value' => '' |
338 | ]; |
339 | |
340 | // Options/labels |
341 | foreach ( $assessmentConfig as $key => $statement ) { |
342 | $assessmentOptions[] = [ |
343 | // All i18n labels are assumed to be prefixed with mediasearch-filter-assessment- |
344 | 'label' => $this->context->msg( 'mediasearch-filter-assessment-' . $key )->text(), |
345 | 'value' => $key |
346 | ]; |
347 | } |
348 | |
349 | // WB statements |
350 | foreach ( $assessmentConfig as $key => $statement ) { |
351 | $assessmentData[] = [ |
352 | 'value' => $key, |
353 | 'statement' => 'haswbstatement:' . $statement |
354 | ]; |
355 | } |
356 | |
357 | return [ |
358 | 'items' => $assessmentOptions, |
359 | 'data' => [ |
360 | 'statementData' => $assessmentData |
361 | ] |
362 | ]; |
363 | } else { |
364 | return []; |
365 | } |
366 | } |
367 | |
368 | /** |
369 | * Get the sort options for each media type. Supported by all types. |
370 | * |
371 | * @param string $type |
372 | * @return array |
373 | */ |
374 | public function getSorts( string $type ): array { |
375 | if ( !in_array( $type, static::ALL_TYPES, true ) ) { |
376 | throw new InvalidArgumentException( "$type is not a valid type" ); |
377 | } |
378 | |
379 | return [ 'items' => [ |
380 | [ |
381 | 'label' => $this->context->msg( 'mediasearch-filter-sort-default' )->text(), |
382 | 'value' => '' |
383 | ], |
384 | [ |
385 | 'label' => $this->context->msg( 'mediasearch-filter-sort-recency' )->text(), |
386 | 'value' => 'recency' |
387 | ] |
388 | ] ]; |
389 | } |
390 | |
391 | /** |
392 | * Parse the on-wiki license mapping page (if one exists) and return an |
393 | * array of arrays, structured like: |
394 | * [ [ 'label' => 'some-label-text', 'value' => 'cc-by' ] ] |
395 | * With one child array for each license group defined in the mapping. |
396 | * |
397 | * Supported by all types except "page" type. |
398 | * |
399 | * @param string $type |
400 | * @return array |
401 | */ |
402 | public function getLicenseGroups( string $type ): array { |
403 | if ( !in_array( $type, static::ALL_TYPES, true ) ) { |
404 | throw new InvalidArgumentException( "$type is not a valid type" ); |
405 | } |
406 | |
407 | if ( |
408 | $this->searchConfig === null || |
409 | !method_exists( HasLicenseFeature::class, 'getConfiguredLicenseMap' ) |
410 | ) { |
411 | // This feature requires a dependency: not installed = feature not supported |
412 | return []; |
413 | } |
414 | |
415 | // Category & page searches do not have license filters |
416 | if ( $type === static::TYPE_PAGE ) { |
417 | return []; |
418 | } |
419 | |
420 | $licenseMappings = HasLicenseFeature::getConfiguredLicenseMap( $this->searchConfig ); |
421 | if ( !$licenseMappings ) { |
422 | return []; |
423 | } |
424 | |
425 | $licenseGroups = []; |
426 | |
427 | // Add the default label |
428 | $licenseGroups[] = [ |
429 | 'label' => $this->context->msg( 'mediasearch-filter-license-any' )->text(), |
430 | 'value' => '' |
431 | ]; |
432 | |
433 | foreach ( array_keys( $licenseMappings ) as $group ) { |
434 | $msgKey = 'mediasearch-filter-license-' . $group; |
435 | |
436 | $licenseGroups[] = [ |
437 | 'label' => $this->context->msg( $msgKey )->text(), |
438 | 'value' => $group |
439 | ]; |
440 | } |
441 | |
442 | // Add the "other" label |
443 | $licenseGroups[] = [ |
444 | 'label' => $this->context->msg( 'mediasearch-filter-license-other' )->text(), |
445 | 'value' => 'other' |
446 | ]; |
447 | |
448 | return [ 'items' => $licenseGroups ]; |
449 | } |
450 | |
451 | /** |
452 | * Get the namespace options. Only supported by "page" type. |
453 | * |
454 | * @param string $type |
455 | * @return array |
456 | */ |
457 | public function getNamespaces( string $type ): array { |
458 | if ( !in_array( $type, static::ALL_TYPES, true ) ) { |
459 | throw new InvalidArgumentException( "$type is not a valid type" ); |
460 | } |
461 | |
462 | if ( $type === static::TYPE_PAGE ) { |
463 | $filterItems = [ |
464 | [ |
465 | // phpcs:ignore Generic.Files.LineLength.TooLong |
466 | 'label' => $this->context->msg( 'mediasearch-filter-namespace-all' )->text(), |
467 | 'value' => static::NAMESPACES_ALL |
468 | ], |
469 | [ |
470 | // phpcs:ignore Generic.Files.LineLength.TooLong |
471 | 'label' => $this->context->msg( 'mediasearch-filter-namespace-discussion' )->text(), |
472 | 'value' => static::NAMESPACES_DISCUSSION |
473 | ], |
474 | [ |
475 | // phpcs:ignore Generic.Files.LineLength.TooLong |
476 | 'label' => $this->context->msg( 'mediasearch-filter-namespace-help' )->text(), |
477 | 'value' => static::NAMESPACES_HELP |
478 | ], |
479 | [ |
480 | // phpcs:ignore Generic.Files.LineLength.TooLong |
481 | 'label' => $this->context->msg( 'mediasearch-filter-namespace-custom' )->text(), |
482 | 'value' => static::NAMESPACES_CUSTOM |
483 | ], |
484 | ]; |
485 | |
486 | return [ |
487 | 'items' => $filterItems, |
488 | 'data' => [ |
489 | 'namespaceGroups' => $this->getNamespaceGroups() |
490 | ], |
491 | ]; |
492 | } else { |
493 | return []; |
494 | } |
495 | } |
496 | |
497 | /** |
498 | * Get namespace data for the different namespace filter groups. |
499 | * |
500 | * @return array |
501 | */ |
502 | public function getNamespaceGroups(): array { |
503 | $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); |
504 | $allNamespaces = $namespaceInfo->getCanonicalNamespaces(); |
505 | |
506 | // $wgNamespacesToBeSearchedDefault is an array with namespace ids as keys, and 1|0 to |
507 | // indicate if the namespace should be searched by default |
508 | if ( $this->mainConfig->get( 'NamespacesToBeSearchedDefault' ) ) { |
509 | $defaultSearchNamespaces = array_filter( |
510 | $this->mainConfig->get( 'NamespacesToBeSearchedDefault' ) |
511 | ); |
512 | } else { |
513 | $defaultSearchNamespaces = [ 0 => 1 ]; |
514 | } |
515 | |
516 | $realNamespaces = array_filter( |
517 | $allNamespaces, |
518 | static function ( $namespaceId ) { |
519 | // Exclude virtual namespaces |
520 | return $namespaceId >= 0; |
521 | }, |
522 | ARRAY_FILTER_USE_KEY |
523 | ); |
524 | $nonFileNamespaces = array_filter( |
525 | $realNamespaces, |
526 | static function ( $namespaceId ) { |
527 | return $namespaceId !== NS_FILE; |
528 | }, |
529 | ARRAY_FILTER_USE_KEY |
530 | ); |
531 | |
532 | $customNamespaces = array_intersect_key( $nonFileNamespaces, $defaultSearchNamespaces ); |
533 | |
534 | $talkNamespaces = array_combine( |
535 | $namespaceInfo->getTalkNamespaces(), |
536 | array_map( static function ( $namespaceId ) use ( $allNamespaces ) { |
537 | return $allNamespaces[$namespaceId]; |
538 | }, $namespaceInfo->getTalkNamespaces() ) |
539 | ); |
540 | |
541 | return [ |
542 | static::NAMESPACES_ALL_INCL_FILE => $realNamespaces, |
543 | static::NAMESPACES_ALL => $nonFileNamespaces, |
544 | static::NAMESPACES_DISCUSSION => $talkNamespaces, |
545 | static::NAMESPACES_HELP => [ |
546 | NS_PROJECT => $namespaceInfo->getCanonicalName( NS_PROJECT ), |
547 | NS_HELP => $namespaceInfo->getCanonicalName( NS_HELP ), |
548 | ], |
549 | static::NAMESPACES_CUSTOM => $customNamespaces |
550 | ]; |
551 | } |
552 | |
553 | /** |
554 | * @param string $input |
555 | * @return int[] |
556 | * @throws InvalidNamespaceGroupException |
557 | */ |
558 | public function getNamespaceIdsFromInput( $input ): array { |
559 | $namespaceGroups = $this->getNamespaceGroups(); |
560 | |
561 | if ( isset( $namespaceGroups[$input] ) ) { |
562 | // namespace is one of the predefined namespace categories |
563 | // for which we have a list of namespace ids handy |
564 | return array_keys( $namespaceGroups[$input] ); |
565 | } |
566 | |
567 | $inputIds = explode( '|', $input ); |
568 | $allowedIds = array_keys( $namespaceGroups[ static::NAMESPACES_ALL_INCL_FILE ] ); |
569 | $verifiedIds = array_intersect( $allowedIds, $inputIds ); |
570 | if ( count( $verifiedIds ) === count( $inputIds ) ) { |
571 | return $verifiedIds; |
572 | } |
573 | |
574 | throw new InvalidNamespaceGroupException( "$input is no valid namespace input" ); |
575 | } |
576 | } |