| 1 | <?php |
| 2 | /* |
| 3 | +--------------------------------------------------------------------+ |
| 4 | | CiviCRM version 5 | |
| 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 and the CiviCRM Licensing Exception. | |
| 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 and the CiviCRM Licensing Exception along | |
| 20 | | with this program; if not, contact CiviCRM LLC | |
| 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 | +--------------------------------------------------------------------+ |
| 25 | */ |
| 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 | * |
| 34 | */ |
| 35 | |
| 36 | /** |
| 37 | * base class for doing all command line operations via civicrm |
| 38 | * used by cli.php |
| 39 | */ |
| 40 | class civicrm_cli { |
| 41 | // required values that must be passed |
| 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; |
| 62 | |
| 63 | // all other arguments populate the parameters |
| 64 | /** |
| 65 | * array that is passed to civicrm_api |
| 66 | * @var array |
| 67 | */ |
| 68 | public $_params = array('version' => 3); |
| 69 | |
| 70 | public $_errors = array(); |
| 71 | |
| 72 | /** |
| 73 | * @return bool |
| 74 | */ |
| 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 | |
| 91 | /** |
| 92 | * Ensure function is being run from the cli. |
| 93 | * |
| 94 | * @return bool |
| 95 | */ |
| 96 | public function _accessing_from_cli() { |
| 97 | if (PHP_SAPI === 'cli') { |
| 98 | return TRUE; |
| 99 | } |
| 100 | else { |
| 101 | die("cli.php can only be run from command line."); |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * @return bool |
| 107 | */ |
| 108 | public function callApi() { |
| 109 | require_once 'api/api.php'; |
| 110 | |
| 111 | CRM_Core_Config::setPermitCacheFlushMode(FALSE); |
| 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 | } |
| 124 | CRM_Core_Config::setPermitCacheFlushMode(TRUE); |
| 125 | CRM_Contact_BAO_Contact_Utils::clearContactCaches(); |
| 126 | |
| 127 | if (!empty($result['is_error'])) { |
| 128 | $this->_log($result['error_message']); |
| 129 | return FALSE; |
| 130 | } |
| 131 | elseif ($this->_output === 'json') { |
| 132 | echo json_encode($result, defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0); |
| 133 | } |
| 134 | elseif ($this->_output) { |
| 135 | print_r($result['values']); |
| 136 | } |
| 137 | return TRUE; |
| 138 | } |
| 139 | |
| 140 | /** |
| 141 | * @return bool |
| 142 | */ |
| 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 | |
| 149 | foreach ($args as $k => $arg) { |
| 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); |
| 162 | $arg = $parts[0]; |
| 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 | } |
| 195 | elseif ($arg == '-J' || $arg == '--json') { |
| 196 | $this->_output = 'json'; |
| 197 | } |
| 198 | elseif ($arg == '-j' || $arg == '--joblog') { |
| 199 | $this->_joblog = TRUE; |
| 200 | } |
| 201 | elseif ($arg == '-sem' || $arg == '--semicolon') { |
| 202 | $this->_semicolon = TRUE; |
| 203 | } |
| 204 | else { |
| 205 | foreach ($this->_additional_arguments as $short => $long) { |
| 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, '--'); |
| 214 | $this->_params[$key] = $value ?? NULL; |
| 215 | } |
| 216 | } |
| 217 | return TRUE; |
| 218 | } |
| 219 | |
| 220 | /** |
| 221 | * @return bool |
| 222 | */ |
| 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"; |
| 228 | $_SERVER['SERVER_SOFTWARE'] = NULL; |
| 229 | $_SERVER['REQUEST_METHOD'] = 'GET'; |
| 230 | |
| 231 | // SCRIPT_FILENAME needed by CRM_Utils_System::cmsRootPath |
| 232 | $_SERVER['SCRIPT_FILENAME'] = __FILE__; |
| 233 | |
| 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); |
| 241 | if (getenv('CIVICRM_SETTINGS')) { |
| 242 | require_once getenv('CIVICRM_SETTINGS'); |
| 243 | } |
| 244 | else { |
| 245 | require_once 'civicrm.config.php'; |
| 246 | } |
| 247 | // autoload |
| 248 | if (!class_exists('CRM_Core_ClassLoader')) { |
| 249 | require_once $civicrm_root . '/CRM/Core/ClassLoader.php'; |
| 250 | } |
| 251 | CRM_Core_ClassLoader::singleton()->register(); |
| 252 | |
| 253 | $this->_config = CRM_Core_Config::singleton(); |
| 254 | |
| 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') { |
| 258 | $_SERVER['HTTP_HOST'] = preg_replace( |
| 259 | '!^https?://([^/]+)/$!i', |
| 260 | '$1', |
| 261 | $this->_config->userFrameworkBaseURL); |
| 262 | } |
| 263 | |
| 264 | $class = 'CRM_Utils_System_' . $this->_config->userFramework; |
| 265 | |
| 266 | $cms = new $class(); |
| 267 | if (!CRM_Utils_System::loadBootstrap(array(), FALSE, FALSE, $civicrm_root)) { |
| 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)) { |
| 280 | if (!CRM_Utils_System::authenticateScript(TRUE, $this->_user, $this->_password, TRUE, FALSE, FALSE)) { |
| 281 | $this->_log(ts("Failed to login as %1. Wrong username or password.", array('1' => $this->_user))); |
| 282 | return FALSE; |
| 283 | } |
| 284 | if (($this->_config->userFramework == 'Joomla' && !$cms->loadUser($this->_user, $this->_password)) || !$cms->loadUser($this->_user)) { |
| 285 | $this->_log(ts("Failed to login as %1", array('1' => $this->_user))); |
| 286 | return FALSE; |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | return TRUE; |
| 291 | } |
| 292 | |
| 293 | /** |
| 294 | * @return bool |
| 295 | */ |
| 296 | private function _validateOptions() { |
| 297 | foreach ($this->_required_arguments as $var) { |
| 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 | |
| 309 | /** |
| 310 | * @param $value |
| 311 | * |
| 312 | * @return string |
| 313 | */ |
| 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 | |
| 320 | /** |
| 321 | * @return string |
| 322 | */ |
| 323 | private function _getUsage() { |
| 324 | $out = "Usage: cli.php -e entity -a action [-u user] [-s site] [--output|--json] [PARAMS]\n"; |
| 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"; |
| 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"; |
| 331 | $out .= " PARAMS is one or more --param=value combinations to pass to the api\n"; |
| 332 | return ts($out); |
| 333 | } |
| 334 | |
| 335 | /** |
| 336 | * @param $error |
| 337 | */ |
| 338 | private function _log($error) { |
| 339 | // fixme, this should call some CRM_Core_Error:: function |
| 340 | // that properly logs |
| 341 | print "$error\n"; |
| 342 | } |
| 343 | |
| 344 | } |
| 345 | |
| 346 | /** |
| 347 | * class used by csv/export.php to export records from |
| 348 | * the database in a csv file format. |
| 349 | */ |
| 350 | class civicrm_cli_csv_exporter extends civicrm_cli { |
| 351 | public $separator = ','; |
| 352 | |
| 353 | /** |
| 354 | */ |
| 355 | public function __construct() { |
| 356 | $this->_required_arguments = array('entity'); |
| 357 | parent::initialize(); |
| 358 | } |
| 359 | |
| 360 | /** |
| 361 | * Run the script. |
| 362 | */ |
| 363 | public function run() { |
| 364 | if ($this->_semicolon) { |
| 365 | $this->separator = ';'; |
| 366 | } |
| 367 | |
| 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); |
| 373 | $first = TRUE; |
| 374 | foreach ($result['values'] as $row) { |
| 375 | if ($first) { |
| 376 | $columns = array_keys($row); |
| 377 | fputcsv($out, $columns, $this->separator, '"'); |
| 378 | $first = FALSE; |
| 379 | } |
| 380 | //handle values returned as arrays (i.e. custom fields that allow multiple selections) by inserting a control character |
| 381 | foreach ($row as &$field) { |
| 382 | if (is_array($field)) { |
| 383 | //convert to string |
| 384 | $field = implode($field, CRM_Core_DAO::VALUE_SEPARATOR) . CRM_Core_DAO::VALUE_SEPARATOR; |
| 385 | } |
| 386 | } |
| 387 | fputcsv($out, $row, $this->separator, '"'); |
| 388 | } |
| 389 | fclose($out); |
| 390 | echo "\n"; |
| 391 | } |
| 392 | |
| 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. |
| 400 | */ |
| 401 | class civicrm_cli_csv_file extends civicrm_cli { |
| 402 | public $header; |
| 403 | public $separator = ','; |
| 404 | |
| 405 | /** |
| 406 | */ |
| 407 | public function __construct() { |
| 408 | $this->_required_arguments = array('entity', 'file'); |
| 409 | $this->_additional_arguments = array('f' => 'file'); |
| 410 | parent::initialize(); |
| 411 | } |
| 412 | |
| 413 | /** |
| 414 | * Run CLI function. |
| 415 | */ |
| 416 | public function run() { |
| 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); |
| 432 | } |
| 433 | |
| 434 | $this->header = $header; |
| 435 | while (($data = fgetcsv($handle, 0, $this->separator)) !== FALSE) { |
| 436 | // skip blank lines |
| 437 | if (count($data) == 1 && is_null($data[0])) { |
| 438 | continue; |
| 439 | } |
| 440 | $this->row++; |
| 441 | if ($this->row % 1000 == 0) { |
| 442 | // Reset PEAR_DB_DATAOBJECT cache to prevent memory leak |
| 443 | CRM_Core_DAO::freeResult(); |
| 444 | } |
| 445 | $params = $this->convertLine($data); |
| 446 | $this->processLine($params); |
| 447 | } |
| 448 | fclose($handle); |
| 449 | } |
| 450 | |
| 451 | /* return a params as expected */ |
| 452 | |
| 453 | /** |
| 454 | * @param $data |
| 455 | * |
| 456 | * @return array |
| 457 | */ |
| 458 | public function convertLine($data) { |
| 459 | $params = array(); |
| 460 | foreach ($this->header as $i => $field) { |
| 461 | //split any multiselect data, denoted with CRM_Core_DAO::VALUE_SEPARATOR |
| 462 | if (strpos($data[$i], CRM_Core_DAO::VALUE_SEPARATOR) !== FALSE) { |
| 463 | $data[$i] = explode(CRM_Core_DAO::VALUE_SEPARATOR, $data[$i]); |
| 464 | $data[$i] = array_combine($data[$i], $data[$i]); |
| 465 | } |
| 466 | $params[$field] = $data[$i]; |
| 467 | } |
| 468 | $params['version'] = 3; |
| 469 | return $params; |
| 470 | } |
| 471 | |
| 472 | } |
| 473 | |
| 474 | /** |
| 475 | * class for processing records to add |
| 476 | * used by csv/import.php |
| 477 | * |
| 478 | */ |
| 479 | class civicrm_cli_csv_importer extends civicrm_cli_csv_file { |
| 480 | |
| 481 | /** |
| 482 | * @param array $params |
| 483 | */ |
| 484 | public function processline($params) { |
| 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 | } |
| 493 | |
| 494 | } |
| 495 | |
| 496 | /** |
| 497 | * class for processing records to delete |
| 498 | * used by csv/delete.php |
| 499 | * |
| 500 | */ |
| 501 | class civicrm_cli_csv_deleter extends civicrm_cli_csv_file { |
| 502 | |
| 503 | /** |
| 504 | * @param array $params |
| 505 | */ |
| 506 | public function processline($params) { |
| 507 | $result = civicrm_api($this->_entity, 'Delete', $params); |
| 508 | if ($result['is_error']) { |
| 509 | echo "\nERROR line " . $this->row . ": " . $result['error_message'] . "\n"; |
| 510 | } |
| 511 | else { |
| 512 | echo "\nline " . $this->row . ": deleted\n"; |
| 513 | } |
| 514 | } |
| 515 | |
| 516 | } |