Skip to content

Commit

Permalink
MDL-55580 core: Process for deprecating a capability
Browse files Browse the repository at this point in the history
* Add a $deprecatedcapabilities variable to deal with deprecated
capabilities

Change-Id: I14f44d331e8a1c4bd9abe9566c78d911c0205583
Co-authored-by: Mark Johnson <mark.johnson@catalyst-eu.net>
  • Loading branch information
Laurent David and marxjohnson committed Oct 11, 2022
1 parent cc4fec2 commit bcc18e2
Show file tree
Hide file tree
Showing 8 changed files with 352 additions and 2 deletions.
1 change: 1 addition & 0 deletions lang/en/cache.php
Expand Up @@ -56,6 +56,7 @@
$string['cachedef_course_image'] = 'Course images';
$string['cachedef_course_user_dates'] = 'The user dates for courses set to relative dates mode';
$string['cachedef_completion'] = 'Activity completion status';
$string['cachedef_deprecatedcapabilities'] = 'System deprecated capabilities list';
$string['cachedef_databasemeta'] = 'Database meta information';
$string['cachedef_eventinvalidation'] = 'Event invalidation';
$string['cachedef_externalbadges'] = 'External badges for particular user';
Expand Down
72 changes: 71 additions & 1 deletion lib/accesslib.php
Expand Up @@ -2525,13 +2525,78 @@ function is_inside_frontpage(context $context) {
function get_capability_info($capabilityname) {
$caps = get_all_capabilities();

// Check for deprecated capability.
if ($deprecatedinfo = get_deprecated_capability_info($capabilityname)) {
if (!empty($deprecatedinfo['replacement'])) {
// Let's try again with this capability if it exists.
if (isset($caps[$deprecatedinfo['replacement']])) {
$capabilityname = $deprecatedinfo['replacement'];
} else {
debugging("Capability '{$capabilityname}' was supposed to be replaced with ".
"'{$deprecatedinfo['replacement']}', which does not exist !");
}
}
$fullmessage = $deprecatedinfo['fullmessage'];
debugging($fullmessage, DEBUG_DEVELOPER);
}
if (!isset($caps[$capabilityname])) {
return null;
}

return (object) $caps[$capabilityname];
}

/**
* Returns deprecation info for this particular capabilty (cached)
*
* Do not use this function except in the get_capability_info
*
* @param string $capabilityname
* @return stdClass|null with deprecation message and potential replacement if not null
*/
function get_deprecated_capability_info($capabilityname) {
// Here if we do like get_all_capabilities, we run into performance issues as the full array is unserialised each time.
// We could have used an adhoc task but this also had performance issue. Last solution was to create a cache using
// the official caches.php file. The performance issue shows in test_permission_evaluation.
$cache = cache::make('core', 'deprecatedcapabilities');
// Cache has not be initialised.
if (!$cache->get('deprecated_capabilities_initialised')) {
// Look for deprecated capabilities in each components.
$allcaps = get_all_capabilities();
$components = [];
$alldeprecatedcaps = [];
foreach ($allcaps as $cap) {
if (!in_array($cap['component'], $components)) {
$components[] = $cap['component'];
$defpath = core_component::get_component_directory($cap['component']).'/db/access.php';
if (file_exists($defpath)) {
$deprecatedcapabilities = [];
require($defpath);
if (!empty($deprecatedcapabilities)) {
foreach ($deprecatedcapabilities as $cname => $cdef) {
$cache->set($cname, $cdef);
}
}
}
}
}
$cache->set('deprecated_capabilities_initialised', true);
}
if (!$cache->has($capabilityname)) {
return null;
}
$deprecatedinfo = $cache->get($capabilityname);
$deprecatedinfo['fullmessage'] = "The capability '{$capabilityname}' is deprecated.";
if (!empty($deprecatedinfo['message'])) {
$deprecatedinfo['fullmessage'] .= $deprecatedinfo['message'];
}
if (!empty($deprecatedinfo['replacement'])) {
$deprecatedinfo['fullmessage'] .=
"It will be replaced by '{$deprecatedinfo['replacement']}'.";
}
return $deprecatedinfo;
}

/**
* Returns all capabilitiy records, preferably from MUC and not database.
*
Expand Down Expand Up @@ -4135,6 +4200,11 @@ function get_user_capability_contexts(string $capability, bool $getcategories, $
$userid = $USER->id;
}

if (!$capinfo = get_capability_info($capability)) {
debugging('Capability "'.$capability.'" was not found! This has to be fixed in code.');
return [false, false];
}

if ($doanything && is_siteadmin($userid)) {
// If the user is a site admin and $doanything is enabled then there is no need to restrict
// the list of courses.
Expand All @@ -4143,7 +4213,7 @@ function get_user_capability_contexts(string $capability, bool $getcategories, $
} else {
// Gets SQL to limit contexts ('x' table) to those where the user has this capability.
list ($contextlimitsql, $contextlimitparams) = \core\access\get_user_capability_course_helper::get_sql(
$userid, $capability);
$userid, $capinfo->name);
if (!$contextlimitsql) {
// If the does not have this capability in any context, return false without querying.
return [false, false];
Expand Down
10 changes: 10 additions & 0 deletions lib/db/caches.php
Expand Up @@ -149,6 +149,16 @@
'ttl' => 3600, // Just in case.
),

// Cache the deprecated capabilities list. See get_deprecated_capability_info in accesslib.
'deprecatedcapabilities' => array(
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => false, // We need to hash the key.
'simpledata' => true,
'staticacceleration' => true,
'staticaccelerationsize' => 1,
'ttl' => 3600, // Just in case.
),

// YUI Module cache.
// This stores the YUI module metadata for Shifted YUI modules in Moodle.
'yuimodules' => array(
Expand Down
151 changes: 151 additions & 0 deletions lib/tests/accesslib_test.php
Expand Up @@ -1970,6 +1970,157 @@ public function test_has_capability_and_friends() {
$this->assertFalse(has_all_capabilities($sca, $coursecontext, 0));
}

/**
* Utility method to fake a plugin
*
* @param string $pluginname plugin name
* @return void
*/
protected function setup_fake_plugin($pluginname) {
global $CFG;
// Here we have to hack the component loader so we can insert our fake plugin and test that
// the access.php works.
$mockedcomponent = new ReflectionClass(core_component::class);
$mockedplugins = $mockedcomponent->getProperty('plugins');
$mockedplugins->setAccessible(true);
$plugins = $mockedplugins->getValue();
$plugins['fake'] = [$pluginname => "{$CFG->dirroot}/lib/tests/fixtures/fakeplugins/$pluginname"];
$mockedplugins->setValue($plugins);
update_capabilities('fake_access');
$this->resetDebugging(); // We have debugging messages here that we need to get rid of.
// End of the component loader mock.
}

/**
* Test get_deprecated_capability_info()
*
* @covers ::get_deprecated_capability_info
*/
public function test_get_deprecated_capability_info() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$coursecontext = context_course::instance($course->id);
$user = $this->getDataGenerator()->create_and_enrol($course);
$this->setup_fake_plugin('access');

// For now we have deprecated fake/access:fakecapability.
$capinfo = get_deprecated_capability_info('fake/access:fakecapability');
$this->assertNotEmpty($capinfo);
$this->assertEquals("The capability 'fake/access:fakecapability' is"
. " deprecated.This capability should not be used anymore.", $capinfo['fullmessage']);
}

