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