Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
63.64% |
84 / 132 |
|
33.33% |
3 / 9 |
CRAP | |
0.00% |
0 / 1 |
QueryGeoSearch | |
63.64% |
84 / 132 |
|
33.33% |
3 / 9 |
62.05 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCacheMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
executeGenerator | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
parseBbox | |
53.85% |
7 / 13 |
|
0.00% |
0 / 1 |
11.82 | |||
run | |
18.42% |
7 / 38 |
|
0.00% |
0 / 1 |
90.18 | |||
getAllowedParams | |
95.71% |
67 / 70 |
|
0.00% |
0 / 1 |
2 | |||
getExamplesMessages | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace GeoData\Api; |
4 | |
5 | use ApiBase; |
6 | use ApiPageSet; |
7 | use ApiQuery; |
8 | use ApiQueryGeneratorBase; |
9 | use GeoData\BoundingBox; |
10 | use GeoData\Coord; |
11 | use GeoData\GeoData; |
12 | use GeoData\Globe; |
13 | use MediaWiki\Title\Title; |
14 | use Wikimedia\ParamValidator\ParamValidator; |
15 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
16 | use WikiPage; |
17 | |
18 | class 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 | } |