Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.77% |
59 / 65 |
|
75.00% |
3 / 4 |
CRAP | |
0.00% |
0 / 1 |
OpenSearchDescriptionHandler | |
90.77% |
59 / 65 |
|
75.00% |
3 / 4 |
12.11 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
100.00% |
46 / 46 |
|
100.00% |
1 / 1 |
5 | |||
getContentType | |
45.45% |
5 / 11 |
|
0.00% |
0 / 1 |
9.06 | |||
getParamSettings | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | /** |
4 | * Copyright (C) 2011-2020 Wikimedia Foundation and others. |
5 | * |
6 | * This program is free software; you can redistribute it and/or modify |
7 | * it under the terms of the GNU General Public License as published by |
8 | * the Free Software Foundation; either version 2 of the License, or |
9 | * (at your option) any later version. |
10 | * |
11 | * This program is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | * GNU General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU General Public License along |
17 | * with this program; if not, write to the Free Software Foundation, Inc., |
18 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
19 | */ |
20 | |
21 | namespace MediaWiki\Rest\Handler; |
22 | |
23 | use ApiOpenSearch; |
24 | use MediaWiki\Config\Config; |
25 | use MediaWiki\HookContainer\HookRunner; |
26 | use MediaWiki\MainConfigNames; |
27 | use MediaWiki\MainConfigSchema; |
28 | use MediaWiki\Rest\Handler; |
29 | use MediaWiki\Rest\Response; |
30 | use MediaWiki\Rest\StringStream; |
31 | use MediaWiki\SpecialPage\SpecialPage; |
32 | use MediaWiki\Utils\UrlUtils; |
33 | use MediaWiki\Xml\Xml; |
34 | use Wikimedia\Http\HttpAcceptParser; |
35 | |
36 | /** |
37 | * Handler for generating an OpenSearch description document. |
38 | * In a nutshell, this tells browsers how and where |
39 | * to submit search queries to get a search results page back, |
40 | * as well as how to get typeahead suggestions (see ApiOpenSearch). |
41 | * |
42 | * This class handles the following routes: |
43 | * - /v1/search |
44 | * |
45 | * @see https://github.com/dewitt/opensearch |
46 | * @see https://www.opensearch.org |
47 | */ |
48 | class OpenSearchDescriptionHandler extends Handler { |
49 | |
50 | private UrlUtils $urlUtils; |
51 | |
52 | /** @see MainConfigSchema::Favicon */ |
53 | private string $favicon; |
54 | |
55 | /** @see MainConfigSchema::OpenSearchTemplates */ |
56 | private array $templates; |
57 | |
58 | public function __construct( Config $config, UrlUtils $urlUtils ) { |
59 | $this->favicon = $config->get( MainConfigNames::Favicon ); |
60 | $this->templates = $config->get( MainConfigNames::OpenSearchTemplates ); |
61 | $this->urlUtils = $urlUtils; |
62 | } |
63 | |
64 | public function execute(): Response { |
65 | $ctype = $this->getContentType(); |
66 | |
67 | $response = $this->getResponseFactory()->create(); |
68 | $response->setHeader( 'Content-type', $ctype ); |
69 | |
70 | // Set an Expires header so that CDN can cache it for a short time |
71 | // Short enough so that the sysadmin barely notices when $wgSitename is changed |
72 | $expiryTime = 600; # 10 minutes |
73 | $response->setHeader( 'Expires', gmdate( 'D, d M Y H:i:s', time() + $expiryTime ) . ' GMT' ); |
74 | $response->setHeader( 'Cache-control', 'max-age=600' ); |
75 | |
76 | $body = new StringStream(); |
77 | |
78 | $body->write( '<?xml version="1.0"?>' ); |
79 | $body->write( Xml::openElement( 'OpenSearchDescription', |
80 | [ |
81 | 'xmlns' => 'http://a9.com/-/spec/opensearch/1.1/', |
82 | 'xmlns:moz' => 'http://www.mozilla.org/2006/browser/search/' ] ) ); |
83 | |
84 | // The spec says the ShortName must be no longer than 16 characters, |
85 | // but 16 is *realllly* short. In practice, browsers don't appear to care |
86 | // when we give them a longer string, so we're no longer attempting to trim. |
87 | // |
88 | // Note: ShortName and the <link title=""> need to match; they are used as |
89 | // a key for identifying if the search engine has been added already, *and* |
90 | // as the display name presented to the end-user. |
91 | // |
92 | // Behavior seems about the same between Firefox and IE 7/8 here. |
93 | // 'Description' doesn't appear to be used by either. |
94 | $fullName = wfMessage( 'opensearch-desc' )->inContentLanguage()->text(); |
95 | $body->write( Xml::element( 'ShortName', null, $fullName ) ); |
96 | $body->write( Xml::element( 'Description', null, $fullName ) ); |
97 | |
98 | // By default we'll use the site favicon. |
99 | // Double-check if IE supports this properly? |
100 | $body->write( Xml::element( 'Image', |
101 | [ |
102 | 'height' => 16, |
103 | 'width' => 16, |
104 | 'type' => 'image/x-icon' |
105 | ], |
106 | (string)$this->urlUtils->expand( $this->favicon, PROTO_CURRENT ) |
107 | ) ); |
108 | |
109 | $urls = []; |
110 | |
111 | // General search template. Given an input term, this should bring up |
112 | // search results or a specific found page. |
113 | // At least Firefox and IE 7 support this. |
114 | $searchPage = SpecialPage::getTitleFor( 'Search' ); |
115 | $urls[] = [ |
116 | 'type' => 'text/html', |
117 | 'method' => 'get', |
118 | 'template' => $searchPage->getCanonicalURL( 'search={searchTerms}' ) ]; |
119 | |
120 | // TODO: add v1/search/ endpoints? |
121 | |
122 | foreach ( $this->templates as $type => $template ) { |
123 | if ( !$template ) { |
124 | $template = ApiOpenSearch::getOpenSearchTemplate( $type ); |
125 | } |
126 | |
127 | if ( $template ) { |
128 | $urls[] = [ |
129 | 'type' => $type, |
130 | 'method' => 'get', |
131 | 'template' => $template, |
132 | ]; |
133 | } |
134 | } |
135 | |
136 | // Allow hooks to override the suggestion URL settings in a more |
137 | // general way than overriding the whole search engine... |
138 | ( new HookRunner( $this->getHookContainer() ) )->onOpenSearchUrls( $urls ); |
139 | |
140 | foreach ( $urls as $attribs ) { |
141 | $body->write( Xml::element( 'Url', $attribs ) ); |
142 | } |
143 | |
144 | // And for good measure, add a link to the straight search form. |
145 | // This is a custom format extension for Firefox, which otherwise |
146 | // sends you to the domain root if you hit "enter" with an empty |
147 | // search box. |
148 | $body->write( Xml::element( 'moz:SearchForm', null, |
149 | $searchPage->getCanonicalURL() ) ); |
150 | |
151 | $body->write( Xml::closeElement( 'OpenSearchDescription' ) ); |
152 | |
153 | $response->setBody( $body ); |
154 | return $response; |
155 | } |
156 | |
157 | /** |
158 | * Returns the content-type to use for the response. |
159 | * Will be either 'application/xml' or 'application/opensearchdescription+xml', |
160 | * depending on the client's preference. |
161 | * |
162 | * @return string |
163 | */ |
164 | private function getContentType(): string { |
165 | $params = $this->getValidatedParams(); |
166 | if ( $params['ctype'] == 'application/xml' ) { |
167 | // Makes testing tweaks about a billion times easier |
168 | return 'application/xml'; |
169 | } |
170 | |
171 | $acceptHeader = $this->getRequest()->getHeader( 'accept' ); |
172 | |
173 | if ( $acceptHeader ) { |
174 | $parser = new HttpAcceptParser(); |
175 | $acceptableTypes = $parser->parseAccept( $acceptHeader[0] ); |
176 | |
177 | foreach ( $acceptableTypes as $acc ) { |
178 | if ( $acc['type'] === 'application/xml' ) { |
179 | return 'application/xml'; |
180 | } |
181 | } |
182 | } |
183 | |
184 | return 'application/opensearchdescription+xml'; |
185 | } |
186 | |
187 | public function getParamSettings() { |
188 | return [ |
189 | 'ctype' => [ |
190 | self::PARAM_SOURCE => 'query', |
191 | ] |
192 | ]; |
193 | } |
194 | |
195 | } |