Merge pull request #9739 from nditech/CRM-19915
[civicrm-core.git] / CRM / Utils / Address / BatchUpdate.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
7e9e8871 4 | CiviCRM version 4.7 |
6a488035 5 +--------------------------------------------------------------------+
0f03f337 6 | Copyright CiviCRM LLC (c) 2004-2017 |
6a488035
TO
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 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035 27
50bfb460
SB
28/**
29 *
30 * @package CRM
0f03f337 31 * @copyright CiviCRM LLC (c) 2004-2017
50bfb460
SB
32 */
33
6a488035
TO
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
6a488035
TO
39 */
40class CRM_Utils_Address_BatchUpdate {
41
42 var $start = NULL;
43 var $end = NULL;
44 var $geocoding = 1;
45 var $parse = 1;
46 var $throttle = 0;
47
48 var $returnMessages = array();
49 var $returnError = 0;
50
5bc392e6 51 /**
d4620d57 52 * Class constructor.
53 *
c490a46a 54 * @param array $params
5bc392e6 55 */
6a488035
TO
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
5bc392e6 65 /**
d4620d57 66 * Run batch update.
67 *
5bc392e6
EM
68 * @return array
69 */
6a488035
TO
70 public function run() {
71
72 $config = &CRM_Core_Config::singleton();
73
74 // do check for geocoding.
75 $processGeocode = FALSE;
76 if (empty($config->geocodeMethod)) {
c8c9ce59 77 if (CRM_Utils_String::strtobool($this->geocoding) === TRUE) {
a08aa8e8 78 $this->returnMessages[] = ts('Error: You need to set a mapping provider under Administer > System Settings > Mapping and Geocoding');
6a488035
TO
79 $this->returnError = 1;
80 $this->returnResult();
81 }
82 }
83 else {
84 $processGeocode = TRUE;
85 // user might want to over-ride.
c8c9ce59 86 if (CRM_Utils_String::strtobool($this->geocoding) === FALSE) {
6a488035
TO
87 $processGeocode = FALSE;
88 }
89 }
90
91 // do check for parse street address.
92 $parseAddress = FALSE;
93 $parseAddress = CRM_Utils_Array::value('street_address_parsing',
94 CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
95 'address_options'
96 ),
97 FALSE
98 );
99 $parseStreetAddress = FALSE;
100 if (!$parseAddress) {
c8c9ce59 101 if (CRM_Utils_String::strtobool($this->parse) === TRUE) {
a08aa8e8 102 $this->returnMessages[] = ts('Error: You need to enable Street Address Parsing under Administer > Localization > Address Settings.');
6a488035
TO
103 $this->returnError = 1;
104 return $this->returnResult();
105 }
106 }
107 else {
108 $parseStreetAddress = TRUE;
109 // user might want to over-ride.
c8c9ce59 110 if (CRM_Utils_String::strtobool($this->parse) === FALSE) {
6a488035
TO
111 $parseStreetAddress = FALSE;
112 }
113 }
114
115 // don't process.
116 if (!$parseStreetAddress && !$processGeocode) {
117 $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.');
118 $this->returnError = 1;
119 return $this->returnResult();
120 }
121
122 // do check for parse street address.
123 return $this->processContacts($config, $processGeocode, $parseStreetAddress);
124 }
125
5bc392e6 126 /**
d4620d57 127 * Process contacts.
128 *
129 * @param CRM_Core_Config $config
130 * @param bool $processGeocode
131 * @param bool $parseStreetAddress
5bc392e6
EM
132 *
133 * @return array
134 * @throws Exception
135 */
00be9182 136 public function processContacts(&$config, $processGeocode, $parseStreetAddress) {
6a488035
TO
137 // build where clause.
138 $clause = array('( c.id = a.contact_id )');
18b8253b 139 $params = array();
6a488035 140 if ($this->start) {
18b8253b
DL
141 $clause[] = "( c.id >= %1 )";
142 $params[1] = array($this->start, 'Integer');
6a488035
TO
143 }
144
145 if ($this->end) {
18b8253b
DL
146 $clause[] = "( c.id <= %2 )";
147 $params[2] = array($this->end, 'Integer');
6a488035
TO
148 }
149
150 if ($processGeocode) {
151 $clause[] = '( a.geo_code_1 is null OR a.geo_code_1 = 0 )';
152 $clause[] = '( a.geo_code_2 is null OR a.geo_code_2 = 0 )';
153 $clause[] = '( a.country_id is not null )';
154 }
155
156 $whereClause = implode(' AND ', $clause);
157
158 $query = "
159 SELECT c.id,
160 a.id as address_id,
161 a.street_address,
162 a.city,
163 a.postal_code,
164 s.name as state,
165 o.name as country
166 FROM civicrm_contact c
167 INNER JOIN civicrm_address a ON a.contact_id = c.id
168 LEFT JOIN civicrm_country o ON a.country_id = o.id
169 LEFT JOIN civicrm_state_province s ON a.state_province_id = s.id
170 WHERE {$whereClause}
171 ORDER BY a.id
172 ";
173
174 $totalGeocoded = $totalAddresses = $totalAddressParsed = 0;
175
18b8253b 176 $dao = CRM_Core_DAO::executeQuery($query, $params);
6a488035 177 if ($processGeocode) {
e7292422 178 require_once str_replace('_', DIRECTORY_SEPARATOR, $config->geocodeMethod) . '.php';
6a488035
TO
179 }
180
6a488035
TO
181 $unparseableContactAddress = array();
182 while ($dao->fetch()) {
183 $totalAddresses++;
184 $params = array(
185 'street_address' => $dao->street_address,
186 'postal_code' => $dao->postal_code,
187 'city' => $dao->city,
188 'state_province' => $dao->state,
189 'country' => $dao->country,
190 );
191
192 $addressParams = array();
193
194 // process geocode.
195 if ($processGeocode) {
196 // loop through the address removing more information
197 // so we can get some geocode for a partial address
198 // i.e. city -> state -> country
199
200 $maxTries = 5;
201 do {
202 if ($this->throttle) {
203 usleep(5000000);
204 }
205
0e6e8724 206 $className = $config->geocodeMethod;
481a74f4 207 $className::format($params, TRUE);
79f1148d
DL
208
209 // see if we got a geocode error, in this case we'll trigger a fatal
210 // CRM-13760
211 if (
212 isset($params['geo_code_error']) &&
213 $params['geo_code_error'] == 'OVER_QUERY_LIMIT'
214 ) {
215 CRM_Core_Error::fatal('Aborting batch geocoding. Hit the over query limit on geocoder.');
216 }
217
6a488035
TO
218 array_shift($params);
219 $maxTries--;
79f1148d
DL
220 } while (
221 (!isset($params['geo_code_1']) || $params['geo_code_1'] == 'null') &&
6a488035
TO
222 ($maxTries > 1)
223 );
224
79f1148d 225 if (isset($params['geo_code_1']) && $params['geo_code_1'] != 'null') {
6a488035 226 $totalGeocoded++;
f78a9f72 227 $addressParams = $params;
6a488035
TO
228 }
229 }
230
231 // parse street address
232 if ($parseStreetAddress) {
233 $parsedFields = CRM_Core_BAO_Address::parseStreetAddress($dao->street_address);
234 $success = TRUE;
235 // consider address is automatically parseable,
236 // when we should found street_number and street_name
8cc574cf 237 if (empty($parsedFields['street_name']) || empty($parsedFields['street_number'])) {
6a488035
TO
238 $success = FALSE;
239 }
240
241 // do check for all elements.
242 if ($success) {
243 $totalAddressParsed++;
244 }
245 elseif ($dao->street_address) {
246 //build contact edit url,
247 //so that user can manually fill the street address fields if the street address is not parsed, CRM-5886
248 $url = CRM_Utils_System::url('civicrm/contact/add', "reset=1&action=update&cid={$dao->id}");
249 $unparseableContactAddress[] = " Contact ID: " . $dao->id . " <a href =\"$url\"> " . $dao->street_address . " </a> ";
250 // reset element values.
251 $parsedFields = array_fill_keys(array_keys($parsedFields), '');
252 }
253 $addressParams = array_merge($addressParams, $parsedFields);
254 }
255
256 // finally update address object.
257 if (!empty($addressParams)) {
258 $address = new CRM_Core_DAO_Address();
259 $address->id = $dao->address_id;
260 $address->copyValues($addressParams);
261 $address->save();
262 $address->free();
263 }
264 }
265
266 $this->returnMessages[] = ts("Addresses Evaluated: %1", array(
389bcebf 267 1 => $totalAddresses,
353ffa53 268 )) . "\n";
6a488035
TO
269 if ($processGeocode) {
270 $this->returnMessages[] = ts("Addresses Geocoded: %1", array(
389bcebf 271 1 => $totalGeocoded,
353ffa53 272 )) . "\n";
6a488035
TO
273 }
274 if ($parseStreetAddress) {
275 $this->returnMessages[] = ts("Street Addresses Parsed: %1", array(
389bcebf 276 1 => $totalAddressParsed,
353ffa53 277 )) . "\n";
6a488035
TO
278 if ($unparseableContactAddress) {
279 $this->returnMessages[] = "<br />\n" . ts("Following is the list of contacts whose address is not parsed:") . "<br />\n";
280 foreach ($unparseableContactAddress as $contactLink) {
281 $this->returnMessages[] = $contactLink . "<br />\n";
282 }
283 }
284 }
285
286 return $this->returnResult();
287 }
288
5bc392e6 289 /**
d4620d57 290 * Return result.
291 *
5bc392e6
EM
292 * @return array
293 */
00be9182 294 public function returnResult() {
353ffa53 295 $result = array();
6a488035
TO
296 $result['is_error'] = $this->returnError;
297 $result['messages'] = implode("", $this->returnMessages);
298 return $result;
299 }
96025800 300
6a488035 301}