Merge pull request #14591 from JKingsnorth/dev/core#1064
[civicrm-core.git] / CRM / Utils / Address / BatchUpdate.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 /**
19 * A PHP cron script to format all the addresses in the database. Currently
20 * it only does geocoding if the geocode values are not set. At a later
21 * stage we will also handle USPS address cleanup and other formatting
22 * issues
23 */
24 class CRM_Utils_Address_BatchUpdate {
25
26 public $start = NULL;
27 public $end = NULL;
28 public $geocoding = 1;
29 public $parse = 1;
30 public $throttle = 0;
31
32 public $returnMessages = [];
33 public $returnError = 0;
34
35 /**
36 * Class constructor.
37 *
38 * @param array $params
39 */
40 public function __construct($params) {
41
42 foreach ($params as $name => $value) {
43 $this->$name = $value;
44 }
45
46 // fixme: more params verification
47 }
48
49 /**
50 * Run batch update.
51 *
52 * @return array
53 */
54 public function run() {
55
56 // do check for geocoding.
57 $processGeocode = FALSE;
58 if (!CRM_Utils_GeocodeProvider::getUsableClassName()) {
59 if (CRM_Utils_String::strtobool($this->geocoding) === TRUE) {
60 $this->returnMessages[] = ts('Error: You need to set a mapping provider under Administer > System Settings > Mapping and Geocoding');
61 $this->returnError = 1;
62 $this->returnResult();
63 }
64 }
65 else {
66 $processGeocode = TRUE;
67 // user might want to over-ride.
68 if (CRM_Utils_String::strtobool($this->geocoding) === FALSE) {
69 $processGeocode = FALSE;
70 }
71 }
72
73 // do check for parse street address.
74 $parseAddress = FALSE;
75 $parseAddress = CRM_Utils_Array::value('street_address_parsing',
76 CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
77 'address_options'
78 ),
79 FALSE
80 );
81 $parseStreetAddress = FALSE;
82 if (!$parseAddress) {
83 if (CRM_Utils_String::strtobool($this->parse) === TRUE) {
84 $this->returnMessages[] = ts('Error: You need to enable Street Address Parsing under Administer > Localization > Address Settings.');
85 $this->returnError = 1;
86 return $this->returnResult();
87 }
88 }
89 else {
90 $parseStreetAddress = TRUE;
91 // user might want to over-ride.
92 if (CRM_Utils_String::strtobool($this->parse) === FALSE) {
93 $parseStreetAddress = FALSE;
94 }
95 }
96
97 // don't process.
98 if (!$parseStreetAddress && !$processGeocode) {
99 $this->returnMessages[] = ts('Error: Both Geocode mapping as well as Street Address Parsing are disabled. You must configure one or both options to use this script.');
100 $this->returnError = 1;
101 return $this->returnResult();
102 }
103
104 // do check for parse street address.
105 return $this->processContacts($processGeocode, $parseStreetAddress);
106 }
107
108 /**
109 * Process contacts.
110 *
111 * @param bool $processGeocode
112 * @param bool $parseStreetAddress
113 *
114 * @return array
115 * @throws Exception
116 */
117 public function processContacts($processGeocode, $parseStreetAddress) {
118 // build where clause.
119 $clause = ['( c.id = a.contact_id )'];
120 $params = [];
121 if ($this->start) {
122 $clause[] = "( c.id >= %1 )";
123 $params[1] = [$this->start, 'Integer'];
124 }
125
126 if ($this->end) {
127 $clause[] = "( c.id <= %2 )";
128 $params[2] = [$this->end, 'Integer'];
129 }
130
131 if ($processGeocode) {
132 $clause[] = '( a.geo_code_1 is null OR a.geo_code_1 = 0 )';
133 $clause[] = '( a.geo_code_2 is null OR a.geo_code_2 = 0 )';
134 $clause[] = '( a.country_id is not null )';
135 }
136
137 $whereClause = implode(' AND ', $clause);
138
139 $query = "
140 SELECT c.id,
141 a.id as address_id,
142 a.street_address,
143 a.city,
144 a.postal_code,
145 a.country_id,
146 s.name as state,
147 o.name as country
148 FROM civicrm_contact c
149 INNER JOIN civicrm_address a ON a.contact_id = c.id
150 LEFT JOIN civicrm_country o ON a.country_id = o.id
151 LEFT JOIN civicrm_state_province s ON a.state_province_id = s.id
152 WHERE {$whereClause}
153 ORDER BY a.id
154 ";
155
156 $totalGeocoded = $totalAddresses = $totalAddressParsed = 0;
157
158 $dao = CRM_Core_DAO::executeQuery($query, $params);
159
160 $unparseableContactAddress = [];
161 while ($dao->fetch()) {
162 $totalAddresses++;
163 $params = [
164 'street_address' => $dao->street_address,
165 'postal_code' => $dao->postal_code,
166 'city' => $dao->city,
167 'state_province' => $dao->state,
168 'country' => $dao->country,
169 'country_id' => $dao->country_id,
170 ];
171
172 $addressParams = [];
173
174 // process geocode.
175 if ($processGeocode) {
176 // loop through the address removing more information
177 // so we can get some geocode for a partial address
178 // i.e. city -> state -> country
179
180 $maxTries = 5;
181 do {
182 if ($this->throttle) {
183 usleep(5000000);
184 }
185
186 CRM_Core_BAO_Address::addGeocoderData($params);
187
188 // see if we got a geocode error, in this case we'll trigger a fatal
189 // CRM-13760
190 if (
191 isset($params['geo_code_error']) &&
192 $params['geo_code_error'] == 'OVER_QUERY_LIMIT'
193 ) {
194 throw new CRM_Core_Exception('Aborting batch geocoding. Hit the over query limit on geocoder.');
195 }
196
197 array_shift($params);
198 $maxTries--;
199 } while (
200 (!isset($params['geo_code_1']) || $params['geo_code_1'] == 'null') &&
201 ($maxTries > 1)
202 );
203
204 if (isset($params['geo_code_1']) && $params['geo_code_1'] != 'null') {
205 $totalGeocoded++;
206 $addressParams = $params;
207 }
208 }
209
210 // parse street address
211 if ($parseStreetAddress) {
212 $parsedFields = CRM_Core_BAO_Address::parseStreetAddress($dao->street_address);
213 $success = TRUE;
214 // consider address is automatically parseable,
215 // when we should found street_number and street_name
216 if (empty($parsedFields['street_name']) || empty($parsedFields['street_number'])) {
217 $success = FALSE;
218 }
219
220 // do check for all elements.
221 if ($success) {
222 $totalAddressParsed++;
223 }
224 elseif ($dao->street_address) {
225 //build contact edit url,
226 //so that user can manually fill the street address fields if the street address is not parsed, CRM-5886
227 $url = CRM_Utils_System::url('civicrm/contact/add', "reset=1&action=update&cid={$dao->id}");
228 $unparseableContactAddress[] = " Contact ID: " . $dao->id . " <a href =\"$url\"> " . $dao->street_address . " </a> ";
229 // reset element values.
230 $parsedFields = array_fill_keys(array_keys($parsedFields), '');
231 }
232 $addressParams = array_merge($addressParams, $parsedFields);
233 }
234
235 // finally update address object.
236 if (!empty($addressParams)) {
237 $address = new CRM_Core_DAO_Address();
238 $address->id = $dao->address_id;
239 $address->copyValues($addressParams);
240 $address->save();
241 }
242 }
243
244 $this->returnMessages[] = ts("Addresses Evaluated: %1", [
245 1 => $totalAddresses,
246 ]) . "\n";
247 if ($processGeocode) {
248 $this->returnMessages[] = ts("Addresses Geocoded: %1", [
249 1 => $totalGeocoded,
250 ]) . "\n";
251 }
252 if ($parseStreetAddress) {
253 $this->returnMessages[] = ts("Street Addresses Parsed: %1", [
254 1 => $totalAddressParsed,
255 ]) . "\n";
256 if ($unparseableContactAddress) {
257 $this->returnMessages[] = "<br />\n" . ts("Following is the list of contacts whose address is not parsed:") . "<br />\n";
258 foreach ($unparseableContactAddress as $contactLink) {
259 $this->returnMessages[] = $contactLink . "<br />\n";
260 }
261 }
262 }
263
264 return $this->returnResult();
265 }
266
267 /**
268 * Return result.
269 *
270 * @return array
271 */
272 public function returnResult() {
273 $result = [];
274 $result['is_error'] = $this->returnError;
275 $result['messages'] = '';
276 // Pad message size to allow for prefix added by CRM_Core_JobManager.
277 $messageSize = 255;
278 // Ensure that each message can fit in the civicrm_job_log.data column.
279 foreach ($this->returnMessages as $message) {
280 $messageSize += strlen($message);
281 if ($messageSize > CRM_Utils_Type::BLOB_SIZE) {
282 $result['messages'] .= '...';
283 break;
284 }
285 $result['messages'] .= $message;
286 }
287 return $result;
288 }
289
290 }