From f875e1082ee1cb31c5906c38c46538a88e13b486 Mon Sep 17 00:00:00 2001 From: ralferlebach Date: Thu, 5 Mar 2026 12:40:48 +0100 Subject: [PATCH 1/7] Version Update + Adding Options for Backup --- backup.php | 111 +++++++++++++++++++++++++---- lang/en/tool_brcli.php | 21 ++++++ lang/pt_br/tool_brcli.php | 21 ++++++ restore.php | 145 +++++++++++++++++++++++++++++++------- version.php | 42 ++++++++--- 5 files changed, 290 insertions(+), 50 deletions(-) diff --git a/backup.php b/backup.php index 956c95a..1de4c32 100644 --- a/backup.php +++ b/backup.php @@ -1,22 +1,63 @@ . + /** - * 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'); +define('CLI_SCRIPT', true); + +require(__DIR__ . '/../../../config.php'); require_once($CFG->libdir.'/clilib.php'); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +/** + * 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 + * @param string $settingname + * @param mixed $value + */ +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. + } +} + // Now get cli options. list($options, $unrecognized) = cli_get_params(array( 'categoryid' => false, 'destination' => '', + 'preset' => 'full', + // Fine-grained overrides (optional). If not provided, preset (if any) applies. + 'users' => null, + 'questionbank' => null, + 'calendarevents' => null, + 'competencies' => null, + 'histories' => null, + 'logs' => null, 'help' => false, ), array('h' => 'help')); @@ -35,8 +76,8 @@ cli_error(get_string('noadminaccount', 'tool_brcli')); } -// Do we need to store backup somewhere else? -$dir = rtrim($options['destination'], '/'); +// Normalise/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')); } @@ -46,7 +87,8 @@ cli_error(get_string('nocategory', 'tool_brcli')); } -$courses = $DB->get_records('course', array('category'=>$options['categoryid'])); +$categoryid = (int)$options['categoryid']; +$courses = $DB->get_records('course', array('category' => $categoryid)); $amount_of_courses = count($courses); $index = 1; @@ -57,24 +99,63 @@ mtrace(get_string('performingbck', 'tool_brcli', $index . '/' . $amount_of_courses)); + // Apply preset / overrides. + $preset = strtolower((string)$options['preset']); + if (!in_array($preset, ['full', 'contentonly'], true)) { + $bc->destroy(); + cli_error(get_string('invalidpreset', 'tool_brcli', $options['preset'])); + } + + $plan = $bc->get_plan(); + + // contentonly = course content without users and typical user-related/course-history data. + if ($preset === 'contentonly') { + tool_brcli_backup_set_if_exists($plan, 'users', 0); + tool_brcli_backup_set_if_exists($plan, 'role_assignments', 0); + tool_brcli_backup_set_if_exists($plan, 'groups', 0); + tool_brcli_backup_set_if_exists($plan, 'comments', 0); + tool_brcli_backup_set_if_exists($plan, 'badges', 0); + tool_brcli_backup_set_if_exists($plan, 'calendarevents', 0); + tool_brcli_backup_set_if_exists($plan, 'userscompletion', 0); + tool_brcli_backup_set_if_exists($plan, 'histories', 0); + tool_brcli_backup_set_if_exists($plan, 'logs', 0); + tool_brcli_backup_set_if_exists($plan, 'questionbank', 0); + tool_brcli_backup_set_if_exists($plan, 'competencies', 0); + tool_brcli_backup_set_if_exists($plan, 'contentbankcontent', 0); + } + + // Fine-grained overrides (only apply if option was provided). + foreach (['users', 'questionbank', 'calendarevents', 'competencies', 'histories', 'logs'] as $name) { + if ($options[$name] !== null) { + tool_brcli_backup_set_if_exists($plan, $name, (int)$options[$name]); + } + } + // 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) { + // Ignore. + } $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; // May be empty if file already moved to target location. // Do we need to store backup somewhere else? 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')); diff --git a/lang/en/tool_brcli.php b/lang/en/tool_brcli.php index 7a4b66c..7e2eb20 100644 --- a/lang/en/tool_brcli.php +++ b/lang/en/tool_brcli.php @@ -16,16 +16,27 @@ $string['performingres'] = 'Restoring backup of the {$a} course...'; $string['operationdone'] = 'Done!'; $string['invalidbackupfile'] = 'Invalid backup file: {$a}'; +$string['invalidpreset'] = 'Invalid preset: {$a}. Supported values: full, contentonly.'; $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. +--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. @@ -33,8 +44,18 @@ Options: --categoryid=INTEGER Category ID where the backup must be restored. --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 '; diff --git a/lang/pt_br/tool_brcli.php b/lang/pt_br/tool_brcli.php index 8b1e3fe..5558b86 100644 --- a/lang/pt_br/tool_brcli.php +++ b/lang/pt_br/tool_brcli.php @@ -16,16 +16,27 @@ $string['performingres'] = 'Restaurando backup do curso {$a}...'; $string['operationdone'] = 'Finalizado!'; $string['invalidbackupfile'] = 'Arquivo de backup inválido: {$a}'; +$string['invalidpreset'] = 'Preset inválido: {$a}. Valores suportados: full, contentonly.'; $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. +--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/ + + # 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'] = @@ -34,8 +45,18 @@ Options: --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/ + + # 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 '; \ No newline at end of file diff --git a/restore.php b/restore.php index caf65d7..6ac41ed 100644 --- a/restore.php +++ b/restore.php @@ -1,22 +1,61 @@ . + /** - * 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'); +define('CLI_SCRIPT', true); + +require(__DIR__ . '/../../../config.php'); require_once($CFG->libdir.'/clilib.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); +/** + * Safely set a restore plan setting if it exists. + * + * @param restore_plan $plan + * @param string $settingname + * @param mixed $value + */ +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. + } +} + // Now get cli options. list($options, $unrecognized) = cli_get_params(array( 'categoryid' => false, 'source' => '', + 'preset' => 'full', + // Fine-grained overrides (optional). + 'users' => null, + 'questionbank' => null, + 'calendarevents' => null, + 'competencies' => null, + 'histories' => null, + 'logs' => null, 'help' => false, ), array('h' => 'help')); @@ -35,7 +74,7 @@ cli_error(get_string('noadminaccount', 'tool_brcli')); } -$dir = rtrim($options['source'], '/'); +$dir = rtrim($options['source'], "/\\"); if (empty($dir) || !file_exists($dir) || !is_dir($dir)) { cli_error(get_string('directoryerror', 'tool_brcli')); } @@ -45,12 +84,26 @@ cli_error(get_string('nocategory', 'tool_brcli')); } + +$preset = strtolower((string)$options['preset']); +if (!in_array($preset, ['full', 'contentonly'], true)) { + cli_error(get_string('invalidpreset', 'tool_brcli', $options['preset'])); +} + $index = 1; $sourcefiles = new FilesystemIterator($dir, FilesystemIterator::SKIP_DOTS); -$amount_of_courses = iterator_count($sourcefiles); +// We count only .mbz files for progress reporting. +$amount_of_courses = 0; +foreach ($sourcefiles as $f) { + if (strtolower((string)$f->getExtension()) === 'mbz') { + $amount_of_courses++; + } +} +// Rewind iterator. +$sourcefiles = new FilesystemIterator($dir, FilesystemIterator::SKIP_DOTS); foreach ($sourcefiles as $sourcefile) { - if ($sourcefile->getExtension() !== 'mbz') { + if (strtolower((string)$sourcefile->getExtension()) !== 'mbz') { continue; } @@ -62,41 +115,83 @@ $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 + $folder = $backupid; // As found in $CFG->dataroot . '/temp/backup/'. + $categoryid = (int)$options['categoryid']; $userdoingrestore = $admin->id; // e.g. 2 == admin $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 / overrides. + $plan = $controller->get_plan(); + if ($preset === 'contentonly') { + tool_brcli_restore_set_if_exists($plan, 'users', 0); + tool_brcli_restore_set_if_exists($plan, 'role_assignments', 0); + tool_brcli_restore_set_if_exists($plan, 'groups', 0); + tool_brcli_restore_set_if_exists($plan, 'comments', 0); + tool_brcli_restore_set_if_exists($plan, 'badges', 0); + tool_brcli_restore_set_if_exists($plan, 'calendarevents', 0); + tool_brcli_restore_set_if_exists($plan, 'userscompletion', 0); + tool_brcli_restore_set_if_exists($plan, 'histories', 0); + tool_brcli_restore_set_if_exists($plan, 'logs', 0); + tool_brcli_restore_set_if_exists($plan, 'questionbank', 0); + tool_brcli_restore_set_if_exists($plan, 'competencies', 0); + tool_brcli_restore_set_if_exists($plan, 'contentbankcontent', 0); + } + + foreach (['users', 'questionbank', 'calendarevents', 'competencies', 'histories', 'logs'] as $name) { + if ($options[$name] !== null) { + tool_brcli_restore_set_if_exists($plan, $name, (int)$options[$name]); + } + } + + $precheck = $controller->execute_precheck(); + if ($precheck !== true) { try { - $transaction->rollback(new Exception('Prechecked failed')); + $transaction->rollback(new Exception('Precheck failed')); } catch (Exception $e) { - unset($transaction); - $controller->destroy(); - unset($controller); - continue; + // Ignore. } + 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')); diff --git a/version.php b/version.php index 25b1a56..e9d5eea 100644 --- a/version.php +++ b/version.php @@ -1,15 +1,37 @@ . + /** - * 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; // YYYYMMDDXX. + +// Require Moodle 4.5.0 (Build: 2024100700) or later. +$plugin->requires = 2024100700.00; + +// Explicitly declare supported major versions (introduced in Moodle 3.9). +$plugin->supported = [405, 501]; + +$plugin->maturity = MATURITY_STABLE; +$plugin->release = 'v1.3'; From 342d178d323b4d1095b12a185870eefd26a01eb6 Mon Sep 17 00:00:00 2001 From: ralferlebach Date: Thu, 5 Mar 2026 12:48:52 +0100 Subject: [PATCH 2/7] Refactoring and adding Unit-Test --- .github/workflows/moodle-ci.yml | 95 +++++++++++++++++++++++++++++++++ backup.php | 38 +++++-------- classes/local/preset.php | 82 ++++++++++++++++++++++++++++ restore.php | 33 +++++------- tests/preset_test.php | 58 ++++++++++++++++++++ 5 files changed, 261 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/moodle-ci.yml create mode 100644 classes/local/preset.php create mode 100644 tests/preset_test.php 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/backup.php b/backup.php index 1de4c32..9567e4f 100644 --- a/backup.php +++ b/backup.php @@ -100,37 +100,25 @@ function tool_brcli_backup_set_if_exists(backup_plan $plan, string $settingname, mtrace(get_string('performingbck', 'tool_brcli', $index . '/' . $amount_of_courses)); // Apply preset / overrides. - $preset = strtolower((string)$options['preset']); - if (!in_array($preset, ['full', 'contentonly'], true)) { - $bc->destroy(); - cli_error(get_string('invalidpreset', 'tool_brcli', $options['preset'])); - } - $plan = $bc->get_plan(); - - // contentonly = course content without users and typical user-related/course-history data. - if ($preset === 'contentonly') { - tool_brcli_backup_set_if_exists($plan, 'users', 0); - tool_brcli_backup_set_if_exists($plan, 'role_assignments', 0); - tool_brcli_backup_set_if_exists($plan, 'groups', 0); - tool_brcli_backup_set_if_exists($plan, 'comments', 0); - tool_brcli_backup_set_if_exists($plan, 'badges', 0); - tool_brcli_backup_set_if_exists($plan, 'calendarevents', 0); - tool_brcli_backup_set_if_exists($plan, 'userscompletion', 0); - tool_brcli_backup_set_if_exists($plan, 'histories', 0); - tool_brcli_backup_set_if_exists($plan, 'logs', 0); - tool_brcli_backup_set_if_exists($plan, 'questionbank', 0); - tool_brcli_backup_set_if_exists($plan, 'competencies', 0); - tool_brcli_backup_set_if_exists($plan, 'contentbankcontent', 0); - } - - // Fine-grained overrides (only apply if option was provided). + $overrides = []; foreach (['users', 'questionbank', 'calendarevents', 'competencies', 'histories', 'logs'] as $name) { if ($options[$name] !== null) { - tool_brcli_backup_set_if_exists($plan, $name, (int)$options[$name]); + $overrides[$name] = (int)$options[$name]; } } + try { + $settings = \tool_brcli\local\preset::build_settings((string)$options['preset'], $overrides); + } catch (invalid_argument_exception $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(); diff --git a/classes/local/preset.php b/classes/local/preset.php new file mode 100644 index 0000000..d2f2b71 --- /dev/null +++ b/classes/local/preset.php @@ -0,0 +1,82 @@ +. + +/** + * Preset + overrides handling for backup/restore plans. + * + * Extracted to make CLI behaviour testable and stable across Moodle versions. + * + * @package tool_brcli + * @copyright 2026 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_brcli\local; + +use invalid_argument_exception; + +final class preset { + public const PRESET_FULL = 'full'; + public const PRESET_CONTENTONLY = 'contentonly'; + + /** + * Returns plan settings based on preset + overrides. + * + * @param string $preset Preset name. + * @param array $overrides e.g. ['users' => 0]. Values are cast to int. + * @param null|array $available If provided, only settings in this whitelist are returned. + * @return array + */ + 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)) { + throw new invalid_argument_exception('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/restore.php b/restore.php index 6ac41ed..c5f6fe4 100644 --- a/restore.php +++ b/restore.php @@ -85,10 +85,7 @@ function tool_brcli_restore_set_if_exists(restore_plan $plan, string $settingnam } -$preset = strtolower((string)$options['preset']); -if (!in_array($preset, ['full', 'contentonly'], true)) { - cli_error(get_string('invalidpreset', 'tool_brcli', $options['preset'])); -} +$preset = (string)$options['preset']; $index = 1; $sourcefiles = new FilesystemIterator($dir, FilesystemIterator::SKIP_DOTS); @@ -140,27 +137,23 @@ function tool_brcli_restore_set_if_exists(restore_plan $plan, string $settingnam // Apply preset / overrides. $plan = $controller->get_plan(); - if ($preset === 'contentonly') { - tool_brcli_restore_set_if_exists($plan, 'users', 0); - tool_brcli_restore_set_if_exists($plan, 'role_assignments', 0); - tool_brcli_restore_set_if_exists($plan, 'groups', 0); - tool_brcli_restore_set_if_exists($plan, 'comments', 0); - tool_brcli_restore_set_if_exists($plan, 'badges', 0); - tool_brcli_restore_set_if_exists($plan, 'calendarevents', 0); - tool_brcli_restore_set_if_exists($plan, 'userscompletion', 0); - tool_brcli_restore_set_if_exists($plan, 'histories', 0); - tool_brcli_restore_set_if_exists($plan, 'logs', 0); - tool_brcli_restore_set_if_exists($plan, 'questionbank', 0); - tool_brcli_restore_set_if_exists($plan, 'competencies', 0); - tool_brcli_restore_set_if_exists($plan, 'contentbankcontent', 0); - } - + $overrides = []; foreach (['users', 'questionbank', 'calendarevents', 'competencies', 'histories', 'logs'] as $name) { if ($options[$name] !== null) { - tool_brcli_restore_set_if_exists($plan, $name, (int)$options[$name]); + $overrides[$name] = (int)$options[$name]; } } + try { + $settings = \tool_brcli\local\preset::build_settings($preset, $overrides); + } catch (invalid_argument_exception $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 { diff --git a/tests/preset_test.php b/tests/preset_test.php new file mode 100644 index 0000000..1dc7347 --- /dev/null +++ b/tests/preset_test.php @@ -0,0 +1,58 @@ +. + +/** + * Unit tests for tool_brcli presets. + * + * @package tool_brcli + */ + +declare(strict_types=1); + +use tool_brcli\local\preset; + +final class tool_brcli_preset_test extends basic_testcase { + + public function test_full_preset_is_empty_by_default(): void { + $settings = preset::build_settings('full'); + $this->assertSame([], $settings); + } + + 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']); + } + + 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']); + } + + public function test_available_filtering_works(): void { + $settings = preset::build_settings('contentonly', [], ['users', 'questionbank']); + $this->assertSame(['users' => 0, 'questionbank' => 0], $settings); + } + + public function test_invalid_preset_throws(): void { + $this->expectException(invalid_argument_exception::class); + preset::build_settings('nope'); + } +} From 5460930ef3c1b4d778dc0116eb04a453fd9ccc2a Mon Sep 17 00:00:00 2001 From: ralferlebach Date: Thu, 5 Mar 2026 13:30:51 +0100 Subject: [PATCH 3/7] Fix code check issues --- backup.php | 101 ++++++++++++++++++++++---------------- classes/local/preset.php | 55 ++++++++++++--------- lang/en/tool_brcli.php | 41 +++++++++++----- lang/pt_br/tool_brcli.php | 40 ++++++++++----- restore.php | 100 ++++++++++++++++++++----------------- tests/preset_test.php | 46 ++++++++++++++++- version.php | 9 +--- 7 files changed, 246 insertions(+), 146 deletions(-) diff --git a/backup.php b/backup.php index 9567e4f..ce1e7e9 100644 --- a/backup.php +++ b/backup.php @@ -25,41 +25,48 @@ define('CLI_SCRIPT', true); require(__DIR__ . '/../../../config.php'); -require_once($CFG->libdir.'/clilib.php'); +require_once($CFG->libdir . '/clilib.php'); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); /** * Safely set a backup plan setting if it exists. * - * Moodle versions and backup modes may expose different settings, so we set only when present. + * Moodle versions and backup modes may expose different settings, + * so we set only when present. * - * @param backup_plan $plan - * @param string $settingname - * @param mixed $value + * @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) { + } catch (\Exception $e) { // Setting does not exist in this Moodle version or mode. + $e; // Prevent unused variable warning. } } -// Now get cli options. -list($options, $unrecognized) = cli_get_params(array( - 'categoryid' => false, - 'destination' => '', - 'preset' => 'full', - // Fine-grained overrides (optional). If not provided, preset (if any) applies. - 'users' => null, - 'questionbank' => null, - 'calendarevents' => null, - 'competencies' => null, - 'histories' => null, - 'logs' => null, - 'help' => false, - ), array('h' => 'help')); +// Now get CLI options. +list($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); @@ -76,41 +83,47 @@ function tool_brcli_backup_set_if_exists(backup_plan $plan, string $settingname, cli_error(get_string('noadminaccount', 'tool_brcli')); } -// Normalise/validate 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')); -} +} -$categoryid = (int)$options['categoryid']; -$courses = $DB->get_records('course', array('category' => $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)); - - // Apply preset / overrides. + $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]; + $overrides[$name] = (int) $options[$name]; } } try { - $settings = \tool_brcli\local\preset::build_settings((string)$options['preset'], $overrides); - } catch (invalid_argument_exception $e) { + $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'])); } @@ -126,10 +139,11 @@ function tool_brcli_backup_set_if_exists(backup_plan $plan, string $settingname, $users = 1; $anonymised = 0; try { - $users = (int)$plan->get_setting('users')->get_value(); - $anonymised = (int)$plan->get_setting('anonymize')->get_value(); - } catch (Exception $e) { - // Ignore. + $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); tool_brcli_backup_set_if_exists($plan, 'filename', $filename); @@ -138,9 +152,9 @@ function tool_brcli_backup_set_if_exists(backup_plan $plan, string $settingname, $bc->finish_ui(); $bc->execute_plan(); $results = $bc->get_results(); - $file = $results['backup_destination'] ?? null; // 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) { $target = $dir . '/' . $filename; if ($file->copy_content_to($target)) { @@ -150,8 +164,9 @@ function tool_brcli_backup_set_if_exists(backup_plan $plan, string $settingname, } } $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 index d2f2b71..5b5622b 100644 --- a/classes/local/preset.php +++ b/classes/local/preset.php @@ -15,52 +15,63 @@ // along with Moodle. If not, see . /** - * Preset + overrides handling for backup/restore plans. + * Preset and overrides handling for backup/restore plans. * * Extracted to make CLI behaviour testable and stable across Moodle versions. * * @package tool_brcli - * @copyright 2026 + * @copyright 2026 Ralf Erlebach * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_brcli\local; -use invalid_argument_exception; - +/** + * 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 + overrides. + * Returns plan settings based on preset and overrides. * - * @param string $preset Preset name. - * @param array $overrides e.g. ['users' => 0]. Values are cast to int. - * @param null|array $available If provided, only settings in this whitelist are returned. - * @return array + * @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)) { - throw new invalid_argument_exception('Invalid preset'); + // 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, + '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, ]; } @@ -69,7 +80,7 @@ public static function build_settings(string $preset, array $overrides = [], ?ar if ($value === null) { continue; } - $settings[(string)$name] = (int)$value; + $settings[(string) $name] = (int) $value; } if ($available !== null) { diff --git a/lang/en/tool_brcli.php b/lang/en/tool_brcli.php index 7e2eb20..c782c95 100644 --- a/lang/en/tool_brcli.php +++ b/lang/en/tool_brcli.php @@ -1,28 +1,44 @@ . + /** - * 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 */ + +defined('MOODLE_INTERNAL') || die(); + $string['pluginname'] = 'Backup and Restore Command-Line Interface'; -$string['unknowoption'] = 'Unknow option: {$a}'; +$string['unknowoption'] = 'Unknown option: {$a}'; $string['noadminaccount'] = 'Error: No admin account was found!'; -$string['directoryerror'] = 'Error: Destination directory does not exists or not writable!'; +$string['directoryerror'] = 'Error: Destination directory does not exist or is 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['invalidpreset'] = 'Invalid preset: {$a}. Supported values: full, contentonly.'; -$string['helpoptionbck'] = -'Perform backup of the courses of a specific category. +$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. @@ -38,12 +54,11 @@ # 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. diff --git a/lang/pt_br/tool_brcli.php b/lang/pt_br/tool_brcli.php index 5558b86..f2b36ed 100644 --- a/lang/pt_br/tool_brcli.php +++ b/lang/pt_br/tool_brcli.php @@ -1,12 +1,29 @@ . + /** - * 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 */ + +defined('MOODLE_INTERNAL') || die(); + $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!'; @@ -17,12 +34,11 @@ $string['operationdone'] = 'Finalizado!'; $string['invalidbackupfile'] = 'Arquivo de backup inválido: {$a}'; $string['invalidpreset'] = 'Preset inválido: {$a}. Valores suportados: full, contentonly.'; -$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. @@ -38,11 +54,9 @@ # 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. -$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. @@ -59,4 +73,4 @@ # 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 -'; \ No newline at end of file +'; diff --git a/restore.php b/restore.php index c5f6fe4..0e40b8e 100644 --- a/restore.php +++ b/restore.php @@ -25,39 +25,45 @@ define('CLI_SCRIPT', true); require(__DIR__ . '/../../../config.php'); -require_once($CFG->libdir.'/clilib.php'); +require_once($CFG->libdir . '/clilib.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Safely set a restore plan setting if it exists. * - * @param restore_plan $plan - * @param string $settingname - * @param mixed $value + * @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) { + } catch (\Exception $e) { // Setting does not exist in this Moodle version or mode. + $e; // Prevent unused variable warning. } } -// Now get cli options. -list($options, $unrecognized) = cli_get_params(array( - 'categoryid' => false, - 'source' => '', - 'preset' => 'full', - // Fine-grained overrides (optional). - 'users' => null, - 'questionbank' => null, - 'calendarevents' => null, - 'competencies' => null, - 'histories' => null, - 'logs' => null, - 'help' => false, - ), array('h' => 'help')); +// Now get CLI options. +list($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); @@ -75,41 +81,42 @@ function tool_brcli_restore_set_if_exists(restore_plan $plan, string $settingnam } $dir = rtrim($options['source'], "/\\"); -if (empty($dir) || !file_exists($dir) || !is_dir($dir)) { +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']; +$preset = (string) $options['preset']; $index = 1; -$sourcefiles = new FilesystemIterator($dir, FilesystemIterator::SKIP_DOTS); -// We count only .mbz files for progress reporting. -$amount_of_courses = 0; +$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') { - $amount_of_courses++; + if (strtolower((string) $f->getExtension()) === 'mbz') { + $amountofcourses++; } } + // Rewind iterator. -$sourcefiles = new FilesystemIterator($dir, FilesystemIterator::SKIP_DOTS); +$sourcefiles = new \FilesystemIterator($dir, \FilesystemIterator::SKIP_DOTS); foreach ($sourcefiles as $sourcefile) { - if (strtolower((string)$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++; @@ -118,13 +125,13 @@ function tool_brcli_restore_set_if_exists(restore_plan $plan, string $settingnam // Transaction. $transaction = $DB->start_delegated_transaction(); - + // Create new course. - $folder = $backupid; // As found in $CFG->dataroot . '/temp/backup/'. - $categoryid = (int)$options['categoryid']; - $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, @@ -135,18 +142,18 @@ function tool_brcli_restore_set_if_exists(restore_plan $plan, string $settingnam backup::TARGET_NEW_COURSE ); - // Apply preset / overrides. + // 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]; + $overrides[$name] = (int) $options[$name]; } } try { $settings = \tool_brcli\local\preset::build_settings($preset, $overrides); - } catch (invalid_argument_exception $e) { + } catch (\InvalidArgumentException $e) { cli_error(get_string('invalidpreset', 'tool_brcli', $preset)); } @@ -157,9 +164,10 @@ function tool_brcli_restore_set_if_exists(restore_plan $plan, string $settingnam $precheck = $controller->execute_precheck(); if ($precheck !== true) { try { - $transaction->rollback(new Exception('Precheck failed')); - } catch (Exception $e) { - // Ignore. + $transaction->rollback(new \Exception('Precheck failed')); + } catch (\Exception $e) { + // Ignore rollback exceptions. + $e; // Prevent unused variable warning. } unset($transaction); $controller->destroy(); @@ -189,4 +197,4 @@ function tool_brcli_restore_set_if_exists(restore_plan $plan, string $settingnam 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 index 1dc7347..dc54e26 100644 --- a/tests/preset_test.php +++ b/tests/preset_test.php @@ -18,19 +18,43 @@ * 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; -final class tool_brcli_preset_test extends basic_testcase { +/** + * 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); @@ -40,19 +64,37 @@ public function test_contentonly_preset_contains_expected_core_flags(): void { $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(invalid_argument_exception::class); + $this->expectException(\InvalidArgumentException::class); preset::build_settings('nope'); } } diff --git a/version.php b/version.php index e9d5eea..7cd5a1a 100644 --- a/version.php +++ b/version.php @@ -25,13 +25,8 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'tool_brcli'; -$plugin->version = 2026030500; // YYYYMMDDXX. - -// Require Moodle 4.5.0 (Build: 2024100700) or later. -$plugin->requires = 2024100700.00; - -// Explicitly declare supported major versions (introduced in Moodle 3.9). +$plugin->version = 2026030500; +$plugin->requires = 2024100700; $plugin->supported = [405, 501]; - $plugin->maturity = MATURITY_STABLE; $plugin->release = 'v1.3'; From 8fcbbe214b85e12ac56b966a61883ae8db0e679b Mon Sep 17 00:00:00 2001 From: ralferlebach Date: Thu, 5 Mar 2026 13:39:48 +0100 Subject: [PATCH 4/7] CodeCheck --- version.php | 1 + 1 file changed, 1 insertion(+) diff --git a/version.php b/version.php index 7cd5a1a..8436b14 100644 --- a/version.php +++ b/version.php @@ -24,6 +24,7 @@ defined('MOODLE_INTERNAL') || die(); + $plugin->component = 'tool_brcli'; $plugin->version = 2026030500; $plugin->requires = 2024100700; From 66944cdbcd5292641c595b949ed2b171ae2cf0b9 Mon Sep 17 00:00:00 2001 From: ralferlebach Date: Thu, 5 Mar 2026 14:51:28 +0100 Subject: [PATCH 5/7] CodeChecks --- .idea/.gitignore | 8 ++++++++ .idea/brcli.iml | 11 +++++++++++ .idea/modules.xml | 8 ++++++++ .idea/php.xml | 22 ++++++++++++++++++++++ .idea/vcs.xml | 7 +++++++ backup.php | 2 +- classes/local/preset.php | 1 - lang/en/tool_brcli.php | 18 +++++++++--------- lang/pt_br/tool_brcli.php | 18 +++++++++--------- restore.php | 2 +- tests/preset_test.php | 1 - version.php | 1 - 12 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/brcli.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/php.xml create mode 100644 .idea/vcs.xml 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/backup.php b/backup.php index ce1e7e9..af2e427 100644 --- a/backup.php +++ b/backup.php @@ -50,7 +50,7 @@ function tool_brcli_backup_set_if_exists(backup_plan $plan, string $settingname, } // Now get CLI options. -list($options, $unrecognized) = cli_get_params( +[$options, $unrecognized] = cli_get_params( [ 'categoryid' => false, 'destination' => '', diff --git a/classes/local/preset.php b/classes/local/preset.php index 5b5622b..3547f45 100644 --- a/classes/local/preset.php +++ b/classes/local/preset.php @@ -34,7 +34,6 @@ * @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'; diff --git a/lang/en/tool_brcli.php b/lang/en/tool_brcli.php index c782c95..eee04a4 100644 --- a/lang/en/tool_brcli.php +++ b/lang/en/tool_brcli.php @@ -24,16 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$string['pluginname'] = 'Backup and Restore Command-Line Interface'; -$string['unknowoption'] = 'Unknown option: {$a}'; -$string['noadminaccount'] = 'Error: No admin account was found!'; $string['directoryerror'] = 'Error: Destination directory does not exist or is 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['invalidpreset'] = 'Invalid preset: {$a}. Supported values: full, contentonly.'; $string['helpoptionbck'] = 'Perform backup of the courses of a specific category. Options: @@ -74,3 +65,12 @@ # 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 f2b36ed..6c325db 100644 --- a/lang/pt_br/tool_brcli.php +++ b/lang/pt_br/tool_brcli.php @@ -24,16 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$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!'; $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['invalidpreset'] = 'Preset inválido: {$a}. Valores suportados: full, contentonly.'; $string['helpoptionbck'] = 'Realiza o backup de todos os cursos de uma categoria. Opções: @@ -74,3 +65,12 @@ # 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['unknowoption'] = 'Opção inválida: {$a}'; +$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['operationdone'] = 'Finalizado!'; diff --git a/restore.php b/restore.php index 0e40b8e..13cce64 100644 --- a/restore.php +++ b/restore.php @@ -47,7 +47,7 @@ function tool_brcli_restore_set_if_exists(restore_plan $plan, string $settingnam } // Now get CLI options. -list($options, $unrecognized) = cli_get_params( +[$options, $unrecognized] = cli_get_params( [ 'categoryid' => false, 'source' => '', diff --git a/tests/preset_test.php b/tests/preset_test.php index dc54e26..4f34885 100644 --- a/tests/preset_test.php +++ b/tests/preset_test.php @@ -37,7 +37,6 @@ * @coversDefaultClass \tool_brcli\local\preset */ final class preset_test extends \basic_testcase { - /** * Test that the full preset returns an empty settings array by default. * diff --git a/version.php b/version.php index 8436b14..7cd5a1a 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,6 @@ defined('MOODLE_INTERNAL') || die(); - $plugin->component = 'tool_brcli'; $plugin->version = 2026030500; $plugin->requires = 2024100700; From 67e7bbeae3cf4098a291586a707c1546b8d5b7ea Mon Sep 17 00:00:00 2001 From: ralferlebach Date: Thu, 5 Mar 2026 14:55:54 +0100 Subject: [PATCH 6/7] CodeChecks --- lang/pt_br/tool_brcli.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/pt_br/tool_brcli.php b/lang/pt_br/tool_brcli.php index 6c325db..57165a9 100644 --- a/lang/pt_br/tool_brcli.php +++ b/lang/pt_br/tool_brcli.php @@ -69,8 +69,8 @@ $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['unknowoption'] = 'Opção inválida: {$a}'; +$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['operationdone'] = 'Finalizado!'; +$string['unknowoption'] = 'Opção inválida: {$a}'; From 55f555cd916a672f7f90a56f0f3935ad3d49edd3 Mon Sep 17 00:00:00 2001 From: ralferlebach Date: Thu, 5 Mar 2026 15:00:46 +0100 Subject: [PATCH 7/7] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) 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.