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