Fix possible out-of-bound array access in GMapsGeocoder::getTextToGeocode
[platal.git] / classes / gmapsgeocoder.php
CommitLineData
4c906759
SJ
1<?php
2/***************************************************************************
c441aabe 3 * Copyright (C) 2003-2014 Polytechnique.org *
4c906759
SJ
4 * http://opensource.polytechnique.org/ *
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 *
17 * along with this program; if not, write to the Free Software *
18 * Foundation, Inc., *
19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
20 ***************************************************************************/
21
0f5f1b70
SJ
22// Implementation of a Geocoder using the Google Maps API v3. Please refer
23// to the following link for details:
24// http://code.google.com/apis/maps/documentation/geocoding/
4c906759 25//
0f5f1b70
SJ
26// It requires the properties gmaps_url to be defined in section Geocoder
27// in plat/al's configuration (platal.ini & platal.conf).
4c906759
SJ
28class GMapsGeocoder extends Geocoder {
29
30 // Maximum number of Geocoding calls to the Google Maps API.
31 const MAX_GMAPS_RPC_CALLS = 5;
32
b1f39e8b
SJ
33 static public function buildStaticMapURL($latitude, $longitude, $color, $separator = '&')
34 {
a2b84de0
SJ
35 if (!$latitude || !$longitude) {
36 return null;
37 }
38
b1f39e8b
SJ
39 $parameters = array(
40 'size' => '300x100',
41 'markers' => 'color:' . $color . '|' . $latitude . ',' . $longitude,
42 'zoom' => '12',
43 'sensor' => 'false'
44 );
b1f39e8b
SJ
45
46 return Platal::globals()->maps->static_map . '?' . http_build_query($parameters, '', $separator);
47 }
48
26ba053e 49 public function getGeocodedAddress(Address $address, $defaultLanguage = null, $forceLanguage = false) {
4e7a3faa
SJ
50 $this->prepareAddress($address);
51 $textAddress = $this->getTextToGeocode($address->text);
2ffc0393 52 if (is_null($defaultLanguage)) {
0f5f1b70 53 $defaultLanguage = Platal::globals()->geocoder->gmaps_language;
2ffc0393 54 }
4c906759
SJ
55
56 // Try to geocode the full address.
72a4c6a8 57 $address->geocoding_calls = 1;
2ffc0393
SJ
58 if (($geocodedData = $this->getPlacemarkForAddress($textAddress, $defaultLanguage))) {
59 $this->getUpdatedAddress($address, $geocodedData, null, $forceLanguage);
4e7a3faa 60 return;
4c906759
SJ
61 }
62
63 // If the full geocoding failed, try to geocode only the final part of the address.
64 // We start by geocoding everything but the first line, and continue until we get
65 // a result. To respect the limit of GMaps calls, we ignore the first few lines
66 // if there are too many address lines.
67 $addressLines = explode("\n", $textAddress);
68 $linesCount = count($addressLines);
69 for ($i = max(1, $linesCount - self::MAX_GMAPS_RPC_CALLS + 1); $i < $linesCount; ++$i) {
70 $extraLines = implode("\n", array_slice($addressLines, 0, $i));
71 $toGeocode = implode("\n", array_slice($addressLines, $i));
72a4c6a8 72 ++$address->geocoding_calls;
2ffc0393
SJ
73 if (($geocodedData = $this->getPlacemarkForAddress($toGeocode, $defaultLanguage))) {
74 $this->getUpdatedAddress($address, $geocodedData, $extraLines, $forceLanguage);
4e7a3faa 75 return;
4c906759
SJ
76 }
77 }
4c906759
SJ
78 }
79
26ba053e 80 public function stripGeocodingFromAddress(Address $address) {
0f5f1b70
SJ
81 $address->formatted_address = '';
82 $address->types = '';
83 $address->latitude = null;
84 $address->longitude = null;
85 $address->southwest_latitude = null;
86 $address->southwest_longitude = null;
87 $address->northeast_latitude = null;
88 $address->northeast_longitude = null;
89 $address->location_type = null;
90 $address->partial_match = false;
73f6c165 91 }
00e5200b 92
4c906759
SJ
93 // Updates the address with the geocoded information from Google Maps. Also
94 // cleans up the final informations.
26ba053e 95 private function getUpdatedAddress(Address $address, array $geocodedData, $extraLines, $forceLanguage) {
803612ae 96 $this->fillAddressWithGeocoding($address, $geocodedData, false);
2ffc0393 97 $this->formatAddress($address, $extraLines, $forceLanguage);
4c906759
SJ
98 }
99
100 // Retrieves the Placemark object (see #getPlacemarkFromJson()) for the @p
101 // address, by querying the Google Maps API. Returns the array on success,
102 // and null otherwise.
2ffc0393 103 private function getPlacemarkForAddress($address, $defaultLanguage) {
803612ae 104 $url = $this->getGeocodingUrl($address, $defaultLanguage);
4c906759
SJ
105 $geoData = $this->getGeoJsonFromUrl($url);
106
0f5f1b70 107 return ($geoData ? $this->getPlacemarkFromJson($geoData, $url) : null);
4c906759
SJ
108 }
109
110 // Prepares address to be geocoded
26ba053e 111 private function prepareAddress(Address $address) {
4e7a3faa 112 $address->text = preg_replace('/\s*\n\s*/m', "\n", trim($address->text));
4c906759
SJ
113 }
114
115 // Builds the Google Maps geocoder url to fetch information about @p address.
116 // Returns the built url.
803612ae 117 private function getGeocodingUrl($address, $defaultLanguage) {
4c906759
SJ
118 global $globals;
119
120 $parameters = array(
0f5f1b70
SJ
121 'language' => $defaultLanguage,
122 'region' => $globals->geocoder->gmaps_region,
123 'sensor' => 'false', // The queried address wasn't obtained from a GPS sensor.
124 'address' => $address, // The queries address.
4c906759
SJ
125 );
126
0f5f1b70 127 return $globals->geocoder->gmaps_url . 'json?' . http_build_query($parameters);
4c906759
SJ
128 }
129
130 // Fetches JSON-encoded data from a Google Maps API url, and decode them.
131 // Returns the json array on success, and null otherwise.
132 private function getGeoJsonFromUrl($url) {
133 global $globals;
134
135 // Prepare a backtrace object to log errors.
136 $bt = null;
137 if ($globals->debug & DEBUG_BT) {
138 if (!isset(PlBacktrace::$bt['Geoloc'])) {
139 new PlBacktrace('Geoloc');
140 }
141 $bt = &PlBacktrace::$bt['Geoloc'];
142 $bt->start($url);
143 }
144
145 // Fetch the geocoding data.
146 $rawData = file_get_contents($url);
147 if (!$rawData) {
148 if ($bt) {
4e7a3faa 149 $bt->stop(0, 'Could not retrieve geocoded address from GoogleMaps.');
4c906759
SJ
150 }
151 return null;
152 }
153
154 // Decode the JSON-encoded data, and check for their validity.
155 $data = json_decode($rawData, true);
156 if ($bt) {
157 $bt->stop(count($data), null, $data);
158 }
159
160 return $data;
161 }
162
163 // Extracts the most appropriate placemark from the JSON data fetched from
0f5f1b70
SJ
164 // Google Maps. Returns a Placemark array on success, and null otherwise.
165 // http://code.google.com/apis/maps/documentation/geocoding/#StatusCodes
166 private function getPlacemarkFromJson(array $data, $url) {
167 // Check for geocoding status.
168 $status = $data['status'];
169
170 // If no result, return null.
171 if ($status == 'ZERO_RESULTS') {
4c906759
SJ
172 return null;
173 }
174
0f5f1b70
SJ
175 // If there are results return the first one.
176 if ($status == 'OK') {
177 return $data['results'][0];
4c906759
SJ
178 }
179
0f5f1b70
SJ
180 // Report the error.
181 $mailer = new PlMailer('profile/geocoding.mail.tpl');
182 $mailer->assign('status', $status);
183 $mailer->assign('url', $url);
184 $mailer->send();
185 return null;
4c906759
SJ
186 }
187
188 // Fills the address with the geocoded data
26ba053e 189 private function fillAddressWithGeocoding(Address $address, $geocodedData, $isLocal) {
0f5f1b70
SJ
190 $address->types = implode(',', $geocodedData['types']);
191 $address->formatted_address = $geocodedData['formatted_address'];
192 $address->components = $geocodedData['address_components'];
193 $address->latitude = $geocodedData['geometry']['location']['lat'];
194 $address->longitude = $geocodedData['geometry']['location']['lng'];
195 $address->southwest_latitude = $geocodedData['geometry']['viewport']['southwest']['lat'];
196 $address->southwest_longitude = $geocodedData['geometry']['viewport']['southwest']['lng'];
197 $address->northeast_latitude = $geocodedData['geometry']['viewport']['northeast']['lat'];
198 $address->northeast_longitude = $geocodedData['geometry']['viewport']['northeast']['lng'];
199 $address->location_type = $geocodedData['geometry']['location_type'];
200 $address->partial_match = isset($geocodedData['partial_match']) ? true : false;
803612ae
SJ
201 }
202
203 // Formats the text of the geocoded address using the unused data and
204 // compares it to the given address. If they are too different, the user
205 // will be asked to choose between them.
26ba053e 206 private function formatAddress(Address $address, $extraLines, $forceLanguage)
803612ae 207 {
0f5f1b70
SJ
208 /* XXX: Check how to integrate this in the new geocoding system.
209 if (!$forceLanguage) {
2ffc0393 210 $languages = XDB::fetchOneCell('SELECT IF(ISNULL(gc1.belongsTo), gl1.language, gl2.language)
803612ae 211 FROM geoloc_countries AS gc1
2ffc0393 212 INNER JOIN geoloc_languages AS gl1 ON (gc1.iso_3166_1_a2 = gl1.iso_3166_1_a2)
803612ae 213 LEFT JOIN geoloc_countries AS gc2 ON (gc1.belongsTo = gc2.iso_3166_1_a2)
2ffc0393 214 LEFT JOIN geoloc_languages AS gl2 ON (gc2.iso_3166_1_a2 = gl2.iso_3166_1_a2)
803612ae
SJ
215 WHERE gc1.iso_3166_1_a2 = {?}',
216 $address->countryId);
217 $toGeocode = substr($address->text, strlen($extraLines));
218 foreach (explode(',', $languages) as $language) {
0f5f1b70 219 if ($language != Platal::globals()->geocoder->gmaps_language) {
803612ae 220 $geocodedData = $this->getPlacemarkForAddress($toGeocode, $language);
0f5f1b70
SJ
221 $this->fillAddressWithGeocoding($address, $geocodedData, true);
222 break;
803612ae
SJ
223 }
224 }
0f5f1b70 225 }*/
4e7a3faa 226 $address->text = str_replace("\n", "\r\n", $address->text);
5a10ab14
SJ
227 }
228
00e5200b
SJ
229 // Trims the name of the real country if it contains an ISO 3166-1 non-country
230 // item. For that purpose, we compare the last but one line of the address with
231 // all non-country items of ISO 3166-1.
4e7a3faa 232 private function getTextToGeocode($text)
00e5200b 233 {
1c305d4c 234 $res = XDB::iterator('SELECT countryEn, country
00e5200b
SJ
235 FROM geoloc_countries
236 WHERE belongsTo IS NOT NULL');
237 $countries = array();
238 foreach ($res as $item) {
239 $countries[] = $item[0];
240 $countries[] = $item[1];
241 }
4e7a3faa 242 $textLines = explode("\n", $text);
00e5200b 243 $countLines = count($textLines);
d745ff21
NI
244 if ($countLines < 2) {
245 return $text;
246 }
00e5200b
SJ
247 $needle = strtoupper(trim($textLines[$countLines - 2]));
248 $isPseudoCountry = false;
96c7ea54
SJ
249 if ($needle) {
250 foreach ($countries as $country) {
251 if (strtoupper($country) === $needle) {
252 $isPseudoCountry = true;
253 break;
254 }
00e5200b
SJ
255 }
256 }
257
258 if ($isPseudoCountry) {
02c4b93a 259 return implode("\n", array_slice($textLines, 0, -1));
00e5200b 260 }
4e7a3faa 261 return $text;
00e5200b 262 }
4c906759
SJ
263}
264
448c8cdc 265// vim:set et sw=4 sts=4 sws=4 foldmethod=marker fenc=utf-8:
4c906759 266?>