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