From 0d18f6d0d4d127efeb6ef44d209958b2ce93d311 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 14 Feb 2023 23:13:42 +0800 Subject: [PATCH] MDL-76867 core_admin: Migrate blocks admin ui to plugin management --- admin/amd/build/block_management_table.min.js | 3 + .../build/block_management_table.min.js.map | 1 + admin/amd/src/block_management_table.js | 72 ++++ admin/blocks.php | 312 +++++------------- .../classes/external/set_block_protection.php | 97 ++++++ .../classes/table/block_management_table.php | 153 +++++++++ admin/tests/behat/manage_blocks.feature | 59 ++++ .../external/set_block_protection_test.php | 112 +++++++ .../tests/behat/apply_presets.feature | 4 +- .../tests/behat/revert_changes.feature | 8 +- .../behat/glossary_random_addblock.feature | 8 +- .../glossary_random_addblock_disabled.feature | 10 +- .../behat/configure_tag_youtube_block.feature | 12 +- lang/en/admin.php | 8 +- lang/en/deprecated.txt | 1 + lib/db/services.php | 6 + .../tiny/tests/plugininfo/tiny_test.php | 14 + lib/tests/plugininfo/block_test.php | 86 +++++ version.php | 2 +- 19 files changed, 714 insertions(+), 254 deletions(-) create mode 100644 admin/amd/build/block_management_table.min.js create mode 100644 admin/amd/build/block_management_table.min.js.map create mode 100644 admin/amd/src/block_management_table.js create mode 100644 admin/classes/external/set_block_protection.php create mode 100644 admin/classes/table/block_management_table.php create mode 100644 admin/tests/behat/manage_blocks.feature create mode 100644 admin/tests/external/set_block_protection_test.php create mode 100644 lib/tests/plugininfo/block_test.php diff --git a/admin/amd/build/block_management_table.min.js b/admin/amd/build/block_management_table.min.js new file mode 100644 index 0000000000000..7a311b2e95c24 --- /dev/null +++ b/admin/amd/build/block_management_table.min.js @@ -0,0 +1,3 @@ +define("core_admin/block_management_table",["exports","./plugin_management_table","core_table/dynamic","core/ajax","core/pending","core/notification"],(function(_exports,_plugin_management_table,_dynamic,_ajax,_pending,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_plugin_management_table=_interopRequireDefault(_plugin_management_table),_pending=_interopRequireDefault(_pending);class _default extends _plugin_management_table.default{constructor(){super(),this.addClickHandler(this.handleBlockProtectToggle)}setBlockProtectState(plugin,state){return(0,_ajax.call)([{methodname:"core_admin_set_block_protection",args:{plugin:plugin,state:state}}])[0]}async handleBlockProtectToggle(tableRoot,e){const stateToggle=e.target.closest('[data-action="toggleprotectstate"]');if(stateToggle){e.preventDefault();const pendingPromise=new _pending.default("core_table/dynamic:processAction");await this.setBlockProtectState(stateToggle.dataset.plugin,"1"===stateToggle.dataset.targetState?1:0);const[updatedRoot]=await Promise.all([(0,_dynamic.refreshTableContent)(tableRoot),(0,_notification.fetchNotifications)()]);updatedRoot.querySelector('[data-action="toggleprotectstate"][data-plugin="'.concat(stateToggle.dataset.plugin,'"]')).focus(),pendingPromise.resolve()}}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=block_management_table.min.js.map \ No newline at end of file diff --git a/admin/amd/build/block_management_table.min.js.map b/admin/amd/build/block_management_table.min.js.map new file mode 100644 index 0000000000000..14210b0352a96 --- /dev/null +++ b/admin/amd/build/block_management_table.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"block_management_table.min.js","sources":["../src/block_management_table.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport PluginManagementTable from './plugin_management_table';\nimport {refreshTableContent} from 'core_table/dynamic';\nimport {call as fetchMany} from 'core/ajax';\nimport Pending from 'core/pending';\nimport {fetchNotifications} from 'core/notification';\n\nexport default class extends PluginManagementTable {\n constructor() {\n super();\n this.addClickHandler(this.handleBlockProtectToggle);\n }\n\n /**\n * Set the block protection state.\n *\n * @param {string} plugin\n * @param {number} state\n * @returns {Promise}\n */\n setBlockProtectState(plugin, state) {\n return fetchMany([{\n methodname: 'core_admin_set_block_protection',\n args: {\n plugin,\n state,\n },\n }])[0];\n }\n\n /**\n * Handle toggling of block protection.\n *\n * @param {HTMLElement} tableRoot\n * @param {Event} e\n */\n async handleBlockProtectToggle(tableRoot, e) {\n const stateToggle = e.target.closest('[data-action=\"toggleprotectstate\"]');\n if (stateToggle) {\n e.preventDefault();\n const pendingPromise = new Pending('core_table/dynamic:processAction');\n\n await this.setBlockProtectState(\n stateToggle.dataset.plugin,\n stateToggle.dataset.targetState === '1' ? 1 : 0\n );\n\n const [updatedRoot] = await Promise.all([\n refreshTableContent(tableRoot),\n fetchNotifications(),\n ]);\n\n // Refocus on the link that as pressed in the first place.\n updatedRoot.querySelector(`[data-action=\"toggleprotectstate\"][data-plugin=\"${stateToggle.dataset.plugin}\"]`).focus();\n pendingPromise.resolve();\n }\n }\n}\n"],"names":["_interopRequireDefault","obj","__esModule","default","_plugin_management_table","_pending","_default","PluginManagementTable","constructor","super","this","addClickHandler","handleBlockProtectToggle","setBlockProtectState","plugin","state","fetchMany","call","methodname","args","async","tableRoot","e","stateToggle","target","closest","preventDefault","pendingPromise","Pending","dataset","targetState","updatedRoot","Promise","all","refreshTableContent","fetchNotifications","querySelector","concat","focus","resolve","_exports"],"mappings":"0OAkBmC,SAAAA,uBAAAC,KAAAA,OAAAA,KAAAA,IAAAC,WAAAD,IAAAE,CAAAA,QAAAF,IAAA,iFAHnCG,yBAAAJ,uBAAAI,0BAGAC,SAAAL,uBAAAK,UAGe,MAAAC,iBAAcC,yBAAAA,QACzBC,cACIC,QACAC,KAAKC,gBAAgBD,KAAKE,yBAC9B,CASAC,qBAAqBC,OAAQC,OACzB,OAAO,EAAAC,MAASC,MAAC,CAAC,CACdC,WAAY,kCACZC,KAAM,CACFL,cACAC,gBAEJ,EACR,CAQAK,+BAA+BC,UAAWC,GACtC,MAAMC,YAAcD,EAAEE,OAAOC,QAAQ,sCACrC,GAAIF,YAAa,CACbD,EAAEI,iBACF,MAAMC,eAAiB,IAAIC,SAAOzB,QAAC,0CAE7BO,KAAKG,qBACPU,YAAYM,QAAQf,OACgB,MAApCS,YAAYM,QAAQC,YAAsB,EAAI,GAGlD,MAAOC,mBAAqBC,QAAQC,IAAI,EACpC,EAAAC,SAAmBA,qBAACb,YACpB,EAAAc,cAAkBA,wBAItBJ,YAAYK,cAAaC,mDAAAA,OAAoDd,YAAYM,QAAQf,OAAW,OAACwB,QAC7GX,eAAeY,SACnB,CACJ,EACH,OAAAC,SAAArC,QAAAG,SAAAkC,SAAArC,OAAA"} \ No newline at end of file diff --git a/admin/amd/src/block_management_table.js b/admin/amd/src/block_management_table.js new file mode 100644 index 0000000000000..d7ddf58412599 --- /dev/null +++ b/admin/amd/src/block_management_table.js @@ -0,0 +1,72 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +import PluginManagementTable from './plugin_management_table'; +import {refreshTableContent} from 'core_table/dynamic'; +import {call as fetchMany} from 'core/ajax'; +import Pending from 'core/pending'; +import {fetchNotifications} from 'core/notification'; + +export default class extends PluginManagementTable { + constructor() { + super(); + this.addClickHandler(this.handleBlockProtectToggle); + } + + /** + * Set the block protection state. + * + * @param {string} plugin + * @param {number} state + * @returns {Promise} + */ + setBlockProtectState(plugin, state) { + return fetchMany([{ + methodname: 'core_admin_set_block_protection', + args: { + plugin, + state, + }, + }])[0]; + } + + /** + * Handle toggling of block protection. + * + * @param {HTMLElement} tableRoot + * @param {Event} e + */ + async handleBlockProtectToggle(tableRoot, e) { + const stateToggle = e.target.closest('[data-action="toggleprotectstate"]'); + if (stateToggle) { + e.preventDefault(); + const pendingPromise = new Pending('core_table/dynamic:processAction'); + + await this.setBlockProtectState( + stateToggle.dataset.plugin, + stateToggle.dataset.targetState === '1' ? 1 : 0 + ); + + const [updatedRoot] = await Promise.all([ + refreshTableContent(tableRoot), + fetchNotifications(), + ]); + + // Refocus on the link that as pressed in the first place. + updatedRoot.querySelector(`[data-action="toggleprotectstate"][data-plugin="${stateToggle.dataset.plugin}"]`).focus(); + pendingPromise.resolve(); + } + } +} diff --git a/admin/blocks.php b/admin/blocks.php index 24bb9d552224a..7d2355080cf82 100644 --- a/admin/blocks.php +++ b/admin/blocks.php @@ -1,244 +1,94 @@ libdir.'/adminlib.php'); - require_once($CFG->libdir.'/blocklib.php'); - require_once($CFG->libdir.'/tablelib.php'); - - admin_externalpage_setup('manageblocks'); - - $confirm = optional_param('confirm', 0, PARAM_BOOL); - $hide = optional_param('hide', 0, PARAM_INT); - $show = optional_param('show', 0, PARAM_INT); - $unprotect = optional_param('unprotect', 0, PARAM_INT); - $protect = optional_param('protect', 0, PARAM_INT); - -/// Print headings - - $strmanageblocks = get_string('manageblocks'); - $struninstall = get_string('uninstallplugin', 'core_admin'); - $strversion = get_string('version'); - $strhide = get_string('hide'); - $strshow = get_string('show'); - $strsettings = get_string('settings'); - $strcourses = get_string('blockinstances', 'admin'); - $strname = get_string('name'); - $strshowblockcourse = get_string('showblockcourse'); - $strprotecthdr = get_string('blockprotect', 'admin'). $OUTPUT->help_icon('blockprotect','admin'); - $strprotect = get_string('blockprotect', 'admin'); - $strunprotect = get_string('blockunprotect', 'admin'); - - - // If data submitted, then process and store. - if (!empty($hide) && confirm_sesskey()) { - if (!$block = $DB->get_record('block', ['id' => $hide])) { - throw new \moodle_exception('blockdoesnotexist', 'error'); - } - - $class = \core_plugin_manager::resolve_plugininfo_class('block'); - $class::enable_plugin($block->name, false); +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Allows the admin to configure blocks (hide/show, uninstall and configure) + * + * @package core_admin + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../config.php'); +require_once("{$CFG->libdir}/adminlib.php"); +require_once("{$CFG->libdir}/blocklib.php"); +require_once("{$CFG->libdir}/tablelib.php"); + +admin_externalpage_setup('manageblocks'); + +$plugin = optional_param('plugin', '', PARAM_PLUGIN); +$action = optional_param('action', '', PARAM_ALPHA); +$unprotect = optional_param('unprotect', 0, PARAM_PLUGIN); +$protect = optional_param('protect', 0, PARAM_PLUGIN); + +$strmanageblocks = get_string('manageblocks'); + +// If data submitted, then process and store. +if (!empty($plugin) && !empty($action) && confirm_sesskey()) { + $manager = \core_plugin_manager::resolve_plugininfo_class('block'); + $pluginname = get_string('pluginname', "block_{$plugin}"); + + if ($action === 'disable' && $manager::enable_plugin($plugin, 0)) { + \core\notification::add( + get_string('plugin_disabled', 'core_admin', $pluginname), + \core\notification::SUCCESS + ); // Settings not required - only pages. admin_get_root(true, false); - } - - if (!empty($show) && confirm_sesskey() ) { - if (!$block = $DB->get_record('block', ['id' => $show])) { - throw new \moodle_exception('blockdoesnotexist', 'error'); - } + } else if ($action === 'enable' && $manager::enable_plugin($plugin, 1)) { + \core\notification::add( + get_string('plugin_enabled', 'core_admin', $pluginname), + \core\notification::SUCCESS + ); - $class = \core_plugin_manager::resolve_plugininfo_class('block'); - $class::enable_plugin($block->name, true); // Settings not required - only pages. admin_get_root(true, false); } - if (!empty($protect) && confirm_sesskey()) { - block_manager::protect_block((int)$protect); - admin_get_root(true, false); // settings not required - only pages - } - - if (!empty($unprotect) && confirm_sesskey()) { - block_manager::unprotect_block((int)$unprotect); - admin_get_root(true, false); // settings not required - only pages - } - - $undeletableblocktypes = block_manager::get_undeletable_block_types(); - - echo $OUTPUT->header(); - echo $OUTPUT->heading($strmanageblocks); - - echo $OUTPUT->notification(get_string('noteunneededblocks', 'admin'), 'info'); -/// Main display starts here - -// Get & sort the existing blocks, Select the id, name & visible fields along with count of number of total blocks & course blocks. - -$sql = "SELECT b.id, b.name, b.visible, COUNT(DISTINCT binst.id) as totalcount, COUNT(DISTINCT bcinst.id) as courseviewcount - FROM {block} b - LEFT JOIN {block_instances} binst ON binst.blockname = b.name - LEFT JOIN {block_instances} bcinst ON bcinst.blockname = b.name AND bcinst.pagetypepattern = 'course-view-*' - GROUP BY b.id, b.name, b.visible, binst.blockname, bcinst.blockname - ORDER BY b.name ASC"; -if (!$blocks = $DB->get_records_sql($sql)) { - throw new \moodle_exception('noblocks', 'error'); // Should never happen. + // Redirect back to the page with out any params. + redirect(new moodle_url('/admin/blocks.php')); } - $incompatible = array(); - -/// Print the table of all blocks - - $table = new flexible_table('admin-blocks-compatible'); - - $table->define_columns(array('name', 'instances', 'version', 'hideshow', 'undeletable', 'settings', 'uninstall')); - $table->define_headers(array($strname, $strcourses, $strversion, $strhide.'/'.$strshow, $strprotecthdr, $strsettings, $struninstall)); - $table->define_baseurl($CFG->wwwroot.'/'.$CFG->admin.'/blocks.php'); - $table->set_attribute('class', 'admintable blockstable generaltable'); - $table->set_attribute('id', 'compatibleblockstable'); - $table->setup(); - $tablerows = array(); - - // Sort blocks using current locale. - $blocknames = array(); - foreach ($blocks as $blockid=>$block) { - $blockname = $block->name; - if (file_exists("$CFG->dirroot/blocks/$blockname/block_$blockname.php")) { - $blocknames[$blockid] = get_string('pluginname', 'block_'.$blockname); - } else { - $blocknames[$blockid] = $blockname; - } - } - core_collator::asort($blocknames); - - foreach ($blocknames as $blockid=>$strblockname) { - $block = $blocks[$blockid]; - $blockname = $block->name; - $dbversion = get_config('block_'.$block->name, 'version'); - - if (!file_exists("$CFG->dirroot/blocks/$blockname/block_$blockname.php")) { - $blockobject = false; - $strblockname = ''.$strblockname.' ('.get_string('missingfromdisk').')'; - $plugin = new stdClass(); - $plugin->version = $dbversion; - - } else { - $plugin = new stdClass(); - $plugin->version = '???'; - if (file_exists("$CFG->dirroot/blocks/$blockname/version.php")) { - include("$CFG->dirroot/blocks/$blockname/version.php"); - } - - if (!$blockobject = block_instance($block->name)) { - $incompatible[] = $block; - continue; - } - } - - if ($uninstallurl = core_plugin_manager::instance()->get_uninstall_url('block_'.$blockname, 'manage')) { - $uninstall = html_writer::link($uninstallurl, $struninstall); - } else { - $uninstall = ''; - } - - $settings = ''; // By default, no configuration - if ($blockobject and $blockobject->has_config()) { - $blocksettings = admin_get_root()->locate('blocksetting' . $block->name); - - if ($blocksettings instanceof admin_externalpage) { - $settings = '' . get_string('settings') . ''; - } else if ($blocksettings instanceof admin_settingpage) { - $settings = ''.$strsettings.''; - } else if (!file_exists($CFG->dirroot.'/blocks/'.$block->name.'/settings.php')) { - // If the block's settings node was not found, we check that the block really provides the settings.php file. - // Note that blocks can inject their settings to other nodes in the admin tree without using the default locations. - // This can be done by assigning null to $setting in settings.php and it is a valid case. - debugging('Warning: block_'.$block->name.' returns true in has_config() but does not provide a settings.php file', - DEBUG_DEVELOPER); - } - } - - // MDL-11167, blocks can be placed on mymoodle, or the blogs page - // and it should not show up on course search page - - $totalcount = $blocks[$blockid]->totalcount; - $count = $blocks[$blockid]->courseviewcount; - if ($count>0) { - $blocklist = "wwwroot}/course/search.php?blocklist=$blockid&sesskey=".sesskey()."\" "; - $blocklist .= "title=\"$strshowblockcourse\" >$totalcount"; - } - else { - $blocklist = "$totalcount"; - } - $class = ''; // Nothing fancy, by default - - if (!$blockobject) { - // ignore - $visible = ''; - } else if ($blocks[$blockid]->visible) { - $visible = ''. - $OUTPUT->pix_icon('t/hide', $strhide) . ''; - } else { - $visible = ''. - $OUTPUT->pix_icon('t/show', $strshow) . ''; - $class = 'dimmed_text'; - } - - if ($dbversion == $plugin->version) { - $version = $dbversion; - } else { - $version = "$dbversion ($plugin->version)"; - } - - if (!$blockobject) { - // ignore - $undeletable = ''; - } else if (in_array($blockname, $undeletableblocktypes)) { - $undeletable = ''. - $OUTPUT->pix_icon('t/unlock', $strunprotect) . ''; - } else { - $undeletable = ''. - $OUTPUT->pix_icon('t/lock', $strprotect) . ''; - } - - $row = array( - $strblockname, - $blocklist, - $version, - $visible, - $undeletable, - $settings, - $uninstall, - ); - $table->add_data($row, $class); - } - - $table->print_html(); - - if (!empty($incompatible)) { - echo $OUTPUT->heading(get_string('incompatibleblocks', 'blockstable', 'admin')); - - $table = new flexible_table('admin-blocks-incompatible'); - - $table->define_columns(array('block', 'uninstall')); - $table->define_headers(array($strname, $struninstall)); - $table->define_baseurl($CFG->wwwroot.'/'.$CFG->admin.'/blocks.php'); - - $table->set_attribute('class', 'incompatibleblockstable generaltable'); +if (!empty($protect) && confirm_sesskey()) { + block_manager::protect_block($protect); + $pluginname = get_string('pluginname', "block_{$protect}"); + \core\notification::add( + get_string('blockprotected', 'core_admin', $pluginname), + \core\notification::SUCCESS + ); + // Settings not required - only pages. + admin_get_root(true, false); +} - $table->setup(); +if (!empty($unprotect) && confirm_sesskey()) { + block_manager::unprotect_block($unprotect); + $pluginname = get_string('pluginname', "block_{$unprotect}"); + \core\notification::add( + get_string('blockunprotected', 'core_admin', $pluginname), + \core\notification::SUCCESS + ); + // Settings not required - only pages. + admin_get_root(true, false); +} - foreach ($incompatible as $block) { - if ($uninstallurl = core_plugin_manager::instance()->get_uninstall_url('block_'.$block->name, 'manage')) { - $uninstall = html_writer::link($uninstallurl, $struninstall); - } else { - $uninstall = ''; - } - $table->add_data(array( - $block->name, - $uninstall, - )); - } - $table->print_html(); - } +echo $OUTPUT->header(); +echo $OUTPUT->heading($strmanageblocks); +echo $OUTPUT->notification(get_string('noteunneededblocks', 'admin'), 'info', false); - echo $OUTPUT->footer(); +// Print the table of all blocks. +$table = new \core_admin\table\block_management_table(); +$table->out(); +echo $OUTPUT->footer(); diff --git a/admin/classes/external/set_block_protection.php b/admin/classes/external/set_block_protection.php new file mode 100644 index 0000000000000..0bb5c22d61c56 --- /dev/null +++ b/admin/classes/external/set_block_protection.php @@ -0,0 +1,97 @@ +. + +namespace core_admin\external; + +use block_manager; +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_value; + +/** + * Web Service to control the state of a plugin. + * + * @package core_admin + * @category external + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class set_block_protection extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'plugin' => new external_value(PARAM_PLUGIN, 'The name of the plugin', VALUE_REQUIRED), + 'state' => new external_value(PARAM_INT, 'The target state', VALUE_REQUIRED), + ]); + } + + /** + * Set the block protection state. + * + * @param string $plugin The name of the plugin + * @param int $state The target state + * @return null + */ + public static function execute( + string $plugin, + int $state, + ): array { + [ + 'plugin' => $plugin, + 'state' => $state, + ] = self::validate_parameters(self::execute_parameters(), [ + 'plugin' => $plugin, + 'state' => $state, + ]); + + $context = \context_system::instance(); + self::validate_context($context); + require_capability('moodle/site:config', $context); + + [, $pluginname] = explode('_', \core_component::normalize_componentname($plugin), 2); + $displayname = get_string('pluginname', $plugin); + + if ($state) { + block_manager::protect_block($pluginname); + \core\notification::add( + get_string('blockprotected', 'core_admin', $displayname), + \core\notification::SUCCESS + ); + } else { + block_manager::unprotect_block($pluginname); + \core\notification::add( + get_string('blockunprotected', 'core_admin', $displayname), + \core\notification::SUCCESS + ); + } + + return []; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([]); + } +} diff --git a/admin/classes/table/block_management_table.php b/admin/classes/table/block_management_table.php new file mode 100644 index 0000000000000..1a4816ec5af58 --- /dev/null +++ b/admin/classes/table/block_management_table.php @@ -0,0 +1,153 @@ +. + +namespace core_admin\table; + +use html_writer; +use moodle_url; +use stdClass; + +/** + * Tiny admin settings. + * + * @package core_admin + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_management_table extends \core_admin\table\plugin_management_table { + + /** @var plugininfo[] A list of blocks which cannot be deleted */ + protected array $undeletableblocktypes; + + /** @var stdClass[] A list of basic block data */ + protected array $blockdata; + + public function __construct() { + global $DB; + parent::__construct(); + $this->undeletableblocktypes = \block_manager::get_undeletable_block_types(); + + $sql = <<blockdata = $DB->get_records_sql($sql); + } + + protected function get_plugintype(): string { + return 'block'; + } + + public function guess_base_url(): void { + $this->define_baseurl( + new moodle_url('/admin/blocks.php') + ); + } + + protected function get_action_url(array $params = []): moodle_url { + return new moodle_url('/admin/blocks.php', $params); + } + + + protected function get_table_js_module(): string { + return 'core_admin/block_management_table'; + } + + protected function get_column_list(): array { + $columns = parent::get_column_list(); + return array_merge( + array_slice($columns, 0, 1, true), + ['instances' => get_string('blockinstances', 'admin')], + array_slice($columns, 1, 2, true), + ['protect' => get_string('blockprotect', 'admin')], + array_slice($columns, 3, null, true), + ); + } + + protected function get_columns_with_help(): array { + return [ + 'protect' => new \help_icon('blockprotect', 'admin'), + ]; + } + + /** + * Render the instances column + * @param stdClass $row + * @return string + */ + protected function col_instances(stdClass $row): string { + $blockdata = $this->blockdata[$row->plugininfo->name]; + if ($blockdata->courseviewcount > 0) { + return html_writer::link( + new moodle_url('/course/search.php', [ + 'blocklist' => $row->plugininfo->name, + 'sesskey' => sesskey(), + ]), + $blockdata->totalcount, + ); + } + + return $blockdata->totalcount; + } + + /** + * Render the protect column. + * + * @param stdClass $row + * @return string + */ + protected function col_protect(stdClass $row): string { + global $OUTPUT; + + $params = [ + 'sesskey' => sesskey(), + ]; + + $protected = in_array($row->plugininfo->name, $this->undeletableblocktypes); + + $pluginname = $row->plugininfo->displayname; + if ($protected) { + $params['unprotect'] = $row->plugininfo->name; + $icon = $OUTPUT->pix_icon('t/unlock', get_string('blockunprotectblock', 'admin', $pluginname)); + } else { + $params['protect'] = $row->plugininfo->name; + $icon = $OUTPUT->pix_icon('t/lock', get_string('blockprotectblock', 'admin', $pluginname)); + } + + return html_writer::link( + $this->get_action_url($params), + $icon, + [ + 'data-action' => 'toggleprotectstate', + 'data-plugin' => $row->plugin, + 'data-target-state' => $protected ? 0 : 1, + ], + ); + return ''; + } +} diff --git a/admin/tests/behat/manage_blocks.feature b/admin/tests/behat/manage_blocks.feature new file mode 100644 index 0000000000000..43f38000b8410 --- /dev/null +++ b/admin/tests/behat/manage_blocks.feature @@ -0,0 +1,59 @@ +@core @core_admin +Feature: An administrator can manage Block plugins + In order to alter the user experience + As an admin + I can manage block plugins + + @javascript + Scenario: An administrator can control the enabled state of block plugins using JavaScript + Given I am logged in as "admin" + And I navigate to "Plugins > Blocks > Manage blocks" in site administration + When I click on "Disable the Latest badges plugin" "link" + Then I should see "The Latest badges plugin has been disabled" + And "Disable the Latest badges plugin" "link" should not exist + But "Enable the Latest badges plugin" "link" should exist + When I click on "Enable the Latest badges plugin" "link" + Then I should see "The Latest badges plugin has been enabled" + And "Enable the Latest badges plugin" "link" should not exist + But "Disable the Latest badges plugin" "link" should exist + + Scenario: An administrator can control the enabled state of block plugins without JavaScript + Given I am logged in as "admin" + And I navigate to "Plugins > Blocks > Manage blocks" in site administration + When I click on "Disable the Latest badges plugin" "link" + Then I should see "The Latest badges plugin has been disabled" + And "Disable the Latest badges plugin" "link" should not exist + But "Enable the Latest badges plugin" "link" should exist + When I click on "Enable the Latest badges plugin" "link" + Then I should see "The Latest badges plugin has been enabled" + And "Enable the Latest badges plugin" "link" should not exist + But "Disable the Latest badges plugin" "link" should exist + + @javascript + Scenario: An administrator can control the protected state of block plugins using JavaScript + Given I am logged in as "admin" + And I navigate to "Plugins > Blocks > Manage blocks" in site administration + When I click on "Protect instances of the Latest badges block" "link" + Then I should see "The Latest badges block is now protected" + And "Protect instances of the Latest badges block" "link" should not exist + But "Unprotect instances of the Latest badges block" "link" should exist + And "Protect instances of the Activities block" "link" should exist + When I click on "Unprotect instances of the Latest badges block" "link" + Then I should see "The Latest badges block is no longer protected" + And "Unprotect instances of the Activities block" "link" should not exist + But "Protect instances of the Latest badges block" "link" should exist + And "Protect instances of the Activities block" "link" should exist + + Scenario: An administrator can control the protected state of block plugins without JavaScript + Given I am logged in as "admin" + And I navigate to "Plugins > Blocks > Manage blocks" in site administration + When I click on "Protect instances of the Latest badges block" "link" + Then I should see "The Latest badges block is now protected" + And "Protect instances of the Latest badges block" "link" should not exist + But "Unprotect instances of the Latest badges block" "link" should exist + And "Protect instances of the Activities block" "link" should exist + When I click on "Unprotect instances of the Latest badges block" "link" + Then I should see "The Latest badges block is no longer protected" + And "Unprotect instances of the Activities block" "link" should not exist + But "Protect instances of the Latest badges block" "link" should exist + And "Protect instances of the Activities block" "link" should exist diff --git a/admin/tests/external/set_block_protection_test.php b/admin/tests/external/set_block_protection_test.php new file mode 100644 index 0000000000000..60cbd4071fba9 --- /dev/null +++ b/admin/tests/external/set_block_protection_test.php @@ -0,0 +1,112 @@ +. + +declare(strict_types=1); + +namespace core_admin\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Unit tests to test block protection changes. + * + * @package core + * @covers \core_admin\external\set_block_protection + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class set_block_protection_test extends \externallib_advanced_testcase { + /** + * Test execute method with no login. + */ + public function test_execute_no_login(): void { + $this->expectException(\require_login_exception::class); + set_block_protection::execute('block_login', 1); + } + + /** + * Test execute method with no login. + */ + public function test_execute_no_capability(): void { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + $this->expectException(\required_capability_exception::class); + set_block_protection::execute('block_login', 1); + } + + /** + * Test the execute function with a range of parameters. + * + * @dataProvider execute_provider + * @param string $block + * @param int $targetstate + * @param bool $isundeletable + */ + public function test_execute( + string $block, + int $targetstate, + bool $isundeletable, + ): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + set_block_protection::execute($block, $targetstate); + + $undeletable = \block_manager::get_undeletable_block_types(); + [, $pluginname] = explode('_', $block, 2); + + if ($isundeletable) { + $this->assertNotFalse(array_search($pluginname, $undeletable)); + } else { + $this->assertFalse(array_search($pluginname, $undeletable)); + } + $this->assertCount(1, \core\notification::fetch()); + } + + /** + * Data provider for test_execute. + * + * @return array + */ + public function execute_provider(): array { + return [ + [ + 'block_login', + 1, + true, + ], + [ + 'block_login', + 0, + false, + ], + ]; + } + + /** + * Assert that an exception is thrown when the block does not exist. + */ + public function execute_block_does_not_exist(): void { + $this->expectException(\dml_missing_record_exception::class); + + set_block_protection::execute('fake_block', 1); + $this->assertDebuggingCalledCount(1); + } +} diff --git a/admin/tool/admin_presets/tests/behat/apply_presets.feature b/admin/tool/admin_presets/tests/behat/apply_presets.feature index 1372e6b32c2ca..63e583789887c 100644 --- a/admin/tool/admin_presets/tests/behat/apply_presets.feature +++ b/admin/tool/admin_presets/tests/behat/apply_presets.feature @@ -18,7 +18,7 @@ Feature: I can apply presets And I navigate to "Plugins > Availability restrictions > Manage restrictions" in site administration And "Hide" "icon" should exist in the "Restriction by grouping" "table_row" And I navigate to "Plugins > Blocks > Manage blocks" in site administration - And "Hide" "icon" should exist in the "Logged in user" "table_row" + And "Disable the Logged in user plugin" "icon" should exist in the "Logged in user" "table_row" And I navigate to "Plugins > Course formats > Manage course formats" in site administration And "Disable" "icon" should exist in the "Social format" "table_row" And I navigate to "Plugins > Question behaviours > Manage question behaviours" in site administration @@ -113,7 +113,7 @@ Feature: I can apply presets And I navigate to "Plugins > Availability restrictions > Manage restrictions" in site administration And "Hide" "icon" should not exist in the "Restriction by grouping" "table_row" And I navigate to "Plugins > Blocks > Manage blocks" in site administration - And "Hide" "icon" should not exist in the "Logged in user" "table_row" + And "Disable the Logged in user plugin" "icon" should not exist in the "Logged in user" "table_row" And I navigate to "Plugins > Course formats > Manage course formats" in site administration And "Disable" "icon" should not exist in the "Social format" "table_row" And I navigate to "Plugins > Question behaviours > Manage question behaviours" in site administration diff --git a/admin/tool/admin_presets/tests/behat/revert_changes.feature b/admin/tool/admin_presets/tests/behat/revert_changes.feature index 1a9aff329b944..d3fcc79b0d7bc 100644 --- a/admin/tool/admin_presets/tests/behat/revert_changes.feature +++ b/admin/tool/admin_presets/tests/behat/revert_changes.feature @@ -20,11 +20,11 @@ Feature: I can revert changes after a load And the field "Enable badges" matches value "0" And the field "Enable competencies" matches value "0" And I navigate to "Plugins > Activity modules > Manage activities" in site administration - And "Hide" "icon" should not exist in the "Chat" "table_row" + And "Disable the Chat plugin" "icon" should not exist in the "Chat" "table_row" And I navigate to "Plugins > Availability restrictions > Manage restrictions" in site administration And "Hide" "icon" should not exist in the "Restriction by grouping" "table_row" And I navigate to "Plugins > Blocks > Manage blocks" in site administration - And "Hide" "icon" should not exist in the "Logged in user" "table_row" + And "Disable the Logged in user plugin" "icon" should not exist in the "Logged in user" "table_row" And I navigate to "Plugins > Course formats > Manage course formats" in site administration And "Disable" "icon" should not exist in the "Social format" "table_row" And I navigate to "Plugins > Question behaviours > Manage question behaviours" in site administration @@ -39,11 +39,11 @@ Feature: I can revert changes after a load Then the field "Enable badges" matches value "1" And the field "Enable competencies" matches value "1" And I navigate to "Plugins > Activity modules > Manage activities" in site administration - And "Hide" "icon" should exist in the "Chat" "table_row" + And "Disable the Chat plugin" "icon" should exist in the "Chat" "table_row" And I navigate to "Plugins > Availability restrictions > Manage restrictions" in site administration And "Hide" "icon" should exist in the "Restriction by grouping" "table_row" And I navigate to "Plugins > Blocks > Manage blocks" in site administration - And "Hide" "icon" should exist in the "Logged in user" "table_row" + And "Disable the Logged in user plugin" "icon" should exist in the "Logged in user" "table_row" And I navigate to "Plugins > Course formats > Manage course formats" in site administration And "Disable" "icon" should exist in the "Social format" "table_row" And I navigate to "Plugins > Question behaviours > Manage question behaviours" in site administration diff --git a/blocks/glossary_random/tests/behat/glossary_random_addblock.feature b/blocks/glossary_random/tests/behat/glossary_random_addblock.feature index 05984b77aae88..e944fd55872b0 100644 --- a/blocks/glossary_random/tests/behat/glossary_random_addblock.feature +++ b/blocks/glossary_random/tests/behat/glossary_random_addblock.feature @@ -1,8 +1,8 @@ @block @block_glossary_random @javascript @addablocklink Feature: Add the glossary random block when main feature is enabled - In order to add the glossary random block to my course - As a teacher - It should be added to courses only if the glossary module is enabled. + In order to add the glossary random block to my course + As a teacher + It should be added to courses only if the glossary module is enabled. Background: Given the following "courses" exist: @@ -17,7 +17,7 @@ Feature: Add the glossary random block when main feature is enabled Scenario: The glossary random block cannot be added when glossary module is disabled Given I navigate to "Plugins > Activity modules > Manage activities" in site administration - And I click on "Hide" "icon" in the "Glossary" "table_row" + And I click on "Disable the Glossary plugin" "icon" in the "Glossary" "table_row" And I am on "Course 1" course homepage with editing mode on When I click on "Add a block" "link" Then I should not see "Random glossary entry" diff --git a/blocks/glossary_random/tests/behat/glossary_random_addblock_disabled.feature b/blocks/glossary_random/tests/behat/glossary_random_addblock_disabled.feature index 0b20b1f390100..ed5c01e6637f8 100644 --- a/blocks/glossary_random/tests/behat/glossary_random_addblock_disabled.feature +++ b/blocks/glossary_random/tests/behat/glossary_random_addblock_disabled.feature @@ -1,8 +1,8 @@ @block @block_glossary_random @javascript Feature: Add the glossary random block when main feature is disabled - In order to add the glossary random block to my course - As a teacher - It should be added to courses only if the glossary module is enabled. + In order to add the glossary random block to my course + As a teacher + It should be added to courses only if the glossary module is enabled. Background: Given the following "courses" exist: @@ -14,7 +14,7 @@ Feature: Add the glossary random block when main feature is disabled Given I turn editing mode on And I add the "Random glossary entry" block When I navigate to "Plugins > Activity modules > Manage activities" in site administration - And I click on "Hide" "icon" in the "Glossary" "table_row" + And I click on "Disable the Glossary plugin" "icon" in the "Glossary" "table_row" And I am on "Course 1" course homepage with editing mode on Then I should see "Random glossary entry" @@ -27,7 +27,7 @@ Feature: Add the glossary random block when main feature is disabled And I click on "Cancel" "button" in the "Delete block?" "dialogue" And I should see "Random glossary entry" When I navigate to "Plugins > Activity modules > Manage activities" in site administration - And I click on "Hide" "icon" in the "Glossary" "table_row" + And I click on "Disable the Glossary plugin" "icon" in the "Glossary" "table_row" And I am on "Course 1" course homepage with editing mode on And I open the "Random glossary entry" blocks action menu And I click on "Delete Random glossary entry block" "link" in the "Random glossary entry" "block" diff --git a/blocks/tag_youtube/tests/behat/configure_tag_youtube_block.feature b/blocks/tag_youtube/tests/behat/configure_tag_youtube_block.feature index 75f82e78c95dc..6fe190fdb295f 100644 --- a/blocks/tag_youtube/tests/behat/configure_tag_youtube_block.feature +++ b/blocks/tag_youtube/tests/behat/configure_tag_youtube_block.feature @@ -1,13 +1,13 @@ @block @block_tag_youtube Feature: Adding and configuring YouTube block - In order to have the YouTube block used - As a admin - I need to add the YouTube block to the tags site page + In order to have the YouTube block used + As a admin + I need to add the YouTube block to the tags site page Background: Given I log in as "admin" And I navigate to "Plugins > Blocks > Manage blocks" in site administration - And I click on "Show" "icon" in the "YouTube" "table_row" + And I click on "Enable the YouTube plugin" "icon" in the "YouTube" "table_row" @javascript Scenario: Category options are not available (except default) in the block settings if the YouTube API key is not set. @@ -16,7 +16,7 @@ Feature: Adding and configuring YouTube block And I follow "Dashboard" And I turn editing mode on And the following config values are set as admin: - | unaddableblocks | | theme_boost| + | unaddableblocks | | theme_boost | # TODO MDL-57120 site "Tags" link not accessible without navigation block. And I add the "Navigation" block if not present And I click on "Site pages" "list_item" in the "Navigation" "block" @@ -38,7 +38,7 @@ Feature: Adding and configuring YouTube block And I follow "Dashboard" And I turn editing mode on And the following config values are set as admin: - | unaddableblocks | | theme_boost| + | unaddableblocks | | theme_boost | And I add the "Navigation" block if not present And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Tags" "link" in the "Navigation" "block" diff --git a/lang/en/admin.php b/lang/en/admin.php index f808377c2733c..d8c43ffb719f4 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -95,10 +95,13 @@ $string['blockinstances'] = 'Instances'; $string['blockmultiple'] = 'Multiple'; $string['blockprotect'] = 'Protect instances'; +$string['blockprotectblock'] = 'Protect instances of the {$a} block'; +$string['blockprotected'] = 'The {$a} block is now protected'; $string['blockprotect_help'] = 'If you lock a particular type of block, then no-one will be able to add or delete instances. (You can, of course, unlock again if you need to edit instances.) This is intended to protect blocks like the navigation and settings which are very hard to get back if accidentally deleted.'; -$string['blockunprotect'] = 'Unprotect'; +$string['blockunprotectblock'] = 'Unprotect instances of the {$a} block'; +$string['blockunprotected'] = 'The {$a} block is no longer protected'; $string['blocksettings'] = 'Manage blocks'; $string['bloglevel'] = 'Blog visibility'; $string['bookmarkadded'] = 'Bookmark added.'; @@ -1583,3 +1586,6 @@ // Deprecated since Moodle 4.1. $string['multilangforceold'] = 'Force old multilang syntax: <span> without the class="multilang" and <lang>'; + +// Deprecated since Moodle 4.2. +$string['blockunprotect'] = 'Unprotect'; diff --git a/lang/en/deprecated.txt b/lang/en/deprecated.txt index c9277cc2de2e0..559ed8ec01127 100644 --- a/lang/en/deprecated.txt +++ b/lang/en/deprecated.txt @@ -81,3 +81,4 @@ showlocks_help,core_grades showanalysisicon,core_grades showanalysisicon_desc,core_grades showanalysisicon_help,core_grades +blockunprotect,core_admin diff --git a/lib/db/services.php b/lib/db/services.php index df01c242b0e63..fbfd7dbdbbd95 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -2965,6 +2965,12 @@ 'type' => 'write', 'ajax' => true, ], + 'core_admin_set_block_protection' => [ + 'classname' => 'core_admin\external\set_block_protection', + 'description' => 'Set the protection state for a block plugin', + 'type' => 'write', + 'ajax' => true, + ], ); $services = array( diff --git a/lib/editor/tiny/tests/plugininfo/tiny_test.php b/lib/editor/tiny/tests/plugininfo/tiny_test.php index 778a6816740de..1334458d07a83 100644 --- a/lib/editor/tiny/tests/plugininfo/tiny_test.php +++ b/lib/editor/tiny/tests/plugininfo/tiny_test.php @@ -71,4 +71,18 @@ public function test_get_enabled_plugins(): void { $this->assertArrayHasKey('autosave', $plugins); $this->assertArrayNotHasKey('h5p', $plugins); } + + /** + * Ensure that the base implementation is used for plugins not supporting ordering. + */ + public function test_sorting_not_supported(): void { + $this->assertFalse(tiny::plugintype_supports_ordering()); + + $this->assertNull(tiny::get_sorted_plugins()); + $this->assertNull(tiny::get_sorted_plugins(true)); + $this->assertNull(tiny::get_sorted_plugins(false)); + + $this->assertFalse(tiny::change_plugin_order('tiny_h5p', \core\plugininfo\base::MOVE_UP)); + $this->assertFalse(tiny::change_plugin_order('tiny_h5p', \core\plugininfo\base::MOVE_DOWN)); + } } diff --git a/lib/tests/plugininfo/block_test.php b/lib/tests/plugininfo/block_test.php new file mode 100644 index 0000000000000..63ed074f0b7ad --- /dev/null +++ b/lib/tests/plugininfo/block_test.php @@ -0,0 +1,86 @@ +. + +declare(strict_types=1); + +namespace core\plugininfo; + +use advanced_testcase; + +/** + * Unit tests for the mod plugininfo class + * + * @package core + * @covers \core\plugininfo\block + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_test extends advanced_testcase { + + /** + * Test the get_enabled_plugins method. + * + * @covers ::get_enabled_plugins + */ + public function test_get_enabled_plugins(): void { + $this->resetAfterTest(); + + // The bigbluebuttonbn plugin is disabled by default. + // Check all default formats. + $plugins = block::get_enabled_plugins(); + $this->assertArrayHasKey('badges', $plugins); + $this->assertArrayHasKey('timeline', $plugins); + $this->assertArrayHasKey('admin_bookmarks', $plugins); + + // Disable a plugin. + block::enable_plugin('timeline', 0); + + $plugins = block::get_enabled_plugins(); + $this->assertArrayHasKey('badges', $plugins); + $this->assertArrayNotHasKey('timeline', $plugins); + $this->assertArrayHasKey('admin_bookmarks', $plugins); + } + + /** + * Test the is_uninstall_allowed method. + * + * @dataProvider is_uninstall_allowed_provider + * @param string $plugin + * @param bool $expected + */ + public function test_is_uninstall_allowed( + string $plugin, + bool $expected, + ): void { + $pluginmanager = \core_plugin_manager::instance(); + $plugininfo = $pluginmanager->get_plugin_info("block_{$plugin}"); + $this->assertEquals($expected, $plugininfo->is_uninstall_allowed()); + } + + public function is_uninstall_allowed_provider(): array { + $plugins = block::get_enabled_plugins(); + return array_map(function ($plugin) { + $expected = true; + if ($plugin === 'settings' || $plugin === 'navigation') { + $expected = false; + } + return [ + 'plugin' => $plugin, + 'expected' => $expected, + ]; + }, array_keys($plugins)); + } +} diff --git a/version.php b/version.php index c1d7010490f01..e7b645c03a7a0 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2023031000.02; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2023031000.03; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.2dev (Build: 20230310)'; // Human-friendly version name