Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
63.64% covered (warning)
63.64%
84 / 132
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
QueryGeoSearch
63.64% covered (warning)
63.64%
84 / 132
33.33% covered (danger)
33.33%
3 / 9
62.05
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
 execute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCacheMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 executeGenerator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseBbox
53.85% covered (warning)
53.85%
7 / 13
0.00% covered (danger)
0.00%
0 / 1
11.82
 run
18.42% covered (danger)
18.42%
7 / 38
0.00% covered (danger)
0.00%
0 / 1
90.18
 getAllowedParams
95.71% covered (success)
95.71%
67 / 70
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace GeoData\Api;
4
5use ApiBase;
6use ApiPageSet;
7use ApiQuery;
8use ApiQueryGeneratorBase;
9use GeoData\BoundingBox;
10use GeoData\Coord;
11use GeoData\GeoData;
12use GeoData\Globe;
13use MediaWiki\Title\Title;
14use Wikimedia\ParamValidator\ParamValidator;
15use Wikimedia\ParamValidator\TypeDef\IntegerDef;
16use WikiPage;
17
18class QueryGeoSearch extends ApiQueryGeneratorBase {
19    private const MIN_RADIUS = 10;
20    private const DEFAULT_RADIUS = 500;
21
22    /**
23     * @var Coord The center of search area
24     */
25    protected $coord;
26
27    /**
28     * @var BoundingBox Bounding box to search in
29     */
30    protected $bbox;
31
32    /**
33     * @var int Search radius
34     */
35    protected $radius;
36
37    /**
38     * @var int Id of the page to search around, exclude from results
39     */
40    protected $idToExclude;
41
42    /**
43     * @param ApiQuery $query
44     * @param string $moduleName
45     */
46    public function __construct( ApiQuery $query, $moduleName ) {
47        parent::__construct( $query, $moduleName, 'gs' );
48    }
49
50    public function execute() {
51        $this->run();
52    }
53
54    /** @inheritDoc */
55    public function getCacheMode( $params ) {
56        return 'public';
57    }
58
59    /** @inheritDoc */
60    public function executeGenerator( $resultPageSet ) {
61        $this->run( $resultPageSet );
62    }
63
64    /**
65     * @param string $bbox
66     * @param Globe $globe
67     * @return BoundingBox
68     */
69    private function parseBbox( string $bbox, Globe $globe ): BoundingBox {
70        $parts = explode( '|', $bbox );
71        $vals = array_map( 'floatval', $parts );
72        if ( count( $parts ) != 4
73            // Pass $parts here for extra validation
74            || !$globe->coordinatesAreValid( $parts[0], $parts[1] )
75            || !$globe->coordinatesAreValid( $parts[2], $parts[3] )
76            || $vals[0] <= $vals[2]
77        ) {
78            $this->dieWithError( 'apierror-geodata-invalidbox', 'invalid-bbox' );
79        }
80        $bbox = new BoundingBox( $vals[0], $vals[1], $vals[2], $vals[3] );
81        $area = $bbox->area();
82        $maxRadius = $this->getConfig()->get( 'MaxGeoSearchRadius' );
83        if ( $area > $maxRadius * $maxRadius * 4 || $area < 100 ) {
84            $this->dieWithError( 'apierror-geodata-boxtoobig', 'toobig' );
85        }
86
87        return $bbox;
88    }
89
90    /**
91     * @param ApiPageSet|null $resultPageSet
92     */
93    protected function run( $resultPageSet = null ): void {
94        $params = $this->extractRequestParams();
95
96        $globe = new Globe( $params['globe'] );
97        $this->requireOnlyOneParameter( $params, 'coord', 'page', 'bbox' );
98        if ( isset( $params['coord'] ) ) {
99            $arr = explode( '|', $params['coord'] );
100            if ( count( $arr ) != 2 || !$globe->coordinatesAreValid( $arr[0], $arr[1] ) ) {
101                $this->dieWithError( 'apierror-geodata-badcoord', 'invalid-coord' );
102            }
103            $this->coord = new Coord( floatval( $arr[0] ), floatval( $arr[1] ), $params['globe'] );
104        } elseif ( isset( $params['page'] ) ) {
105            $t = Title::newFromText( $params['page'] );
106            if ( !$t || !$t->canExist() ) {
107                $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['page'] ) ] );
108            }
109            if ( !$t->exists() ) {
110                $this->dieWithError(
111                    [ 'apierror-missingtitle-byname', wfEscapeWikiText( $t->getPrefixedText() ) ], 'missingtitle'
112                );
113            }
114
115            $pageId = $t->getArticleID();
116            $this->coord = GeoData::getPageCoordinates( $pageId );
117            if ( !$this->coord ) {
118                $this->dieWithError( 'apierror-geodata-nocoord', 'no-coordinates' );
119            }
120            $this->idToExclude = $pageId;
121        } elseif ( isset( $params['bbox'] ) ) {
122            $this->bbox = $this->parseBbox( $params['bbox'], $globe );
123            // Even when using bbox, we need a center to sort by distance
124            $this->coord = $this->bbox->center();
125        } else {
126            $this->dieDebug( __METHOD__, 'Logic error' );
127        }
128
129        // retrieve some fields only if page set needs them
130        if ( $resultPageSet === null ) {
131            $this->addTables( 'page' );
132            $this->addFields( [ 'page_id', 'page_namespace', 'page_title' ] );
133        } else {
134            $pageQuery = WikiPage::getQueryInfo();
135            $this->addTables( $pageQuery['tables'] );
136            $this->addFields( $pageQuery['fields'] );
137            $this->addJoinConds( $pageQuery['joins'] );
138        }
139        $this->addWhereFld( 'page_namespace', $params['namespace'] );
140
141        $this->radius = intval( $params['radius'] );
142
143        if ( $resultPageSet === null ) {
144            $this->getResult()->addIndexedTagName( [ 'query', $this->getModuleName() ],
145                $this->getModulePrefix()
146            );
147        }
148    }
149
150    /** @inheritDoc */
151    public function getAllowedParams() {
152        $propTypes = [ 'type', 'name', 'dim', 'country', 'region', 'globe' ];
153        $primaryTypes = [ 'primary', 'secondary', 'all' ];
154        $config = $this->getConfig();
155        $maxRadius = $config->get( 'MaxGeoSearchRadius' );
156        $defaultGlobe = $config->get( 'DefaultGlobe' );
157
158        $params = [
159            'coord' => [
160                ParamValidator::PARAM_TYPE => 'string',
161                ApiBase::PARAM_HELP_MSG_APPEND => [
162                    'geodata-api-help-coordinates-format',
163                ],
164            ],
165            'page' => [
166                ParamValidator::PARAM_TYPE => 'string',
167            ],
168            'bbox' => [
169                ParamValidator::PARAM_TYPE => 'string',
170            ],
171            'radius' => [
172                ParamValidator::PARAM_TYPE => 'integer',
173                ParamValidator::PARAM_DEFAULT => min( self::DEFAULT_RADIUS, $maxRadius ),
174                IntegerDef::PARAM_MIN => self::MIN_RADIUS,
175                IntegerDef::PARAM_MAX => $maxRadius,
176                ApiBase::PARAM_RANGE_ENFORCE => true,
177            ],
178            'maxdim' => [
179                ParamValidator::PARAM_TYPE => 'integer',
180            ],
181            'sort' => [
182                ParamValidator::PARAM_TYPE => [ 'distance', 'relevance' ],
183                ParamValidator::PARAM_DEFAULT => 'distance',
184                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
185            ],
186            'limit' => [
187                ParamValidator::PARAM_DEFAULT => 10,
188                ParamValidator::PARAM_TYPE => 'limit',
189                IntegerDef::PARAM_MIN => 1,
190                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
191                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
192            ],
193            // @todo: globe selection disabled until we have a real use case
194            'globe' => [
195                ParamValidator::PARAM_TYPE => (array)$defaultGlobe,
196                ParamValidator::PARAM_DEFAULT => $defaultGlobe,
197            ],
198            'namespace' => [
199                ParamValidator::PARAM_TYPE => 'namespace',
200                ParamValidator::PARAM_DEFAULT => NS_MAIN,
201                ParamValidator::PARAM_ISMULTI => true,
202            ],
203            'prop' => [
204                ParamValidator::PARAM_TYPE => $propTypes,
205                ParamValidator::PARAM_DEFAULT => 'globe',
206                ParamValidator::PARAM_ISMULTI => true,
207                ApiBase::PARAM_HELP_MSG_PER_VALUE => array_map( static function ( $i ) use ( $propTypes ) {
208                    return 'apihelp-query+coordinates-paramvalue-prop-' . $propTypes[$i];
209                }, array_flip( $propTypes ) ),
210            ],
211            'primary' => [
212                ParamValidator::PARAM_TYPE => $primaryTypes,
213                ParamValidator::PARAM_DEFAULT => 'primary',
214                ApiBase::PARAM_HELP_MSG_PER_VALUE => array_map( static function ( $i ) use ( $primaryTypes ) {
215                    return 'apihelp-query+coordinates-paramvalue-primary-' . $primaryTypes[$i];
216                }, array_flip( $primaryTypes ) ),
217            ],
218        ];
219        if ( $config->get( 'GeoDataDebug' ) ) {
220            $params['debug'] = [
221                ParamValidator::PARAM_TYPE => 'boolean',
222            ];
223        }
224        return $params;
225    }
226
227    /** @inheritDoc */
228    protected function getExamplesMessages() {
229        return [
230            'action=query&list=geosearch&gsradius=10000&gscoord=37.786971|-122.399677'
231                => 'apihelp-query+geosearch-example-1',
232            'action=query&list=geosearch&gsbbox=37.8|-122.3|37.7|-122.4'
233                => 'apihelp-query+geosearch-example-2',
234        ];
235    }
236
237    /** @inheritDoc */
238    public function getHelpUrls() {
239        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:GeoData#list.3Dgeosearch';
240    }
241}