/**
* Test get_deprecated_capability_info() through has_capability
*
* @covers ::get_deprecated_capability_info
*/
public function test_get_deprecated_capability_info_through_has_capability() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$coursecontext = context_course::instance($course->id);
$user = $this->getDataGenerator()->create_and_enrol($course);
$this->setup_fake_plugin('access');

// For now we have deprecated fake/access:fakecapability.
$hascap = has_capability('fake/access:fakecapability', $coursecontext, $user);
$this->assertTrue($hascap);
$this->assertDebuggingCalled("The capability 'fake/access:fakecapability' is deprecated."
. "This capability should not be used anymore.");
}

/**
* Test get_deprecated_capability_info() through get_user_capability_contexts()
*
* @covers ::get_deprecated_capability_info
*/
public function test_get_deprecated_capability_info_through_get_user_capability_contexts() {
$this->resetAfterTest();
$category = $this->getDataGenerator()->create_category();
$course = $this->getDataGenerator()->create_course(['categoryid' => $category->id]);
$user = $this->getDataGenerator()->create_and_enrol($course);
$this->setup_fake_plugin('access');

// For now we have deprecated fake/access:fakecapability.
list($categories, $courses) = get_user_capability_contexts('fake/access:fakecapability', false, $user->id);
$this->assertNotEmpty($courses);
$this->assertDebuggingCalled("The capability 'fake/access:fakecapability' is deprecated."
. "This capability should not be used anymore.");
}

