Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
5.17% covered (danger)
5.17%
9 / 174
21.43% covered (danger)
21.43%
3 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
QuickSearchLookup
5.17% covered (danger)
5.17%
9 / 174
21.43% covered (danger)
21.43%
3 / 14
2096.37
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMain
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setInstance
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRequest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 msg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setFirstResult
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
9.16
 setTitle
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 needsFirstResult
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 outputLookup
1.11% covered (danger)
1.11%
1 / 90
0.00% covered (danger)
0.00%
0 / 1
87.33
 getPageMeta
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
20
 getTextExtract
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getPageImage
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getPageCoord
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
 buildOSMParams
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3use MediaWiki\MediaWikiServices;
4
5class QuickSearchLookup {
6    private static $instance = null;
7
8    /** @var Title|null the Title object for this QuickSearchLookup object */
9    private $title;
10
11    /** @var RequestContext Main RequestContext object */
12    private $context;
13
14    /** @var array Page metadata */
15    private $metadata;
16
17    /**
18     * Constructor
19     *
20     * @param RequestContext $context
21     */
22    public function __construct( IContextSource $context ) {
23        $this->context = $context;
24    }
25
26    /**
27     * Singleton function
28     *
29     * @return QuickSearchLookup
30     */
31    public static function getMain() {
32        if ( !isset( self::$instance ) ) {
33            self::$instance = new self( RequestContext::getMain() );
34        }
35        return self::$instance;
36    }
37
38    /**
39     * Set a main instance.
40     * @param QuickSearchLookup|null $instance
41     */
42    public static function setInstance( $instance ) {
43        self::$instance = $instance;
44    }
45
46    /**
47     * Helper function, returns RequestContext::getRequest
48     *
49     * @return WebRequest
50     */
51    private function getRequest() {
52        return $this->context->getRequest();
53    }
54
55    /**
56     * Helper function, same as RequestContext::msg()
57     *
58     * @param string $key
59     * @return Message
60     */
61    private function msg( $key ) {
62        return $this->context->msg( $key );
63    }
64
65    /**
66     * The given title will be used as the Title in this QuickSearchLookup object.
67     *
68     * @param string|Title $titleTerm The Title object of the frist search result,
69     * or a search term user as a first Title
70     *
71     * @return bool
72     */
73    public function setFirstResult( $titleTerm ) {
74        if ( $titleTerm instanceof Title && $titleTerm->exists() ) {
75            $this->setTitle( $titleTerm );
76            return true;
77        } elseif ( is_string( $titleTerm ) ) {
78            // check, if the term is the exact name of a title in this wiki
79            $title = Title::newFromText( $titleTerm );
80            if ( $title && $title->exists() ) {
81                $this->setTitle( $title );
82                return true;
83            }
84        }
85        return false;
86    }
87
88    /**
89     * Does various checks and set's the given title.
90     *
91     * @param Title $title
92     */
93    private function setTitle( Title $title ) {
94        // check for redirects
95        if ( $title->isRedirect() ) {
96            // get the new target (the redirect target)
97            if ( method_exists( MediaWikiServices::class, 'getWikiPageFactory' ) ) {
98                // MW 1.36+
99                $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
100            } else {
101                $page = WikiPage::factory( $title );
102            }
103            if ( !$page->exists() ) {
104                return;
105            }
106            $target = $page->getRedirectTarget();
107        } else {
108            $target = $title;
109        }
110        $this->title = $target;
111    }
112
113    /**
114     * Checks, if the first title is already set
115     *
116     * @return bool
117     */
118    public function needsFirstResult() {
119        return !isset( $this->title );
120    }
121
122    /**
123     * Adds the QuickSearchLookup panel element to the given OutputPage object
124     *
125     * @param OutputPage $out
126     */
127    public function outputLookup( OutputPage $out ) {
128        global $wgLang;
129
130        // only add the panel, if the given title exist to avoid
131        // an empty panel
132        if ( $this->title && $this->title->exists() ) {
133            // the panel is build with OOUI, enable it
134            $out->enableOOUI();
135            $title = $this->title->getText();
136            $elements = [];
137            $out->addModuleStyles( [ 'ext.QuickSearchLookup' ] );
138
139            // get the info about the Page images added to the firs title
140            $imageInfo = $this->getPageImage( $title );
141
142            // If there is a page image, add it in the correct orientation
143            if ( $imageInfo ) {
144                // get the orientation for this image
145                $orientation = ( $imageInfo['thumb']['width'] < $imageInfo['thumb']['height'] ? 'upright' : 'cross' );
146                $imageTag = new OOUI\Tag( 'img' );
147                $imageTag->setAttributes( [
148                    'src' => $imageInfo['thumb']['source'],
149                    'class' => 'mw-search-quicklookup-image mw-search-quicklookup-image-' . $orientation,
150                ] );
151                $imageLink = Title::newFromText( $imageInfo['pageimage'], NS_FILE );
152                $linkTag = new OOUI\Tag( 'a' );
153                $linkTag
154                    ->setAttributes( [
155                        'href' => $imageLink->getLocalURL(),
156                        'class' => 'image',
157                    ] )
158                    ->appendContent( $imageTag );
159                $elements[] = $linkTag;
160            }
161
162            // try to get some text from the page
163            $text = $this->getTextExtract( $title );
164            if ( $text ) {
165                // the layout for the text, with an additional css class to add a margin for
166                // the ButtonWidget
167                $layout = new OOUI\Layout();
168                $layout
169                    ->appendContent( $text )
170                    ->addClasses( [
171                        'mw-search-quicklookup-text',
172                        // this class adds space between the text and the read more button (which is positioned
173                        // aboslute) and will be removed if the expand map button is present
174                        'mw-search-quicklookup-textmargin'
175                    ] );
176
177                // if there are page coordinates, add an OSM map
178                $coord = $this->getPageCoord( $title );
179                if ( $coord ) {
180                    // add the JavaScript module to expand the map
181                    $out->addModules( [ 'ext.QuickSearchLookup.script' ] );
182
183                    // add the params to the url params list
184                    $urlParamsArray = [
185                        'params' => $this->buildOSMParams( $coord ),
186                        'title' => $title,
187                        'lang' => MediaWikiServices::getInstance()->getContentLanguage()->getCode(),
188                        'uselang' => $wgLang->getCode(),
189                    ];
190                    // convert array to url encoded list
191                    $urlParams = wfArrayToCgi( $urlParamsArray );
192                    // built the complete URL
193                    $iframeLink = wfAppendQuery( "//tools.wmflabs.org/wiwosm/osm-on-ol/kml-on-ol.php", $urlParams );
194
195                    // create a new iframe tag to add OSM map under the text snippet
196                    $iframe = new OOUI\Tag( 'iframe' );
197                    $iframe->setAttributes( [
198                        'id' => 'openstreetmap',
199                        'class' => 'mw-search-quicklookup-osm',
200                        'src' => $iframeLink,
201                        'width' => '100%',
202                        'height' => '100%',
203                    ] );
204                    // the expand button allows a user to make the map bigger without clicking on permalink
205                    $expandButton = new OOUI\ButtonWidget( [
206                        'label' => $this->msg( 'quicksearchlookup-expand' )->text(),
207                    ] );
208                    $expandButton->addClasses( [
209                        'mw-search-quicklookup-expand',
210                        // the button is hidden by default and will be visible if JS is enabled
211                        'hidden'
212                    ] );
213                    // add OSM map to the layout
214                    $layout
215                        ->appendContent( $iframe );
216                }
217
218                $elements[] = $layout;
219            }
220
221            // if there are elements, add them to the output in a PanelLayout
222            if ( $elements ) {
223                // build a ButtonWidget, with a custom class to position is absolute
224                $button = new OOUI\ButtonWidget( [
225                    'label' => $this->msg( 'quicksearchlookup-readmore' )->text(),
226                    'href' => $this->title->getLocalUrl(),
227                ] );
228                $button->addClasses( [
229                    'mw-search-quicklookup-readmore'
230                ] );
231
232                // if there is an OSM map, show an "Expand" button at the right sode
233                if ( isset( $expandButton ) ) {
234                    $elements[] = $expandButton;
235                }
236
237                // then add the read more button
238                $elements[] = new OOUI\FieldLayout( $button );
239
240                $panel = new OOUI\PanelLayout( [
241                    'expanded' => false,
242                    'padded' => true,
243                    'framed' => true,
244                ] );
245
246                $panel->appendContent(
247                    new OOUI\FieldsetLayout( [
248                        'label' => $title,
249                        'items' => $elements,
250                    ] )
251                );
252                $out->addHtml( Html::rawElement(
253                        'div',
254                        [
255                            'class' => 'mw-search-quicklookup',
256                        ],
257                        $panel
258                    )
259                );
260            }
261        }
262    }
263
264    /**
265     * If not already done, performs an internal Api request to get
266     * page data like page images and a short text snippet.
267     *
268     * @param string $title The title to lookup
269     * @return array The page meta data
270     */
271    protected function getPageMeta( $title ) {
272        if ( !$this->metadata ) {
273            $params = new DerivativeRequest(
274                $this->getRequest(),
275                [
276                    'action' => 'query',
277                    'prop' => 'extracts|pageimages|coordinates',
278                    'pithumbsize' => 800,
279                    'exchars' => 450,
280                    'explaintext' => true,
281                    'exintro' => true,
282                    'coprop' => 'type|name|dim|country|region',
283                    'titles' => $title,
284                ],
285                true
286            );
287            $api = new ApiMain( $params );
288            $api->execute();
289            $data = $api->getResult()->getResultData();
290            foreach ( $data['query']['pages'] as $id => $page ) {
291                if ( isset( $page['pageid'] ) ) {
292                    $this->metadata = $page;
293                }
294            }
295        }
296        return $this->metadata;
297    }
298
299    /**
300     * Get the TextExtract specific data from page meta data,
301     * if any, otherwise an empty string.
302     *
303     * @param string $title The title to lookup
304     * @return string
305     */
306    private function getTextExtract( $title ) {
307        // try to get text from TextExtracts
308        $page = $this->getPageMeta( $title );
309        if ( $page && isset( $page['extract']['*'] ) ) {
310            return $page['extract']['*'];
311        }
312
313        return '';
314    }
315
316    /**
317     * Get the PageImages specific data from page meta data,
318     * if any, otherwise false.
319     *
320     * @param string $title The title to lookup
321     * @return array|bool
322     */
323    private function getPageImage( $title ) {
324        // try to get a page image
325        $page = $this->getPageMeta( $title );
326        if ( $page && isset( $page['thumbnail'] ) ) {
327            $data = [ 'thumb' => $page['thumbnail'] ];
328            $data['pageimage'] = $page['pageimage'];
329            return $data;
330        }
331
332        return false;
333    }
334
335    /**
336     * Extracts any GeoData related information from the API respond.
337     *
338     * @param string $title The title to lookup
339     * @return array|bool
340     */
341    private function getPageCoord( $title ) {
342        $page = $this->getPageMeta( $title );
343        // check, if there are coordinates for this title and if they are on earth
344        if (
345            $page &&
346            isset( $page['coordinates'] )
347        ) {
348            if ( isset( $page['coordinates'][0]['globe'] ) && $page['coordinates'][0]['globe'] !== "earth" ) {
349                return false;
350            }
351            $info = $page['coordinates'][0];
352            return [
353                'lat' => $info['lat'],
354                'lon' => $info['lon'],
355                'region' => isset( $info['region'] ) ? $info['region'] : null,
356                'type' => isset( $info['type'] ) ? $info['type'] : null,
357                'dim' => isset( $info['dim'] ) ? $info['dim'] : null,
358            ];
359        }
360
361        return false;
362    }
363
364    /**
365     * Helper to generate a list of parameters for the "params" url parameter.
366     *
367     * @param array $data Data to check and add in key/value format
368     * @return string
369     */
370    private function buildOSMParams( array $data ) {
371        $res = '';
372        // build the params for the URL
373        if ( $data['lat'] < 0 ) {
374            $res .= $data['lat'] . '_S_';
375        } else {
376            $res .= $data['lat'] . '_N_';
377        }
378
379        if ( $data['long'] < 0 ) {
380            $res .= $data['lon'] . '_W_';
381        } else {
382            $res .= $data['lon'] . '_E_';
383        }
384        unset( $data['lat'], $data['lon'] );
385        foreach ( $data as $type => $info ) {
386            if ( $info ) {
387                $res .= '_' . $type . ':' . $info;
388            }
389        }
390        return $res;
391    }
392}