Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.14% covered (success)
97.14%
68 / 70
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
97.14% covered (success)
97.14%
68 / 70
75.00% covered (warning)
75.00%
6 / 8
19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 onSpecialPageBeforeExecute
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
5
 getJsConfigVars
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 redirectToNamespacedRequest
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getDefaultNamespaces
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 isNamespacedSearch
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 onSpecialSearchResultsPrepend
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 onGetPreferences
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace AdvancedSearch;
4
5use ExtensionRegistry;
6use MediaWiki\Config\Config;
7use MediaWiki\Hook\SpecialSearchResultsPrependHook;
8use MediaWiki\Html\Html;
9use MediaWiki\Languages\LanguageNameUtils;
10use MediaWiki\Output\OutputPage;
11use MediaWiki\Preferences\Hook\GetPreferencesHook;
12use MediaWiki\Request\WebRequest;
13use MediaWiki\SpecialPage\Hook\SpecialPageBeforeExecuteHook;
14use MediaWiki\SpecialPage\SpecialPage;
15use MediaWiki\Specials\SpecialSearch;
16use MediaWiki\User\Options\UserOptionsLookup;
17use MediaWiki\User\User;
18use MediaWiki\User\UserIdentity;
19use MessageLocalizer;
20use MimeAnalyzer;
21use SearchEngineConfig;
22
23/**
24 * @license GPL-2.0-or-later
25 */
26class Hooks implements
27    SpecialPageBeforeExecuteHook,
28    GetPreferencesHook,
29    SpecialSearchResultsPrependHook
30{
31
32    private UserOptionsLookup $userOptionsLookup;
33    private LanguageNameUtils $languageNameUtils;
34    private SearchEngineConfig $searchEngineConfig;
35    private MimeAnalyzer $mimeAnalyzer;
36
37    public function __construct(
38        UserOptionsLookup $userOptionsLookup,
39        LanguageNameUtils $languageNameUtils,
40        SearchEngineConfig $searchEngineConfig,
41        MimeAnalyzer $mimeAnalyzer
42    ) {
43        $this->userOptionsLookup = $userOptionsLookup;
44        $this->languageNameUtils = $languageNameUtils;
45        $this->searchEngineConfig = $searchEngineConfig;
46        $this->mimeAnalyzer = $mimeAnalyzer;
47    }
48
49    /**
50     * @see https://www.mediawiki.org/wiki/Manual:Hooks/SpecialPageBeforeExecute
51     *
52     * @param SpecialPage $special
53     * @param string|null $subpage
54     * @return false|void false to abort the execution of the special page, "void" otherwise
55     */
56    public function onSpecialPageBeforeExecute( $special, $subpage ) {
57        if ( $special->getName() !== 'Search' ) {
58            return;
59        }
60
61        $user = $special->getUser();
62        $outputPage = $special->getOutput();
63
64        /**
65         * If the user is logged in and has explicitly requested to disable the extension, don't load.
66         * Ensure namespaces are always part of search URLs
67         */
68        if ( $user->isNamed() &&
69            $this->userOptionsLookup->getBoolOption( $user, 'advancedsearch-disable' )
70        ) {
71            return;
72        }
73
74        /**
75         * Ensure the current URL is specifying the namespaces which are to be used
76         */
77        $redirect = $this->redirectToNamespacedRequest( $special );
78        if ( $redirect !== null ) {
79            $outputPage->redirect( $redirect );
80            // Abort execution of the SpecialPage by returning false since we are redirecting
81            return false;
82        }
83
84        $outputPage->addModules( [
85            'ext.advancedSearch.init',
86            'ext.advancedSearch.searchtoken',
87        ] );
88
89        $outputPage->addModuleStyles( 'ext.advancedSearch.initialstyles' );
90
91        $outputPage->addJsConfigVars( $this->getJsConfigVars(
92            $special->getContext(),
93            $special->getConfig(),
94            ExtensionRegistry::getInstance()
95        ) );
96    }
97
98    /**
99     * @param MessageLocalizer $context
100     * @param Config $config
101     * @param ExtensionRegistry $extensionRegistry
102     * @return array
103     */
104    private function getJsConfigVars(
105        MessageLocalizer $context,
106        Config $config,
107        ExtensionRegistry $extensionRegistry
108    ): array {
109        $vars = [
110            'advancedSearch.mimeTypes' =>
111                ( new MimeTypeConfigurator( $this->mimeAnalyzer ) )->getMimeTypes(
112                    $config->get( 'FileExtensions' )
113                ),
114            'advancedSearch.tooltips' => ( new TooltipGenerator( $context ) )->generateTooltips(),
115            'advancedSearch.namespacePresets' => $config->get( 'AdvancedSearchNamespacePresets' ),
116            'advancedSearch.deepcategoryEnabled' => $config->get( 'AdvancedSearchDeepcatEnabled' ),
117            'advancedSearch.searchableNamespaces' =>
118                SearchableNamespaceListBuilder::getCuratedNamespaces(
119                    $this->searchEngineConfig->searchableNamespaces()
120                ),
121        ];
122
123        if ( $extensionRegistry->isLoaded( 'Translate' ) ) {
124            $vars += [ 'advancedSearch.languages' =>
125                $this->languageNameUtils->getLanguageNames()
126            ];
127        }
128
129        return $vars;
130    }
131
132    /**
133     * If the request does not contain any namespaces, redirect to URL with user default namespaces
134     * @param SpecialPage $special
135     * @return string|null the URL to redirect to or null if not needed
136     */
137    private function redirectToNamespacedRequest( SpecialPage $special ): ?string {
138        if ( !self::isNamespacedSearch( $special->getRequest() ) ) {
139            $namespacedSearchUrl = $special->getRequest()->getFullRequestURL();
140            $queryParts = [];
141            foreach ( $this->getDefaultNamespaces( $special->getUser() ) as $ns ) {
142                $queryParts['ns' . $ns] = '1';
143            }
144            return wfAppendQuery( $namespacedSearchUrl, $queryParts );
145        }
146        return null;
147    }
148
149    /**
150     * Retrieves the default namespaces for the current user
151     *
152     * @param UserIdentity $user The user to lookup default namespaces for
153     * @return int[] List of namespaces to be searched by default
154     */
155    private function getDefaultNamespaces( UserIdentity $user ): array {
156        return $this->searchEngineConfig->userNamespaces( $user ) ?: $this->searchEngineConfig->defaultNamespaces();
157    }
158
159    /**
160     * Checks if there is a search request, and it already specifies namespaces.
161     * @param WebRequest $request
162     * @return bool
163     */
164    private static function isNamespacedSearch( WebRequest $request ): bool {
165        if ( $request->getRawVal( 'search', '' ) === '' ) {
166            return true;
167        }
168
169        foreach ( $request->getValueNames() as $requestKey ) {
170            if ( preg_match( '/^ns\d+$/', $requestKey ) ) {
171                return true;
172            }
173        }
174        return false;
175    }
176
177    /**
178     * @see https://www.mediawiki.org/wiki/Manual:Hooks/SpecialSearchResultsPrepend
179     *
180     * @param SpecialSearch $specialSearch
181     * @param OutputPage $output
182     * @param string $term
183     */
184    public function onSpecialSearchResultsPrepend( $specialSearch, $output, $term ) {
185        $output->addHTML(
186            Html::rawElement(
187                'div',
188                [ 'class' => 'mw-search-spinner' ],
189                Html::element( 'div', [ 'class' => 'mw-search-spinner-bounce' ] )
190            )
191        );
192    }
193
194    /**
195     * @param User $user
196     * @param array[] &$preferences
197     */
198    public function onGetPreferences( $user, &$preferences ) {
199        $preferences['advancedsearch-disable'] = [
200            'type' => 'toggle',
201            'label-message' => 'advancedsearch-preference-disable',
202            'section' => 'searchoptions/advancedsearch',
203            'help-message' => 'advancedsearch-preference-help',
204        ];
205    }
206}