province abbreviation patch - issue 724
[civicrm-core.git] / bin / cli.class.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
6a488035
TO
5 +--------------------------------------------------------------------+
6 | Copyright Tech To The People http:tttp.eu (c) 2008 |
7 +--------------------------------------------------------------------+
8 | |
9 | CiviCRM is free software; you can copy, modify, and distribute it |
10 | under the terms of the GNU Affero General Public License |
c73475ea 11 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
6a488035
TO
12 | |
13 | CiviCRM is distributed in the hope that it will be useful, but |
14 | WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
16 | See the GNU Affero General Public License for more details. |
17 | |
18 | You should have received a copy of the GNU Affero General Public |
c73475ea
WA
19 | License and the CiviCRM Licensing Exception along |
20 | with this program; if not, contact CiviCRM LLC |
6a488035
TO
21 | at info[AT]civicrm[DOT]org. If you have questions about the |
22 | GNU Affero General Public License or the licensing of CiviCRM, |
23 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
24 +--------------------------------------------------------------------+
d25dd0ee 25 */
6a488035
TO
26
27/**
28 * This files provides several classes for doing command line work with
29 * CiviCRM. civicrm_cli is the base class. It's used by cli.php.
30 *
31 * In addition, there are several additional classes that inherit
32 * civicrm_cli to do more precise functions.
33 *
5396af74 34 */
6a488035
TO
35
36/**
37 * base class for doing all command line operations via civicrm
38 * used by cli.php
5396af74 39 */
6a488035
TO
40class civicrm_cli {
41 // required values that must be passed
683bf891
SL
42 /**
43 * via the command line
44 * @var array
45 */
46 public $_required_arguments = array('action', 'entity');
47 public $_additional_arguments = array();
48 public $_entity = NULL;
49 public $_action = NULL;
50 public $_output = FALSE;
51 public $_joblog = FALSE;
52 public $_semicolon = FALSE;
53 public $_config;
54
55 /**
56 * optional arguments
57 * @var string
58 */
59 public $_site = 'localhost';
60 public $_user = NULL;
61 public $_password = NULL;
6a488035
TO
62
63 // all other arguments populate the parameters
683bf891
SL
64 /**
65 * array that is passed to civicrm_api
66 * @var array
67 */
68 public $_params = array('version' => 3);
6a488035 69
683bf891 70 public $_errors = array();
6a488035 71
4e87860d
EM
72 /**
73 * @return bool
74 */
6a488035
TO
75 public function initialize() {
76 if (!$this->_accessing_from_cli()) {
77 return FALSE;
78 }
79 if (!$this->_parseOptions()) {
80 return FALSE;
81 }
82 if (!$this->_bootstrap()) {
83 return FALSE;
84 }
85 if (!$this->_validateOptions()) {
86 return FALSE;
87 }
88 return TRUE;
89 }
90
cd5823ae
EM
91 /**
92 * Ensure function is being run from the cli.
93 *
94 * @return bool
95 */
6a488035
TO
96 public function _accessing_from_cli() {
97 if (PHP_SAPI === 'cli') {
98 return TRUE;
99 }
100 else {
9c8d6e29 101 die("cli.php can only be run from command line.");
6a488035
TO
102 }
103 }
104
4e87860d
EM
105 /**
106 * @return bool
107 */
6a488035
TO
108 public function callApi() {
109 require_once 'api/api.php';
110
0626851e 111 CRM_Core_Config::setPermitCacheFlushMode(FALSE);
6a488035
TO
112 // CRM-9822 -'execute' action always goes thru Job api and always writes to log
113 if ($this->_action != 'execute' && $this->_joblog) {
114 require_once 'CRM/Core/JobManager.php';
115 $facility = new CRM_Core_JobManager();
116 $facility->setSingleRunParams($this->_entity, $this->_action, $this->_params, 'From Cli.php');
117 $facility->executeJobByAction($this->_entity, $this->_action);
118 }
119 else {
120 // CRM-9822 cli.php calls don't require site-key, so bypass site-key authentication
121 $this->_params['auth'] = FALSE;
122 $result = civicrm_api($this->_entity, $this->_action, $this->_params);
123 }
0626851e 124 CRM_Core_Config::setPermitCacheFlushMode(TRUE);
125 CRM_Contact_BAO_Contact_Utils::clearContactCaches();
6a488035 126
ab874c22 127 if (!empty($result['is_error'])) {
6a488035
TO
128 $this->_log($result['error_message']);
129 return FALSE;
130 }
15fe6ffd
TO
131 elseif ($this->_output === 'json') {
132 echo json_encode($result, defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0);
133 }
6a488035
TO
134 elseif ($this->_output) {
135 print_r($result['values']);
136 }
137 return TRUE;
138 }
139
4e87860d
EM
140 /**
141 * @return bool
142 */
6a488035
TO
143 private function _parseOptions() {
144 $args = $_SERVER['argv'];
145 // remove the first argument, which is the name
146 // of this script
147 array_shift($args);
148
94b8a9c2 149 foreach ($args as $k => $arg) {
6a488035
TO
150 // sanitize all user input
151 $arg = $this->_sanitize($arg);
152
153 // if we're not parsing an option signifier
154 // continue to the next one
155 if (!preg_match('/^-/', $arg)) {
156 continue;
157 }
158
159 // find the value of this arg
160 if (preg_match('/=/', $arg)) {
161 $parts = explode('=', $arg);
56fdfc52 162 $arg = $parts[0];
6a488035
TO
163 $value = $parts[1];
164 }
165 else {
166 if (isset($args[$k + 1])) {
167 $next_arg = $this->_sanitize($args[$k + 1]);
168 // if the next argument is not another option
169 // it's the value for this argument
170 if (!preg_match('/^-/', $next_arg)) {
171 $value = $next_arg;
172 }
173 }
174 }
175
176 // parse the special args first
177 if ($arg == '-e' || $arg == '--entity') {
178 $this->_entity = $value;
179 }
180 elseif ($arg == '-a' || $arg == '--action') {
181 $this->_action = $value;
182 }
183 elseif ($arg == '-s' || $arg == '--site') {
184 $this->_site = $value;
185 }
186 elseif ($arg == '-u' || $arg == '--user') {
187 $this->_user = $value;
188 }
189 elseif ($arg == '-p' || $arg == '--password') {
190 $this->_password = $value;
191 }
192 elseif ($arg == '-o' || $arg == '--output') {
193 $this->_output = TRUE;
194 }
26604a65 195 elseif ($arg == '-J' || $arg == '--json') {
15fe6ffd
TO
196 $this->_output = 'json';
197 }
6a488035
TO
198 elseif ($arg == '-j' || $arg == '--joblog') {
199 $this->_joblog = TRUE;
200 }
4dbe0684
T
201 elseif ($arg == '-sem' || $arg == '--semicolon') {
202 $this->_semicolon = TRUE;
203 }
6a488035 204 else {
c8c10d52 205 foreach ($this->_additional_arguments as $short => $long) {
6a488035
TO
206 if ($arg == '-' . $short || $arg == '--' . $long) {
207 $property = '_' . $long;
208 $this->$property = $value;
209 continue;
210 }
211 }
212 // all other arguments are parameters
213 $key = ltrim($arg, '--');
2e1f50d6 214 $this->_params[$key] = $value ?? NULL;
6a488035
TO
215 }
216 }
217 return TRUE;
218 }
219
4e87860d
EM
220 /**
221 * @return bool
222 */
6a488035
TO
223 private function _bootstrap() {
224 // so the configuration works with php-cli
225 $_SERVER['PHP_SELF'] = "/index.php";
226 $_SERVER['HTTP_HOST'] = $this->_site;
227 $_SERVER['REMOTE_ADDR'] = "127.0.0.1";
ddd885e6 228 $_SERVER['SERVER_SOFTWARE'] = NULL;
56fdfc52 229 $_SERVER['REQUEST_METHOD'] = 'GET';
ddd885e6 230
6a488035
TO
231 // SCRIPT_FILENAME needed by CRM_Utils_System::cmsRootPath
232 $_SERVER['SCRIPT_FILENAME'] = __FILE__;
ddd885e6 233
6a488035
TO
234 // CRM-8917 - check if script name starts with /, if not - prepend it.
235 if (ord($_SERVER['SCRIPT_NAME']) != 47) {
236 $_SERVER['SCRIPT_NAME'] = '/' . $_SERVER['SCRIPT_NAME'];
237 }
238
239 $civicrm_root = dirname(__DIR__);
240 chdir($civicrm_root);
dc1e9be8
TO
241 if (getenv('CIVICRM_SETTINGS')) {
242 require_once getenv('CIVICRM_SETTINGS');
243 }
244 else {
245 require_once 'civicrm.config.php';
246 }
6a488035 247 // autoload
481a74f4 248 if (!class_exists('CRM_Core_ClassLoader')) {
c1f3c6da
BS
249 require_once $civicrm_root . '/CRM/Core/ClassLoader.php';
250 }
6a488035
TO
251 CRM_Core_ClassLoader::singleton()->register();
252
253 $this->_config = CRM_Core_Config::singleton();
4e87860d 254
84f65b2e
KW
255 // HTTP_HOST will be 'localhost' unless overwritten with the -s argument.
256 // Now we have a Config object, we can set it from the Base URL.
257 if ($_SERVER['HTTP_HOST'] == 'localhost') {
a9e80afc 258 $_SERVER['HTTP_HOST'] = preg_replace(
56fdfc52
TO
259 '!^https?://([^/]+)/$!i',
260 '$1',
261 $this->_config->userFrameworkBaseURL);
84f65b2e 262 }
6a488035
TO
263
264 $class = 'CRM_Utils_System_' . $this->_config->userFramework;
265
266 $cms = new $class();
323696fa 267 if (!CRM_Utils_System::loadBootstrap(array(), FALSE, FALSE, $civicrm_root)) {
6a488035
TO
268 $this->_log(ts("Failed to bootstrap CMS"));
269 return FALSE;
270 }
271
272 if (strtolower($this->_entity) == 'job') {
273 if (!$this->_user) {
274 $this->_log(ts("Jobs called from cli.php require valid user as parameter"));
275 return FALSE;
276 }
277 }
278
279 if (!empty($this->_user)) {
22e263ad 280 if (!CRM_Utils_System::authenticateScript(TRUE, $this->_user, $this->_password, TRUE, FALSE, FALSE)) {
bec3fc7c
BS
281 $this->_log(ts("Failed to login as %1. Wrong username or password.", array('1' => $this->_user)));
282 return FALSE;
283 }
b596c3e9 284 if (($this->_config->userFramework == 'Joomla' && !$cms->loadUser($this->_user, $this->_password)) || !$cms->loadUser($this->_user)) {
6a488035
TO
285 $this->_log(ts("Failed to login as %1", array('1' => $this->_user)));
286 return FALSE;
287 }
288 }
289
290 return TRUE;
291 }
292
4e87860d
EM
293 /**
294 * @return bool
295 */
6a488035 296 private function _validateOptions() {
0ab25183 297 foreach ($this->_required_arguments as $var) {
6a488035
TO
298 $index = '_' . $var;
299 if (empty($this->$index)) {
300 $missing_arg = '--' . $var;
301 $this->_log(ts("The %1 argument is required", array(1 => $missing_arg)));
302 $this->_log($this->_getUsage());
303 return FALSE;
304 }
305 }
306 return TRUE;
307 }
308
4e87860d
EM
309 /**
310 * @param $value
311 *
312 * @return string
313 */
6a488035
TO
314 private function _sanitize($value) {
315 // restrict user input - we should not be needing anything
316 // other than normal alpha numeric plus - and _.
317 return trim(preg_replace('#^[^a-zA-Z0-9\-_=/]$#', '', $value));
318 }
319
4e87860d
EM
320 /**
321 * @return string
322 */
6a488035 323 private function _getUsage() {
15fe6ffd 324 $out = "Usage: cli.php -e entity -a action [-u user] [-s site] [--output|--json] [PARAMS]\n";
6a488035
TO
325 $out .= " entity is the name of the entity, e.g. Contact, Event, etc.\n";
326 $out .= " action is the name of the action e.g. Get, Create, etc.\n";
327 $out .= " user is an optional username to run the script as\n";
328 $out .= " site is the domain name of the web site (for Drupal multi site installs)\n";
15fe6ffd
TO
329 $out .= " --output will pretty print the result from the api call\n";
330 $out .= " --json will print the result from the api call as JSON\n";
6a488035
TO
331 $out .= " PARAMS is one or more --param=value combinations to pass to the api\n";
332 return ts($out);
333 }
334
4e87860d
EM
335 /**
336 * @param $error
337 */
6a488035
TO
338 private function _log($error) {
339 // fixme, this should call some CRM_Core_Error:: function
340 // that properly logs
341 print "$error\n";
342 }
96025800 343
6a488035
TO
344}
345
346/**
347 * class used by csv/export.php to export records from
348 * the database in a csv file format.
5396af74 349 */
6a488035 350class civicrm_cli_csv_exporter extends civicrm_cli {
683bf891 351 public $separator = ',';
6a488035 352
4e87860d 353 /**
4e87860d 354 */
5396af74 355 public function __construct() {
6a488035
TO
356 $this->_required_arguments = array('entity');
357 parent::initialize();
358 }
359
4dbe0684
T
360 /**
361 * Run the script.
362 */
5396af74 363 public function run() {
4dbe0684
T
364 if ($this->_semicolon) {
365 $this->separator = ';';
366 }
367
6a488035
TO
368 $out = fopen("php://output", 'w');
369 fputcsv($out, $this->columns, $this->separator, '"');
370
371 $this->row = 1;
372 $result = civicrm_api($this->_entity, 'Get', $this->_params);
a9e80afc 373 $first = TRUE;
6a488035 374 foreach ($result['values'] as $row) {
22e263ad 375 if ($first) {
6a488035
TO
376 $columns = array_keys($row);
377 fputcsv($out, $columns, $this->separator, '"');
a9e80afc 378 $first = FALSE;
6a488035 379 }
0d5a3f9d
JL
380 //handle values returned as arrays (i.e. custom fields that allow multiple selections) by inserting a control character
381 foreach ($row as &$field) {
22e263ad 382 if (is_array($field)) {
0d5a3f9d 383 //convert to string
a9e80afc 384 $field = implode($field, CRM_Core_DAO::VALUE_SEPARATOR) . CRM_Core_DAO::VALUE_SEPARATOR;
0d5a3f9d
JL
385 }
386 }
6a488035
TO
387 fputcsv($out, $row, $this->separator, '"');
388 }
389 fclose($out);
390 echo "\n";
391 }
96025800 392
6a488035
TO
393}
394
395/**
396 * base class used by both civicrm_cli_csv_import
397 * and civicrm_cli_csv_deleter to add or delete
398 * records based on those found in a csv file
399 * passed to the script.
5396af74 400 */
6a488035 401class civicrm_cli_csv_file extends civicrm_cli {
683bf891
SL
402 public $header;
403 public $separator = ',';
6a488035 404
4e87860d 405 /**
4e87860d 406 */
5396af74 407 public function __construct() {
a9e80afc 408 $this->_required_arguments = array('entity', 'file');
6a488035
TO
409 $this->_additional_arguments = array('f' => 'file');
410 parent::initialize();
411 }
412
cd5823ae
EM
413 /**
414 * Run CLI function.
415 */
5396af74 416 public function run() {
6a488035
TO
417 $this->row = 1;
418 $handle = fopen($this->_file, "r");
419
420 if (!$handle) {
421 die("Could not open file: " . $this->_file . ". Please provide an absolute path.\n");
422 }
423
424 //header
425 $header = fgetcsv($handle, 0, $this->separator);
426 // In case fgetcsv couldn't parse the header and dumped the whole line in 1 array element
427 // Try a different separator char
428 if (count($header) == 1) {
429 $this->separator = ";";
430 rewind($handle);
431 $header = fgetcsv($handle, 0, $this->separator);
6a488035
TO
432 }
433
434 $this->header = $header;
435 while (($data = fgetcsv($handle, 0, $this->separator)) !== FALSE) {
436 // skip blank lines
4f99ca55
TO
437 if (count($data) == 1 && is_null($data[0])) {
438 continue;
a9e80afc 439 }
6a488035 440 $this->row++;
6c07d162
J
441 if ($this->row % 1000 == 0) {
442 // Reset PEAR_DB_DATAOBJECT cache to prevent memory leak
39a545dc 443 CRM_Core_DAO::freeResult();
6c07d162 444 }
6a488035
TO
445 $params = $this->convertLine($data);
446 $this->processLine($params);
447 }
448 fclose($handle);
6a488035
TO
449 }
450
451 /* return a params as expected */
683bf891 452
4e87860d
EM
453 /**
454 * @param $data
455 *
456 * @return array
457 */
5396af74 458 public function convertLine($data) {
6a488035
TO
459 $params = array();
460 foreach ($this->header as $i => $field) {
2d8f9c75 461 //split any multiselect data, denoted with CRM_Core_DAO::VALUE_SEPARATOR
0d5a3f9d 462 if (strpos($data[$i], CRM_Core_DAO::VALUE_SEPARATOR) !== FALSE) {
a9e80afc 463 $data[$i] = explode(CRM_Core_DAO::VALUE_SEPARATOR, $data[$i]);
0d5a3f9d
JL
464 $data[$i] = array_combine($data[$i], $data[$i]);
465 }
6a488035
TO
466 $params[$field] = $data[$i];
467 }
468 $params['version'] = 3;
469 return $params;
470 }
96025800 471
6a488035
TO
472}
473
474/**
475 * class for processing records to add
476 * used by csv/import.php
477 *
5396af74 478 */
6a488035 479class civicrm_cli_csv_importer extends civicrm_cli_csv_file {
683bf891 480
4e87860d 481 /**
c490a46a 482 * @param array $params
4e87860d 483 */
5396af74 484 public function processline($params) {
6a488035
TO
485 $result = civicrm_api($this->_entity, 'Create', $params);
486 if ($result['is_error']) {
487 echo "\nERROR line " . $this->row . ": " . $result['error_message'] . "\n";
488 }
489 else {
490 echo "\nline " . $this->row . ": created " . $this->_entity . " id: " . $result['id'] . "\n";
491 }
492 }
96025800 493
6a488035
TO
494}
495
496/**
497 * class for processing records to delete
498 * used by csv/delete.php
499 *
5396af74 500 */
6a488035 501class civicrm_cli_csv_deleter extends civicrm_cli_csv_file {
683bf891 502
4e87860d 503 /**
c490a46a 504 * @param array $params
4e87860d 505 */
5396af74 506 public function processline($params) {
6a488035
TO
507 $result = civicrm_api($this->_entity, 'Delete', $params);
508 if ($result['is_error']) {
509 echo "\nERROR line " . $this->row . ": " . $result['error_message'] . "\n";
0db6c3e1
TO
510 }
511 else {
6a488035
TO
512 echo "\nline " . $this->row . ": deleted\n";
513 }
514 }
96025800 515
6a488035 516}