diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml new file mode 100644 index 0000000..8374965 --- /dev/null +++ b/.github/workflows/moodle-ci.yml @@ -0,0 +1,95 @@ +name: Moodle Plugin CI + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-22.04 + + services: + mariadb: + image: mariadb:10.11 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: "true" + MYSQL_CHARACTER_SET_SERVER: "utf8mb4" + MYSQL_COLLATION_SERVER: "utf8mb4_unicode_ci" + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 3 + + strategy: + fail-fast: false + matrix: + include: + - moodle-branch: 'MOODLE_405_STABLE' + php: '8.1' + database: 'mariadb' + extensions: 'mbstring, intl, xml, soap, zip, gd, opcache, sodium' + - moodle-branch: 'MOODLE_405_STABLE' + php: '8.3' + database: 'mariadb' + extensions: 'mbstring, intl, xml, soap, zip, gd, opcache, sodium' + - moodle-branch: 'MOODLE_501_STABLE' + php: '8.2' + database: 'mariadb' + extensions: 'mbstring, intl, xml, soap, zip, gd, opcache, sodium' + - moodle-branch: 'MOODLE_501_STABLE' + php: '8.4' + database: 'mariadb' + extensions: 'mbstring, intl, xml, soap, zip, gd, opcache, sodium' + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + path: plugin + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: ${{ matrix.extensions }} + ini-values: max_input_vars=5000 + coverage: none + + - name: Initialise moodle-plugin-ci + run: | + composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^4 + echo $(cd ci/bin; pwd) >> $GITHUB_PATH + echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH + sudo locale-gen en_AU.UTF-8 + echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV + + - name: Install moodle-plugin-ci + run: | + moodle-plugin-ci install --plugin ./plugin --db-host=127.0.0.1 + env: + DB: ${{ matrix.database }} + MOODLE_BRANCH: ${{ matrix.moodle-branch }} + + - name: PHP Lint + if: ${{ !cancelled() }} + run: moodle-plugin-ci phplint + + - name: Moodle Code Checker + if: ${{ !cancelled() }} + run: moodle-plugin-ci phpcs --max-warnings 0 + + - name: Moodle PHPDoc Checker + if: ${{ !cancelled() }} + run: moodle-plugin-ci phpdoc --max-warnings 0 + + - name: Validating + if: ${{ !cancelled() }} + run: moodle-plugin-ci validate + + - name: PHPUnit tests + if: ${{ !cancelled() }} + run: moodle-plugin-ci phpunit + + - name: Mark cancelled jobs as failed + if: ${{ cancelled() }} + run: exit 1 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/brcli.iml b/.idea/brcli.iml new file mode 100644 index 0000000..f5ccc9f --- /dev/null +++ b/.idea/brcli.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ae0fe90 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..133d465 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..8306744 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 4c68c56..5c6b91f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Please type the commands below to know how to use this plugin: `sudo -u www-data /usr/bin/php admin/tool/brcli/restore.php --help` ## Release notes +* v1.3 - additional options for backupping (neglect users, log, histories etc.) added * v1.2 - the description of the plugin was improved. * v1.1 - it is mandatory to inform the destination folder. * v1.0 - the restore feature is available. diff --git a/backup.php b/backup.php index 956c95a..af2e427 100644 --- a/backup.php +++ b/backup.php @@ -1,24 +1,72 @@ . + /** - * admin tool brcli - * Backup & restore command line interface - * @package admin - * @subpackage tool - * @author Paulo Júnior based on /admin/cli/backup.php + * Backup courses of a category via CLI. + * + * @package tool_brcli + * @copyright 2019 Paulo Júnior * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define('CLI_SCRIPT', 1); -require(__DIR__.'/../../../config.php'); -require_once($CFG->libdir.'/clilib.php'); +define('CLI_SCRIPT', true); + +require(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir . '/clilib.php'); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); -// Now get cli options. -list($options, $unrecognized) = cli_get_params(array( - 'categoryid' => false, - 'destination' => '', - 'help' => false, - ), array('h' => 'help')); +/** + * Safely set a backup plan setting if it exists. + * + * Moodle versions and backup modes may expose different settings, + * so we set only when present. + * + * @param backup_plan $plan The backup plan instance. + * @param string $settingname The name of the setting to set. + * @param mixed $value The value to assign. + * @return void + */ +function tool_brcli_backup_set_if_exists(backup_plan $plan, string $settingname, $value): void { + try { + $setting = $plan->get_setting($settingname); + $setting->set_value($value); + } catch (\Exception $e) { + // Setting does not exist in this Moodle version or mode. + $e; // Prevent unused variable warning. + } +} + +// Now get CLI options. +[$options, $unrecognized] = cli_get_params( + [ + 'categoryid' => false, + 'destination' => '', + 'preset' => 'full', + 'users' => null, + 'questionbank' => null, + 'calendarevents' => null, + 'competencies' => null, + 'histories' => null, + 'logs' => null, + 'help' => false, + ], + [ + 'h' => 'help', + ] +); if ($unrecognized) { $unrecognized = implode("\n ", $unrecognized); @@ -35,54 +83,90 @@ cli_error(get_string('noadminaccount', 'tool_brcli')); } -// Do we need to store backup somewhere else? -$dir = rtrim($options['destination'], '/'); +// Normalise and validate destination. +$dir = rtrim($options['destination'], "/\\"); if (empty($dir) || !file_exists($dir) || !is_dir($dir) || !is_writable($dir)) { cli_error(get_string('directoryerror', 'tool_brcli')); } // Check that the category exists. -if ($DB->count_records('course_categories', array('id'=>$options['categoryid'])) == 0) { +if ($DB->count_records('course_categories', ['id' => $options['categoryid']]) == 0) { cli_error(get_string('nocategory', 'tool_brcli')); -} +} -$courses = $DB->get_records('course', array('category'=>$options['categoryid'])); -$amount_of_courses = count($courses); +$categoryid = (int) $options['categoryid']; +$courses = $DB->get_records('course', ['category' => $categoryid]); +$amountofcourses = count($courses); $index = 1; foreach ($courses as $cs) { - $bc = new backup_controller(backup::TYPE_1COURSE, $cs->id, backup::FORMAT_MOODLE, - backup::INTERACTIVE_YES, backup::MODE_GENERAL, $admin->id); - - mtrace(get_string('performingbck', 'tool_brcli', $index . '/' . $amount_of_courses)); + $bc = new backup_controller( + backup::TYPE_1COURSE, + $cs->id, + backup::FORMAT_MOODLE, + backup::INTERACTIVE_YES, + backup::MODE_GENERAL, + $admin->id + ); + + mtrace(get_string('performingbck', 'tool_brcli', $index . '/' . $amountofcourses)); + + // Apply preset and overrides. + $plan = $bc->get_plan(); + $overrides = []; + foreach (['users', 'questionbank', 'calendarevents', 'competencies', 'histories', 'logs'] as $name) { + if ($options[$name] !== null) { + $overrides[$name] = (int) $options[$name]; + } + } + + try { + $settings = \tool_brcli\local\preset::build_settings((string) $options['preset'], $overrides); + } catch (\InvalidArgumentException $e) { + $bc->destroy(); + cli_error(get_string('invalidpreset', 'tool_brcli', $options['preset'])); + } + + foreach ($settings as $name => $value) { + tool_brcli_backup_set_if_exists($plan, $name, $value); + } // Set the default filename. $format = $bc->get_format(); $type = $bc->get_type(); $id = $bc->get_id(); - $users = $bc->get_plan()->get_setting('users')->get_value(); - $anonymised = $bc->get_plan()->get_setting('anonymize')->get_value(); + $users = 1; + $anonymised = 0; + try { + $users = (int) $plan->get_setting('users')->get_value(); + $anonymised = (int) $plan->get_setting('anonymize')->get_value(); + } catch (\Exception $e) { + // Settings may not exist; use defaults. + $e; // Prevent unused variable warning. + } $filename = backup_plan_dbops::get_default_backup_filename($format, $type, $id, $users, $anonymised); - $bc->get_plan()->get_setting('filename')->set_value($filename); + tool_brcli_backup_set_if_exists($plan, 'filename', $filename); // Execution. $bc->finish_ui(); $bc->execute_plan(); $results = $bc->get_results(); - $file = $results['backup_destination']; // May be empty if file already moved to target location. + $file = $results['backup_destination'] ?? null; - // Do we need to store backup somewhere else? + // Store backup to the destination directory if needed. if ($file) { - if ($file->copy_content_to($dir.'/'.$filename)) { + $target = $dir . '/' . $filename; + if ($file->copy_content_to($target)) { $file->delete(); } else { mtrace(get_string('directoryerror', 'tool_brcli')); } } $bc->destroy(); - $index = $index + 1; + $index++; } + mtrace(get_string('operationdone', 'tool_brcli')); -exit(0); \ No newline at end of file +exit(0); diff --git a/classes/local/preset.php b/classes/local/preset.php new file mode 100644 index 0000000..3547f45 --- /dev/null +++ b/classes/local/preset.php @@ -0,0 +1,92 @@ +. + +/** + * Preset and overrides handling for backup/restore plans. + * + * Extracted to make CLI behaviour testable and stable across Moodle versions. + * + * @package tool_brcli + * @copyright 2026 Ralf Erlebach + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_brcli\local; + +/** + * Preset helper for backup and restore CLI plans. + * + * @package tool_brcli + * @copyright 2026 Ralf Erlebach + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class preset { + /** @var string Full backup preset name. */ + public const PRESET_FULL = 'full'; + + /** @var string Content-only backup preset name. */ + public const PRESET_CONTENTONLY = 'contentonly'; + + /** + * Returns plan settings based on preset and overrides. + * + * @param string $preset Preset name ('full' or 'contentonly'). + * @param array $overrides Key/value overrides, e.g. ['users' => 0]. Values are cast to int. + * @param array|null $available If provided, only settings in this whitelist are returned. + * @return array + * @throws \InvalidArgumentException If the preset name is not recognised. + */ + public static function build_settings(string $preset, array $overrides = [], ?array $available = null): array { + $preset = strtolower(trim($preset)); + if (!in_array($preset, [self::PRESET_FULL, self::PRESET_CONTENTONLY], true)) { + // Use a core PHP exception type here to avoid depending on Moodle exception class names. + throw new \InvalidArgumentException('Invalid preset'); + } + + $settings = []; + + if ($preset === self::PRESET_CONTENTONLY) { + $settings = [ + 'users' => 0, + 'role_assignments' => 0, + 'groups' => 0, + 'comments' => 0, + 'badges' => 0, + 'calendarevents' => 0, + 'userscompletion' => 0, + 'histories' => 0, + 'logs' => 0, + 'questionbank' => 0, + 'competencies' => 0, + 'contentbankcontent' => 0, + ]; + } + + foreach ($overrides as $name => $value) { + if ($value === null) { + continue; + } + $settings[(string) $name] = (int) $value; + } + + if ($available !== null) { + $available = array_flip($available); + $settings = array_intersect_key($settings, $available); + } + + return $settings; + } +} diff --git a/lang/en/tool_brcli.php b/lang/en/tool_brcli.php index 7a4b66c..eee04a4 100644 --- a/lang/en/tool_brcli.php +++ b/lang/en/tool_brcli.php @@ -1,40 +1,76 @@ . + /** - * admin tool brcli - * Backup & restore command line interface - * @package admin - * @subpackage tool - * @author Paulo Júnior + * Language strings for tool_brcli (English). + * + * @package tool_brcli + * @copyright 2019 Paulo Júnior * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['pluginname'] = 'Backup and Restore Command-Line Interface'; -$string['unknowoption'] = 'Unknow option: {$a}'; -$string['noadminaccount'] = 'Error: No admin account was found!'; -$string['directoryerror'] = 'Error: Destination directory does not exists or not writable!'; -$string['nocategory'] = 'Error: No category was found!'; -$string['performingbck'] = 'Performing backup of the {$a} course...'; -$string['performingres'] = 'Restoring backup of the {$a} course...'; -$string['operationdone'] = 'Done!'; -$string['invalidbackupfile'] = 'Invalid backup file: {$a}'; -$string['helpoptionbck'] = -'Perform backup of the courses of a specific category. + +defined('MOODLE_INTERNAL') || die(); + +$string['directoryerror'] = 'Error: Destination directory does not exist or is not writable!'; +$string['helpoptionbck'] = 'Perform backup of the courses of a specific category. Options: --categoryid=INTEGER Category ID for backup. ---destination=STRING Path where to store backup file. +--destination=STRING Path where to store backup file. +--preset=STRING Backup preset. full (default) or contentonly. +--users=0|1 Override: include user data. +--questionbank=0|1 Override: include question bank. +--calendarevents=0|1 Override: include calendar events. +--competencies=0|1 Override: include competencies. +--histories=0|1 Override: include grade histories. +--logs=0|1 Override: include logs. -h, --help Print out this help. Example: sudo -u www-data /usr/bin/php admin/tool/brcli/backup.php --categoryid=1 --destination=/moodle/backup/ + + # Content-only backups (no users, question bank, calendar, competencies, logs, histories, etc.) + sudo -u www-data /usr/bin/php admin/tool/brcli/backup.php --categoryid=1 --destination=/moodle/backup/ --preset=contentonly '; -$string['helpoptionres'] = -'Restore all backup files belong to a specific folder. +$string['helpoptionres'] = 'Restore all backup files belonging to a specific folder. Options: --categoryid=INTEGER Category ID where the backup must be restored. ---source=STRING Path where the backup files (.mbz) are. +--source=STRING Path where the backup files (.mbz) are. +--preset=STRING Restore preset. full (default) or contentonly. +--users=0|1 Override: restore user data. +--questionbank=0|1 Override: restore question bank. +--calendarevents=0|1 Override: restore calendar events. +--competencies=0|1 Override: restore competencies. +--histories=0|1 Override: restore grade histories. +--logs=0|1 Override: restore logs. -h, --help Print out this help. Example: sudo -u www-data /usr/bin/php admin/tool/brcli/restore.php --categoryid=1 --source=/moodle/backup/ + + # Restore as content-only (ignore user data, question bank, calendar, competencies, logs, histories, etc.) + sudo -u www-data /usr/bin/php admin/tool/brcli/restore.php --categoryid=1 --source=/moodle/backup/ --preset=contentonly '; +$string['invalidbackupfile'] = 'Invalid backup file: {$a}'; +$string['invalidpreset'] = 'Invalid preset: {$a}. Supported values: full, contentonly.'; +$string['noadminaccount'] = 'Error: No admin account was found!'; +$string['nocategory'] = 'Error: No category was found!'; +$string['operationdone'] = 'Done!'; +$string['performingbck'] = 'Performing backup of the {$a} course...'; +$string['performingres'] = 'Restoring backup of the {$a} course...'; +$string['pluginname'] = 'Backup and Restore Command-Line Interface'; +$string['unknowoption'] = 'Unknown option: {$a}'; diff --git a/lang/pt_br/tool_brcli.php b/lang/pt_br/tool_brcli.php index 8b1e3fe..57165a9 100644 --- a/lang/pt_br/tool_brcli.php +++ b/lang/pt_br/tool_brcli.php @@ -1,41 +1,76 @@ . + /** - * admin tool brcli - * Backup & restore command line interface - * @package admin - * @subpackage tool - * @author Paulo Júnior + * Language strings for tool_brcli (Brazilian Portuguese). + * + * @package tool_brcli + * @copyright 2019 Paulo Júnior * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['pluginname'] = 'Interface de linha de comando para backup e restauração'; -$string['unknowoption'] = 'Opção inválida: {$a}'; -$string['noadminaccount'] = 'Erro: Não há uma conta de administrador cadastrada!'; + +defined('MOODLE_INTERNAL') || die(); + $string['directoryerror'] = 'Erro: O diretório de destino informado não existe ou não pode ser escrito!'; -$string['nocategory'] = 'Erro: A categoria informada não existe!'; -$string['performingbck'] = 'Iniciando backup do curso {$a}...'; -$string['performingres'] = 'Restaurando backup do curso {$a}...'; -$string['operationdone'] = 'Finalizado!'; -$string['invalidbackupfile'] = 'Arquivo de backup inválido: {$a}'; -$string['helpoptionbck'] = -'Realiza o backup de todos os cursos de uma categoria. +$string['helpoptionbck'] = 'Realiza o backup de todos os cursos de uma categoria. Opções: --categoryid=INTEGER ID da categoria cujo backup será feito. ---destination=STRING Caminho onde serão armazenados os arquivos de backup. +--destination=STRING Caminho onde serão armazenados os arquivos de backup. +--preset=STRING Preset do backup. full (padrão) ou contentonly. +--users=0|1 Override: incluir dados de usuários. +--questionbank=0|1 Override: incluir banco de questões. +--calendarevents=0|1 Override: incluir eventos do calendário. +--competencies=0|1 Override: incluir competências. +--histories=0|1 Override: incluir histórico de notas. +--logs=0|1 Override: incluir logs. -h, --help Exibe a ajuda. Exemplo: sudo -u www-data /usr/bin/php admin/tool/brcli/backup.php --categoryid=1 --destination=/moodle/backup/ -'; -$string['helpoptionres'] = -'Restaura todos os arquivos de backup contidos em um diretório. + # Backup apenas do conteúdo (sem usuários, banco de questões, calendário, competências, logs, históricos, etc.) + sudo -u www-data /usr/bin/php admin/tool/brcli/backup.php --categoryid=1 --destination=/moodle/backup/ --preset=contentonly +'; +$string['helpoptionres'] = 'Restaura todos os arquivos de backup contidos em um diretório. -Options: +Opções: --categoryid=INTEGER ID da categoria onde os backup serão restaurados. --source=STRING Caminho onde os arquivos de backup (.mbz) estão armazenados. +--preset=STRING Preset da restauração. full (padrão) ou contentonly. +--users=0|1 Override: restaurar dados de usuários. +--questionbank=0|1 Override: restaurar banco de questões. +--calendarevents=0|1 Override: restaurar eventos do calendário. +--competencies=0|1 Override: restaurar competências. +--histories=0|1 Override: restaurar histórico de notas. +--logs=0|1 Override: restaurar logs. -h, --help Exibe a ajuda. Exemplo: sudo -u www-data /usr/bin/php admin/tool/brcli/restore.php --categoryid=1 --source=/moodle/backup/ -'; \ No newline at end of file + + # Restaura apenas o conteúdo (ignora usuários, banco de questões, calendário, competências, logs, históricos, etc.) + sudo -u www-data /usr/bin/php admin/tool/brcli/restore.php --categoryid=1 --source=/moodle/backup/ --preset=contentonly +'; +$string['invalidbackupfile'] = 'Arquivo de backup inválido: {$a}'; +$string['invalidpreset'] = 'Preset inválido: {$a}. Valores suportados: full, contentonly.'; +$string['noadminaccount'] = 'Erro: Não há uma conta de administrador cadastrada!'; +$string['nocategory'] = 'Erro: A categoria informada não existe!'; +$string['operationdone'] = 'Finalizado!'; +$string['performingbck'] = 'Iniciando backup do curso {$a}...'; +$string['performingres'] = 'Restaurando backup do curso {$a}...'; +$string['pluginname'] = 'Interface de linha de comando para backup e restauração'; +$string['unknowoption'] = 'Opção inválida: {$a}'; diff --git a/restore.php b/restore.php index caf65d7..13cce64 100644 --- a/restore.php +++ b/restore.php @@ -1,24 +1,69 @@ . + /** - * admin tool brcli - * Backup & restore command line interface - * @package admin - * @subpackage tool - * @author Paulo Júnior based on https://github.com/mudrd8mz/moodle-toolbox/blob/master/mbz/mbz-restorecourses.php + * Restore all .mbz backups from a folder into new courses within a category. + * + * @package tool_brcli + * @copyright 2019 Paulo Júnior * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define('CLI_SCRIPT', 1); -require(__DIR__.'/../../../config.php'); -require_once($CFG->libdir.'/clilib.php'); +define('CLI_SCRIPT', true); + +require(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir . '/clilib.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); -// Now get cli options. -list($options, $unrecognized) = cli_get_params(array( - 'categoryid' => false, - 'source' => '', - 'help' => false, - ), array('h' => 'help')); +/** + * Safely set a restore plan setting if it exists. + * + * @param restore_plan $plan The restore plan instance. + * @param string $settingname The name of the setting to set. + * @param mixed $value The value to assign. + * @return void + */ +function tool_brcli_restore_set_if_exists(restore_plan $plan, string $settingname, $value): void { + try { + $setting = $plan->get_setting($settingname); + $setting->set_value($value); + } catch (\Exception $e) { + // Setting does not exist in this Moodle version or mode. + $e; // Prevent unused variable warning. + } +} + +// Now get CLI options. +[$options, $unrecognized] = cli_get_params( + [ + 'categoryid' => false, + 'source' => '', + 'preset' => 'full', + 'users' => null, + 'questionbank' => null, + 'calendarevents' => null, + 'competencies' => null, + 'histories' => null, + 'logs' => null, + 'help' => false, + ], + [ + 'h' => 'help', + ] +); if ($unrecognized) { $unrecognized = implode("\n ", $unrecognized); @@ -35,70 +80,121 @@ cli_error(get_string('noadminaccount', 'tool_brcli')); } -$dir = rtrim($options['source'], '/'); -if (empty($dir) || !file_exists($dir) || !is_dir($dir)) { +$dir = rtrim($options['source'], "/\\"); +if (empty($dir) || !file_exists($dir) || !is_dir($dir)) { cli_error(get_string('directoryerror', 'tool_brcli')); } // Check that the category exists. -if ($DB->count_records('course_categories', array('id'=>$options['categoryid'])) == 0) { +if ($DB->count_records('course_categories', ['id' => $options['categoryid']]) == 0) { cli_error(get_string('nocategory', 'tool_brcli')); -} +} + +$preset = (string) $options['preset']; $index = 1; -$sourcefiles = new FilesystemIterator($dir, FilesystemIterator::SKIP_DOTS); -$amount_of_courses = iterator_count($sourcefiles); +$sourcefiles = new \FilesystemIterator($dir, \FilesystemIterator::SKIP_DOTS); + +// Count only .mbz files for progress reporting. +$amountofcourses = 0; +foreach ($sourcefiles as $f) { + if (strtolower((string) $f->getExtension()) === 'mbz') { + $amountofcourses++; + } +} + +// Rewind iterator. +$sourcefiles = new \FilesystemIterator($dir, \FilesystemIterator::SKIP_DOTS); foreach ($sourcefiles as $sourcefile) { - if ($sourcefile->getExtension() !== 'mbz') { + if (strtolower((string) $sourcefile->getExtension()) !== 'mbz') { continue; } - mtrace(get_string('performingres', 'tool_brcli', $index . '/' . $amount_of_courses)); + mtrace(get_string('performingres', 'tool_brcli', $index . '/' . $amountofcourses)); // Extract the file. $packer = get_file_packer('application/vnd.moodle.backup'); $backupid = restore_controller::get_tempdir_name(SITEID, $admin->id); - $path = "$CFG->tempdir/backup/$backupid/"; + $path = $CFG->tempdir . '/backup/' . $backupid . '/'; if (!$packer->extract_to_pathname($sourcefile->getPathname(), $path)) { mtrace(get_string('invalidbackupfile', 'tool_brcli', $sourcefile->getFilename())); + $index++; + continue; } // Transaction. $transaction = $DB->start_delegated_transaction(); - + // Create new course. - $folder = $backupid; // as found in: $CFG->dataroot . '/temp/backup/' - $categoryid = $options['categoryid']; // e.g. 1 == Miscellaneous - $userdoingrestore = $admin->id; // e.g. 2 == admin - $courseid = restore_dbops::create_new_course('', '', $categoryid); - + $folder = $backupid; + $categoryid = (int) $options['categoryid']; + $userdoingrestore = $admin->id; + $courseid = restore_dbops::create_new_course('', '', $categoryid); + // Restore backup into course. - $controller = new restore_controller($folder, $courseid, - backup::INTERACTIVE_NO, backup::MODE_GENERAL, $userdoingrestore, - backup::TARGET_NEW_COURSE); - if ($controller->execute_precheck()) { - $controller->execute_plan(); - } else { + $controller = new restore_controller( + $folder, + $courseid, + backup::INTERACTIVE_NO, + backup::MODE_GENERAL, + $userdoingrestore, + backup::TARGET_NEW_COURSE + ); + + // Apply preset and overrides. + $plan = $controller->get_plan(); + $overrides = []; + foreach (['users', 'questionbank', 'calendarevents', 'competencies', 'histories', 'logs'] as $name) { + if ($options[$name] !== null) { + $overrides[$name] = (int) $options[$name]; + } + } + + try { + $settings = \tool_brcli\local\preset::build_settings($preset, $overrides); + } catch (\InvalidArgumentException $e) { + cli_error(get_string('invalidpreset', 'tool_brcli', $preset)); + } + + foreach ($settings as $name => $value) { + tool_brcli_restore_set_if_exists($plan, $name, $value); + } + + $precheck = $controller->execute_precheck(); + if ($precheck !== true) { try { - $transaction->rollback(new Exception('Prechecked failed')); - } catch (Exception $e) { - unset($transaction); - $controller->destroy(); - unset($controller); - continue; + $transaction->rollback(new \Exception('Precheck failed')); + } catch (\Exception $e) { + // Ignore rollback exceptions. + $e; // Prevent unused variable warning. } + unset($transaction); + $controller->destroy(); + unset($controller); + $index++; + continue; } - $index = $index + 1; + $controller->execute_plan(); + + $index++; -// Commit and clean up. + // Commit and clean up. $transaction->allow_commit(); unset($transaction); $controller->destroy(); unset($controller); + + // Remove extracted temp backup. + if (!empty($backupid)) { + $temppath = $CFG->tempdir . '/backup/' . $backupid; + if (file_exists($temppath)) { + fulldelete($temppath); + } + } } mtrace(get_string('operationdone', 'tool_brcli')); -exit(0); \ No newline at end of file +exit(0); diff --git a/tests/preset_test.php b/tests/preset_test.php new file mode 100644 index 0000000..4f34885 --- /dev/null +++ b/tests/preset_test.php @@ -0,0 +1,99 @@ +. + +/** + * Unit tests for tool_brcli presets. + * + * @package tool_brcli + * @copyright 2026 Ralf Erlebach + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +declare(strict_types=1); + +namespace tool_brcli; + +use tool_brcli\local\preset; + +/** + * Preset unit tests. + * + * @package tool_brcli + * @copyright 2026 Ralf Erlebach + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \tool_brcli\local\preset + */ +final class preset_test extends \basic_testcase { + /** + * Test that the full preset returns an empty settings array by default. + * + * @covers ::build_settings + * @return void + */ + public function test_full_preset_is_empty_by_default(): void { + $settings = preset::build_settings('full'); + $this->assertSame([], $settings); + } + + /** + * Test that the contentonly preset contains the expected core flags. + * + * @covers ::build_settings + * @return void + */ + public function test_contentonly_preset_contains_expected_core_flags(): void { + $settings = preset::build_settings('contentonly'); + $this->assertArrayHasKey('users', $settings); + $this->assertSame(0, $settings['users']); + $this->assertSame(0, $settings['questionbank']); + $this->assertSame(0, $settings['calendarevents']); + $this->assertSame(0, $settings['competencies']); + } + + /** + * Test that overrides take precedence over preset defaults. + * + * @covers ::build_settings + * @return void + */ + public function test_overrides_take_precedence(): void { + $settings = preset::build_settings('contentonly', ['users' => 1, 'questionbank' => 1]); + $this->assertSame(1, $settings['users']); + $this->assertSame(1, $settings['questionbank']); + } + + /** + * Test that filtering by available settings works correctly. + * + * @covers ::build_settings + * @return void + */ + public function test_available_filtering_works(): void { + $settings = preset::build_settings('contentonly', [], ['users', 'questionbank']); + $this->assertSame(['users' => 0, 'questionbank' => 0], $settings); + } + + /** + * Test that an invalid preset name throws an exception. + * + * @covers ::build_settings + * @return void + */ + public function test_invalid_preset_throws(): void { + $this->expectException(\InvalidArgumentException::class); + preset::build_settings('nope'); + } +} diff --git a/version.php b/version.php index 25b1a56..7cd5a1a 100644 --- a/version.php +++ b/version.php @@ -1,15 +1,32 @@ . + /** - * admin tool brcli - * Backup & restore command line interface - * @package admin - * @subpackage tool - * @author Paulo Júnior + * Plugin version and other meta-data are defined here. + * + * @package tool_brcli + * @copyright 2019 Paulo Júnior * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2019031500; // The current plugin version (Date: YYYYMMDDXX) -$plugin->requires = 2015111605; // Requires this Moodle version -$plugin->component = 'tool_brcli'; // Full name of the plugin (used for diagnostics) -$plugin->maturity = MATURITY_STABLE; -$plugin->release = 'v1.2'; \ No newline at end of file + +$plugin->component = 'tool_brcli'; +$plugin->version = 2026030500; +$plugin->requires = 2024100700; +$plugin->supported = [405, 501]; +$plugin->maturity = MATURITY_STABLE; +$plugin->release = 'v1.3';