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