/**
* Test get_deprecated_capability_info with a capability that does not exist
*
* @param string $capability the capability name
* @param array $debugmessages the debug messsages we expect
* @param bool $expectedexisting does the capability exist
* @covers ::get_deprecated_capability_info
* @dataProvider deprecated_capabilities_use_cases
*/
public function test_get_deprecated_capability_specific_cases(string $capability, array $debugmessages,
bool $expectedexisting) {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$coursecontext = context_course::instance($course->id);
$user = $this->getDataGenerator()->create_and_enrol($course);
$this->setup_fake_plugin('access');

// For now we have deprecated fake/access:fakecapability.
$this->resetDebugging();
$hascap = has_capability($capability, $coursecontext, $user);
$this->assertEquals($expectedexisting, $hascap);
$this->assertDebuggingCalledCount(count($debugmessages), $debugmessages);
}

/**
* Specific use case for deprecated capabilities
*
* @return array
*/
public function deprecated_capabilities_use_cases() {
return [
'capability missing' => [
'fake/access:missingcapability',
[
"Capability \"fake/access:missingcapability\" was not found! This has to be fixed in code."
],
false
],
'replacement no info' => [
'fake/access:replacementnoinfo',
[
"The capability 'fake/access:replacementnoinfo' is deprecated.",
],
true
],
'replacement missing' => [
'fake/access:replacementmissing',
[
"The capability 'fake/access:replacementmissing' is deprecated.This capability should not be used anymore.",
],
true
],
'replacement with non existing cap' => [
'fake/access:replacementwithwrongcapability',
[
"Capability 'fake/access:replacementwithwrongcapability' was supposed to be replaced with"
. " 'fake/access:nonexistingcapabilty', which does not exist !",
"The capability 'fake/access:replacementwithwrongcapability' is deprecated."
. "This capability should not be used anymore.It will be replaced by 'fake/access:nonexistingcapabilty'."
],
true
],
'replacement with existing' => [
'fake/access:replacementwithexisting', // Existing capability buf for a different role.
[
"The capability 'fake/access:replacementwithexisting' is deprecated.This capability should not be used anymore."
. "It will be replaced by 'fake/access:existingcapability'.",
],
false // As the capability is applied to managers, we should not have this capability for this simple user.
],
];
}

/**
* Test that assigning a fake cap does not return.
*
Expand Down
80 changes: 80 additions & 0 deletions lib/tests/fixtures/fakeplugins/access/db/access.php
@@ -0,0 +1,80 @@
<?php
// 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 <http://www.gnu.org/licenses/>.

/**
* Fake component for testing
*
* @package core
* @copyright 2022 Laurent David <laurent.david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

defined('MOODLE_INTERNAL') || die();

// Fake capabilities.
$capabilities = [
'fake/access:fakecapability' => [
'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_CONFIG | RISK_DATALOSS,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => [
'user' => CAP_ALLOW
]
],
'fake/access:existingcapability' => [
'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_CONFIG | RISK_DATALOSS,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => [
'manager' => CAP_ALLOW
]
],
'fake/access:replacementnoinfo' => [
'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_CONFIG | RISK_DATALOSS,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => [
'user' => CAP_ALLOW
]
],
'fake/access:replacementmissing' => [
'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_CONFIG | RISK_DATALOSS,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => [
'user' => CAP_ALLOW
]
],
'fake/access:replacementwithwrongcapability' => [
'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_CONFIG | RISK_DATALOSS,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => [
'user' => CAP_ALLOW
]
],
];

// Deprecated capabilities - MDL-55580.
$deprecatedcapabilities = [
'fake/access:fakecapability' => ['replacement' => '', 'message' => 'This capability should not be used anymore.'],
'fake/access:replacementmissing' => ['message' => 'This capability should not be used anymore.'],
'fake/access:replacementnoinfo' => [],
'fake/access:replacementwithwrongcapability' => ['replacement' => 'fake/access:nonexistingcapabilty',
'message' => 'This capability should not be used anymore.'],
'fake/access:replacementwithexisting' => ['replacement' => 'fake/access:existingcapability',
'message' => 'This capability should not be used anymore.'],
];
29 changes: 29 additions & 0 deletions lib/tests/fixtures/fakeplugins/access/version.php
@@ -0,0 +1,29 @@
<?php
// 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 <http://www.gnu.org/licenses/>.

/**
* Fake component for testing
*
* @package core
* @copyright 2022 Laurent David <laurent.david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

defined('MOODLE_INTERNAL') || die();

$plugin->version = 2022050200;
$plugin->requires = 2022041200;
$plugin->component = 'fake_access';

0 comments on commit bcc18e2

Please sign in to comment.