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