php:
# We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
- 7.3
- - 7.1.30 # Make this sticky because current default version (7.1.11) has a bug with redis-extension output (MDL-66062)
+ - 7.2
addons:
postgresql: "9.6"
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+/* eslint-env node */
+
/**
- * Grunt configuration
+ * Calculate the cwd, taking into consideration the `root` option (for Windows).
+ *
+ * @param {Object} grunt
+ * @returns {String} The current directory as best we can determine
*/
+const getCwd = grunt => {
+ const fs = require('fs');
+ const path = require('path');
-/* eslint-env node */
-module.exports = function(grunt) {
- var path = require('path'),
- tasks = {},
- cwd = process.env.PWD || process.cwd(),
- async = require('async'),
- DOMParser = require('xmldom').DOMParser,
- xpath = require('xpath'),
- semver = require('semver'),
- watchman = require('fb-watchman'),
- watchmanClient = new watchman.Client(),
- gruntFilePath = process.cwd();
-
- // Verify the node version is new enough.
- var expected = semver.validRange(grunt.file.readJSON('package.json').engines.node);
- var actual = semver.valid(process.version);
- if (!semver.satisfies(actual, expected)) {
- grunt.fail.fatal('Node version not satisfied. Require ' + expected + ', version installed: ' + actual);
- }
+ let cwd = fs.realpathSync(process.env.PWD || process.cwd());
// Windows users can't run grunt in a subdirectory, so allow them to set
// the root by passing --root=path/to/dir.
if (grunt.option('root')) {
- var root = grunt.option('root');
+ const root = grunt.option('root');
if (grunt.file.exists(__dirname, root)) {
- cwd = path.join(__dirname, root);
+ cwd = fs.realpathSync(path.join(__dirname, root));
grunt.log.ok('Setting root to ' + cwd);
} else {
grunt.fail.fatal('Setting root to ' + root + ' failed - path does not exist');
}
}
+ return cwd;
+};
+
+/**
+ * Register any stylelint tasks.
+ *
+ * @param {Object} grunt
+ * @param {Array} files
+ * @param {String} fullRunDir
+ */
+const registerStyleLintTasks = (grunt, files, fullRunDir) => {
+ const getCssConfigForFiles = files => {
+ return {
+ stylelint: {
+ css: {
+ // Use a fully-qualified path.
+ src: files,
+ options: {
+ configOverrides: {
+ rules: {
+ // These rules have to be disabled in .stylelintrc for scss compat.
+ "at-rule-no-unknown": true,
+ }
+ }
+ }
+ },
+ },
+ };
+ };
+
+ const getScssConfigForFiles = files => {
+ return {
+ stylelint: {
+ scss: {
+ options: {syntax: 'scss'},
+ src: files,
+ },
+ },
+ };
+ };
+
+ let hasCss = true;
+ let hasScss = true;
+
+ if (files) {
+ // Specific files were passed. Just set them up.
+ grunt.config.merge(getCssConfigForFiles(files));
+ grunt.config.merge(getScssConfigForFiles(files));
+ } else {
+ // The stylelint system does not handle the case where there was no file to lint.
+ // Check whether there are any files to lint in the current directory.
+ const glob = require('glob');
+
+ const scssSrc = [];
+ glob.sync(`${fullRunDir}/**/*.scss`).forEach(path => scssSrc.push(path));
+
+ if (scssSrc.length) {
+ grunt.config.merge(getScssConfigForFiles(scssSrc));
+ } else {
+ hasScss = false;
+ }
+
+ const cssSrc = [];
+ glob.sync(`${fullRunDir}/**/*.css`).forEach(path => cssSrc.push(path));
+
+ if (cssSrc.length) {
+ grunt.config.merge(getCssConfigForFiles(cssSrc));
+ } else {
+ hasCss = false;
+ }
+ }
+
+ const scssTasks = ['sass'];
+ if (hasScss) {
+ scssTasks.unshift('stylelint:scss');
+ }
+ grunt.registerTask('scss', scssTasks);
+
+ const cssTasks = [];
+ if (hasCss) {
+ cssTasks.push('stylelint:css');
+ }
+ grunt.registerTask('rawcss', cssTasks);
+
+ grunt.registerTask('css', ['scss', 'rawcss']);
+};
+
+/**
+ * Grunt configuration.
+ *
+ * @param {Object} grunt
+ */
+module.exports = function(grunt) {
+ const path = require('path');
+ const tasks = {};
+ const async = require('async');
+ const DOMParser = require('xmldom').DOMParser;
+ const xpath = require('xpath');
+ const semver = require('semver');
+ const watchman = require('fb-watchman');
+ const watchmanClient = new watchman.Client();
+ const fs = require('fs');
+ const ComponentList = require(path.resolve('GruntfileComponents.js'));
+
+ // Verify the node version is new enough.
+ var expected = semver.validRange(grunt.file.readJSON('package.json').engines.node);
+ var actual = semver.valid(process.version);
+ if (!semver.satisfies(actual, expected)) {
+ grunt.fail.fatal('Node version not satisfied. Require ' + expected + ', version installed: ' + actual);
+ }
+
+ // Detect directories:
+ // * gruntFilePath The real path on disk to this Gruntfile.js
+ // * cwd The current working directory, which can be overridden by the `root` option
+ // * relativeCwd The cwd, relative to the Gruntfile.js
+ // * componentDirectory The root directory of the component if the cwd is in a valid component
+ // * inComponent Whether the cwd is in a valid component
+ // * runDir The componentDirectory or cwd if not in a component, relative to Gruntfile.js
+ // * fullRunDir The full path to the runDir
+ const gruntFilePath = fs.realpathSync(process.cwd());
+ const cwd = getCwd(grunt);
+ const relativeCwd = cwd.replace(new RegExp(`${gruntFilePath}/?`), '');
+ const componentDirectory = ComponentList.getOwningComponentDirectory(relativeCwd);
+ const inComponent = !!componentDirectory;
+ const runDir = inComponent ? componentDirectory : relativeCwd;
+ const fullRunDir = fs.realpathSync(gruntFilePath + path.sep + runDir);
+ grunt.log.debug(`The cwd was detected as ${cwd} with a fullRunDir of ${fullRunDir}`);
+
+ if (inComponent) {
+ grunt.log.ok(`Running tasks for component directory ${componentDirectory}`);
+ }
+
var files = null;
if (grunt.option('files')) {
// Accept a comma separated list of files to process.
files = grunt.option('files').split(',');
}
- var inAMD = path.basename(cwd) == 'amd';
+ const inAMD = path.basename(cwd) == 'amd';
// Globbing pattern for matching all AMD JS source files.
- var amdSrc = [];
- if (inAMD) {
- amdSrc.push(cwd + "/src/*.js");
- amdSrc.push(cwd + "/src/**/*.js");
+ let amdSrc = [];
+ if (inComponent) {
+ amdSrc.push(componentDirectory + "/amd/src/*.js");
+ amdSrc.push(componentDirectory + "/amd/src/**/*.js");
} else {
- amdSrc.push("**/amd/src/*.js");
- amdSrc.push("**/amd/src/**/*.js");
+ amdSrc = ComponentList.getAmdSrcGlobList();
+ }
+
+ let yuiSrc = [];
+ if (inComponent) {
+ yuiSrc.push(componentDirectory + "/yui/src/**/*.js");
+ } else {
+ yuiSrc = ComponentList.getYuiSrcGlobList(gruntFilePath + '/');
}
/**
* @return {array} The list of thirdparty paths.
*/
var getThirdPartyPathsFromXML = function() {
- var thirdpartyfiles = grunt.file.expand('*/**/thirdpartylibs.xml');
- var libs = ['node_modules/', 'vendor/'];
+ const thirdpartyfiles = ComponentList.getThirdPartyLibsList(gruntFilePath + '/');
+ const libs = ['node_modules/', 'vendor/'];
thirdpartyfiles.forEach(function(file) {
- var dirname = path.dirname(file);
+ const dirname = path.dirname(file);
- var doc = new DOMParser().parseFromString(grunt.file.read(file));
- var nodes = xpath.select("/libraries/library/location/text()", doc);
+ const doc = new DOMParser().parseFromString(grunt.file.read(file));
+ const nodes = xpath.select("/libraries/library/location/text()", doc);
- nodes.forEach(function(node) {
- var lib = path.join(dirname, node.toString());
- if (grunt.file.isDir(lib)) {
- // Ensure trailing slash on dirs.
- lib = lib.replace(/\/?$/, '/');
- }
+ nodes.forEach(function(node) {
+ let lib = path.join(dirname, node.toString());
+ if (grunt.file.isDir(lib)) {
+ // Ensure trailing slash on dirs.
+ lib = lib.replace(/\/?$/, '/');
+ }
- // Look for duplicate paths before adding to array.
- if (libs.indexOf(lib) === -1) {
- libs.push(lib);
- }
- });
+ // Look for duplicate paths before adding to array.
+ if (libs.indexOf(lib) === -1) {
+ libs.push(lib);
+ }
+ });
});
+
return libs;
};
options: {quiet: !grunt.option('show-lint-warnings')},
amd: {src: files ? files : amdSrc},
// Check YUI module source files.
- yui: {src: files ? files : ['**/yui/src/**/*.js', '!*/**/yui/src/*/meta/*.js']}
+ yui: {src: files ? files : yuiSrc},
},
babel: {
options: {
nospawn: true // We need not to spawn so config can be changed dynamically.
},
amd: {
- files: ['**/amd/src/**/*.js'],
+ files: inComponent
+ ? ['amd/src/*.js', 'amd/src/**/*.js']
+ : ['**/amd/src/**/*.js'],
tasks: ['amd']
},
boost: {
- files: ['**/theme/boost/scss/**/*.scss'],
+ files: [inComponent ? 'scss/**/*.scss' : 'theme/boost/scss/**/*.scss'],
tasks: ['scss']
},
rawcss: {
- files: ['**/*.css', '**/theme/**/!(moodle.css|editor.css)'],
+ files: [
+ '**/*.css',
+ ],
+ excludes: [
+ '**/moodle.css',
+ '**/editor.css',
+ ],
tasks: ['rawcss']
},
yui: {
- files: ['**/yui/src/**/*.js'],
+ files: inComponent
+ ? ['yui/src/*.json', 'yui/src/**/*.js']
+ : ['**/yui/src/**/*.js'],
tasks: ['yui']
},
gherkinlint: {
- files: ['**/tests/behat/*.feature'],
+ files: [inComponent ? 'tests/behat/*.feature' : '**/tests/behat/*.feature'],
tasks: ['gherkinlint']
}
},
shifter: {
options: {
recursive: true,
- paths: files ? files : [cwd]
+ // Shifter takes a relative path.
+ paths: files ? files : [runDir]
}
},
gherkinlint: {
files: files ? files : ['**/tests/behat/*.feature'],
}
},
- stylelint: {
- scss: {
- options: {syntax: 'scss'},
- src: files ? files : ['*/**/*.scss']
- },
- css: {
- src: files ? files : ['*/**/*.css'],
- options: {
- configOverrides: {
- rules: {
- // These rules have to be disabled in .stylelintrc for scss compat.
- "at-rule-no-unknown": true,
- }
- }
- }
- }
- }
});
/**
* Generate ignore files (utilising thirdpartylibs.xml data)
*/
tasks.ignorefiles = function() {
- // An array of paths to third party directories.
- var thirdPartyPaths = getThirdPartyPathsFromXML();
- // Generate .eslintignore.
- var eslintIgnores = ['# Generated by "grunt ignorefiles"', '*/**/yui/src/*/meta/', '*/**/build/'].concat(thirdPartyPaths);
- grunt.file.write('.eslintignore', eslintIgnores.join('\n'));
- // Generate .stylelintignore.
- var stylelintIgnores = [
- '# Generated by "grunt ignorefiles"',
- '**/yui/build/*',
- 'theme/boost/style/moodle.css',
- 'theme/classic/style/moodle.css',
- ].concat(thirdPartyPaths);
- grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
+ // An array of paths to third party directories.
+ const thirdPartyPaths = getThirdPartyPathsFromXML();
+ // Generate .eslintignore.
+ const eslintIgnores = [
+ '# Generated by "grunt ignorefiles"',
+ '*/**/yui/src/*/meta/',
+ '*/**/build/',
+ ].concat(thirdPartyPaths);
+ grunt.file.write('.eslintignore', eslintIgnores.join('\n'));
+
+ // Generate .stylelintignore.
+ const stylelintIgnores = [
+ '# Generated by "grunt ignorefiles"',
+ '**/yui/build/*',
+ 'theme/boost/style/moodle.css',
+ 'theme/classic/style/moodle.css',
+ ].concat(thirdPartyPaths);
+ grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
};
/**
grunt: true,
// Run from current working dir and inherit stdio from process.
opts: {
- cwd: cwd,
+ cwd: fullRunDir,
stdio: 'inherit'
},
args: [task, filesOption]
);
};
- var watchConfig = grunt.config.get(['watch']);
- watchConfig = Object.keys(watchConfig).reduce(function(carry, key) {
+ const originalWatchConfig = grunt.config.get(['watch']);
+ const watchConfig = Object.keys(originalWatchConfig).reduce(function(carry, key) {
if (key == 'options') {
return carry;
}
- var value = watchConfig[key];
- var fileGlobs = value.files;
- var taskNames = value.tasks;
+ const value = originalWatchConfig[key];
+
+ const taskNames = value.tasks;
+ const files = value.files;
+ let excludes = [];
+ if (value.excludes) {
+ excludes = value.excludes;
+ }
taskNames.forEach(function(taskName) {
- carry[taskName] = fileGlobs;
+ carry[taskName] = {
+ files,
+ excludes,
+ };
});
return carry;
resp.files.forEach(function(file) {
grunt.log.ok('File changed: ' + file.name);
- var fullPath = cwd + '/' + file.name;
+ var fullPath = fullRunDir + '/' + file.name;
Object.keys(watchConfig).forEach(function(task) {
- var fileGlobs = watchConfig[task];
- var match = fileGlobs.every(function(fileGlob) {
- return grunt.file.isMatch(fileGlob, fullPath);
+
+ const fileGlobs = watchConfig[task].files;
+ var match = fileGlobs.some(function(fileGlob) {
+ return grunt.file.isMatch(`**/${fileGlob}`, fullPath);
});
+
if (match) {
// If we are watching a subdirectory then the file.name will be relative
// to that directory. However the grunt tasks expect the file paths to be
});
// Initiate the watch on the current directory.
- watchmanClient.command(['watch-project', cwd], function(watchError, watchResponse) {
+ watchmanClient.command(['watch-project', fullRunDir], function(watchError, watchResponse) {
if (watchError) {
grunt.log.error('Error initiating watch:', watchError);
watchTaskDone(1);
return;
}
- // Use the matching patterns specified in the watch config.
+ // Generate the expression query used by watchman.
+ // Documentation is limited, but see https://facebook.github.io/watchman/docs/expr/allof.html for examples.
+ // We generate an expression to match any value in the files list of all of our tasks, but excluding
+ // all value in the excludes list of that task.
+ //
+ // [anyof, [
+ // [allof, [
+ // [anyof, [
+ // ['match', validPath, 'wholename'],
+ // ['match', validPath, 'wholename'],
+ // ],
+ // [not,
+ // [anyof, [
+ // ['match', invalidPath, 'wholename'],
+ // ['match', invalidPath, 'wholename'],
+ // ],
+ // ],
+ // ],
+ var matchWholeName = fileGlob => ['match', fileGlob, 'wholename'];
var matches = Object.keys(watchConfig).map(function(task) {
- var fileGlobs = watchConfig[task];
- var fileGlobMatches = fileGlobs.map(function(fileGlob) {
- return ['match', fileGlob, 'wholename'];
- });
+ const matchAll = [];
+ matchAll.push(['anyof'].concat(watchConfig[task].files.map(matchWholeName)));
- return ['allof'].concat(fileGlobMatches);
+ if (watchConfig[task].excludes.length) {
+ matchAll.push(['not', ['anyof'].concat(watchConfig[task].excludes.map(matchWholeName))]);
+ }
+
+ return ['allof'].concat(matchAll);
});
+ matches = ['anyof'].concat(matches);
+
var sub = {
- expression: ["anyof"].concat(matches),
+ expression: matches,
// Which fields we're interested in.
fields: ["name", "size", "type"],
// Add our time constraint.
return;
}
- grunt.log.ok('Listening for changes to files in ' + cwd);
+ grunt.log.ok('Listening for changes to files in ' + fullRunDir);
});
});
});
grunt.registerTask('amd', ['eslint:amd', 'babel']);
grunt.registerTask('js', ['amd', 'yui']);
- // Register CSS taks.
- grunt.registerTask('css', ['stylelint:scss', 'sass', 'stylelint:css']);
- grunt.registerTask('scss', ['stylelint:scss', 'sass']);
- grunt.registerTask('rawcss', ['stylelint:css']);
+ // Register CSS tasks.
+ registerStyleLintTasks(grunt, files, fullRunDir);
// Register the startup task.
grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup);
--- /dev/null
+// 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/>.
+
+/**
+ * Helper functions for working with Moodle component names, directories, and sources.
+ *
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+"use strict";
+/* eslint-env node */
+
+/** @var {Object} A list of subsystems in Moodle */
+const componentData = {};
+
+/**
+ * Load details of all moodle modules.
+ *
+ * @returns {object}
+ */
+const fetchComponentData = () => {
+ const fs = require('fs');
+ const path = require('path');
+ const glob = require('glob');
+ const gruntFilePath = process.cwd();
+
+ if (!Object.entries(componentData).length) {
+ componentData.subsystems = {};
+ componentData.pathList = [];
+
+ // Fetch the component definiitions from the distributed JSON file.
+ const components = JSON.parse(fs.readFileSync(`${gruntFilePath}/lib/components.json`));
+
+ // Build the list of moodle subsystems.
+ componentData.subsystems.lib = 'core';
+ componentData.pathList.push(process.cwd() + path.sep + 'lib');
+ for (const [component, thisPath] of Object.entries(components.subsystems)) {
+ if (thisPath) {
+ // Prefix "core_" to the front of the subsystems.
+ componentData.subsystems[thisPath] = `core_${component}`;
+ componentData.pathList.push(process.cwd() + path.sep + thisPath);
+ }
+ }
+
+ // The list of components incldues the list of subsystems.
+ componentData.components = componentData.subsystems;
+
+ // Go through each of the plugintypes.
+ Object.entries(components.plugintypes).forEach(([pluginType, pluginTypePath]) => {
+ // We don't allow any code in this place..?
+ glob.sync(`${pluginTypePath}/*/version.php`).forEach(versionPath => {
+ const componentPath = fs.realpathSync(path.dirname(versionPath));
+ const componentName = path.basename(componentPath);
+ const frankenstyleName = `${pluginType}_${componentName}`;
+ componentData.components[`${pluginTypePath}/${componentName}`] = frankenstyleName;
+ componentData.pathList.push(componentPath);
+
+ // Look for any subplugins.
+ const subPluginConfigurationFile = `${componentPath}/db/subplugins.json`;
+ if (fs.existsSync(subPluginConfigurationFile)) {
+ const subpluginList = JSON.parse(fs.readFileSync(fs.realpathSync(subPluginConfigurationFile)));
+
+ Object.entries(subpluginList.plugintypes).forEach(([subpluginType, subpluginTypePath]) => {
+ glob.sync(`${subpluginTypePath}/*/version.php`).forEach(versionPath => {
+ const componentPath = fs.realpathSync(path.dirname(versionPath));
+ const componentName = path.basename(componentPath);
+ const frankenstyleName = `${subpluginType}_${componentName}`;
+
+ componentData.components[`${subpluginTypePath}/${componentName}`] = frankenstyleName;
+ componentData.pathList.push(componentPath);
+ });
+ });
+ }
+ });
+ });
+
+ }
+
+ return componentData;
+};
+
+/**
+ * Get the list of paths to build AMD sources.
+ *
+ * @returns {Array}
+ */
+const getAmdSrcGlobList = () => {
+ const globList = [];
+ fetchComponentData().pathList.forEach(componentPath => {
+ globList.push(`${componentPath}/amd/src/*.js`);
+ globList.push(`${componentPath}/amd/src/**/*.js`);
+ });
+
+ return globList;
+};
+
+/**
+ * Get the list of paths to build YUI sources.
+ *
+ * @param {String} relativeTo
+ * @returns {Array}
+ */
+const getYuiSrcGlobList = relativeTo => {
+ const globList = [];
+ fetchComponentData().pathList.forEach(componentPath => {
+ const relativeComponentPath = componentPath.replace(relativeTo, '');
+ globList.push(`${relativeComponentPath}/yui/src/**/*.js`);
+ });
+
+ return globList;
+};
+
+/**
+ * Get the list of paths to thirdpartylibs.xml.
+ *
+ * @param {String} relativeTo
+ * @returns {Array}
+ */
+const getThirdPartyLibsList = relativeTo => {
+ const fs = require('fs');
+
+ return fetchComponentData().pathList
+ .map(componentPath => componentPath.replace(relativeTo, '') + '/thirdpartylibs.xml')
+ .filter(path => fs.existsSync(path))
+ .sort();
+};
+
+/**
+ * Find the name of the component matching the specified path.
+ *
+ * @param {String} path
+ * @returns {String|null} Name of matching component.
+ */
+const getComponentFromPath = path => {
+ const componentList = fetchComponentData().components;
+
+ if (componentList.hasOwnProperty(path)) {
+ return componentList[path];
+ }
+
+ return null;
+};
+
+/**
+ * Check whether the supplied path, relative to the Gruntfile.js, is in a known component.
+ *
+ * @param {String} checkPath The path to check
+ * @returns {String|null}
+ */
+const getOwningComponentDirectory = checkPath => {
+ const path = require('path');
+
+ const pathList = fetchComponentData().components;
+ for (const componentPath of Object.keys(pathList)) {
+ if (checkPath === componentPath) {
+ return componentPath;
+ }
+ if (checkPath.startsWith(componentPath + path.sep)) {
+ return componentPath;
+ }
+ }
+
+ return null;
+};
+
+module.exports = {
+ getAmdSrcGlobList,
+ getComponentFromPath,
+ getOwningComponentDirectory,
+ getYuiSrcGlobList,
+ getThirdPartyLibsList,
+};
$return = optional_param('return','', PARAM_ALPHA);
$adminediting = optional_param('adminedit', -1, PARAM_BOOL);
-require_admin();
+require_login(0, false);
$PAGE->set_context(context_system::instance());
$PAGE->set_url('/admin/category.php', array('category' => $category));
$PAGE->set_pagetype('admin-setting-' . $category);
</CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
+ <MOODLE version="3.9" requires="3.5">
+ <UNICODE level="required">
+ <FEEDBACK>
+ <ON_ERROR message="unicoderequired" />
+ </FEEDBACK>
+ </UNICODE>
+ <DATABASE level="required">
+ <VENDOR name="mariadb" version="10.2.29" />
+ <VENDOR name="mysql" version="5.6" />
+ <VENDOR name="postgres" version="9.5" />
+ <VENDOR name="mssql" version="11.0" />
+ <VENDOR name="oracle" version="11.2" />
+ </DATABASE>
+ <PHP version="7.2.0" level="required">
+ </PHP>
+ <PCREUNICODE level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="pcreunicodewarning" />
+ </FEEDBACK>
+ </PCREUNICODE>
+ <PHP_EXTENSIONS>
+ <PHP_EXTENSION name="iconv" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="iconvrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="mbstring" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="mbstringrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="curl" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="curlrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="openssl" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="opensslrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="tokenizer" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="tokenizerrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="xmlrpc" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="xmlrpcrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="soap" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="soaprecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="ctype" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="ctyperequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="zip" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="ziprequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="zlib" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="gd" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="gdrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="simplexml" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="simplexmlrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="spl" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="splrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="pcre" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="dom" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="xml" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="xmlreader" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="intl" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="intlrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="json" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="hash" level="required"/>
+ <PHP_EXTENSION name="fileinfo" level="required"/>
+ </PHP_EXTENSIONS>
+ <PHP_SETTINGS>
+ <PHP_SETTING name="memory_limit" value="96M" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="settingmemorylimit" />
+ </FEEDBACK>
+ </PHP_SETTING>
+ <PHP_SETTING name="file_uploads" value="1" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="settingfileuploads" />
+ </FEEDBACK>
+ </PHP_SETTING>
+ <PHP_SETTING name="opcache.enable" value="1" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="opcacherecommended" />
+ </FEEDBACK>
+ </PHP_SETTING>
+ </PHP_SETTINGS>
+ <CUSTOM_CHECKS>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="unsupporteddbstorageengine" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="quizattemptsupgradedmessage" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="slashargumentswarning" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="unsupporteddbtablerowformat" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="unoconvwarning" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="libcurlwarning" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="unsupporteddbfileformat" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="unsupporteddbfilepertable" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="unsupporteddblargeprefix" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="ishttpswarning" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="incompleteunicodesupport" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ <CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="sixtyfourbitswarning" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ </CUSTOM_CHECKS>
+ </MOODLE>
</COMPATIBILITY_MATRIX>
if ($mnet_peer && !empty($mnet_peer->deleted)) {
$radioarray = array();
$radioarray[] = $mform->createElement('static', 'deletedinfo', '',
- $OUTPUT->container(get_string('deletedhostinfo', 'mnet'), 'deletedhostinfo'));
+ $OUTPUT->container(get_string('deletedhostinfo', 'mnet'), 'alert alert-warning'));
$radioarray[] = $mform->createElement('radio', 'deleted', '', get_string('yes'), 1);
$radioarray[] = $mform->createElement('radio', 'deleted', '', get_string('no'), 0);
$mform->addGroup($radioarray, 'radioar', get_string('deleted'), array(' ', ' '), false);
if ($version) {
$row[] = $version;
} else {
- $row[] = html_writer::tag('span', get_string('nodatabase', 'admin'), array('class' => 'disabled'));
+ $row[] = html_writer::tag('span', get_string('nodatabase', 'admin'), array('class' => 'text-muted'));
}
// Other question types required by this one.
if ($version) {
$row[] = $version;
} else {
- $row[] = html_writer::tag('span', get_string('nodatabase', 'admin'), array('class' => 'disabled'));
+ $row[] = html_writer::tag('span', get_string('nodatabase', 'admin'), array('class' => 'text-muted'));
}
// Other question types required by this one.
$out .= $this->output->container(get_string('cancelinstallinfodir', 'core_plugin', $pluginfo->rootdir));
if ($repotype = $pluginman->plugin_external_source($pluginfo->component)) {
$out .= $this->output->container(get_string('uninstalldeleteconfirmexternal', 'core_plugin', $repotype),
- 'uninstalldeleteconfirmexternal');
+ 'alert alert-warning mt-2');
}
}
if ($repotype = $pluginman->plugin_external_source($pluginfo->component)) {
$confirm .= $this->output->container(get_string('uninstalldeleteconfirmexternal', 'core_plugin', $repotype),
- 'uninstalldeleteconfirmexternal');
+ 'alert alert-warning mt-2');
}
// After any uninstall we must execute full upgrade to finish the cleanup!
* @return string HTML to output.
*/
protected function warning($message, $type = 'warning') {
- return $this->box($message, 'generalbox admin' . $type);
+ return $this->box($message, 'generalbox alert alert-' . $type);
}
/**
return $this->warning(get_string('datarootsecuritywarning', 'admin', $CFG->dataroot));
} else if ($insecuredataroot == INSECURE_DATAROOT_ERROR) {
- return $this->warning(get_string('datarootsecurityerror', 'admin', $CFG->dataroot), 'error');
+ return $this->warning(get_string('datarootsecurityerror', 'admin', $CFG->dataroot), 'danger');
} else {
return '';
if ($devlibdir) {
$moreinfo = new moodle_url('/report/security/index.php');
$warning = get_string('devlibdirpresent', 'core_admin', ['moreinfourl' => $moreinfo->out()]);
- return $this->warning($warning, 'error');
+ return $this->warning($warning, 'danger');
} else {
return '';
return $this->warning(
$this->container(get_string('maturitycorewarning', 'admin', $maturitylevel)) .
$this->container($this->doc_link('admin/versions', get_string('morehelp'))),
- 'error');
+ 'danger');
}
/*
}
$warning = (get_string('testsiteupgradewarning', 'admin', $testsite));
- return $this->warning($warning, 'error');
+ return $this->warning($warning, 'danger');
}
/**
$level = 'warning';
if ($maturity == MATURITY_ALPHA) {
- $level = 'error';
+ $level = 'danger';
}
$maturitylevel = get_string('maturity' . $maturity, 'admin');
protected function release_notes_link() {
$releasenoteslink = get_string('releasenoteslink', 'admin', 'http://docs.moodle.org/dev/Releases');
$releasenoteslink = str_replace('target="_blank"', 'onclick="this.target=\'_blank\'"', $releasenoteslink); // extremely ugly validation hack
- return $this->box($releasenoteslink, 'generalbox releasenoteslink');
+ return $this->box($releasenoteslink, 'generalbox alert alert-info');
}
/**
get_string('status'),
);
$servertable->colclasses = array('centeralign name', 'centeralign info', 'leftalign report', 'leftalign plugin', 'centeralign status');
- $servertable->attributes['class'] = 'admintable environmenttable generaltable';
+ $servertable->attributes['class'] = 'admintable environmenttable generaltable table-sm';
$servertable->id = 'serverstatus';
$serverdata = array('ok'=>array(), 'warn'=>array(), 'error'=>array());
get_string('status'),
);
$othertable->colclasses = array('aligncenter info', 'alignleft report', 'alignleft plugin', 'aligncenter status');
- $othertable->attributes['class'] = 'admintable environmenttable generaltable';
+ $othertable->attributes['class'] = 'admintable environmenttable generaltable table-sm';
$othertable->id = 'otherserverstatus';
$otherdata = array('ok'=>array(), 'warn'=>array(), 'error'=>array());
protected $id;
/** Added to the class="" attribute on output. */
- protected $classes = array('rolecap');
+ protected $classes = array('rolecap table-hover');
/** Default number of capabilities in the table for the search UI to be shown. */
const NUM_CAPS_FOR_SEARCH = 12;
$this->contextname = $contextname;
$this->stryes = get_string('yes');
$this->strno = get_string('no');
+ $this->add_classes(['table-striped']);
}
protected function add_header_cells() {
foreach ($levels as $level => $classname) {
$this->allcontextlevels[$level] = context_helper::get_level_name($level);
}
+ $this->add_classes(['table-striped']);
}
protected function load_current_permissions() {
// Allowed roles.
$allow = optional_param_array('allowassign', null, PARAM_INT);
if (!is_null($allow)) {
- $this->allowassign = $allow;
+ $this->allowassign = array_filter($allow);
}
$allow = optional_param_array('allowoverride', null, PARAM_INT);
if (!is_null($allow)) {
- $this->allowoverride = $allow;
+ $this->allowoverride = array_filter($allow);
}
$allow = optional_param_array('allowswitch', null, PARAM_INT);
if (!is_null($allow)) {
- $this->allowswitch = $allow;
+ $this->allowswitch = array_filter($allow);
}
$allow = optional_param_array('allowview', null, PARAM_INT);
if (!is_null($allow)) {
- $this->allowview = $allow;
+ $this->allowview = array_filter($allow);
}
// Now read the permissions for each capability.
if ($this->roleid == 0) {
$options[-1] = get_string('thisnewrole', 'core_role');
}
- return html_writer::select($options, 'allow'.$type.'[]', $selected, false, array('multiple' => 'multiple',
+ return
+ html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'allow'.$type.'[]', 'value' => "")) .
+ html_writer::select($options, 'allow'.$type.'[]', $selected, false, array('multiple' => 'multiple',
'size' => 10, 'class' => 'form-control'));
}
$rowattributes = parent::get_row_attributes($capability);
if ($this->permissions[$capability->name] !== 0) {
if (empty($rowattributes['class'])) {
- $rowattributes['class'] = "overriddenpermission";
+ $rowattributes['class'] = "overriddenpermission table-warning";
} else {
- $rowattributes['class'] .= " overriddenpermission";
+ $rowattributes['class'] .= " overriddenpermission table-warning";
}
}
return $rowattributes;
<label {{#labelfor}}for="{{labelfor}}"{{/labelfor}}>
{{{title}}}
{{#override}}
- <div class="form-overridden">{{override}}</div>
+ <div class="alert alert-info">{{override}}</div>
{{/override}}
{{#warning}}
- <div class="form-warning">{{warning}}</div>
+ <div class="alert alert-warning">{{warning}}</div>
{{/warning}}
</label>
<span class="form-shortname d-block small text-muted">{{{name}}}</span>
* size - form element size
* value - form element value
* id - element id
+ * forced - has value been defined in config.php
Example context (json):
{
"name": "test",
"id": "test0",
"size": "8",
- "value": "secret"
+ "value": "secret",
+ "forced": false
}
}}
+{{#forced}}
+ <div class="form-password">
+ <input type="text"
+ name = "{{ name }}"
+ id="{{ id }}"
+ value="********"
+ size="{{ size }}"
+ class="form-control"
+ disabled
+ >
+ </div>
+{{/forced}}
+{{^forced}}
<div class="form-password">
<span data-passwordunmask="wrapper" data-passwordunmaskid="{{ id }}">
<span data-passwordunmask="editor">
new PasswordUnmask("{{ id }}");
});
{{/js}}
+{{/forced}}
<h3 class="adminpagetitle"><a href="{{url}}">{{{title}}}</a></h3>
<ul class="adminpagepath" aria-label="{{#str}} pagepath, core {{/str}}">
{{#path}}
- <li>{{.}}</li>
+ <li class="small text-muted">{{.}}</li>
{{/path}}
</ul>
<fieldset class="adminsettings">
Background:
Given the following "users" exist:
- | username | firstname | lastname | email | auth | confirmed | lastip |
- | user1 | User | One | one@example.com | manual | 0 | 127.0.1.1 |
- | user2 | User | Two | two@example.com | ldap | 1 | 0.0.0.0 |
- | user3 | User | Three | three@example.com | manual | 1 | 0.0.0.0 |
- | user4 | User | Four | four@example.com | ldap | 0 | 127.0.1.2 |
+ | username | firstname | lastname | email | auth | confirmed | lastip | institution | department |
+ | user1 | User | One | one@example.com | manual | 0 | 127.0.1.1 | moodle | red |
+ | user2 | User | Two | two@example.com | ldap | 1 | 0.0.0.0 | moodle | blue |
+ | user3 | User | Three | three@example.com | manual | 1 | 0.0.0.0 | | |
+ | user4 | User | Four | four@example.com | ldap | 0 | 127.0.1.2 | | |
And the following "cohorts" exist:
| name | idnumber |
| Cohort 1 | CH1 |
And I should see "User Two"
And I should see "User Three"
And I should see "User Four"
+
+ Scenario: Filter users by institution and department
+ When I set the field "id_institution" to "moodle"
+ And I press "Add filter"
+ Then I should see "User One"
+ And I should see "User Two"
+ And I should not see "User Three"
+ And I should not see "User Four"
+ And I set the field "id_department" to "red"
+ And I press "Add filter"
+ And I should see "User One"
+ And I should not see "User Two"
\ No newline at end of file
$form->addElement('select', 'roles', get_string('roleslabel', 'tool_capability'), $roles, $attributes);
$form->setType('roles', PARAM_TEXT);
+ $form->addElement('checkbox', 'onlydiff',
+ get_string('filters', 'tool_capability'),
+ get_string('onlydiff', 'tool_capability'));
+ $form->setType('onlydiff', PARAM_BOOL);
+
$form->addElement('submit', 'submitbutton', get_string('getreport', 'tool_capability'));
}
$rolestoshow = array();
$roleids = array('0');
$cleanedroleids = array();
+$onlydiff = false;
if ($data = $form->get_data()) {
$roleids = array();
}
}
}
+
+ if (isset($data->onlydiff)) {
+ $onlydiff = $data->onlydiff;
+ }
}
\tool_capability\event\report_viewed::create()->trigger();
// If we have a capability, generate the report.
if (count($capabilities) && count($rolestoshow)) {
/* @var tool_capability_renderer $renderer */
- echo $renderer->capability_comparison_table($capabilities, $context->id, $rolestoshow);
+ echo $renderer->capability_comparison_table($capabilities, $context->id, $rolestoshow, $onlydiff);
}
// Footer.
// If there are any role overrides here, print them.
if (!empty($contexts[$contextid]->rolecapabilities)) {
$rowcounter = 0;
- echo '<table class="generaltable rolecaps"><tbody>';
+ echo '<table class="generaltable table-striped"><tbody>';
foreach ($allroles as $role) {
if (isset($contexts[$contextid]->rolecapabilities[$role->id])) {
$permission = $contexts[$contextid]->rolecapabilities[$role->id];
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+$string['onlydiff'] = 'Show differences only';
$string['capabilitylabel'] = 'Capability:';
$string['capabilityreport'] = 'Capability overview';
$string['eventreportviewed'] = 'Report viewed';
+$string['filters'] = 'Filter results';
$string['forroles'] = 'For roles {$a}';
$string['getreport'] = 'Get the overview';
$string['changeoverrides'] = 'Change overrides in this context';
$string['changeroles'] = 'Change role definitions';
$string['intro'] = 'This report shows, for a particular capability, what permission that capability has in the definition of every role (or a selection of roles), and everywhere in the site where that capability is overridden.';
$string['pluginname'] = 'Capability overview';
+$string['nodifferences'] = 'There are no differences to show between selected roles in this context';
$string['reportforcapability'] = 'Report for capability \'{$a}\'';
$string['reportsettings'] = 'Report settings';
$string['roleslabel'] = 'Roles:';
* @param array $capabilities An array of capabilities to show comparison for.
* @param int $contextid The context we are displaying for.
* @param array $roles An array of roles to show comparison for.
+ * @param bool $onlydiff show only different permissions
* @return string
*/
- public function capability_comparison_table(array $capabilities, $contextid, array $roles) {
+ public function capability_comparison_table(array $capabilities, $contextid, array $roles, $onlydiff=false) {
$strpermissions = $this->get_permission_strings();
$permissionclasses = $this->get_permission_classes();
$row = new html_table_row(array($captitle));
+ $permissiontypes = array();
foreach ($roles as $role) {
if (isset($contexts[$contextid]->rolecapabilities[$role->id])) {
$permission = $contexts[$contextid]->rolecapabilities[$role->id];
} else {
$permission = CAP_INHERIT;
}
+ if (!in_array($permission, $permissiontypes)) {
+ $permissiontypes[] = $permission;
+ }
$cell = new html_table_cell($strpermissions[$permission]);
$cell->attributes['class'] = $permissionclasses[$permission];
$row->cells[] = $cell;
}
-
- $table->data[] = $row;
+ if (!$onlydiff || count($permissiontypes) > 1) {
+ $table->data[] = $row;
+ }
}
// Start the list item, and print the context name as a link to the place to make changes.
$title = get_string('permissionsincontext', 'core_role', $context->get_context_name());
$html = $this->output->heading(html_writer::link($url, $title), 3);
- $html .= html_writer::table($table);
+ if (!empty($table->data)) {
+ $html .= html_writer::table($table);
+ } else {
+ $html .= html_writer::tag('p', get_string('nodifferences', 'tool_capability'));
+ }
// If there are any child contexts, print them recursively.
if (!empty($contexts[$contextid]->children)) {
foreach ($contexts[$contextid]->children as $childcontextid) {
- $html .= $this->capability_comparison_table($capabilities, $childcontextid, $roles, true);
+ $html .= $this->capability_comparison_table($capabilities, $childcontextid, $roles, $onlydiff);
}
}
return $html;
--- /dev/null
+@tool @tool_capability
+Feature: show capabilities for selected roles
+ In order to check roles capabilities
+ As an admin
+ I need to be able to customize capabilities report viewing only specific roles and capabilities
+
+ Background:
+ Given the following "roles" exist:
+ | shortname | name | archetype |
+ | studenteq | Studenteq | student |
+ | studentdf | Studentdf | student |
+ And the following "permission overrides" exist:
+ | capability | permission | role | contextlevel | reference |
+ | moodle/course:changefullname | Allow | studentdf | System | |
+ | moodle/course:changeshortname | Prohibit | studentdf | System | |
+ | moodle/course:changeidnumber | Prevent | studentdf | System | |
+ And I log in as "admin"
+ And I navigate to "Users > Permissions > Capability overview" in site administration
+
+ Scenario: visualize capabilities table with a limited number of capabilities
+ When I set the following fields to these values:
+ | Capability: | moodle/course:changefullname, moodle/course:changeshortname |
+ | Roles: | Studentdf |
+ And I click on "Get the overview" "button"
+ Then I should see "moodle/course:changefullname" in the "comparisontable" "table"
+ And I should see "moodle/course:changeshortname" in the "comparisontable" "table"
+ And I should not see "moodle/course:changecategory" in the "comparisontable" "table"
+
+ Scenario: visualize an allow capability
+ When I set the following fields to these values:
+ | Capability: | moodle/course:changefullname |
+ | Roles: | Studentdf |
+ And I click on "Get the overview" "button"
+ Then I should see "Allow" in the "comparisontable" "table"
+ And I should not see "Prevent" in the "comparisontable" "table"
+ And I should not see "Prohibit" in the "comparisontable" "table"
+ And I should not see "Not set" in the "comparisontable" "table"
+
+ Scenario: visualize a prohibit capability
+ When I set the following fields to these values:
+ | Capability: | moodle/course:changeshortname |
+ | Roles: | Studentdf |
+ And I click on "Get the overview" "button"
+ Then I should not see "Allow" in the "comparisontable" "table"
+ And I should not see "Prevent" in the "comparisontable" "table"
+ And I should see "Prohibit" in the "comparisontable" "table"
+ And I should not see "Not set" in the "comparisontable" "table"
+
+ Scenario: visualize a not set capability
+ When I set the following fields to these values:
+ | Capability: | moodle/course:changecategory |
+ | Roles: | Studentdf |
+ And I click on "Get the overview" "button"
+ Then I should not see "Allow" in the "comparisontable" "table"
+ And I should not see "Prevent" in the "comparisontable" "table"
+ And I should not see "Prohibit" in the "comparisontable" "table"
+ And I should see "Not set" in the "comparisontable" "table"
+
+ Scenario: visualize more than one role
+ When I set the following fields to these values:
+ | Capability: | moodle/course:changecategory |
+ | Roles: | Student, Studentdf |
+ And I click on "Get the overview" "button"
+ Then I should see "Student" in the "comparisontable" "table"
+ And I should see "Studentdf" in the "comparisontable" "table"
+ And I should not see "Teacher" in the "comparisontable" "table"
+
+ Scenario: visualize all roles without selecting any role
+ When I set the following fields to these values:
+ | Capability: | moodle/course:changecategory |
+ And I click on "Get the overview" "button"
+ Then I should see "Student" in the "comparisontable" "table"
+ And I should see "Studentdf" in the "comparisontable" "table"
+ And I should see "Teacher" in the "comparisontable" "table"
+
+ Scenario: visualize all roles by selecting All option
+ When I set the following fields to these values:
+ | Capability: | moodle/course:changecategory |
+ | Roles: | All |
+ And I click on "Get the overview" "button"
+ Then I should see "Student" in the "comparisontable" "table"
+ And I should see "Studentdf" in the "comparisontable" "table"
+ And I should see "Teacher" in the "comparisontable" "table"
+
+ @javascript
+ Scenario: filter capability list using javascript
+ Given I should see "moodle/site:config" in the "Capability" "field"
+ And I should see "moodle/course:change" in the "Capability" "field"
+ When I wait until the page is ready
+ And I set the field "capabilitysearch" to "moodle/course:change"
+ Then I should see "moodle/course:change" in the "Capability" "field"
+ And I should not see "moodle/site:config" in the "Capability" "field"
+
+ @javascript
+ Scenario: selecting capabilities using filters
+ Given I should see "moodle/course:change" in the "Capability" "field"
+ When I wait until the page is ready
+ And I set the field "capabilitysearch" to "moodle/course:change"
+ When I set the following fields to these values:
+ | Capability: | moodle/course:changecategory |
+ | Roles: | Student |
+ And I set the field "capabilitysearch" to ""
+ And I click on "Get the overview" "button"
+ Then I should see "moodle/course:changecategory" in the "comparisontable" "table"
--- /dev/null
+@tool @tool_capability
+Feature: show only differences between roles for selected capabilities
+ In order to check roles capabilities
+ As an admin
+ I need to be able to filter capabilities report viewing only role differences
+
+ Background:
+ Given the following "roles" exist:
+ | shortname | name | archetype |
+ | studenteq | Studenteq | student |
+ | studentdf | Studentdf | student |
+ And the following "permission overrides" exist:
+ | capability | permission | role | contextlevel | reference |
+ | moodle/course:changefullname | Allow | studentdf | System | |
+ | moodle/course:changeshortname | Prohibit | studentdf | System | |
+ And I log in as "admin"
+ And I navigate to "Users > Permissions > Capability overview" in site administration
+
+ Scenario: Compare identical roles
+ When I set the following fields to these values:
+ | Capability: | moodle/course:changefullname, moodle/course:changeshortname, moodle/course:changeidnumber, moodle/course:changesummary |
+ | Roles: | Student, Studenteq |
+ And I set the field "Show differences only" to "1"
+ And I click on "Get the overview" "button"
+ Then I should see "There are no differences to show between selected roles in this context"
+
+ Scenario: Compare different roles
+ When I set the following fields to these values:
+ | Capability: | moodle/course:changefullname, moodle/course:changeshortname, moodle/course:changeidnumber, moodle/course:changesummary |
+ | Roles: | Student, Studentdf |
+ And I set the field "Show differences only" to "1"
+ And I click on "Get the overview" "button"
+ Then I should not see "There are no differences to show between selected roles in this context"
+ And I should see "moodle/course:changefullname" in the "comparisontable" "table"
+ And I should see "moodle/course:changeshortname" in the "comparisontable" "table"
+ And I should not see "moodle/course:changesummary" in the "comparisontable" "table"
+
+ Scenario: Compare different roles but comparing capabilities that are equals on both
+ When I set the following fields to these values:
+ | Capability: | moodle/course:changeidnumber, moodle/course:changesummary |
+ | Roles: | Student, Studentdf |
+ And I set the field "Show differences only" to "1"
+ And I click on "Get the overview" "button"
+ Then I should see "There are no differences to show between selected roles in this context"
+
+ Scenario: Compare all roles without selecting specific role
+ When I set the following fields to these values:
+ | Capability: | moodle/course:changefullname, moodle/site:config |
+ And I set the field "Show differences only" to "1"
+ And I click on "Get the overview" "button"
+ Then I should not see "moodle/site:config" in the "comparisontable" "table"
+ And I should see "moodle/course:changefullname" in the "comparisontable" "table"
+
+ Scenario: Compare all roles without selecting specific role on not defined capability
+ When I set the following fields to these values:
+ | Capability: | moodle/site:config |
+ And I set the field "Show differences only" to "1"
+ And I click on "Get the overview" "button"
+ Then I should see "There are no differences to show between selected roles in this context"
+
+ Scenario: Comparing only one role
+ When I set the following fields to these values:
+ | Capability: | moodle/course:changefullname, moodle/course:changeshortname, moodle/course:changeidnumber, moodle/course:changesummary |
+ | Roles: | Student |
+ And I set the field "Show differences only" to "1"
+ And I click on "Get the overview" "button"
+ Then I should see "There are no differences to show between selected roles in this context"
this.button = this.form.all('input[type=submit]');
this.lastsearch = this.form.one('input[name=search]');
- var div = Y.Node.create('<div id="capabilitysearchui"></div>'),
+ var div = Y.Node.create('<div id="capabilitysearchui" data-fieldtype="text"></div>'),
label = Y.Node.create('<label for="capabilitysearch">' + this.get('strsearch') + '</label>');
this.input = Y.Node.create('<input type="text" id="capabilitysearch" />');
function xmldb_tool_customlang_upgrade($oldversion) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
*
* @param string $component the name of the component
* @param array $strings
+ * @return void
*/
protected static function dump_strings($lang, $component, $strings) {
global $CFG;
if ($lang !== clean_param($lang, PARAM_LANG)) {
- debugging('Unable to dump local strings for non-installed language pack .'.s($lang));
- return false;
+ throw new moodle_exception('Unable to dump local strings for non-installed language pack .'.s($lang));
}
if ($component !== clean_param($component, PARAM_COMPONENT)) {
throw new coding_exception('Incorrect component name');
}
if (!$filename = self::get_component_filename($component)) {
- debugging('Unable to find the filename for the component '.s($component));
- return false;
+ throw new moodle_exception('Unable to find the filename for the component '.s($component));
}
if ($filename !== clean_param($filename, PARAM_FILE)) {
throw new coding_exception('Incorrect file name '.s($filename));
}
if (!$f = fopen($filepath, 'w')) {
- debugging('Unable to write '.s($filepath));
- return false;
+ throw new moodle_exception('Unable to write '.s($filepath));
}
fwrite($f, <<<EOF
<?php
'requestedby' => $requestedby->fullname,
'requesttype' => $typetext,
'requestdate' => userdate($requestdata->timecreated),
- 'requestorigin' => $SITE->fullname,
+ 'requestorigin' => format_string($SITE->fullname, true, ['context' => context_system::instance()]),
'requestoriginurl' => new moodle_url('/'),
'requestcomments' => $requestdata->messagehtml,
'datarequestsurl' => $datarequestsurl
use action_link;
use coding_exception;
+use context_system;
use core\message\message;
use core\task\adhoc_task;
use core_user;
$message->contexturl = $datarequestsurl;
$message->contexturlname = get_string('datarequests', 'tool_dataprivacy');
// Message to the recipient.
- $messagetextdata['message'] = get_string('resultdownloadready', 'tool_dataprivacy', $SITE->fullname);
+ $messagetextdata['message'] = get_string('resultdownloadready', 'tool_dataprivacy',
+ format_string($SITE->fullname, true, ['context' => context_system::instance()]));
// Prepare download link.
$downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $thing->get_itemid(),
$thing->get_filepath(), $thing->get_filename(), true);
// No point notifying a deleted user in Moodle.
$message->notification = 0;
// Message to the recipient.
- $messagetextdata['message'] = get_string('resultdeleted', 'tool_dataprivacy', $SITE->fullname);
+ $messagetextdata['message'] = get_string('resultdeleted', 'tool_dataprivacy',
+ format_string($SITE->fullname, true, ['context' => context_system::instance()]));
// Message will be sent to the deleted user via email only.
$emailonly = true;
break;
<hr />
<div class="p-l-3">
<dl class="row">
- <dt class="col-xs-3">
+ <dt class="col-3">
{{#link}}
<a href="#{{name}}"><strong style="word-wrap:break-word">{{name}}</strong></a>
{{/link}}
{{/link}}
<div class="small text-muted" style="word-wrap:break-word">{{type}}</div>
</dt>
- <dd class="col-xs-9">{{summary}}</dd>
+ <dd class="col-9">{{summary}}</dd>
</dl>
<dl>
{{#fields}}
<div class="row">
- <dt class="col-xs-3 font-weight-normal" style="word-wrap:break-word">{{field_name}}</dt>
- <dd class="col-xs-9">{{field_summary}}</dd>
+ <dt class="col-3 font-weight-normal" style="word-wrap:break-word">{{field_name}}</dt>
+ <dd class="col-9">{{field_summary}}</dd>
</div>
{{/fields}}
</dl>
<hr />
<div class="p-l-3">
<div class="row">
- <div class="col-xs-12">
+ <div class="col-12">
{{nullprovider}}
</div>
</div>
$datarequest = api::create_data_request($student->id, api::DATAREQUEST_TYPE_EXPORT);
$requestid = $datarequest->get('id');
+
+ // Login as a user without DPO role.
+ $this->setUser($teacher);
+ $this->expectException(required_capability_exception::class);
+ api::approve_data_request($requestid);
}
/**
$out = $this->heading(get_string('pluginname', 'tool_filetypes'));
if ($restricted) {
$out .= html_writer::div(
- html_writer::div(get_string('configoverride', 'admin'), 'form-overridden'),
+ html_writer::div(get_string('configoverride', 'admin'), 'alert alert-info'),
'', array('id' => 'adminsettings'));
}
if (count($combined) > 1) {
.path-admin-tool-filetypes .generaltable .nonstandard {
font-weight: bold;
}
-
-/* Spacing around the 'Defined in config.php' stripe */
-.path-admin-tool-filetypes .form-overridden {
- display: inline-block;
- margin-bottom: 1em;
- padding: 4px 6px;
-}
function xmldb_tool_log_upgrade($oldversion) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_logstore_database_upgrade($oldversion) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_logstore_standard_upgrade($oldversion) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
+++ /dev/null
-<?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/>.
-
-/**
- * This file contains renamed classes mappings.
- *
- * @package tool_lp
- * @copyright 2016 Frédéric Massart - FMCorz.net
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-$renamedclasses = array(
- 'tool_lp\\external\\cohort_summary_exporter' => 'core_cohort\\external\\cohort_summary_exporter',
- 'tool_lp\\external\\course_module_summary_exporter' => 'core_course\\external\\course_module_summary_exporter',
- 'tool_lp\\external\\course_summary_exporter' => 'core_course\\external\\course_summary_exporter',
- 'tool_lp\\form\\persistent' => 'core\\form\\persistent',
-);
function xmldb_tool_monitor_upgrade($oldversion) {
global $CFG, $DB;
- if ($oldversion < 2017021300) {
-
- // Delete "orphaned" subscriptions.
- $sql = "SELECT DISTINCT s.courseid
- FROM {tool_monitor_subscriptions} s
- LEFT OUTER JOIN {course} c ON c.id = s.courseid
- WHERE s.courseid <> 0 and c.id IS NULL";
- $deletedcourses = $DB->get_field_sql($sql);
- if ($deletedcourses) {
- list($sql, $params) = $DB->get_in_or_equal($deletedcourses);
- $DB->execute("DELETE FROM {tool_monitor_subscriptions} WHERE courseid " . $sql, $params);
- }
-
- // Monitor savepoint reached.
- upgrade_plugin_savepoint(true, 2017021300, 'tool', 'monitor');
- }
-
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
'keep-alive' => 0,
'showsql' => false,
'showdebugging' => false,
+ 'ignorelimits' => false,
], [
'h' => 'help',
'e' => 'execute',
'k' => 'keep-alive',
+ 'i' => 'ignorelimits',
]
);
--showdebugging Show developer level debugging information
-e, --execute Run all queued adhoc tasks
-k, --keep-alive=N Keep this script alive for N seconds and poll for new adhoc tasks
+ -i --ignorelimits Ignore task_adhoc_concurrency_limit and task_adhoc_max_runtime limits
Example:
\$sudo -u www-data /usr/bin/php admin/tool/task/cli/adhoc_task.php --execute
set_debugging(DEBUG_DEVELOPER, true);
}
+$checklimits = empty($options['ignorelimits']);
+
core_php_time_limit::raise();
// Increase memory limit.
// Emulate normal session - we use admin account by default.
cron_setup_user();
-// Start output log.
-$timestart = time();
-$timenow = $timestart;
-$finishtime = $timenow + (int)$options['keep-alive'];
-$humantimenow = date('r', $timenow);
-mtrace("Server Time: {$humantimenow}\n");
-
-// Run all adhoc tasks.
-$taskcount = 0;
-$waiting = false;
-while (!\core\task\manager::static_caches_cleared_since($timestart)) {
-
- $task = \core\task\manager::get_next_adhoc_task($timenow);
-
- if ($task) {
- if ($waiting) {
- cli_writeln('');
- }
- $waiting = false;
- cron_run_inner_adhoc_task($task);
- $taskcount++;
- unset($task);
- } else {
- if (time() > $finishtime) {
- break;
- }
- if (!$waiting) {
- cli_write('Waiting for more adhoc tasks to be queued ');
- } else {
- cli_write('.');
- }
- $waiting = true;
- sleep(1);
- $timenow = time();
- }
-}
-if ($waiting) {
- cli_writeln('');
-}
-
-mtrace("Ran {$taskcount} adhoc tasks found at {$humantimenow}");
+$humantimenow = date('r', time());
+$keepalive = (int)$options['keep-alive'];
+mtrace("Server Time: {$humantimenow}\n");
+cron_run_adhoc_tasks(time(), $keepalive, $checklimits);
$string['resettasktodefaults'] = 'Reset task schedule to defaults';
$string['resettasktodefaults_help'] = 'This will discard any local changes and revert the schedule for this task back to its original settings.';
$string['runnow'] = 'Run now';
+$string['runagain'] = 'Run again';
$string['runnow_confirm'] = 'Are you sure you want to run this task \'{$a}\' now? The task will run on the web server and may take some time to complete.';
$string['runpattern'] = 'Run pattern';
$string['scheduledtasks'] = 'Scheduled tasks';
echo html_writer::end_tag('pre');
$output = $PAGE->get_renderer('tool_task');
+
+// Re-run the specified task (this will output an error if it doesn't exist).
+echo $OUTPUT->single_button(new moodle_url('/admin/tool/task/schedule_task.php',
+ array('task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey())),
+ get_string('runagain', 'tool_task'));
echo $output->link_back();
echo $OUTPUT->footer();
$string['invalidencoding'] = 'Invalid encoding';
$string['invalidmode'] = 'Invalid mode selected';
$string['invalideupdatemode'] = 'Invalid update mode selected';
-$string['invalidvisibilitymode'] = 'Invalid visibility mode given';
+$string['invalidvisibilitymode'] = 'Invalid visible mode';
$string['invalidroles'] = 'Invalid role names: {$a}';
$string['invalidshortname'] = 'Invalid shortname';
$string['missingmandatoryfields'] = 'Missing value for mandatory fields: {$a}';
// Is this the first step?
if (this.isFirstStep(stepConfig.stepNumber)) {
- template.find('[data-role="previous"]').prop('disabled', true);
+ template.find('[data-role="previous"]').hide();
} else {
template.find('[data-role="previous"]').prop('disabled', false);
}
// Is this the final step?
if (this.isLastStep(stepConfig.stepNumber)) {
- template.find('[data-role="next"]').prop('disabled', true);
+ template.find('[data-role="next"]').hide();
+ template.find('[data-role="end"]').removeClass("btn-secondary").addClass("btn-primary");
} else {
template.find('[data-role="next"]').prop('disabled', false);
}
function xmldb_tool_usertours_upgrade($oldversion) {
global $CFG, $DB;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
$result = $this->launch('get_db_directories');
// Display list of DB directories if everything is ok
if ($result && !empty($XMLDB->dbdirs)) {
- $o .= '<table id="listdirectories" border="0" cellpadding="5" cellspacing="1" class="admintable generaltable">';
+ $o .= '<table id="listdirectories" border="0" cellpadding="5" cellspacing="1"' .
+ ' class="table-striped table-sm admintable generaltable">';
$row = 0;
foreach ($XMLDB->dbdirs as $key => $dbdir) {
// Detect if this is the lastused dir
This files describes API changes in /admin/*.
+=== 3.9 ===
+
+* The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-65809 for more info:
+ - upgrade_fix_block_instance_configuration()
+ - upgrade_theme_is_from_family()
+ - upgrade_find_theme_location()
+ - linkcoursesectionsupgradescriptwasrun setting
+ - upgrade_block_positions()
+
=== 3.8 ===
* Admin setting "Open to Google" (opentogoogle) has been renamed to the more generic "Open to search engines" (opentowebcrawlers).
$table->head = array ();
$table->colclasses = array();
$table->head[] = $fullnamedisplay;
- $table->attributes['class'] = 'admintable generaltable';
+ $table->attributes['class'] = 'admintable generaltable table-sm';
foreach ($extracolumns as $field) {
$table->head[] = ${$field};
}
function xmldb_auth_cas_upgrade($oldversion) {
global $CFG;
- if ($oldversion < 2017020700) {
- // Convert info in config plugins from auth/cas to auth_cas.
- upgrade_fix_config_auth_plugin_names('cas');
- upgrade_fix_config_auth_plugin_defaults('cas');
- upgrade_plugin_savepoint(true, 2017020700, 'auth', 'cas');
- }
-
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_auth_db_upgrade($oldversion) {
global $CFG, $DB;
- if ($oldversion < 2017032800) {
- // Convert info in config plugins from auth/db to auth_db
- upgrade_fix_config_auth_plugin_names('db');
- upgrade_fix_config_auth_plugin_defaults('db');
- upgrade_plugin_savepoint(true, 2017032800, 'auth', 'db');
- }
-
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_auth_email_upgrade($oldversion) {
global $CFG, $DB;
- if ($oldversion < 2017020700) {
- // Convert info in config plugins from auth/email to auth_email.
- upgrade_fix_config_auth_plugin_names('email');
- upgrade_fix_config_auth_plugin_defaults('email');
- upgrade_plugin_savepoint(true, 2017020700, 'auth', 'email');
- }
-
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_auth_ldap_upgrade($oldversion) {
global $CFG;
- if ($oldversion < 2017020700) {
- // Convert info in config plugins from auth/ldap to auth_ldap.
- upgrade_fix_config_auth_plugin_names('ldap');
- upgrade_fix_config_auth_plugin_defaults('ldap');
- upgrade_plugin_savepoint(true, 2017020700, 'auth', 'ldap');
- }
-
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- if ($oldversion < 2017080100) {
- // The "auth_ldap/coursecreators" setting was replaced with "auth_ldap/coursecreatorcontext" (created
- // dynamically from system-assignable roles) - so migrate any existing value to the first new slot.
- if ($ldapcontext = get_config('auth_ldap', 'creators')) {
- // Get info about the role that the old coursecreators setting would apply.
- $creatorrole = get_archetype_roles('coursecreator');
- $creatorrole = array_shift($creatorrole); // We can only use one, let's use the first.
-
- // Create new setting.
- set_config($creatorrole->shortname . 'context', $ldapcontext, 'auth_ldap');
-
- // Delete old setting.
- set_config('creators', null, 'auth_ldap');
-
- upgrade_plugin_savepoint(true, 2017080100, 'auth', 'ldap');
- }
- }
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_auth_manual_upgrade($oldversion) {
global $CFG;
- if ($oldversion < 2017020700) {
- // Convert info in config plugins from auth/manual to auth_manual.
- upgrade_fix_config_auth_plugin_names('manual');
- upgrade_fix_config_auth_plugin_defaults('manual');
- upgrade_plugin_savepoint(true, 2017020700, 'auth', 'manual');
- }
-
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_auth_mnet_upgrade($oldversion) {
global $CFG;
- if ($oldversion < 2017020700) {
- // Convert info in config plugins from auth/mnet to auth_mnet.
- upgrade_fix_config_auth_plugin_names('mnet');
- upgrade_fix_config_auth_plugin_defaults('mnet');
- upgrade_plugin_savepoint(true, 2017020700, 'auth', 'mnet');
- }
-
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_auth_none_upgrade($oldversion) {
global $CFG, $DB;
- if ($oldversion < 2017020700) {
- // Convert info in config plugins from auth/none to auth_none.
- upgrade_fix_config_auth_plugin_names('none');
- upgrade_fix_config_auth_plugin_defaults('none');
- upgrade_plugin_savepoint(true, 2017020700, 'auth', 'none');
- }
-
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
$dbman = $DB->get_manager();
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_auth_shibboleth_upgrade($oldversion) {
global $CFG, $DB;
- if ($oldversion < 2017020700) {
- // Convert info in config plugins from auth/shibboleth to auth_shibboleth.
- upgrade_fix_config_auth_plugin_names('shibboleth');
- upgrade_fix_config_auth_plugin_defaults('shibboleth');
- upgrade_plugin_savepoint(true, 2017020700, 'auth', 'shibboleth');
- }
-
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
This files describes API changes in /auth/* - plugins,
information provided here is intended especially for developers.
+=== 3.9 ===
+
+* The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-65809 for more info:
+ - upgrade_fix_config_auth_plugin_names()
+ - upgrade_fix_config_auth_plugin_defaults()
+
=== 3.7 ===
* get_password_change_info() method is added to the base class and returns an array containing the subject and body of the message
module.exports = ({template, types}) => {
const fs = require('fs');
const path = require('path');
- const glob = require('glob');
const cwd = process.cwd();
-
- // Static variable to hold the modules.
- let moodleSubsystems = null;
- let moodlePlugins = null;
-
- /**
- * Parse Moodle's JSON files containing the lists of components.
- *
- * The values are stored in the static variables because we
- * only need to load them once per transpiling run.
- */
- function loadMoodleModules() {
- moodleSubsystems = {'lib': 'core'};
- moodlePlugins = {};
- let components = fs.readFileSync('lib/components.json');
- components = JSON.parse(components);
-
- for (const [component, path] of Object.entries(components.subsystems)) {
- if (path) {
- // Prefix "core_" to the front of the subsystems.
- moodleSubsystems[path] = `core_${component}`;
- }
- }
-
- for (const [component, path] of Object.entries(components.plugintypes)) {
- if (path) {
- moodlePlugins[path] = component;
- }
- }
-
- for (const file of glob.sync('**/db/subplugins.json')) {
- var rawContents = fs.readFileSync(file);
- var subplugins = JSON.parse(rawContents);
-
- for (const [component, path] of Object.entries(subplugins.plugintypes)) {
- if (path) {
- moodlePlugins[path] = component;
- }
- }
- }
- }
+ const ComponentList = require(path.resolve('GruntfileComponents.js'));
/**
* Search the list of components that match the given file name
const fileName = file.replace('.js', '');
// Check subsystems first which require an exact match.
- if (moodleSubsystems.hasOwnProperty(componentPath)) {
- return `${moodleSubsystems[componentPath]}/${fileName}`;
- }
-
- // It's not a subsystem so it must be a plugin. Moodle defines root folders
- // where plugins can be installed so our path with be <plugin_root>/<plugin_name>.
- // Let's separate the two.
- let pathParts = componentPath.split('/');
- const pluginName = pathParts.pop();
- const pluginPath = pathParts.join('/');
-
- // The plugin path mutch match exactly because some plugins are subplugins of
- // other plugins which means their paths would partially match.
- if (moodlePlugins.hasOwnProperty(pluginPath)) {
- return `${moodlePlugins[pluginPath]}_${pluginName}/${fileName}`;
+ const componentName = ComponentList.getComponentFromPath(componentPath);
+ if (componentName) {
+ return `${componentName}/${fileName}`;
}
// This matches the previous PHP behaviour that would throw an exception
// if it couldn't parse an AMD file.
- throw new Error('Unable to find module name for ' + searchFileName);
+ throw new Error(`Unable to find module name for ${searchFileName} (${componentPath}::${file}}`);
}
/**
pre() {
this.seenDefine = false;
this.addedReturnForDefaultExport = false;
-
- if (moodleSubsystems === null) {
- loadMoodleModules();
- }
},
visitor: {
// Plugin ordering is only respected if we visit the "Program" node.
$url = $component->get_url();
$output = html_writer::start_tag('div', array('class' => 'restore-course-search form-inline mb-1'));
- $output .= html_writer::start_tag('div', array('class' => 'rcs-results w-75'));
+ $output .= html_writer::start_tag('div', array('class' => 'rcs-results table-sm w-75'));
$table = new html_table();
$table->head = array('', get_string('shortnamecourse'), get_string('fullnamecourse'));
$url = $component->get_url();
$output = html_writer::start_tag('div', array('class' => 'restore-course-search form-inline mb-1'));
- $output .= html_writer::start_tag('div', array('class' => 'rcs-results w-75'));
+ $output .= html_writer::start_tag('div', array('class' => 'rcs-results table-sm w-75'));
$table = new html_table();
$table->head = array('', get_string('name'), get_string('description'));
$paging = new paging_bar($badges->totalcount, $badges->page, $badges->perpage, $this->page->url, 'page');
$htmlpagingbar = $this->render($paging);
$table = new html_table();
- $table->attributes['class'] = 'collection';
+ $table->attributes['class'] = 'table table-bordered table-striped';
$sortbyname = $this->helper_sortable_heading(get_string('name'),
'name', $badges->sort, $badges->dir);
$htmlpagingbar = $this->render($paging);
$table = new html_table();
- $table->attributes['class'] = 'collection';
+ $table->attributes['class'] = 'table table-bordered table-striped';
$sortbyname = $this->helper_sortable_heading(get_string('name'),
'name', $badges->sort, $badges->dir);
function xmldb_block_badges_upgrade($oldversion, $block) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_block_calendar_month_upgrade($oldversion, $block) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_block_calendar_upcoming_upgrade($oldversion, $block) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_block_completionstatus_upgrade($oldversion, $block) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_block_course_summary_upgrade($oldversion, $block) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_block_html_upgrade($oldversion) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_block_navigation_upgrade($oldversion, $block) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_block_quiz_results_upgrade($oldversion, $block) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_block_recent_activity_upgrade($oldversion, $block) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_block_rss_client_upgrade($oldversion) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_block_section_links_upgrade($oldversion, $block) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_block_selfcompletion_upgrade($oldversion, $block) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
function xmldb_block_settings_upgrade($oldversion, $block) {
global $CFG;
- // Automatically generated Moodle v3.3.0 release upgrade line.
- // Put any upgrade step following this.
-
- // Automatically generated Moodle v3.4.0 release upgrade line.
- // Put any upgrade step following this.
-
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
}
var calendarEvents = result.events.filter(function(event) {
- // Do not include events that does not have a due date.
- return event.eventtype != "open" && event.eventtype != "opensubmission";
+ if (event.eventtype == "open" || event.eventtype == "opensubmission") {
+ var dayTimestamp = UserDate.getUserMidnightForTimestamp(event.timesort, midnight);
+ return dayTimestamp > midnight;
+ }
+ return true;
});
// We expect to receive limit + 1 events back from the server.
// Any less means there are no more events to load.
*/
class BulkWriteResult
{
+ /** @var WriteResult */
private $writeResult;
+
+ /** @var mixed[] */
private $insertedIds;
+
+ /** @var boolean */
private $isAcknowledged;
/**
- * Constructor.
- *
* @param WriteResult $writeResult
* @param mixed[] $insertedIds
*/
namespace MongoDB;
-use MongoDB\BSON\Serializable;
-use MongoDB\Driver\Cursor;
+use Iterator;
+use MongoDB\Driver\CursorId;
use MongoDB\Driver\Exception\ConnectionException;
use MongoDB\Driver\Exception\RuntimeException;
use MongoDB\Driver\Exception\ServerException;
-use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Exception\ResumeTokenException;
-use IteratorIterator;
-use Iterator;
+use MongoDB\Model\ChangeStreamIterator;
+use function call_user_func;
+use function in_array;
/**
* Iterator for a change stream.
*/
const CURSOR_NOT_FOUND = 43;
- private static $errorCodeCappedPositionLost = 136;
- private static $errorCodeInterrupted = 11601;
- private static $errorCodeCursorKilled = 237;
+ /** @var array */
+ private static $nonResumableErrorCodes = [
+ 136, // CappedPositionLost
+ 237, // CursorKilled
+ 11601, // Interrupted
+ ];
- private $resumeToken;
+ /** @var callable */
private $resumeCallable;
- private $csIt;
+
+ /** @var ChangeStreamIterator */
+ private $iterator;
+
+ /** @var integer */
private $key = 0;
- private $hasAdvanced = false;
/**
- * Constructor.
+ * Whether the change stream has advanced to its first result. This is used
+ * to determine whether $key should be incremented after an iteration event.
*
+ * @var boolean
+ */
+ private $hasAdvanced = false;
+
+ /**
* @internal
- * @param Cursor $cursor
- * @param callable $resumeCallable
+ * @param ChangeStreamIterator $iterator
+ * @param callable $resumeCallable
*/
- public function __construct(Cursor $cursor, callable $resumeCallable)
+ public function __construct(ChangeStreamIterator $iterator, callable $resumeCallable)
{
+ $this->iterator = $iterator;
$this->resumeCallable = $resumeCallable;
- $this->csIt = new IteratorIterator($cursor);
}
/**
*/
public function current()
{
- return $this->csIt->current();
+ return $this->iterator->current();
}
/**
- * @return \MongoDB\Driver\CursorId
+ * @return CursorId
*/
public function getCursorId()
{
- return $this->csIt->getInnerIterator()->getId();
+ return $this->iterator->getInnerIterator()->getId();
+ }
+
+ /**
+ * Returns the resume token for the iterator's current position.
+ *
+ * Null may be returned if no change documents have been iterated and the
+ * server did not include a postBatchResumeToken in its aggregate or getMore
+ * command response.
+ *
+ * @return array|object|null
+ */
+ public function getResumeToken()
+ {
+ return $this->iterator->getResumeToken();
}
/**
if ($this->valid()) {
return $this->key;
}
+
return null;
}
/**
* @see http://php.net/iterator.next
* @return void
+ * @throws ResumeTokenException
*/
public function next()
{
try {
- $this->csIt->next();
- if ($this->valid()) {
- if ($this->hasAdvanced) {
- $this->key++;
- }
- $this->hasAdvanced = true;
- $this->resumeToken = $this->extractResumeToken($this->csIt->current());
- }
- /* If the cursorId is 0, the server has invalidated the cursor so we
- * will never perform another getMore. This means that we cannot
- * resume and we can therefore unset the resumeCallable, which will
- * free any reference to Watch. This will also free the only
- * reference to an implicit session, since any such reference
- * belongs to Watch. */
- if ((string) $this->getCursorId() === '0') {
- $this->resumeCallable = null;
- }
+ $this->iterator->next();
+ $this->onIteration($this->hasAdvanced);
} catch (RuntimeException $e) {
- if ($this->isResumableError($e)) {
- $this->resume();
- }
+ $this->resumeOrThrow($e);
}
}
/**
* @see http://php.net/iterator.rewind
* @return void
+ * @throws ResumeTokenException
*/
public function rewind()
{
try {
- $this->csIt->rewind();
- if ($this->valid()) {
- $this->hasAdvanced = true;
- $this->resumeToken = $this->extractResumeToken($this->csIt->current());
- }
- // As with next(), free the callable once we know it will never be used.
- if ((string) $this->getCursorId() === '0') {
- $this->resumeCallable = null;
- }
+ $this->iterator->rewind();
+ /* Unlike next() and resume(), the decision to increment the key
+ * does not depend on whether the change stream has advanced. This
+ * ensures that multiple calls to rewind() do not alter state. */
+ $this->onIteration(false);
} catch (RuntimeException $e) {
- if ($this->isResumableError($e)) {
- $this->resume();
- }
+ $this->resumeOrThrow($e);
}
}
*/
public function valid()
{
- return $this->csIt->valid();
+ return $this->iterator->valid();
}
/**
- * Extracts the resume token (i.e. "_id" field) from the change document.
+ * Determines if an exception is a resumable error.
*
- * @param array|document $document Change document
- * @return mixed
- * @throws InvalidArgumentException
- * @throws ResumeTokenException if the resume token is not found or invalid
+ * @see https://github.com/mongodb/specifications/blob/master/source/change-streams/change-streams.rst#resumable-error
+ * @param RuntimeException $exception
+ * @return boolean
*/
- private function extractResumeToken($document)
+ private function isResumableError(RuntimeException $exception)
{
- if ( ! is_array($document) && ! is_object($document)) {
- throw InvalidArgumentException::invalidType('$document', $document, 'array or object');
+ if ($exception instanceof ConnectionException) {
+ return true;
}
- if ($document instanceof Serializable) {
- return $this->extractResumeToken($document->bsonSerialize());
+ if (! $exception instanceof ServerException) {
+ return false;
}
- $resumeToken = is_array($document)
- ? (isset($document['_id']) ? $document['_id'] : null)
- : (isset($document->_id) ? $document->_id : null);
-
- if ( ! isset($resumeToken)) {
- throw ResumeTokenException::notFound();
+ if ($exception->hasErrorLabel('NonResumableChangeStreamError')) {
+ return false;
}
- if ( ! is_array($resumeToken) && ! is_object($resumeToken)) {
- throw ResumeTokenException::invalidType($resumeToken);
+ if (in_array($exception->getCode(), self::$nonResumableErrorCodes)) {
+ return false;
}
- return $resumeToken;
+ return true;
}
/**
- * Determines if an exception is a resumable error.
+ * Perform housekeeping after an iteration event.
*
- * @see https://github.com/mongodb/specifications/blob/master/source/change-streams/change-streams.rst#resumable-error
- * @param RuntimeException $exception
- * @return boolean
+ * @param boolean $incrementKey Increment $key if there is a current result
+ * @throws ResumeTokenException
*/
- private function isResumableError(RuntimeException $exception)
+ private function onIteration($incrementKey)
{
- if ($exception instanceof ConnectionException) {
- return true;
+ /* If the cursorId is 0, the server has invalidated the cursor and we
+ * will never perform another getMore nor need to resume since any
+ * remaining results (up to and including the invalidate event) will
+ * have been received in the last response. Therefore, we can unset the
+ * resumeCallable. This will free any reference to Watch as well as the
+ * only reference to any implicit session created therein. */
+ if ((string) $this->getCursorId() === '0') {
+ $this->resumeCallable = null;
}
- if ( ! $exception instanceof ServerException) {
- return false;
+ /* Return early if there is not a current result. Avoid any attempt to
+ * increment the iterator's key. */
+ if (! $this->valid()) {
+ return;
}
- if (in_array($exception->getCode(), [self::$errorCodeCappedPositionLost, self::$errorCodeCursorKilled, self::$errorCodeInterrupted])) {
- return false;
+ if ($incrementKey) {
+ $this->key++;
}
- return true;
+ $this->hasAdvanced = true;
}
/**
- * Creates a new changeStream after a resumable server error.
+ * Recreates the ChangeStreamIterator after a resumable server error.
*
* @return void
*/
private function resume()
{
- $newChangeStream = call_user_func($this->resumeCallable, $this->resumeToken);
- $this->csIt = $newChangeStream->csIt;
- $this->csIt->rewind();
+ $this->iterator = call_user_func($this->resumeCallable, $this->getResumeToken(), $this->hasAdvanced);
+ $this->iterator->rewind();
+
+ $this->onIteration($this->hasAdvanced);
+ }
+
+ /**
+ * Either resumes after a resumable error or re-throws the exception.
+ *
+ * @param RuntimeException $exception
+ * @throws RuntimeException
+ */
+ private function resumeOrThrow(RuntimeException $exception)
+ {
+ if ($this->isResumableError($exception)) {
+ $this->resume();
+
+ return;
+ }
+
+ throw $exception;
}
}
namespace MongoDB;
+use MongoDB\Driver\Exception\InvalidArgumentException as DriverInvalidArgumentException;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
use MongoDB\Driver\Manager;
use MongoDB\Driver\ReadConcern;
use MongoDB\Driver\ReadPreference;
+use MongoDB\Driver\Session;
use MongoDB\Driver\WriteConcern;
-use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
-use MongoDB\Driver\Exception\InvalidArgumentException as DriverInvalidArgumentException;
use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Exception\UnexpectedValueException;
use MongoDB\Exception\UnsupportedException;
+use MongoDB\Model\BSONArray;
+use MongoDB\Model\BSONDocument;
use MongoDB\Model\DatabaseInfoIterator;
use MongoDB\Operation\DropDatabase;
use MongoDB\Operation\ListDatabases;
use MongoDB\Operation\Watch;
+use function is_array;
class Client
{
+ /** @var array */
private static $defaultTypeMap = [
- 'array' => 'MongoDB\Model\BSONArray',
- 'document' => 'MongoDB\Model\BSONDocument',
- 'root' => 'MongoDB\Model\BSONDocument',
+ 'array' => BSONArray::class,
+ 'document' => BSONDocument::class,
+ 'root' => BSONDocument::class,
];
+
+ /** @var integer */
private static $wireVersionForReadConcern = 4;
+
+ /** @var integer */
private static $wireVersionForWritableCommandWriteConcern = 5;
+ /** @var Manager */
private $manager;
+
+ /** @var ReadConcern */
private $readConcern;
+
+ /** @var ReadPreference */
private $readPreference;
+
+ /** @var string */
private $uri;
+
+ /** @var array */
private $typeMap;
+
+ /** @var WriteConcern */
private $writeConcern;
/**
*/
public function dropDatabase($databaseName, array $options = [])
{
- if ( ! isset($options['typeMap'])) {
+ if (! isset($options['typeMap'])) {
$options['typeMap'] = $this->typeMap;
}
- $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+ $server = select_server($this->manager, $options);
- if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
$options['writeConcern'] = $this->writeConcern;
}
* List databases.
*
* @see ListDatabases::__construct() for supported options
+ * @param array $options
* @return DatabaseInfoIterator
* @throws UnexpectedValueException if the command response was malformed
* @throws InvalidArgumentException for parameter/option parsing errors
public function listDatabases(array $options = [])
{
$operation = new ListDatabases($options);
- $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+ $server = select_server($this->manager, $options);
return $operation->execute($server);
}
* Start a new client session.
*
* @see http://php.net/manual/en/mongodb-driver-manager.startsession.php
- * @param array $options Session options
- * @return MongoDB\Driver\Session
+ * @param array $options Session options
+ * @return Session
*/
public function startSession(array $options = [])
{
*/
public function watch(array $pipeline = [], array $options = [])
{
- if ( ! isset($options['readPreference'])) {
+ if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
$options['readPreference'] = $this->readPreference;
}
- $server = $this->manager->selectServer($options['readPreference']);
+ $server = select_server($this->manager, $options);
- if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
$options['readConcern'] = $this->readConcern;
}
- if ( ! isset($options['typeMap'])) {
+ if (! isset($options['typeMap'])) {
$options['typeMap'] = $this->typeMap;
}
namespace MongoDB;
use MongoDB\BSON\JavascriptInterface;
-use MongoDB\BSON\Serializable;
-use MongoDB\ChangeStream;
use MongoDB\Driver\Cursor;
+use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
use MongoDB\Driver\Manager;
use MongoDB\Driver\ReadConcern;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\WriteConcern;
-use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Exception\UnexpectedValueException;
use MongoDB\Exception\UnsupportedException;
+use MongoDB\Model\BSONArray;
+use MongoDB\Model\BSONDocument;
use MongoDB\Model\IndexInfo;
use MongoDB\Model\IndexInfoIterator;
use MongoDB\Operation\Aggregate;
use MongoDB\Operation\BulkWrite;
-use MongoDB\Operation\CreateIndexes;
use MongoDB\Operation\Count;
use MongoDB\Operation\CountDocuments;
+use MongoDB\Operation\CreateIndexes;
use MongoDB\Operation\DeleteMany;
use MongoDB\Operation\DeleteOne;
use MongoDB\Operation\Distinct;
use MongoDB\Operation\UpdateOne;
use MongoDB\Operation\Watch;
use Traversable;
+use function array_diff_key;
+use function array_intersect_key;
+use function current;
+use function is_array;
+use function strlen;
class Collection
{
+ /** @var array */
private static $defaultTypeMap = [
- 'array' => 'MongoDB\Model\BSONArray',
- 'document' => 'MongoDB\Model\BSONDocument',
- 'root' => 'MongoDB\Model\BSONDocument',
+ 'array' => BSONArray::class,
+ 'document' => BSONDocument::class,
+ 'root' => BSONDocument::class,
];
+
+ /** @var integer */
private static $wireVersionForFindAndModifyWriteConcern = 4;
+
+ /** @var integer */
private static $wireVersionForReadConcern = 4;
+
+ /** @var integer */
private static $wireVersionForWritableCommandWriteConcern = 5;
+ /** @var integer */
+ private static $wireVersionForReadConcernWithWriteStage = 8;
+
+ /** @var string */
private $collectionName;
+
+ /** @var string */
private $databaseName;
+
+ /** @var Manager */
private $manager;
+
+ /** @var ReadConcern */
private $readConcern;
+
+ /** @var ReadPreference */
private $readPreference;
+
+ /** @var array */
private $typeMap;
+
+ /** @var WriteConcern */
private $writeConcern;
/**
}
if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) {
- throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], 'MongoDB\Driver\ReadConcern');
+ throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], ReadConcern::class);
}
if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
- throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
+ throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], ReadPreference::class);
}
if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
}
if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
- throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
+ throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class);
}
$this->manager = $manager;
*/
public function aggregate(array $pipeline, array $options = [])
{
- $hasOutStage = \MongoDB\is_last_pipeline_operator_out($pipeline);
+ $hasWriteStage = is_last_pipeline_operator_write($pipeline);
- if ( ! isset($options['readPreference'])) {
+ if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
$options['readPreference'] = $this->readPreference;
}
- if ($hasOutStage) {
+ if ($hasWriteStage) {
$options['readPreference'] = new ReadPreference(ReadPreference::RP_PRIMARY);
}
- $server = $this->manager->selectServer($options['readPreference']);
+ $server = select_server($this->manager, $options);
- /* A "majority" read concern is not compatible with the $out stage, so
- * avoid providing the Collection's read concern if it would conflict.
+ /* MongoDB 4.2 and later supports a read concern when an $out stage is
+ * being used, but earlier versions do not.
+ *
+ * A read concern is also not compatible with transactions.
*/
- if ( ! isset($options['readConcern']) &&
- ! ($hasOutStage && $this->readConcern->getLevel() === ReadConcern::MAJORITY) &&
- \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ if (! isset($options['readConcern']) &&
+ server_supports_feature($server, self::$wireVersionForReadConcern) &&
+ ! is_in_transaction($options) &&
+ ( ! $hasWriteStage || server_supports_feature($server, self::$wireVersionForReadConcernWithWriteStage))
+ ) {
$options['readConcern'] = $this->readConcern;
}
- if ( ! isset($options['typeMap'])) {
+ if (! isset($options['typeMap'])) {
$options['typeMap'] = $this->typeMap;
}
- if ($hasOutStage && ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ if ($hasWriteStage &&
+ ! isset($options['writeConcern']) &&
+ server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) &&
+ ! is_in_transaction($options)) {
$options['writeConcern'] = $this->writeConcern;
}
*/
public function bulkWrite(array $operations, array $options = [])
{
- if ( ! isset($options['writeConcern'])) {
+ if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
$options['writeConcern'] = $this->writeConcern;
}
$operation = new BulkWrite($this->databaseName, $this->collectionName, $operations, $options);
- $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+ $server = select_server($this->manager, $options);
return $operation->execute($server);
}
*/
public function count($filter = [], array $options = [])
{
- if ( ! isset($options['readPreference'])) {
+ if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
$options['readPreference'] = $this->readPreference;
}
- $server = $this->manager->selectServer($options['readPreference']);
+ $server = select_server($this->manager, $options);
- if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
$options['readConcern'] = $this->readConcern;
}
*/
public function countDocuments($filter = [], array $options = [])
{
- if ( ! isset($options['readPreference'])) {
+ if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
$options['readPreference'] = $this->readPreference;
}
- $server = $this->manager->selectServer($options['readPreference']);
+ $server = select_server($this->manager, $options);
- if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
$options['readConcern'] = $this->readConcern;
}
*/
public function createIndexes(array $indexes, array $options = [])
{
- $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+ $server = select_server($this->manager, $options);
- if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
$options['writeConcern'] = $this->writeConcern;
}
*/
public function deleteMany($filter, array $options = [])
{
- if ( ! isset($options['writeConcern'])) {
+ if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
$options['writeConcern'] = $this->writeConcern;
}
$operation = new DeleteMany($this->databaseName, $this->collectionName, $filter, $options);
- $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+ $server = select_server($this->manager, $options);
return $operation->execute($server);
}
*/
public function deleteOne($filter, array $options = [])
{
- if ( ! isset($options['writeConcern'])) {
+ if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
$options['writeConcern'] = $this->writeConcern;
}
$operation = new DeleteOne($this->databaseName, $this->collectionName, $filter, $options);
- $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+ $server = select_server($this->manager, $options);
return $operation->execute($server);
}
* Finds the distinct values for a specified field across the collection.
*
* @see Distinct::__construct() for supported options
- * @param string $fieldName Field for which to return distinct values
- * @param array|object $filter Query by which to filter documents
- * @param array $options Command options
+ * @param string $fieldName Field for which to return distinct values
+ * @param array|object $filter Query by which to filter documents
+ * @param array $options Command options
* @return mixed[]
* @throws UnexpectedValueException if the command response was malformed
* @throws UnsupportedException if options are not supported by the selected server
*/
public function distinct($fieldName, $filter = [], array $options = [])
{
- if ( ! isset($options['readPreference'])) {
+ if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
$options['readPreference'] = $this->readPreference;
}
- $server = $this->manager->selectServer($options['readPreference']);
+ if (! isset($options['typeMap'])) {
+ $options['typeMap'] = $this->typeMap;
+ }
+
+ $server = select_server($this->manager, $options);
- if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
$options['readConcern'] = $this->readConcern;
}
*/
public function drop(array $options = [])
{
- if ( ! isset($options['typeMap'])) {
+ if (! isset($options['typeMap'])) {
$options['typeMap'] = $this->typeMap;
}
- $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+ $server = select_server($this->manager, $options);
- if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
$options['writeConcern'] = $this->writeConcern;
}
*
* @see DropIndexes::__construct() for supported options
* @param string|IndexInfo $indexName Index name or model object
- * @param array $options Additional options
+ * @param array $options Additional options
* @return array|object Command result document
* @throws UnsupportedException if options are not supported by the selected server
* @throws InvalidArgumentException for parameter/option parsing errors
throw new InvalidArgumentException('dropIndexes() must be used to drop multiple indexes');
}
- if ( ! isset($options['typeMap'])) {
+ if (! isset($options['typeMap'])) {
$options['typeMap'] = $this->typeMap;
}
- $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+ $server = select_server($this->manager, $options);
- if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
$options['writeConcern'] = $this->writeConcern;
}
*/
public function dropIndexes(array $options = [])
{
- if ( ! isset($options['typeMap'])) {
+ if (! isset($options['typeMap'])) {
$options['typeMap'] = $this->typeMap;
}
- $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+ $server = select_server($this->manager, $options);
- if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern)) {
+ if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) {
$options['writeConcern'] = $this->writeConcern;
}
* @throws InvalidArgumentException for parameter/option parsing errors
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
- public function EstimatedDocumentCount(array $options = [])
+ public function estimatedDocumentCount(array $options = [])
{
- if ( ! isset($options['readPreference'])) {
+ if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
$options['readPreference'] = $this->readPreference;
}
- $server = $this->manager->selectServer($options['readPreference']);
+ $server = select_server($this->manager, $options);
- if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
$options['readConcern'] = $this->readConcern;
}
*
* @see Explain::__construct() for supported options
* @see http://docs.mongodb.org/manual/reference/command/explain/
- * @param Explainable $explainable Command on which to run explain
- * @param array $options Additional options
+ * @param Explainable $explainable Command on which to run explain
+ * @param array $options Additional options
* @return array|object
* @throws UnsupportedException if explainable or options are not supported by the selected server
* @throws InvalidArgumentException for parameter/option parsing errors
*/
public function explain(Explainable $explainable, array $options = [])
{
- if ( ! isset($options['readPreference'])) {
+ if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
$options['readPreference'] = $this->readPreference;
}
- if ( ! isset($options['typeMap'])) {
+ if (! isset($options['typeMap'])) {
$options['typeMap'] = $this->typeMap;
}
- $server = $this->manager->selectServer($options['readPreference']);
+ $server = select_server($this->manager, $options);
$operation = new Explain($this->databaseName, $explainable, $options);
*/
public function find($filter = [], array $options = [])
{
- if ( ! isset($options['readPreference'])) {
+ if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
$options['readPreference'] = $this->readPreference;
}
- $server = $this->manager->selectServer($options['readPreference']);
+ $server = select_server($this->manager, $options);
- if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
$options['readConcern'] = $this->readConcern;
}
- if ( ! isset($options['typeMap'])) {
+ if (! isset($options['typeMap'])) {
$options['typeMap'] = $this->typeMap;
}
*/
public function findOne($filter = [], array $options = [])
{
- if ( ! isset($options['readPreference'])) {
+ if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
$options['readPreference'] = $this->readPreference;
}
- $server = $this->manager->selectServer($options['readPreference']);
+ $server = select_server($this->manager, $options);
- if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
+ if (! isset($options['readConcern']) && server_supports_feature($server, self::$wireVersionForReadConcern) && ! is_in_transaction($options)) {
$options['readConcern'] = $this->readConcern;
}
- if ( ! isset($options['typeMap'])) {
+ if (! isset($options['typeMap'])) {
$options['typeMap'] = $this->typeMap;
}
*/
public function findOneAndDelete($filter, array $options = [])
{
- $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+ $server = select_server($this->manager, $options);
- if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern)) {
+ if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern) && ! is_in_transaction($options)) {
$options['writeConcern'] = $this->writeConcern;
}
- if ( ! isset($options['typeMap'])) {
+ if (! isset($options['typeMap'])) {
$options['typeMap'] = $this->typeMap;
}
*/
public function findOneAndReplace($filter, $replacement, array $options = [])
{
- $server = $this->manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
+ $server = select_server($this->manager, $options);
- if ( ! isset($options['writeConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern)) {
+ if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForFindAndModifyWriteConcern) && ! is_in_transaction($options)) {
$options['writeConcern'] = $this->writeConcern;
}
- if ( ! isset($options['typeMap'])) {
+ if (! isset($options['typeMap'])) {
$options['typeMap'] = $this->typeMap;
}
<