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