Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Class cli_helper
  19   *
  20   * @package     tool_uploaduser
  21   * @copyright   2020 Marina Glancy
  22   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace tool_uploaduser;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  use tool_uploaduser\local\cli_progress_tracker;
  30  
  31  require_once($CFG->dirroot.'/user/profile/lib.php');
  32  require_once($CFG->dirroot.'/user/lib.php');
  33  require_once($CFG->dirroot.'/group/lib.php');
  34  require_once($CFG->dirroot.'/cohort/lib.php');
  35  require_once($CFG->libdir.'/csvlib.class.php');
  36  require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/uploaduser/locallib.php');
  37  require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/uploaduser/user_form.php');
  38  require_once($CFG->libdir . '/clilib.php');
  39  
  40  /**
  41   * Helper method for CLI script to upload users (also has special wrappers for cli* functions for phpunit testing)
  42   *
  43   * @package     tool_uploaduser
  44   * @copyright   2020 Marina Glancy
  45   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  46   */
  47  class cli_helper {
  48  
  49      /** @var string */
  50      protected $operation;
  51      /** @var array */
  52      protected $clioptions;
  53      /** @var array */
  54      protected $unrecognized;
  55      /** @var string */
  56      protected $progresstrackerclass;
  57  
  58      /** @var process */
  59      protected $process;
  60  
  61      /**
  62       * cli_helper constructor.
  63       *
  64       * @param string|null $progresstrackerclass
  65       */
  66      public function __construct(?string $progresstrackerclass = null) {
  67          $this->progresstrackerclass = $progresstrackerclass ?? cli_progress_tracker::class;
  68          $optionsdefinitions = $this->options_definitions();
  69          $longoptions = [];
  70          $shortmapping = [];
  71          foreach ($optionsdefinitions as $key => $option) {
  72              $longoptions[$key] = $option['default'];
  73              if (!empty($option['alias'])) {
  74                  $shortmapping[$option['alias']] = $key;
  75              }
  76          }
  77  
  78          list($this->clioptions, $this->unrecognized) = cli_get_params(
  79              $longoptions,
  80              $shortmapping
  81          );
  82      }
  83  
  84      /**
  85       * Options used in this CLI script
  86       *
  87       * @return array
  88       */
  89      protected function options_definitions(): array {
  90          $options = [
  91              'help' => [
  92                  'hasvalue' => false,
  93                  'description' => get_string('clihelp', 'tool_uploaduser'),
  94                  'default' => 0,
  95                  'alias' => 'h',
  96              ],
  97              'file' => [
  98                  'hasvalue' => 'PATH',
  99                  'description' => get_string('clifile', 'tool_uploaduser'),
 100                  'default' => null,
 101                  'validation' => function($file) {
 102                      if (!$file) {
 103                          $this->cli_error(get_string('climissingargument', 'tool_uploaduser', 'file'));
 104                      }
 105                      if ($file && (!file_exists($file) || !is_readable($file))) {
 106                          $this->cli_error(get_string('clifilenotreadable', 'tool_uploaduser', $file));
 107                      }
 108                  }
 109              ],
 110          ];
 111          $form = new \admin_uploaduser_form1();
 112          [$elements, $defaults] = $form->get_form_for_cli();
 113          $options += $this->prepare_form_elements_for_cli($elements, $defaults);
 114          // Specify pseudo-column 'type1' to force the form to populate the legacy role mapping selector
 115          // but only if user is allowed to assign roles in courses (otherwise form validation will fail).
 116          $columns = uu_allowed_roles() ? ['type1'] : [];
 117          $form = new \admin_uploaduser_form2(null, ['columns' => $columns, 'data' => []]);
 118          [$elements, $defaults] = $form->get_form_for_cli();
 119          $options += $this->prepare_form_elements_for_cli($elements, $defaults);
 120          return $options;
 121      }
 122  
 123      /**
 124       * Print help for export
 125       */
 126      public function print_help(): void {
 127          $this->cli_writeln(get_string('clititle', 'tool_uploaduser'));
 128          $this->cli_writeln('');
 129          $this->print_help_options($this->options_definitions());
 130          $this->cli_writeln('');
 131          $this->cli_writeln('Example:');
 132          $this->cli_writeln('$sudo -u www-data /usr/bin/php admin/tool/uploaduser/cli/uploaduser.php --file=PATH');
 133      }
 134  
 135      /**
 136       * Get CLI option
 137       *
 138       * @param string $key
 139       * @return mixed|null
 140       */
 141      public function get_cli_option(string $key) {
 142          return $this->clioptions[$key] ?? null;
 143      }
 144  
 145      /**
 146       * Write a text to the given stream
 147       *
 148       * @param string $text text to be written
 149       */
 150      protected function cli_write($text): void {
 151          if (PHPUNIT_TEST) {
 152              echo $text;
 153          } else {
 154              cli_write($text);
 155          }
 156      }
 157  
 158      /**
 159       * Write error notification
 160       * @param string $text
 161       * @return void
 162       */
 163      protected function cli_problem($text): void {
 164          if (PHPUNIT_TEST) {
 165              echo $text;
 166          } else {
 167              cli_problem($text);
 168          }
 169      }
 170  
 171      /**
 172       * Write a text followed by an end of line symbol to the given stream
 173       *
 174       * @param string $text text to be written
 175       */
 176      protected function cli_writeln($text): void {
 177          $this->cli_write($text . PHP_EOL);
 178      }
 179  
 180      /**
 181       * Write to standard error output and exit with the given code
 182       *
 183       * @param string $text
 184       * @param int $errorcode
 185       * @return void (does not return)
 186       */
 187      protected function cli_error($text, $errorcode = 1): void {
 188          $this->cli_problem($text);
 189          $this->die($errorcode);
 190      }
 191  
 192      /**
 193       * Wrapper for "die()" method so we can unittest it
 194       *
 195       * @param mixed $errorcode
 196       * @throws \moodle_exception
 197       */
 198      protected function die($errorcode): void {
 199          if (!PHPUNIT_TEST) {
 200              die($errorcode);
 201          } else {
 202              throw new \moodle_exception('CLI script finished with error code '.$errorcode);
 203          }
 204      }
 205  
 206      /**
 207       * Display as CLI table
 208       *
 209       * @param array $column1
 210       * @param array $column2
 211       * @param int $indent
 212       * @return string
 213       */
 214      protected function convert_to_table(array $column1, array $column2, int $indent = 0): string {
 215          $maxlengthleft = 0;
 216          $left = [];
 217          $column1 = array_values($column1);
 218          $column2 = array_values($column2);
 219          foreach ($column1 as $i => $l) {
 220              $left[$i] = str_repeat(' ', $indent) . $l;
 221              if (strlen('' . $column2[$i])) {
 222                  $maxlengthleft = max($maxlengthleft, strlen($l) + $indent);
 223              }
 224          }
 225          $maxlengthright = 80 - $maxlengthleft - 1;
 226          $output = '';
 227          foreach ($column2 as $i => $r) {
 228              if (!strlen('' . $r)) {
 229                  $output .= $left[$i] . "\n";
 230                  continue;
 231              }
 232              $right = wordwrap($r, $maxlengthright, "\n");
 233              $output .= str_pad($left[$i], $maxlengthleft) . ' ' .
 234                  str_replace("\n", PHP_EOL . str_repeat(' ', $maxlengthleft + 1), $right) . PHP_EOL;
 235          }
 236          return $output;
 237      }
 238  
 239      /**
 240       * Display available CLI options as a table
 241       *
 242       * @param array $options
 243       */
 244      protected function print_help_options(array $options): void {
 245          $left = [];
 246          $right = [];
 247          foreach ($options as $key => $option) {
 248              if ($option['hasvalue'] !== false) {
 249                  $l = "--$key={$option['hasvalue']}";
 250              } else if (!empty($option['alias'])) {
 251                  $l = "-{$option['alias']}, --$key";
 252              } else {
 253                  $l = "--$key";
 254              }
 255              $left[] = $l;
 256              $right[] = $option['description'];
 257          }
 258          $this->cli_write('Options:' . PHP_EOL . $this->convert_to_table($left, $right));
 259      }
 260  
 261      /**
 262       * Process the upload
 263       */
 264      public function process(): void {
 265          // First, validate all arguments.
 266          $definitions = $this->options_definitions();
 267          foreach ($this->clioptions as $key => $value) {
 268              if ($validator = $definitions[$key]['validation'] ?? null) {
 269                  $validator($value);
 270              }
 271          }
 272  
 273          // Read the CSV file.
 274          $iid = \csv_import_reader::get_new_iid('uploaduser');
 275          $cir = new \csv_import_reader($iid, 'uploaduser');
 276          $cir->load_csv_content(file_get_contents($this->get_cli_option('file')),
 277              $this->get_cli_option('encoding'), $this->get_cli_option('delimiter_name'));
 278          $csvloaderror = $cir->get_error();
 279  
 280          if (!is_null($csvloaderror)) {
 281              $this->cli_error(get_string('csvloaderror', 'error', $csvloaderror), 1);
 282          }
 283  
 284          // Start upload user process.
 285          $this->process = new \tool_uploaduser\process($cir, $this->progresstrackerclass);
 286          $filecolumns = $this->process->get_file_columns();
 287  
 288          $form = $this->mock_form(['columns' => $filecolumns, 'data' => ['iid' => $iid, 'previewrows' => 1]], $this->clioptions);
 289  
 290          if (!$form->is_validated()) {
 291              $errors = $form->get_validation_errors();
 292              $this->cli_error(get_string('clivalidationerror', 'tool_uploaduser') . PHP_EOL .
 293                  $this->convert_to_table(array_keys($errors), array_values($errors), 2));
 294          }
 295  
 296          $this->process->set_form_data($form->get_data());
 297          $this->process->process();
 298      }
 299  
 300      /**
 301       * Mock form submission
 302       *
 303       * @param array $customdata
 304       * @param array $submitteddata
 305       * @return \admin_uploaduser_form2
 306       */
 307      protected function mock_form(array $customdata, array $submitteddata): \admin_uploaduser_form2 {
 308          global $USER;
 309          $submitteddata['description'] = ['text' => $submitteddata['description'], 'format' => FORMAT_HTML];
 310  
 311          // Now mock the form submission.
 312          $submitteddata['_qf__admin_uploaduser_form2'] = 1;
 313          $oldignoresesskey = $USER->ignoresesskey ?? null;
 314          $USER->ignoresesskey = true;
 315          $form = new \admin_uploaduser_form2(null, $customdata, 'post', '', [], true, $submitteddata);
 316          $USER->ignoresesskey = $oldignoresesskey;
 317  
 318          $form->set_data($submitteddata);
 319          return $form;
 320      }
 321  
 322      /**
 323       * Prepare form elements for CLI
 324       *
 325       * @param \HTML_QuickForm_element[] $elements
 326       * @param array $defaults
 327       * @return array
 328       */
 329      protected function prepare_form_elements_for_cli(array $elements, array $defaults): array {
 330          $options = [];
 331          foreach ($elements as $element) {
 332              if ($element instanceof \HTML_QuickForm_submit || $element instanceof \HTML_QuickForm_static) {
 333                  continue;
 334              }
 335              $type = $element->getType();
 336              if ($type === 'html' || $type === 'hidden' || $type === 'header') {
 337                  continue;
 338              }
 339  
 340              $name = $element->getName();
 341              if ($name === null || preg_match('/^mform_isexpanded_/', $name)
 342                  || preg_match('/^_qf__/', $name)) {
 343                  continue;
 344              }
 345  
 346              $label = $element->getLabel();
 347              if (!strlen($label) && method_exists($element, 'getText')) {
 348                  $label = $element->getText();
 349              }
 350              $default = $defaults[$element->getName()] ?? null;
 351  
 352              $postfix = '';
 353              $possiblevalues = null;
 354              if ($element instanceof \HTML_QuickForm_select) {
 355                  $selectoptions = $element->_options;
 356                  $possiblevalues = [];
 357                  foreach ($selectoptions as $option) {
 358                      $possiblevalues[] = '' . $option['attr']['value'];
 359                  }
 360                  if (count($selectoptions) < 10) {
 361                      $postfix .= ':';
 362                      foreach ($selectoptions as $option) {
 363                          $postfix .= "\n  ".$option['attr']['value']." - ".$option['text'];
 364                      }
 365                  }
 366                  if (!array_key_exists($name, $defaults)) {
 367                      $firstoption = reset($selectoptions);
 368                      $default = $firstoption['attr']['value'];
 369                  }
 370              }
 371  
 372              if ($element instanceof \HTML_QuickForm_checkbox) {
 373                  $postfix = ":\n  0|1";
 374                  $possiblevalues = ['0', '1'];
 375              }
 376  
 377              if ($default !== null & $default !== '') {
 378                  $postfix .= "\n  ".get_string('clidefault', 'tool_uploaduser')." ".$default;
 379              }
 380              $options[$name] = [
 381                  'hasvalue' => 'VALUE',
 382                  'description' => $label.$postfix,
 383                  'default' => $default,
 384              ];
 385              if ($possiblevalues !== null) {
 386                  $options[$name]['validation'] = function($v) use ($possiblevalues, $name) {
 387                      if (!in_array('' . $v, $possiblevalues)) {
 388                          $this->cli_error(get_string('clierrorargument', 'tool_uploaduser',
 389                              (object)['name' => $name, 'values' => join(', ', $possiblevalues)]));
 390                      }
 391                  };
 392              }
 393          }
 394          return $options;
 395      }
 396  
 397      /**
 398       * Get process statistics.
 399       *
 400       * @return array
 401       */
 402      public function get_stats(): array {
 403          return $this->process->get_stats();
 404      }
 405  }