{
+ 'plugins': [
+ 'promise',
+ ],
'env': {
'browser': true,
'amd': true
'unicode-bom': 'error',
'wrap-regex': 'off',
+ // === Promises ===
+ 'promise/always-return': 'warn',
+ 'promise/no-return-wrap': 'warn',
+ 'promise/param-names': 'warn',
+ 'promise/catch-or-return': ['warn', {terminationMethod: ['catch', 'fail']}],
+ 'promise/no-native': 'warn',
+ 'promise/no-promise-in-callback': 'warn',
+ 'promise/no-callback-in-promise': 'warn',
+ 'promise/avoid-new': 'warn',
+
// === Deprecations ===
"no-restricted-properties": ['warn', {
'object': 'M',
'property': 'str',
'message': 'Use AMD module "core/str" or M.util.get_string()'
}],
+
}
}
opts: {stdio: 'inherit', env: process.env}
}, function(error, result, code) {
// Propagate the exit code.
- done(code);
+ done(code === 0);
});
};
$mediancoursesize = get_config('hub', 'site_mediancoursesize_' . $cleanhuburl);
$participantnumberaveragecfg = get_config('hub', 'site_participantnumberaverage_' . $cleanhuburl);
$modulenumberaveragecfg = get_config('hub', 'site_modulenumberaverage_' . $cleanhuburl);
+ // Mobile related information.
+ $mobileservicesenabled = get_config('hub', 'site_mobileservicesenabled_' . $cleanhuburl);
+ $mobilenotificacionsenabled = get_config('hub', 'site_mobilenotificacionsenabled_' . $cleanhuburl);
+ $registereduserdevices = get_config('hub', 'site_registereduserdevices_' . $cleanhuburl);
+ $registeredactiveuserdevices = get_config('hub', 'site_registeredactiveuserdevices_' . $cleanhuburl);
//hidden parameters
$mform->addElement('hidden', 'huburl', $huburl);
require_once($CFG->libdir . '/badgeslib.php');
$badges = $DB->count_records_select('badge', 'status <> ' . BADGE_STATUS_ARCHIVED);
$issuedbadges = $DB->count_records('badge_issued');
+ // Mobile related information.
+ $ismobileenabled = false;
+ $aremobilenotificationsenabled = false;
+ $registereduserdevicescount = 0;
+ $registeredactiveuserdevicescount = 0;
+ if (!empty($CFG->enablewebservices) && !empty($CFG->enablemobilewebservice)) {
+ $ismobileenabled = true;
+ $registereduserdevicescount = $DB->count_records('user_devices');
+ $airnotifierextpath = $CFG->dirroot . '/message/output/airnotifier/externallib.php';
+ if (file_exists($airnotifierextpath)) { // Maybe some one uninstalled the plugin.
+ require_once($airnotifierextpath);
+ $aremobilenotificationsenabled = (bool) message_airnotifier_external::is_system_configured();
+ $registeredactiveuserdevicescount = $DB->count_records('message_airnotifier_devices', array('enable' => 1));
+ }
+ }
if (HUB_MOODLEORGHUBURL != $huburl) {
$mform->addElement('checkbox', 'courses', get_string('sendfollowinginfo', 'hub'),
" " . get_string('modulenumberaverage', 'hub', $modulenumberaverage));
$mform->setDefault('modulenumberaverage', $modulenumberaveragecfg != -1);
$mform->setType('modulenumberaverage', PARAM_FLOAT);
+
+ $mobileservicestatus = $ismobileenabled ? 'yes' : 'no';
+ $mform->addElement('checkbox', 'mobileservicesenabled', '',
+ " " . get_string('mobileservicesenabled', 'hub', $mobileservicestatus));
+ $mform->setDefault('mobileservicesenabled', $mobileservicesenabled != -1);
+ $mform->setType('mobileservicesenabled', PARAM_INT);
+
+ $mobilenotificationsstatus = $aremobilenotificationsenabled ? 'yes' : 'no';
+ $mform->addElement('checkbox', 'mobilenotificacionsenabled', '',
+ " " . get_string('mobilenotificacionsenabled', 'hub', $mobilenotificationsstatus));
+ $mform->setDefault('mobilenotificacionsenabled', $mobilenotificacionsenabled != -1);
+ $mform->setType('mobilenotificacionsenabled', PARAM_INT);
+
+ $mform->addElement('checkbox', 'registereduserdevices', '',
+ " " . get_string('registereduserdevices', 'hub', $registereduserdevicescount));
+ $mform->setDefault('registereduserdevices', $registereduserdevices != -1);
+ $mform->setType('registereduserdevices', PARAM_INT);
+
+ $mform->addElement('checkbox', 'registeredactiveuserdevices', '',
+ " " . get_string('registeredactiveuserdevices', 'hub', $registeredactiveuserdevicescount));
+ $mform->setDefault('registeredactiveuserdevices', $registeredactiveuserdevices != -1);
+ $mform->setType('registeredactiveuserdevices', PARAM_INT);
} else {
$mform->addElement('static', 'courseslabel', get_string('sendfollowinginfo', 'hub'),
" " . get_string('coursesnumber', 'hub', $coursecount));
" " . get_string('modulenumberaverage', 'hub', $modulenumberaverage));
$mform->addElement('hidden', 'modulenumberaverage', 1);
$mform->setType('modulenumberaverage', PARAM_FLOAT);
+
+ $mobileservicestatus = $ismobileenabled ? 'yes' : 'no';
+ $mform->addElement('static', 'mobileservicesenabledlabel', '',
+ " " . get_string('mobileservicesenabled', 'hub', $mobileservicestatus));
+ $mform->addElement('hidden', 'mobileservicesenabled', 1);
+ $mform->setType('mobileservicesenabled', PARAM_INT);
+
+ $mobilenotificationsstatus = $aremobilenotificationsenabled ? 'yes' : 'no';
+ $mform->addElement('static', 'mobilenotificacionsenabledlabel', '',
+ " " . get_string('mobilenotificacionsenabled', 'hub', $mobilenotificationsstatus));
+ $mform->addElement('hidden', 'mobilenotificacionsenabled', 1);
+ $mform->setType('mobilenotificacionsenabled', PARAM_INT);
+
+ $mform->addElement('static', 'registereduserdeviceslabel', '',
+ " " . get_string('registereduserdevices', 'hub', $registereduserdevicescount));
+ $mform->addElement('hidden', 'registereduserdevices', 1);
+ $mform->setType('registereduserdevices', PARAM_INT);
+
+ $mform->addElement('static', 'registeredactiveuserdeviceslabel', '',
+ " " . get_string('registeredactiveuserdevices', 'hub', $registeredactiveuserdevicescount));
+ $mform->addElement('hidden', 'registeredactiveuserdevices', 1);
+ $mform->setType('registeredactiveuserdevices', PARAM_INT);
}
//check if it's a first registration or update
$siteinfo['moodleversion'] = $CFG->version;
$siteinfo['moodlerelease'] = $CFG->release;
$siteinfo['url'] = $CFG->wwwroot;
+ // Mobile related information.
+ $siteinfo['mobileservicesenabled'] = 0;
+ $siteinfo['mobilenotificacionsenabled'] = 0;
+ $siteinfo['registereduserdevices'] = 0;
+ $siteinfo['registeredactiveuserdevices'] = 0;
+ if (!empty($CFG->enablewebservices) && !empty($CFG->enablemobilewebservice)) {
+ $siteinfo['mobileservicesenabled'] = 1;
+ $siteinfo['registereduserdevices'] = $DB->count_records('user_devices');
+ $airnotifierextpath = $CFG->dirroot . '/message/output/airnotifier/externallib.php';
+ if (file_exists($airnotifierextpath)) { // Maybe some one uninstalled the plugin.
+ require_once($airnotifierextpath);
+ $siteinfo['mobilenotificacionsenabled'] = message_airnotifier_external::is_system_configured();
+ $siteinfo['registeredactiveuserdevices'] = $DB->count_records('message_airnotifier_devices', array('enable' => 1));
+ }
+ }
return $siteinfo;
}
// Set to -1 all optional data marked as "don't send" by the admin.
// The function get_site_info() will not calculate the optional data if config is set to -1.
$inputnames = array('courses', 'users', 'roleassignments', 'posts', 'questions', 'resources',
- 'badges', 'issuedbadges', 'modulenumberaverage', 'participantnumberaverage');
+ 'badges', 'issuedbadges', 'modulenumberaverage', 'participantnumberaverage',
+ 'mobileservicesenabled', 'mobilenotificacionsenabled', 'registereduserdevices', 'registeredactiveuserdevices');
foreach ($inputnames as $inputname) {
if (empty($fromform->{$inputname})) {
$fromform->{$inputname} = -1;
set_config('site_issuedbadges_' . $cleanhuburl, $fromform->issuedbadges, 'hub');
set_config('site_modulenumberaverage_' . $cleanhuburl, $fromform->modulenumberaverage, 'hub');
set_config('site_participantnumberaverage_' . $cleanhuburl, $fromform->participantnumberaverage, 'hub');
+ set_config('site_mobileservicesenabled_' . $cleanhuburl, $fromform->mobileservicesenabled, 'hub');
+ set_config('site_mobilenotificacionsenabled_' . $cleanhuburl, $fromform->mobilenotificacionsenabled, 'hub');
+ set_config('site_registereduserdevices_' . $cleanhuburl, $fromform->registereduserdevices, 'hub');
+ set_config('site_registeredactiveuserdevices_' . $cleanhuburl, $fromform->registeredactiveuserdevices, 'hub');
}
/////// UPDATE ACTION ////////
$fromform->modulenumberaverage = $siteinfo['modulenumberaverage'];
$fromform->participantnumberaverage = $siteinfo['participantnumberaverage'];
$fromform->street = $siteinfo['street'];
+ $fromform->mobileservicesenabled = $siteinfo['mobileservicesenabled'];
+ $fromform->mobilenotificacionsenabled = $siteinfo['mobilenotificacionsenabled'];
+ $fromform->registereduserdevices = $siteinfo['registereduserdevices'];
+ $fromform->registeredactiveuserdevices = $siteinfo['registeredactiveuserdevices'];
$params = (array) $fromform; //we are using the form input as the redirection parameters (token, url and name)
$optionalsubsystems->add(new admin_setting_configcheckbox('enableglobalsearch', new lang_string('enableglobalsearch', 'admin'),
new lang_string('enableglobalsearch_desc', 'admin'), 0, 1, 0));
- $choices = array();
- $choices[0] = new lang_string('no');
- $choices[1] = new lang_string('yes');
- $optionalsubsystems->add(new admin_setting_configselect('allowstealth', new lang_string('allowstealthmodules'),
- new lang_string('allowstealthmodules_help'), 0, $choices));
+ $optionalsubsystems->add(new admin_setting_configcheckbox('allowstealth', new lang_string('allowstealthmodules'),
+ new lang_string('allowstealthmodules_help'), 0, 1, 0));
}
--- /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/>.
+}}
+{{!
+ @template core_admin/setting_filetypes
+
+ Renders the admin_setting_filetypes setting element.
+
+ Context variables required for this template:
+ * id - element id
+ * name - form element name
+ * value - element value
+ * descriptions - data for the core_form/filetypes-descriptions template
+
+ Example context (json):
+ {
+ "id": "test0",
+ "name": "test",
+ "value": ".jpg,.gif",
+ "descriptions": {
+ "hasdescriptions": true,
+ "descriptions": [
+ {
+ "description": "Image (JPEG)",
+ "extensions": ".jpeg .jpe .jpg"
+ },
+ {
+ "description": "Image (GIF)",
+ "extensions": ".gif"
+ }
+ ]
+ }
+ }
+}}
+<div class="form-text defaultsnext">
+ <input type="text" name="{{name}}" value="{{value}}" size="30" id="{{id}}" class="text-ltr">
+ <span data-filetypesbrowser="{{id}}"></span>
+ <div data-filetypesdescriptions="{{id}}">{{#descriptions}}{{>core_form/filetypes-descriptions}}{{/descriptions}}</div>
+</div>
html,
self._afterRender.bind(self)
);
+ return;
}).fail(Notification.exception);
};
return self._render().then(function(html) {
self._find('[data-region="action-selector"]').replaceWith(html);
self._afterRender();
+ return;
});
};
pagerender = 'tool_lp/plan_page';
pageregion = 'plan-page';
}
-
ajax.call(requests)[requests.length - 1].then(function(context) {
- return templates.render(pagerender, context).done(function(html, js) {
- $('[data-region="' + pageregion + '"]').replaceWith(html);
- templates.runTemplateJS(js);
- });
- }, notification.exception);
+ return templates.render(pagerender, context);
+ }).then(function(html, js) {
+ $('[data-region="' + pageregion + '"]').replaceWith(html);
+ templates.runTemplateJS(js);
+ return;
+ }).catch(notification.exception);
});
}
// We're done, let's trigger a change.
self._templateLoaded = true;
self._triggerChange();
+ return;
});
};
var promises = ajax.call(calls);
promises[calls.length - 1].then(function(context) {
- return templates.render('tool_lp/related_competencies', context).done(function(html, js) {
- $('[data-region="relatedcompetencies"]').replaceWith(html);
- templates.runTemplateJS(js);
- updatedRelatedCompetencies();
- });
- }, notification.exception);
+ return templates.render('tool_lp/related_competencies', context);
+ }).then(function(html, js) {
+ $('[data-region="relatedcompetencies"]').replaceWith(html);
+ templates.runTemplateJS(js);
+ updatedRelatedCompetencies();
+ return;
+ }).catch(notification.exception);
});
}
relatedTarget.ruleconfig = config.ruleconfig;
renderCompetencySummary(relatedTarget);
}
- }, notification.exception);
+ return;
+ }).catch(notification.exception);
};
/**
type: strs[1]
};
}
- }).then(function() {
- return templates.render('tool_lp/competency_summary', context).then(function(html) {
- $('[data-region="competencyinfo"]').html(html);
- $('[data-action="deleterelation"]').on('click', deleteRelatedHandler);
- });
- }).then(function() {
+ return context;
+ }).then(function(context) {
+ return templates.render('tool_lp/competency_summary', context);
+ }).then(function(html) {
+ $('[data-region="competencyinfo"]').html(html);
+ $('[data-action="deleterelation"]').on('click', deleteRelatedHandler);
return templates.render('tool_lp/loading', {});
}).then(function(html, js) {
templates.replaceNodeContents('[data-region="relatedcompetencies"]', html, js);
- }).done(function() {
- ajax.call([{
+ return ajax.call([{
methodname: 'tool_lp_data_for_related_competencies_section',
- args: {competencyid: competency.id},
- done: function(context) {
- return templates.render('tool_lp/related_competencies', context).done(function(html, js) {
- $('[data-region="relatedcompetencies"]').replaceWith(html);
- templates.runTemplateJS(js);
- updatedRelatedCompetencies();
- });
- }
- }]);
- }).fail(notification.exception);
+ args: {competencyid: competency.id}
+ }])[0];
+ }).then(function(context) {
+ return templates.render('tool_lp/related_competencies', context);
+ }).then(function(html, js) {
+ $('[data-region="relatedcompetencies"]').replaceWith(html);
+ templates.runTemplateJS(js);
+ updatedRelatedCompetencies();
+ return;
+ }).catch(notification.exception);
};
/**
// Log Competency viewed event.
triggerCompetencyViewedEvent(competency);
}
-
strSelectedTaxonomy(level).then(function(str) {
selectedTitle.text(str);
- });
+ return;
+ }).catch(notification.exception);
strAddTaxonomy(sublevel).then(function(str) {
btn.show()
.find('[data-region="term"]')
.text(str);
- });
+ return;
+ }).catch(notification.exception);
// We handled this event so consume it.
evt.preventDefault();
if (!self._singleFramework) {
self._find('[data-action="chooseframework"]').change(function(e) {
self._frameworkId = $(e.target).val();
- self._loadCompetencies().then(self._refresh.bind(self));
+ self._loadCompetencies().then(self._refresh.bind(self)).catch(Notification.exception);
});
}
*/
Picker.prototype.display = function() {
var self = this;
- return self._render().then(function(html) {
- return Str.get_string('competencypicker', 'tool_lp').then(function(title) {
- self._popup = new Dialogue(
- title,
- html,
- self._afterRender.bind(self)
- );
- });
- }).fail(Notification.exception);
+ return $.when(Str.get_string('competencypicker', 'tool_lp'), self._render())
+ .then(function(title, render) {
+ self._popup = new Dialogue(
+ title,
+ render[0],
+ self._afterRender.bind(self)
+ );
+ return;
+ }).catch(Notification.exception);
};
/**
return self._render().then(function(html) {
self._find('[data-region="competencylinktree"]').replaceWith(html);
self._afterRender();
+ return;
});
};
if (!self._singlePlan) {
self._find('[data-action="chooseplan"]').change(function(e) {
self._planId = $(e.target).val();
- self._loadCompetencies().then(self._refresh.bind(self));
+ self._loadCompetencies().then(self._refresh.bind(self))
+ .catch(Notification.exception);
});
}
};
if (!self._competency) {
return false;
}
- return self._render().then(function(html) {
- return Str.get_string('competencyrule', 'tool_lp').then(function(title) {
- self._popup = new Dialogue(
- title,
- html,
- self._afterRender.bind(self)
- );
- });
+ return $.when(Str.get_string('competencyrule', 'tool_lp'), self._render())
+ .then(function(title, render) {
+ self._popup = new Dialogue(
+ title,
+ render[0],
+ self._afterRender.bind(self)
+ );
+ return;
}).fail(Notification.exception);
};
*/
RuleConfig.prototype._initOutcomes = function() {
var self = this;
-
return Outcomes.getAll().then(function(outcomes) {
self._outcomesOption = outcomes;
+ return;
});
};
RuleConfig.prototype._initRules = function() {
var self = this,
promises = [];
-
$.each(self._rules, function(index, rule) {
var promise = rule.init().then(function() {
rule.setTargetCompetency(self._competency);
rule.on('change', self._afterRuleConfigChange.bind(self));
+ return;
}, function() {
// Upon failure remove the rule, and resolve the promise.
self._rules.splice(index, 1);
self._afterChange();
return;
}
-
rule.injectTemplate(container).then(function() {
container.show();
- }, function() {
- container.empty().hide();
+ return;
}).always(function() {
self._afterChange();
+ }).catch(function() {
+ container.empty().hide();
});
};
}]);
promise[0].then(function() {
parent.remove();
+ return;
}).fail(Notification.exception);
}
);
includes: includes
}
}]);
-
promise[0].then(function(results) {
var promises = [],
i = 0;
i++;
});
success(results.cohorts);
+ return;
});
- }, failure);
+ }).catch(failure);
}
};
i++;
});
success(results.users);
+ return;
});
- }, failure);
+ }).catch(failure);
}
};
* @return {Promise}
*/
list: function(contextId, options) {
- var promise,
- args = {
+ var args = {
context: {
contextid: contextId
}
};
$.extend(args, typeof options === 'undefined' ? {} : options);
- promise = Ajax.call([{
+ return Ajax.call([{
methodname: 'core_competency_list_competency_frameworks',
args: args
}])[0];
-
- return promise.fail(Notification.exception);
},
/**
* @param {String} query The query string.
* @param {Function} callback A callback function receiving an array of results.
*/
+ /* eslint-disable promise/no-callback-in-promise */
transport: function(selector, query, callback) {
var el = $(selector),
contextId = el.data('contextid'),
if (!contextId) {
throw new Error('The attribute data-contextid is required on ' + selector);
}
-
this.list(contextId, {
query: query,
onlyvisible: onlyVisible,
- }).then(callback);
+ }).then(callback).catch(Notification.exception);
}
};
Str.get_string('competencyframeworkroot', 'tool_lp').then(function(rootframework) {
$(self.staticElementSelector).html(rootframework);
$(self.inputHiddenSelector).val(data.competencyId);
+ return;
}).fail(Notification.exception);
}
};
* Callback to render the region template.
*
* @param {Object} context The context for the template.
+ * @return {Promise}
*/
PlanActions.prototype._renderView = function(context) {
var self = this;
- templates.render(self._template, context)
- .done(function(newhtml, newjs) {
+ return templates.render(self._template, context)
+ .then(function(newhtml, newjs) {
$(self._region).replaceWith(newhtml);
templates.runTemplateJS(newjs);
- })
- .fail(notification.exception);
+ return;
+ });
};
/**
*/
PlanActions.prototype._callAndRefresh = function(calls, planData) {
var self = this;
-
calls.push({
methodname: self._contextMethod,
args: self._getContextArgs(planData)
});
// Apply all the promises, and refresh when the last one is resolved.
- return $.when.apply($.when, ajax.call(calls))
+ return $.when.apply($, ajax.call(calls))
.then(function() {
- self._renderView(arguments[arguments.length - 1]);
+ return self._renderView(arguments[arguments.length - 1]);
})
.fail(notification.exception);
};
done: this._contextLoaded.bind(this),
fail: notification.exception
}]);
-
// Log the user competency viewed in plan event.
requests[0].then(function(result) {
var eventMethodName = 'core_competency_user_competency_viewed_in_plan';
if (result.plan.iscompleted) {
eventMethodName = 'core_competency_user_competency_plan_viewed';
}
- ajax.call([{
+ return ajax.call([{
methodname: eventMethodName,
- args: {competencyid: competencyId, userid: userId, planid: planId},
- fail: notification.exception
- }]);
- });
+ args: {competencyid: competencyId, userid: userId, planid: planId}
+ }])[0];
+ }).catch(notification.exception);
};
/**
Ajax.call([call])[0].then(function() {
this._trigger('review-request-cancelled', data);
this._trigger('status-changed', data);
- }.bind(this), function() {
+ }.bind(this)).catch(function() {
this._trigger('error-occured', data);
}.bind(this));
};
Ajax.call([call])[0].then(function() {
this._trigger('review-requested', data);
this._trigger('status-changed', data);
- }.bind(this), function() {
+ }.bind(this)).catch(function() {
this._trigger('error-occured', data);
}.bind(this));
};
competencyid: data.competencyid
}
};
-
Ajax.call([call])[0].then(function() {
this._trigger('review-started', data);
this._trigger('status-changed', data);
- }.bind(this), function() {
+ }.bind(this)).catch(function() {
this._trigger('error-occured', data);
}.bind(this));
};
Ajax.call([call])[0].then(function() {
this._trigger('review-stopped', data);
this._trigger('status-changed', data);
- }.bind(this), function() {
+ }.bind(this)).catch(function() {
this._trigger('error-occured', data);
}.bind(this));
};
* Callback to render the region template.
*
* @param {Object} context The context for the template.
+ * @return {Promise}
*/
UserEvidenceActions.prototype._renderView = function(context) {
var self = this;
- templates.render(self._template, context)
- .done(function(newhtml, newjs) {
+ return templates.render(self._template, context)
+ .then(function(newhtml, newjs) {
templates.replaceNode($(self._region), newhtml, newjs);
- })
- .fail(notification.exception);
+ return;
+ });
};
/**
*/
UserEvidenceActions.prototype._callAndRefresh = function(calls, evidenceData) {
var self = this;
-
calls.push({
methodname: self._contextMethod,
args: self._getContextArgs(evidenceData)
// Apply all the promises, and refresh when the last one is resolved.
return $.when.apply($.when, ajax.call(calls))
.then(function() {
- self._renderView(arguments[arguments.length - 1]);
+ return self._renderView(arguments[arguments.length - 1]);
})
.fail(notification.exception);
};
args: {
component: component,
template: name,
- themename: config.theme
+ themename: config.theme,
+ includecomments: true
}
}, {
methodname: 'tool_templatelibrary_load_canonical_template',
templates.render('tool_usertours/tourstep', {})
).then(function(response, template) {
usertours.startBootstrapTour(tourId, template[0], response.tourconfig);
+ return;
}).fail(notification.exception);
},
if (response.startTour) {
usertours.fetchTour(response.startTour);
}
+ return;
}).fail(notification.exception);
}
};
if (!empty($user->picture)) {
return false;
}
+ if (!empty($CFG->enablegravatar)) {
+ return false;
+ }
+
$picture = $this->get_static_user_picture();
if (empty($picture)) {
return false;
protected function set_in_database($availability) {
global $DB;
- $DB->set_field('course_sections', 'availability', $availability,
- array('id' => $this->section->id));
+
+ $section = new \stdClass();
+ $section->id = $this->section->id;
+ $section->availability = $availability;
+ $section->timemodified = time();
+ $DB->update_record('course_sections', $section);
}
/**
// Save the updated course module.
if ($changed) {
- $DB->set_field('course_sections', 'availability', json_encode($tree->save()),
- array('id' => $section->id));
+ $updatesection = new \stdClass();
+ $updatesection->id = $section->id;
+ $updatesection->availability = json_encode($tree->save());
+ $updatesection->timemodified = time();
+ $DB->update_record('course_sections', $updatesection);
+
$anychanged = true;
}
}
$section = new backup_nested_element('section', array('id'), array(
'number', 'name', 'summary', 'summaryformat', 'sequence', 'visible',
- 'availabilityjson'));
+ 'availabilityjson', 'timemodified'));
// attach format plugin structure to $section element, only one allowed
$this->add_plugin_structure('format', $section, false);
if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
$sectionrec = array(
'course' => $this->get_courseid(),
- 'section' => $i);
+ 'section' => $i,
+ 'timemodified' => time());
$DB->insert_record('course_sections', $sectionrec); // missing section created
}
}
$section = new stdclass();
$section->course = $this->get_courseid();
$section->section = $data->number;
+ $section->timemodified = isset($data->timemodified) ? $this->apply_date_offset($data->timemodified) : 0;
// Section doesn't exist, create it with all the info from backup
- if (!$secrec = $DB->get_record('course_sections', (array)$section)) {
+ if (!$secrec = $DB->get_record('course_sections', ['course' => $this->get_courseid(), 'section' => $data->number])) {
$section->name = $data->name;
$section->summary = $data->summary;
$section->summaryformat = $data->summaryformat;
array('id' => $availfield->coursesectionid), MUST_EXIST);
$newvalue = \core_availability\info::add_legacy_availability_field_condition(
$currentvalue, $availfield, $show);
- $DB->set_field('course_sections', 'availability', $newvalue,
- array('id' => $availfield->coursesectionid));
+
+ $section = new stdClass();
+ $section->id = $availfield->coursesectionid;
+ $section->availability = $newvalue;
+ $section->timemodified = time();
+ $DB->update_record('course_sections', $section);
}
}
if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1
$sectionrec = array(
'course' => $this->get_courseid(),
- 'section' => 0);
+ 'section' => 0,
+ 'timemodified' => time());
$DB->insert_record('course_sections', $sectionrec); // section 0
$sectionrec = array(
'course' => $this->get_courseid(),
- 'section' => 1);
+ 'section' => 1,
+ 'timemodified' => time());
$data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
}
$data->groupingid= $this->get_mappingid('grouping', $data->groupingid); // grouping
} else {
$sequence = $newitemid;
}
- $DB->set_field('course_sections', 'sequence', $sequence, array('id' => $data->section));
+
+ $updatesection = new \stdClass();
+ $updatesection->id = $data->section;
+ $updatesection->sequence = $sequence;
+ $updatesection->timemodified = time();
+ $DB->update_record('course_sections', $updatesection);
// If there is the legacy showavailability data, store this for later use.
// (This data is not present when restoring 'new' backups.)
// Request data from the server.
return promise.then(function(result) {
- return result.events;
- }).then(function(calendarEvents) {
- if (!calendarEvents.length || (calendarEvents.length < limit)) {
- // We have no more events so mark the list as done.
+ if (!result.events.length) {
+ // No events, nothing to do.
setLoadedAll(root);
+ return 0;
}
- if (calendarEvents.length) {
- // Remember the last id we've seen.
- root.attr('data-last-id', calendarEvents[calendarEvents.length - 1].id);
-
- // Render the events.
- return render(root, calendarEvents).then(function(renderCount) {
- updateContentVisibility(root, calendarEvents.length);
-
- if (renderCount < calendarEvents.length) {
- // if the number of events that was rendered is less than
- // the number we sent for rendering we can assume that there
- // are no groups to add them in. Since the ordering of the
- // events is guaranteed it means that any future requests will
- // also yield events that can't be rendered, so let's not bother
- // sending any more requests.
- setLoadedAll(root);
- }
- });
- } else {
- updateContentVisibility(root, calendarEvents.length);
+ var calendarEvents = result.events;
+
+ // Remember the last id we've seen.
+ root.attr('data-last-id', calendarEvents[calendarEvents.length - 1].id);
+
+ if (calendarEvents.length < limit) {
+ // No more events to load, disable loading button.
+ setLoadedAll(root);
}
+
+ // Render the events.
+ return render(root, calendarEvents).then(function(renderCount) {
+ if (renderCount < calendarEvents.length) {
+ // if the number of events that was rendered is less than
+ // the number we sent for rendering we can assume that there
+ // are no groups to add them in. Since the ordering of the
+ // events is guaranteed it means that any future requests will
+ // also yield events that can't be rendered, so let's not bother
+ // sending any more requests.
+ setLoadedAll(root);
+ }
+ return calendarEvents.length;
+ });
+ }).then(function(eventCount) {
+ return updateContentVisibility(root, eventCount);
}).fail(
Notification.exception
).always(function() {
--- /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/>.
+
+/**
+ * Javascript used to save the user's tab preference.
+ *
+ * @package block_myoverview
+ * @copyright 2017 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery', 'core/ajax', 'core/custom_interaction_events'], function($, Ajax, CustomEvents) {
+
+ /**
+ * Registers an event that saves the user's tab preference when switching between them.
+ *
+ * @param {object} root The container element
+ */
+ var registerEventListeners = function(root) {
+ CustomEvents.define(root, [CustomEvents.events.activate]);
+ root.on(CustomEvents.events.activate, "[data-toggle='tab']", function(e) {
+ var tabname = $(e.currentTarget).data('tabname');
+ // Bootstrap does not change the URL when using BS tabs, so need to do this here.
+ // Also check to make sure the browser supports the history API.
+ if (typeof window.history.pushState === "function") {
+ window.history.pushState(null, null, '?myoverviewtab=' + tabname);
+ }
+ var request = {
+ methodname: 'core_user_update_user_preferences',
+ args: {
+ preferences: [
+ {
+ type: 'block_myoverview_last_tab',
+ value: tabname
+ }
+ ]
+ }
+ };
+
+ Ajax.call([request])[0]
+ .fail(Notification.exception);
+ });
+ };
+
+ return {
+ registerEventListeners: registerEventListeners
+ };
+});
return $this->content;
}
- $renderable = new \block_myoverview\output\main();
+ // Check if the tab to select wasn't passed in the URL, if so see if the user has any preference.
+ if (!$tab = optional_param('myoverviewtab', null, PARAM_ALPHA)) {
+ // Check if the user has no preference, if so get the site setting.
+ if (!$tab = get_user_preferences('block_myoverview_last_tab')) {
+ $config = get_config('block_myoverview');
+ $tab = $config->defaulttab;
+ }
+ }
+
+ $renderable = new \block_myoverview\output\main($tab);
$renderer = $this->page->get_renderer('block_myoverview');
$this->content = new stdClass();
public function applicable_formats() {
return array('my' => true);
}
+
+ /**
+ * This block does contain a configuration settings.
+ *
+ * @return boolean
+ */
+ public function has_config() {
+ return true;
+ }
}
use templatable;
use core_completion\progress;
+require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
require_once($CFG->libdir . '/completionlib.php');
/**
*/
class main implements renderable, templatable {
+ /**
+ * @var string The tab to display.
+ */
+ public $tab;
+
+ /**
+ * Constructor.
+ *
+ * @param string $tab The tab to display.
+ */
+ public function __construct($tab) {
+ $this->tab = $tab;
+ }
+
/**
* Export this data so it can be used as the context for a mustache template.
*
$nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
$noeventsurl = $output->image_url('activities', 'block_myoverview')->out();
+ // Now, set the tab we are going to be viewing.
+ $viewingtimeline = false;
+ $viewingcourses = false;
+ if ($this->tab == BLOCK_MYOVERVIEW_TIMELINE_VIEW) {
+ $viewingtimeline = true;
+ } else {
+ $viewingcourses = true;
+ }
+
return [
'midnight' => usergetmidnight(time()),
'coursesview' => $coursesview->export_for_template($output),
'urls' => [
'nocourses' => $nocoursesurl,
'noevents' => $noeventsurl
- ]
+ ],
+ 'viewingtimeline' => $viewingtimeline,
+ 'viewingcourses' => $viewingcourses
];
}
}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+$string['defaulttab'] = 'Default tab';
+$string['defaulttab_desc'] = 'This is the default tab that will be shown to a user.';
$string['future'] = 'Future';
$string['inprogress'] = 'In progress';
$string['morecourses'] = 'More courses';
--- /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/>.
+
+/**
+ * Contains functions called by core.
+ *
+ * @package block_myoverview
+ * @copyright 2017 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The timeline view.
+ */
+define('BLOCK_MYOVERVIEW_TIMELINE_VIEW', 'timeline');
+
+/**
+ * The courses view.
+ */
+define('BLOCK_MYOVERVIEW_COURSES_VIEW', 'courses');
+
+/**
+ * Returns the name of the user preferences as well as the details this plugin uses.
+ *
+ * @return array
+ */
+function block_myoverview_user_preferences() {
+ $preferences = array();
+ $preferences['block_myoverview_last_tab'] = array(
+ 'type' => PARAM_ALPHA,
+ 'null' => NULL_NOT_ALLOWED,
+ 'default' => BLOCK_MYOVERVIEW_TIMELINE_VIEW,
+ 'choices' => array(BLOCK_MYOVERVIEW_TIMELINE_VIEW, BLOCK_MYOVERVIEW_COURSES_VIEW)
+ );
+
+ return $preferences;
+}
--- /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/>.
+
+/**
+ * Settings for the overview block.
+ *
+ * @package block_myoverview
+ * @copyright 2017 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
+
+if ($ADMIN->fulltree) {
+
+ $options = [
+ BLOCK_MYOVERVIEW_TIMELINE_VIEW => get_string('timeline', 'block_myoverview'),
+ BLOCK_MYOVERVIEW_COURSES_VIEW => get_string('courses')
+ ];
+
+ $settings->add(new admin_setting_configselect('block_myoverview/defaulttab',
+ get_string('defaulttab', 'block_myoverview'),
+ get_string('defaulttab_desc', 'block_myoverview'), 'timeline', $options));
+}
}}
<div id="block-myoverview-{{uniqid}}" class="block-myoverview" data-region="myoverview">
- <ul class="nav nav-tabs" role="tablist">
+ <ul id="block-myoverview-view-choices-{{uniqid}}" class="nav nav-tabs" role="tablist">
<li class="nav-item">
- <a class="nav-link active" href="#myoverview_timeline_view" role="tab" data-toggle="tab">
+ <a class="nav-link {{#viewingtimeline}}active{{/viewingtimeline}}" href="#myoverview_timeline_view" role="tab" data-toggle="tab" data-tabname="timeline">
{{#str}} timeline, block_myoverview {{/str}}
</a>
</li>
<li class="nav-item">
- <a class="nav-link" href="#myoverview_courses_view" role="tab" data-toggle="tab">
+ <a class="nav-link {{#viewingcourses}}active{{/viewingcourses}}" href="#myoverview_courses_view" role="tab" data-toggle="tab" data-tabname="courses">
{{#str}} courses {{/str}}
</a>
</li>
</ul>
<div class="tab-content content-centred">
- <div role="tabpanel" class="tab-pane fade in active" id="myoverview_timeline_view">
+ <div role="tabpanel" class="tab-pane fade {{#viewingtimeline}}in active{{/viewingtimeline}}" id="myoverview_timeline_view">
{{> block_myoverview/timeline-view }}
</div>
- <div role="tabpanel" class="tab-pane fade" id="myoverview_courses_view">
+ <div role="tabpanel" class="tab-pane fade {{#viewingcourses}}in active{{/viewingcourses}}" id="myoverview_courses_view">
{{#coursesview}}
{{> block_myoverview/courses-view }}
{{/coursesview}}
</div>
</div>
</div>
+{{#js}}
+require(['jquery', 'block_myoverview/tab_preferences'], function($, TabPreferences) {
+ var root = $('#block-myoverview-view-choices-{{uniqid}}');
+ TabPreferences.registerEventListeners(root);
+});
+{{/js}}
And I am on "Course 1" course homepage
And I follow "Test choice 1"
And I follow "Dashboard" in the user menu
+ And I click on "Timeline" "link" in the "Course overview" "block"
And I click on "Sort by courses" "link" in the "Course overview" "block"
And I should see "100%" in the "Course overview" "block"
And I click on "Courses" "link" in the "Course overview" "block"
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2017051500; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2017051502; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2017050500; // Requires this Moodle version.
$plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics).
$result = $data;
}
}
- if ($result) {
+ if ($result !== false) {
if ($this->perfdebug) {
cache_helper::record_cache_hit('** static acceleration **', $this->definition);
}
*/
class cache_request extends cache {
// This comment appeases code pre-checker ;) !
-}
\ No newline at end of file
+}
$startstats[$requestid]['stores']['cachestore_static']['sets']);
}
+ public function test_static_cache() {
+ global $CFG;
+ $this->resetAfterTest(true);
+ $CFG->perfdebug = 15;
+
+ // Create cache store with static acceleration.
+ $instance = cache_config_testing::instance();
+ $applicationid = 'phpunit/applicationperf';
+ $instance->phpunit_add_definition($applicationid, array(
+ 'mode' => cache_store::MODE_APPLICATION,
+ 'component' => 'phpunit',
+ 'area' => 'applicationperf',
+ 'simplekeys' => true,
+ 'staticacceleration' => true,
+ 'staticaccelerationsize' => 3
+ ));
+
+ $application = cache::make('phpunit', 'applicationperf');
+
+ // Check that stores register sets.
+ $this->assertTrue($application->set('setMe1', 1));
+ $this->assertTrue($application->set('setMe2', 0));
+ $this->assertTrue($application->set('setMe3', array()));
+ $this->assertTrue($application->get('setMe1') !== false);
+ $this->assertTrue($application->get('setMe2') !== false);
+ $this->assertTrue($application->get('setMe3') !== false);
+
+ // Check that the static acceleration worked, even on empty arrays and the number 0.
+ $endstats = cache_helper::get_stats();
+ $this->assertEquals(0, $endstats[$applicationid]['stores']['** static acceleration **']['misses']);
+ $this->assertEquals(3, $endstats[$applicationid]['stores']['** static acceleration **']['hits']);
+ }
+
public function test_performance_debug_off() {
global $CFG;
$this->resetAfterTest(true);
$rrule = "FREQ=MONTHLY;INTERVAL=12;BYMONTHDAY=2";
$mang = new rrule_manager($rrule);
- $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
+ $untildate = new DateTime();
+ $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+ $until = $untildate->getTimestamp();
$mang->parse_rrule();
$mang->create_events($this->event);
$rrule = "FREQ=MONTHLY;INTERVAL=12;BYDAY=1MO";
$mang = new rrule_manager($rrule);
- $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
+ $untildate = new DateTime();
+ $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+ $until = $untildate->getTimestamp();
$mang->parse_rrule();
$mang->create_events($this->event);
$syscontext = context_system::instance();
$hassystem = has_capability($capability, $syscontext, $userid);
- $access = get_user_access_sitewide($userid);
+ $access = get_user_roles_sitewide_accessdata($userid);
// Build up a list of level 2 contexts (candidates to be user context).
$filtercontexts = array();
+ // Build list of roles to check overrides.
+ $roles = array();
+
foreach ($access['ra'] as $path => $role) {
$parts = explode('/', $path);
if (count($parts) == 3) {
// We know this is not a user context because there is another path with more than 2 levels.
unset($filtercontexts[$parts[2]]);
}
+ $roles = array_merge($roles, $role);
}
// Add all contexts in which a role may be overidden.
- foreach ($access['rdef'] as $pathandroleid => $def) {
- $matches = array();
- if (!isset($def[$capability])) {
- // The capability is not mentioned, we can ignore.
- continue;
- }
-
- list($contextpath, $roleid) = explode(':', $pathandroleid, 2);
- $parts = explode('/', $contextpath);
- if (count($parts) != 3) {
- // Only get potential user contexts, they only ever have 2 slashes /parentId/Id.
- continue;
+ $rdefs = get_role_definitions($roles);
+ foreach ($rdefs as $roledef) {
+ foreach ($roledef as $path => $caps) {
+ if (!isset($caps[$capability])) {
+ // The capability is not mentioned, we can ignore.
+ continue;
+ }
+ $parts = explode('/', $path);
+ if (count($parts) === 3) {
+ // Only get potential user contexts, they only ever have 2 slashes /parentId/Id.
+ $filtercontexts[$parts[2]] = $parts[2];
+ }
}
-
- $filtercontexts[$parts[2]] = $parts[2];
}
// No interesting contexts - return all or no results.
* @param {String} titlestr string for "title" attribute (if different from stringname)
* @param {String} titlecomponent
* @param {String} newaction new value for data-action attribute of the link
+ * @return {Promise} promise which is resolved when the replacement has completed
*/
var replaceActionItem = function(actionitem, image, stringname,
stringcomponent, titlestr, titlecomponent, newaction) {
- str.get_string(stringname, stringcomponent).done(function(newstring) {
- actionitem.find('span.menu-action-text').html(newstring);
- actionitem.attr('title', newstring);
- });
+
+ var stringRequests = [{key: stringname, component: stringcomponent}];
if (titlestr) {
- str.get_string(titlestr, titlecomponent).then(function(newtitle) {
- templates.renderPix(image, 'core', newtitle).then(function(html) {
- actionitem.find('.icon').replaceWith(html);
- });
- actionitem.attr('title', newtitle);
- });
- } else {
- templates.renderPix(image, 'core', '').then(function(html) {
- actionitem.find('.icon').replaceWith(html);
- });
+ stringRequests.push({key: titlestr, component: titlecomponent});
}
- actionitem.attr('data-action', newaction);
+
+ return str.get_strings(stringRequests).then(function(strings) {
+ actionitem.find('span.menu-action-text').html(strings[0]);
+ actionitem.attr('title', strings[0]);
+
+ var title = '';
+ if (titlestr) {
+ title = strings[1];
+ actionitem.attr('title', title);
+ }
+ return templates.renderPix(image, 'core', title);
+ }).then(function(pixhtml) {
+ actionitem.find('.icon').replaceWith(pixhtml);
+ actionitem.attr('data-action', newaction);
+ return;
+ }).catch(notification.exception);
};
/**
$data = $this->connectionpoint->get_data();
+ // Backup may not include the end date, so set it to 0.
+ $enddate = isset($data['tags']['enddate']) ? $data['tags']['enddate'] : 0;
+
// Set the automatic end date setting and the course end date (if applicable).
- $this->update_automatic_end_date($data['tags']['enddate']);
+ $this->update_automatic_end_date($enddate);
if (isset($data['tags']['numsections'])) {
// Update course sections visibility.
$cw->name = null;
$cw->visible = 1;
$cw->availability = null;
+ $cw->timemodified = time();
$cw->id = $DB->insert_record("course_sections", $cw);
// Now move it to the specified position.
// Update record in the DB and course format options.
$data['id'] = $section->id;
+ $data['timemodified'] = time();
$DB->update_record('course_sections', $data);
rebuild_course_cache($courseid, true);
course_get_format($courseid)->update_section_format_options($data);
}
}
+ public function test_update_course_section_time_modified() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ // Create the course with sections.
+ $course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
+ $sections = $DB->get_records('course_sections', array('course' => $course->id));
+
+ // Get the last section's time modified value.
+ $section = array_pop($sections);
+ $oldtimemodified = $section->timemodified;
+
+ // Update the section.
+ $this->waitForSecond(); // Ensuring that the section update occurs at a different timestamp.
+ course_update_section($course, $section, array());
+
+ // Check that the time has changed.
+ $section = $DB->get_record('course_sections', array('id' => $section->id));
+ $newtimemodified = $section->timemodified;
+ $this->assertGreaterThan($oldtimemodified, $newtimemodified);
+ }
+
public function test_course_add_cm_to_section() {
global $DB;
$this->resetAfterTest(true);
$mform =& $this->_form;
if ($id = $mform->getElementValue('id')) {
- $grade_item = grade_item::fetch(array('id'=>$id));
+ $gradeitem = grade_item::fetch(array('id' => $id));
+ $parentcategory = $gradeitem->get_parent_category();
+ } else {
+ // If we do not have an id, we are creating a new grade item.
+ $gradeitem = new grade_item(array('courseid' => $COURSE->id, 'itemtype' => 'manual'), false);
+
+ // Assign the course category to this grade item.
+ $parentcategory = grade_category::fetch_course_category($COURSE->id);
+ $gradeitem->parent_category = $parentcategory;
+ }
+
+ if (!$gradeitem->is_raw_used()) {
+ $mform->removeElement('plusfactor');
+ $mform->removeElement('multfactor');
+ }
- if (!$grade_item->is_raw_used()) {
- $mform->removeElement('plusfactor');
- $mform->removeElement('multfactor');
+ if ($gradeitem->is_outcome_item()) {
+ // We have to prevent incompatible modifications of outcomes if outcomes disabled.
+ $mform->removeElement('grademax');
+ if ($mform->elementExists('grademin')) {
+ $mform->removeElement('grademin');
}
+ $mform->removeElement('gradetype');
+ $mform->removeElement('display');
+ $mform->removeElement('decimals');
+ $mform->hardFreeze('scaleid');
- if ($grade_item->is_outcome_item()) {
- // we have to prevent incompatible modifications of outcomes if outcomes disabled
- $mform->removeElement('grademax');
+ } else {
+ if ($gradeitem->is_external_item()) {
+ // Following items are set up from modules and should not be overrided by user.
if ($mform->elementExists('grademin')) {
- $mform->removeElement('grademin');
+ // The site setting grade_report_showmin may have prevented grademin being added to the form.
+ $mform->hardFreeze('grademin');
+ }
+ $mform->hardFreeze('itemname,gradetype,grademax,scaleid');
+ if ($gradeitem->itemnumber == 0) {
+ // The idnumber of grade itemnumber 0 is synced with course_modules.
+ $mform->hardFreeze('idnumber');
}
- $mform->removeElement('gradetype');
- $mform->removeElement('display');
- $mform->removeElement('decimals');
- $mform->hardFreeze('scaleid');
- } else {
- if ($grade_item->is_external_item()) {
- // following items are set up from modules and should not be overrided by user
+ // For external items we can not change the grade type, even if no grades exist, so if it is set to
+ // scale, then remove the grademax and grademin fields from the form - no point displaying them.
+ if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
+ $mform->removeElement('grademax');
if ($mform->elementExists('grademin')) {
- // The site setting grade_report_showmin may have prevented grademin being added to the form.
- $mform->hardFreeze('grademin');
- }
- $mform->hardFreeze('itemname,gradetype,grademax,scaleid');
- if ($grade_item->itemnumber == 0) {
- // the idnumber of grade itemnumber 0 is synced with course_modules
- $mform->hardFreeze('idnumber');
+ $mform->removeElement('grademin');
}
+ } else { // Not using scale, so remove it.
+ $mform->removeElement('scaleid');
+ }
- // For external items we can not change the grade type, even if no grades exist, so if it is set to
- // scale, then remove the grademax and grademin fields from the form - no point displaying them.
- if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
- $mform->removeElement('grademax');
- if ($mform->elementExists('grademin')) {
- $mform->removeElement('grademin');
- }
- } else { // Not using scale, so remove it.
- $mform->removeElement('scaleid');
- }
+ // Always remove the rescale grades element if it's an external item.
+ $mform->removeElement('rescalegrades');
+ } else if ($gradeitem->has_grades()) {
+ // Can't change the grade type or the scale if there are grades.
+ $mform->hardFreeze('gradetype, scaleid');
- // Always remove the rescale grades element if it's an external item.
+ // If we are using scales then remove the unnecessary rescale and grade fields.
+ if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
$mform->removeElement('rescalegrades');
- } else if ($grade_item->has_grades()) {
- // Can't change the grade type or the scale if there are grades.
- $mform->hardFreeze('gradetype, scaleid');
-
- // If we are using scales then remove the unnecessary rescale and grade fields.
- if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
- $mform->removeElement('rescalegrades');
- $mform->removeElement('grademax');
- if ($mform->elementExists('grademin')) {
- $mform->removeElement('grademin');
- }
- } else { // Remove the scale field.
- $mform->removeElement('scaleid');
- // Set the maximum grade to disabled unless a grade is chosen.
- $mform->disabledIf('grademax', 'rescalegrades', 'eq', '');
+ $mform->removeElement('grademax');
+ if ($mform->elementExists('grademin')) {
+ $mform->removeElement('grademin');
}
- } else {
- // Remove the rescale element if there are no grades.
- $mform->removeElement('rescalegrades');
+ } else { // Remove the scale field.
+ $mform->removeElement('scaleid');
+ // Set the maximum grade to disabled unless a grade is chosen.
+ $mform->disabledIf('grademax', 'rescalegrades', 'eq', '');
}
+ } else {
+ // Remove the rescale element if there are no grades.
+ $mform->removeElement('rescalegrades');
}
+ }
+
+ // If we wanted to change parent of existing item - we would have to verify there are no circular references in parents!!!
+ if ($id && $mform->elementExists('parentcategory')) {
+ $mform->hardFreeze('parentcategory');
+ }
+
+ $parentcategory->apply_forced_settings();
- // if we wanted to change parent of existing item - we would have to verify there are no circular references in parents!!!
- if ($mform->elementExists('parentcategory')) {
- $mform->hardFreeze('parentcategory');
+ if (!$parentcategory->is_aggregationcoef_used()) {
+ if ($mform->elementExists('aggregationcoef')) {
+ $mform->removeElement('aggregationcoef');
}
- $parent_category = $grade_item->get_parent_category();
- $parent_category->apply_forced_settings();
+ } else {
+ $coefstring = $gradeitem->get_coefstring();
- if (!$parent_category->is_aggregationcoef_used()) {
- if ($mform->elementExists('aggregationcoef')) {
- $mform->removeElement('aggregationcoef');
+ if ($coefstring !== '') {
+ if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') {
+ // The advcheckbox is not compatible with disabledIf!
+ $coefstring = 'aggregationcoefextrasum';
+ $element =& $mform->createElement('checkbox', 'aggregationcoef', get_string($coefstring, 'grades'));
+ } else {
+ $element =& $mform->createElement('text', 'aggregationcoef', get_string($coefstring, 'grades'));
}
-
- } else {
- $coefstring = $grade_item->get_coefstring();
-
- if ($coefstring !== '') {
- if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') {
- // advcheckbox is not compatible with disabledIf!
- $coefstring = 'aggregationcoefextrasum';
- $element =& $mform->createElement('checkbox', 'aggregationcoef', get_string($coefstring, 'grades'));
- } else {
- $element =& $mform->createElement('text', 'aggregationcoef', get_string($coefstring, 'grades'));
- }
- if ($mform->elementExists('parentcategory')) {
- $mform->insertElementBefore($element, 'parentcategory');
- } else {
- $mform->insertElementBefore($element, 'id');
- }
- $mform->addHelpButton('aggregationcoef', $coefstring, 'grades');
+ if ($mform->elementExists('parentcategory')) {
+ $mform->insertElementBefore($element, 'parentcategory');
+ } else {
+ $mform->insertElementBefore($element, 'id');
}
- $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_NONE);
- $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_TEXT);
- $mform->disabledIf('aggregationcoef', 'parentcategory', 'eq', $parent_category->id);
+ $mform->addHelpButton('aggregationcoef', $coefstring, 'grades');
}
+ $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_NONE);
+ $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_TEXT);
+ $mform->disabledIf('aggregationcoef', 'parentcategory', 'eq', $parentcategory->id);
+ }
- // Remove fields used by natural weighting if the parent category is not using natural weighting.
- // Or if the item is a scale and scales are not used in aggregation.
- if ($parent_category->aggregation != GRADE_AGGREGATE_SUM
- || (empty($CFG->grade_includescalesinaggregation) && $grade_item->gradetype == GRADE_TYPE_SCALE)) {
- if ($mform->elementExists('weightoverride')) {
- $mform->removeElement('weightoverride');
- }
- if ($mform->elementExists('aggregationcoef2')) {
- $mform->removeElement('aggregationcoef2');
- }
+ // Remove fields used by natural weighting if the parent category is not using natural weighting.
+ // Or if the item is a scale and scales are not used in aggregation.
+ if ($parentcategory->aggregation != GRADE_AGGREGATE_SUM
+ || (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE)) {
+ if ($mform->elementExists('weightoverride')) {
+ $mform->removeElement('weightoverride');
}
+ if ($mform->elementExists('aggregationcoef2')) {
+ $mform->removeElement('aggregationcoef2');
+ }
+ }
- if ($category = $grade_item->get_item_category()) {
- if ($category->aggregation == GRADE_AGGREGATE_SUM) {
- if ($mform->elementExists('gradetype')) {
- $mform->hardFreeze('gradetype');
- }
- if ($mform->elementExists('grademin')) {
- $mform->hardFreeze('grademin');
- }
- if ($mform->elementExists('grademax')) {
- $mform->hardFreeze('grademax');
- }
- if ($mform->elementExists('scaleid')) {
- $mform->removeElement('scaleid');
- }
+ if ($category = $gradeitem->get_item_category()) {
+ if ($category->aggregation == GRADE_AGGREGATE_SUM) {
+ if ($mform->elementExists('gradetype')) {
+ $mform->hardFreeze('gradetype');
+ }
+ if ($mform->elementExists('grademin')) {
+ $mform->hardFreeze('grademin');
+ }
+ if ($mform->elementExists('grademax')) {
+ $mform->hardFreeze('grademax');
+ }
+ if ($mform->elementExists('scaleid')) {
+ $mform->removeElement('scaleid');
}
}
-
- } else {
- // all new items are manual, children of course category
- $mform->removeElement('plusfactor');
- $mform->removeElement('multfactor');
- $mform->removeElement('rescalegrades');
}
// no parent header for course category
echo '<input type="hidden" value="grader" name="report"/>';
echo '<input type="hidden" value="'.$page.'" name="page"/>';
echo $reporthtml;
- echo '<div class="submit"><input type="submit" id="gradersubmit" value="'.s(get_string('savechanges')).'" /></div>';
+ echo '<div class="submit"><input type="submit" id="gradersubmit" class="btn btn-primary"
+ value="'.s(get_string('savechanges')).'" /></div>';
echo '</div></form>';
} else {
echo $reporthtml;
--- /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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thisdirection'] = 'ltr';
+$string['thislanguage'] = 'Bislama';
$string['admindirname'] = 'Adminディレクトリ';
$string['availablelangs'] = '利用可能な言語パック';
$string['chooselanguagehead'] = '言語を選択してください。';
-$string['chooselanguagesub'] = 'ã\82¤ã\83³ã\82¹ã\83\88ã\83¼ã\83«ã\81®ã\81¿ã\81«使用する言語を選択してください。この言語はサイトのデフォルト言語としても使用されます。後でサイト言語を変更することができます。';
+$string['chooselanguagesub'] = 'ã\82¤ã\83³ã\82¹ã\83\88ã\83¼ã\83«ã\81«ã\81®ã\81¿使用する言語を選択してください。この言語はサイトのデフォルト言語としても使用されます。後でサイト言語を変更することができます。';
$string['clialreadyconfigured'] = '設定ファイルconfig.phpはすでに登録されています。このサイトをインストールする場合、admin/cli/install_database.phpを使用してください。';
$string['clialreadyinstalled'] = '設定ファイルconfig.phpは、すでに登録されています。このサイトをアップグレードする場合、admin/cli/upgrade.phpを使用してください。';
$string['cliinstallheader'] = 'Moodle {$a} コマンドラインインストールプログラム';
$string['clialreadyinstalled'] = 'Het configuratiebestand config.php bestaat al. Maak aub gebruik van admin/cli/install_database.php als je Moodle voor deze site wenst te upgraden.';
$string['cliinstallheader'] = 'Moodle {$a} command line installatieprogramma';
$string['databasehost'] = 'Databank host:';
-$string['databasename'] = 'Datanbanknaam:';
+$string['databasename'] = 'Databanknaam:';
$string['databasetypehead'] = 'Kies databankdriver';
$string['dataroot'] = 'Gegevensmap';
$string['datarootpermission'] = 'Toestemming datamappen';
$string['admindirname'] = 'Pasta de administração';
$string['availablelangs'] = 'Pacotes linguísticos disponíveis';
$string['chooselanguagehead'] = 'Selecione um idioma';
-$string['chooselanguagesub'] = 'Selecione o idioma a utilizar durante a instalação. Poderá depois selecionar um outro idioma para o site e para os utilizadores.';
+$string['chooselanguagesub'] = 'Selecione o idioma a utilizar durante a instalação. Poderá depois selecionar outro(s) idioma(s) para o site e para os utilizadores.';
$string['clialreadyconfigured'] = 'O ficheiro config.php já existe, use admin/cli/install_database.php para instalar o Moodle para este site.';
$string['clialreadyinstalled'] = 'O ficheiro config.php já existe, use admin/cli/install_database.php para atualizar o Moodle para este site.';
$string['cliinstallheader'] = 'Programa para instalação do Moodle <b>{$a}</b> através da linha de comandos';
<p>Não deve ser diretamente acessível através da web.</p>
<p> Se a diretoria não existir atualmente, o processo de instalação tentará criá-la.</p>';
$string['pathssubdirroot'] = 'Caminho completo para a diretoria que contém o código Moodle.';
-$string['pathssubwwwroot'] = 'Endereço web completo de acesso ao Moodle. Não é possível aceder ao Moodle usando mais do que um endereço. Se o site tiver mais do que um endereço público, devem ser configurados redirecionamentos permanentes em todos eles, à exceção deste. Se o site pode ser acedido a partir da Internet e de Intranet, então use o endereço público aqui. Se o endereço atual não está correto, então altere o endereço indicado na barra de endereço do seu navegador e reinicie a instalação.';
+$string['pathssubwwwroot'] = 'Endereço web completo de acesso ao Moodle. Não é possível aceder ao Moodle usando mais do que um endereço. Se o site tiver mais do que um endereço público, devem ser configurados redirecionamentos permanentes em todos eles, à exceção deste. Se o site pode ser acedido a partir da Internet e de Intranet, então use o endereço público aqui. Se o endereço atual não está correto, altere o endereço indicado na barra de endereço do seu navegador e reinicie a instalação.';
$string['pathsunsecuredataroot'] = 'A localização da pasta de dados não é segura';
$string['pathswrongadmindir'] = 'A pasta <b>admin</b> não existe';
$string['phpextension'] = 'Extensão <b>{$a}</b> do PHP';
$string['cachedef_tagindexbuilder'] = 'Search results for tagged items';
$string['cachedef_questiondata'] = 'Question definitions';
$string['cachedef_repositories'] = 'Repositories instances data';
+$string['cachedef_roledefs'] = 'Role definitions';
$string['cachedef_grade_categories'] = 'Grade category queries';
$string['cachedef_string'] = 'Language string cache';
$string['cachedef_tags'] = 'Tags collections and areas';
$string['err_numeric'] = 'You must enter a number here.';
$string['err_rangelength'] = 'You must enter between {$a->format[0]} and {$a->format[1]} characters here.';
$string['err_required'] = 'You must supply a value here.';
+$string['filetypesany'] = 'All file types';
+$string['filetypesnotwhitelisted'] = 'These file types are not allowed here: {$a}';
+$string['filetypesothers'] = 'Other files';
$string['general'] = 'General';
$string['hideadvanced'] = 'Hide advanced';
$string['hour'] = 'Hour';
$string['licence_help'] = 'Select the licence you want to distribute your course under.';
$string['licence_link'] = 'licenses';
$string['logourl'] = 'Logo URL';
+$string['mobilenotificacionsenabled'] = 'Mobile notifications enabled ({$a})';
+$string['mobileservicesenabled'] = 'Mobile services enabled ({$a})';
$string['modulenumberaverage'] = 'Average number of course modules ({$a})';
$string['moodleorg'] = 'Moodle';
$string['mustselectsubject'] = 'You must select a subject';
$string['registrationinfo'] = 'Registration information';
$string['registeredmoodleorg'] = 'Moodle ({$a})';
$string['registeredon'] = 'Where your site is registered';
+$string['registereduserdevices'] = 'Number of user\'s registered mobile devices ({$a})';
+$string['registeredactiveuserdevices'] = 'Number of active user\'s registered mobile devices ({$a})';
$string['registermoochtips'] = 'Register your site with Moodle to get security alerts and access to Moodle.net, our course sharing platform.';
$string['registersite'] = 'Register with {$a}';
$string['registerwith'] = 'Register with a hub';
$string['subcategoryof'] = 'Subcategory of {$a}';
$string['submit'] = 'Submit';
$string['success'] = 'Success';
+$string['successduration'] = 'Success ({$a} seconds)';
$string['summary'] = 'Summary';
$string['summary_help'] = 'The idea of a summary is a short text to prepare students for the activities within the topic or week. The text is shown on the course page under the section name.';
$string['summaryof'] = 'Summary of {$a}';
* - load_all_capabilities()
* - reload_all_capabilities()
* - has_capability_in_accessdata()
- * - get_user_access_sitewide()
- * - load_course_context()
- * - load_role_access_by_context()
+ * - get_user_roles_sitewide_accessdata()
* - etc.
*
* <b>Name conventions</b>
* [$contextpath] = array($roleid=>$roleid)
* </code>
*
- * Role definitions are stored like this
- * (no cap merge is done - so it's compact)
- *
- * <code>
- * $accessdata['rdef']["$contextpath:$roleid"]['mod/forum:viewpost'] = 1
- * ['mod/forum:editallpost'] = -1
- * ['mod/forum:startdiscussion'] = -1000
- * </code>
- *
- * See how has_capability_in_accessdata() walks up the tree.
- *
- * First we only load rdef and ra down to the course level, but not below.
- * This keeps accessdata small and compact. Below-the-course ra/rdef
- * are loaded as needed. We keep track of which courses we have loaded ra/rdef in
- * <code>
- * $accessdata['loaded'] = array($courseid1=>1, $courseid2=>1)
- * </code>
- *
* <b>Stale accessdata</b>
*
* For the logged-in user, accessdata is long-lived.
*/
global $ACCESSLIB_PRIVATE;
$ACCESSLIB_PRIVATE = new stdClass();
+$ACCESSLIB_PRIVATE->cacheroledefs = array(); // Holds site-wide role definitions.
$ACCESSLIB_PRIVATE->dirtycontexts = null; // Dirty contexts cache, loaded from DB once per page
$ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the cache of $accessdata structure for users (including $USER)
-$ACCESSLIB_PRIVATE->rolepermissions = array(); // role permissions cache - helps a lot with mem usage
/**
* Clears accesslib's private caches. ONLY BE USED BY UNIT TESTS
$ACCESSLIB_PRIVATE->dirtycontexts = null;
$ACCESSLIB_PRIVATE->accessdatabyuser = array();
- $ACCESSLIB_PRIVATE->rolepermissions = array();
+ $ACCESSLIB_PRIVATE->cacheroledefs = array();
+
+ $cache = cache::make('core', 'roledefs');
+ $cache->purge();
if ($resetcontexts) {
context_helper::reset_caches();
}
/**
- * Gets the accessdata for role "sitewide" (system down to course)
+ * Clears accesslib's private cache of a specific role or roles. ONLY BE USED FROM THIS LIBRARY FILE!
+ *
+ * This reset does not touch global $USER.
+ *
+ * @access private
+ * @param int|array $roles
+ * @return void
+ */
+function accesslib_clear_role_cache($roles) {
+ global $ACCESSLIB_PRIVATE;
+
+ if (!is_array($roles)) {
+ $roles = [$roles];
+ }
+
+ foreach ($roles as $role) {
+ if (isset($ACCESSLIB_PRIVATE->cacheroledefs[$role])) {
+ unset($ACCESSLIB_PRIVATE->cacheroledefs[$role]);
+ }
+ }
+
+ $cache = cache::make('core', 'roledefs');
+ $cache->delete_many($roles);
+}
+
+/**
+ * Role is assigned at system context.
*
* @access private
* @param int $roleid
* @return array
*/
function get_role_access($roleid) {
- global $DB, $ACCESSLIB_PRIVATE;
+ $accessdata = get_empty_accessdata();
+ $accessdata['ra']['/'.SYSCONTEXTID] = array((int)$roleid => (int)$roleid);
+ return $accessdata;
+}
- /* Get it in 1 DB query...
- * - relevant role caps at the root and down
- * to the course level - but not below
- */
+/**
+ * Fetch raw "site wide" role definitions.
+ * Even MUC static acceleration cache appears a bit slow for this.
+ * Important as can be hit hundreds of times per page.
+ *
+ * @param array $roleids List of role ids to fetch definitions for.
+ * @return array Complete definition for each requested role.
+ */
+function get_role_definitions(array $roleids) {
+ global $ACCESSLIB_PRIVATE;
- //TODO: MUC - this could be cached in shared memory to speed up first page loading, web crawlers, etc.
+ if (empty($roleids)) {
+ return array();
+ }
- $accessdata = get_empty_accessdata();
+ // Grab all keys we have not yet got in our static cache.
+ if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
+ $cache = cache::make('core', 'roledefs');
+ $ACCESSLIB_PRIVATE->cacheroledefs += array_filter($cache->get_many($uncached));
- $accessdata['ra']['/'.SYSCONTEXTID] = array((int)$roleid => (int)$roleid);
+ // Check we have the remaining keys from the MUC.
+ if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
+ $uncached = get_role_definitions_uncached($uncached);
+ $ACCESSLIB_PRIVATE->cacheroledefs += $uncached;
+ $cache->set_many($uncached);
+ }
+ }
- // Overrides for the role IN ANY CONTEXTS down to COURSE - not below -.
+ // Return just the roles we need.
+ return array_intersect_key($ACCESSLIB_PRIVATE->cacheroledefs, array_flip($roleids));
+}
- /*
- $sql = "SELECT ctx.path,
- rc.capability, rc.permission
- FROM {context} ctx
- JOIN {role_capabilities} rc ON rc.contextid = ctx.id
- LEFT JOIN {context} cctx
- ON (cctx.contextlevel = ".CONTEXT_COURSE." AND ctx.path LIKE ".$DB->sql_concat('cctx.path',"'/%'").")
- WHERE rc.roleid = ? AND cctx.id IS NULL";
- $params = array($roleid);
- */
+/**
+ * Query raw "site wide" role definitions.
+ *
+ * @param array $roleids List of role ids to fetch definitions for.
+ * @return array Complete definition for each requested role.
+ */
+function get_role_definitions_uncached(array $roleids) {
+ global $DB;
+
+ if (empty($roleids)) {
+ return array();
+ }
- // Note: the commented out query is 100% accurate but slow, so let's cheat instead by hardcoding the blocks mess directly.
+ list($sql, $params) = $DB->get_in_or_equal($roleids);
+ $rdefs = array();
- $sql = "SELECT COALESCE(ctx.path, bctx.path) AS path, rc.capability, rc.permission
+ $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
FROM {role_capabilities} rc
- LEFT JOIN {context} ctx ON (ctx.id = rc.contextid AND ctx.contextlevel <= ".CONTEXT_COURSE.")
- LEFT JOIN ({context} bctx
- JOIN {block_instances} bi ON (bi.id = bctx.instanceid)
- JOIN {context} pctx ON (pctx.id = bi.parentcontextid AND pctx.contextlevel < ".CONTEXT_COURSE.")
- ) ON (bctx.id = rc.contextid AND bctx.contextlevel = ".CONTEXT_BLOCK.")
- WHERE rc.roleid = :roleid AND (ctx.id IS NOT NULL OR bctx.id IS NOT NULL)";
- $params = array('roleid'=>$roleid);
-
- // we need extra caching in CLI scripts and cron
+ JOIN {context} ctx ON rc.contextid = ctx.id
+ WHERE rc.roleid $sql
+ ORDER BY ctx.path, rc.roleid, rc.capability";
$rs = $DB->get_recordset_sql($sql, $params);
- foreach ($rs as $rd) {
- $k = "{$rd->path}:{$roleid}";
- $accessdata['rdef'][$k][$rd->capability] = (int)$rd->permission;
- }
- $rs->close();
- // share the role definitions
- foreach ($accessdata['rdef'] as $k=>$unused) {
- if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) {
- $ACCESSLIB_PRIVATE->rolepermissions[$k] = $accessdata['rdef'][$k];
+ foreach ($rs as $rd) {
+ if (!isset($rdefs[$rd->roleid][$rd->path])) {
+ if (!isset($rdefs[$rd->roleid])) {
+ $rdefs[$rd->roleid] = array();
+ }
+ $rdefs[$rd->roleid][$rd->path] = array();
}
- $accessdata['rdef_count']++;
- $accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k];
+ $rdefs[$rd->roleid][$rd->path][$rd->capability] = (int) $rd->permission;
}
- return $accessdata;
+ $rs->close();
+ return $rdefs;
}
/**
$access =& $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
}
-
- // Load accessdata for below-the-course context if necessary,
- // all contexts at and above all courses are already loaded
- if ($context->contextlevel != CONTEXT_COURSE and $coursecontext = $context->get_course_context(false)) {
- load_course_context($userid, $coursecontext, $access);
- }
-
return has_capability_in_accessdata($capability, $context, $access);
}
}
// Now find out what access is given to each role, going bottom-->up direction
+ $rdefs = get_role_definitions(array_keys($roles));
$allowed = false;
+
foreach ($roles as $roleid => $ignored) {
foreach ($paths as $path) {
- if (isset($accessdata['rdef']["{$path}:$roleid"][$capability])) {
- $perm = (int)$accessdata['rdef']["{$path}:$roleid"][$capability];
+ if (isset($rdefs[$roleid][$path][$capability])) {
+ $perm = (int)$rdefs[$roleid][$path][$capability];
if ($perm === CAP_PROHIBIT) {
// any CAP_PROHIBIT found means no permission for the user
return false;
}
/**
- * Return a nested array showing role assignments
- * all relevant role capabilities for the user at
- * site/course_category/course levels
- *
- * We do _not_ delve deeper than courses because the number of
- * overrides at the module/block levels can be HUGE.
- *
- * [ra] => [/path][roleid]=roleid
- * [rdef] => [/path:roleid][capability]=permission
+ * Return a nested array showing all role assignments for the user.
+ * [ra] => [contextpath][roleid] = roleid
*
* @access private
* @param int $userid - the id of the user
* @return array access info array
*/
-function get_user_access_sitewide($userid) {
- global $CFG, $DB, $ACCESSLIB_PRIVATE;
-
- /* Get in a few cheap DB queries...
- * - role assignments
- * - relevant role caps
- * - above and within this user's RAs
- * - below this user's RAs - limited to course level
- */
+function get_user_roles_sitewide_accessdata($userid) {
+ global $CFG, $DB;
- // raparents collects paths & roles we need to walk up the parenthood to build the minimal rdef
- $raparents = array();
$accessdata = get_empty_accessdata();
// start with the default role
if (!empty($CFG->defaultuserroleid)) {
$syscontext = context_system::instance();
$accessdata['ra'][$syscontext->path][(int)$CFG->defaultuserroleid] = (int)$CFG->defaultuserroleid;
- $raparents[$CFG->defaultuserroleid][$syscontext->id] = $syscontext->id;
}
// load the "default frontpage role"
$frontpagecontext = context_course::instance(get_site()->id);
if ($frontpagecontext->path) {
$accessdata['ra'][$frontpagecontext->path][(int)$CFG->defaultfrontpageroleid] = (int)$CFG->defaultfrontpageroleid;
- $raparents[$CFG->defaultfrontpageroleid][$frontpagecontext->id] = $frontpagecontext->id;
}
}
- // preload every assigned role at and above course context
+ // Preload every assigned role.
$sql = "SELECT ctx.path, ra.roleid, ra.contextid
FROM {role_assignments} ra
- JOIN {context} ctx
- ON ctx.id = ra.contextid
- LEFT JOIN {block_instances} bi
- ON (ctx.contextlevel = ".CONTEXT_BLOCK." AND bi.id = ctx.instanceid)
- LEFT JOIN {context} bpctx
- ON (bpctx.id = bi.parentcontextid)
- WHERE ra.userid = :userid
- AND (ctx.contextlevel <= ".CONTEXT_COURSE." OR bpctx.contextlevel < ".CONTEXT_COURSE.")";
- $params = array('userid'=>$userid);
- $rs = $DB->get_recordset_sql($sql, $params);
+ JOIN {context} ctx ON ctx.id = ra.contextid
+ WHERE ra.userid = :userid";
+
+ $rs = $DB->get_recordset_sql($sql, array('userid' => $userid));
+
foreach ($rs as $ra) {
// RAs leafs are arrays to support multi-role assignments...
$accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid;
- $raparents[$ra->roleid][$ra->contextid] = $ra->contextid;
}
- $rs->close();
-
- if (empty($raparents)) {
- return $accessdata;
- }
-
- // now get overrides of interesting roles in all interesting child contexts
- // hopefully we will not run out of SQL limits here,
- // users would have to have very many roles at/above course context...
- $sqls = array();
- $params = array();
-
- static $cp = 0;
- foreach ($raparents as $roleid=>$ras) {
- $cp++;
- list($sqlcids, $cids) = $DB->get_in_or_equal($ras, SQL_PARAMS_NAMED, 'c'.$cp.'_');
- $params = array_merge($params, $cids);
- $params['r'.$cp] = $roleid;
- $sqls[] = "(SELECT ctx.path, rc.roleid, rc.capability, rc.permission
- FROM {role_capabilities} rc
- JOIN {context} ctx
- ON (ctx.id = rc.contextid)
- JOIN {context} pctx
- ON (pctx.id $sqlcids
- AND (ctx.id = pctx.id
- OR ctx.path LIKE ".$DB->sql_concat('pctx.path',"'/%'")."
- OR pctx.path LIKE ".$DB->sql_concat('ctx.path',"'/%'")."))
- LEFT JOIN {block_instances} bi
- ON (ctx.contextlevel = ".CONTEXT_BLOCK." AND bi.id = ctx.instanceid)
- LEFT JOIN {context} bpctx
- ON (bpctx.id = bi.parentcontextid)
- WHERE rc.roleid = :r{$cp}
- AND (ctx.contextlevel <= ".CONTEXT_COURSE." OR bpctx.contextlevel < ".CONTEXT_COURSE.")
- )";
- }
-
- // fixed capability order is necessary for rdef dedupe
- $rs = $DB->get_recordset_sql(implode("\nUNION\n", $sqls). "ORDER BY capability", $params);
- foreach ($rs as $rd) {
- $k = $rd->path.':'.$rd->roleid;
- $accessdata['rdef'][$k][$rd->capability] = (int)$rd->permission;
- }
$rs->close();
- // share the role definitions
- foreach ($accessdata['rdef'] as $k=>$unused) {
- if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) {
- $ACCESSLIB_PRIVATE->rolepermissions[$k] = $accessdata['rdef'][$k];
- }
- $accessdata['rdef_count']++;
- $accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k];
- }
-
return $accessdata;
}
-/**
- * Add to the access ctrl array the data needed by a user for a given course.
- *
- * This function injects all course related access info into the accessdata array.
- *
- * @access private
- * @param int $userid the id of the user
- * @param context_course $coursecontext course context
- * @param array $accessdata accessdata array (modified)
- * @return void modifies $accessdata parameter
- */
-function load_course_context($userid, context_course $coursecontext, &$accessdata) {
- global $DB, $CFG, $ACCESSLIB_PRIVATE;
-
- if (empty($coursecontext->path)) {
- // weird, this should not happen
- return;
- }
-
- if (isset($accessdata['loaded'][$coursecontext->instanceid])) {
- // already loaded, great!
- return;
- }
-
- $roles = array();
-
- if (empty($userid)) {
- if (!empty($CFG->notloggedinroleid)) {
- $roles[$CFG->notloggedinroleid] = $CFG->notloggedinroleid;
- }
-
- } else if (isguestuser($userid)) {
- if ($guestrole = get_guest_role()) {
- $roles[$guestrole->id] = $guestrole->id;
- }
-
- } else {
- // Interesting role assignments at, above and below the course context
- list($parentsaself, $params) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'pc_');
- $params['userid'] = $userid;
- $params['children'] = $coursecontext->path."/%";
- $sql = "SELECT ra.*, ctx.path
- FROM {role_assignments} ra
- JOIN {context} ctx ON ra.contextid = ctx.id
- WHERE ra.userid = :userid AND (ctx.id $parentsaself OR ctx.path LIKE :children)";
- $rs = $DB->get_recordset_sql($sql, $params);
-
- // add missing role definitions
- foreach ($rs as $ra) {
- $accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid;
- $roles[$ra->roleid] = $ra->roleid;
- }
- $rs->close();
-
- // add the "default frontpage role" when on the frontpage
- if (!empty($CFG->defaultfrontpageroleid)) {
- $frontpagecontext = context_course::instance(get_site()->id);
- if ($frontpagecontext->id == $coursecontext->id) {
- $roles[$CFG->defaultfrontpageroleid] = $CFG->defaultfrontpageroleid;
- }
- }
-
- // do not forget the default role
- if (!empty($CFG->defaultuserroleid)) {
- $roles[$CFG->defaultuserroleid] = $CFG->defaultuserroleid;
- }
- }
-
- if (!$roles) {
- // weird, default roles must be missing...
- $accessdata['loaded'][$coursecontext->instanceid] = 1;
- return;
- }
-
- // now get overrides of interesting roles in all interesting contexts (this course + children + parents)
- $params = array('pathprefix' => $coursecontext->path . '/%');
- list($parentsaself, $rparams) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'pc_');
- $params = array_merge($params, $rparams);
- list($roleids, $rparams) = $DB->get_in_or_equal($roles, SQL_PARAMS_NAMED, 'r_');
- $params = array_merge($params, $rparams);
-
- $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
- FROM {context} ctx
- JOIN {role_capabilities} rc ON rc.contextid = ctx.id
- WHERE rc.roleid $roleids
- AND (ctx.id $parentsaself OR ctx.path LIKE :pathprefix)
- ORDER BY rc.capability"; // fixed capability order is necessary for rdef dedupe
- $rs = $DB->get_recordset_sql($sql, $params);
-
- $newrdefs = array();
- foreach ($rs as $rd) {
- $k = $rd->path.':'.$rd->roleid;
- if (isset($accessdata['rdef'][$k])) {
- continue;
- }
- $newrdefs[$k][$rd->capability] = (int)$rd->permission;
- }
- $rs->close();
-
- // share new role definitions
- foreach ($newrdefs as $k=>$unused) {
- if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) {
- $ACCESSLIB_PRIVATE->rolepermissions[$k] = $newrdefs[$k];
- }
- $accessdata['rdef_count']++;
- $accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k];
- }
-
- $accessdata['loaded'][$coursecontext->instanceid] = 1;
-
- // we want to deduplicate the USER->access from time to time, this looks like a good place,
- // because we have to do it before the end of session
- dedupe_user_access();
-}
-
-/**
- * Add to the access ctrl array the data needed by a role for a given context.
- *
- * The data is added in the rdef key.
- * This role-centric function is useful for role_switching
- * and temporary course roles.
- *
- * @access private
- * @param int $roleid the id of the user
- * @param context $context needs path!
- * @param array $accessdata accessdata array (is modified)
- * @return array
- */
-function load_role_access_by_context($roleid, context $context, &$accessdata) {
- global $DB, $ACCESSLIB_PRIVATE;
-
- /* Get the relevant rolecaps into rdef
- * - relevant role caps
- * - at ctx and above
- * - below this ctx
- */
-
- if (empty($context->path)) {
- // weird, this should not happen
- return;
- }
-
- list($parentsaself, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'pc_');
- $params['roleid'] = $roleid;
- $params['childpath'] = $context->path.'/%';
-
- $sql = "SELECT ctx.path, rc.capability, rc.permission
- FROM {role_capabilities} rc
- JOIN {context} ctx ON (rc.contextid = ctx.id)
- WHERE rc.roleid = :roleid AND (ctx.id $parentsaself OR ctx.path LIKE :childpath)
- ORDER BY rc.capability"; // fixed capability order is necessary for rdef dedupe
- $rs = $DB->get_recordset_sql($sql, $params);
-
- $newrdefs = array();
- foreach ($rs as $rd) {
- $k = $rd->path.':'.$roleid;
- if (isset($accessdata['rdef'][$k])) {
- continue;
- }
- $newrdefs[$k][$rd->capability] = (int)$rd->permission;
- }
- $rs->close();
-
- // share new role definitions
- foreach ($newrdefs as $k=>$unused) {
- if (!isset($ACCESSLIB_PRIVATE->rolepermissions[$k])) {
- $ACCESSLIB_PRIVATE->rolepermissions[$k] = $newrdefs[$k];
- }
- $accessdata['rdef_count']++;
- $accessdata['rdef'][$k] =& $ACCESSLIB_PRIVATE->rolepermissions[$k];
- }
-}
-
/**
* Returns empty accessdata structure.
*
function get_empty_accessdata() {
$accessdata = array(); // named list
$accessdata['ra'] = array();
- $accessdata['rdef'] = array();
- $accessdata['rdef_count'] = 0; // this bloody hack is necessary because count($array) is slooooowwww in PHP
- $accessdata['rdef_lcc'] = 0; // rdef_count during the last compression
- $accessdata['loaded'] = array(); // loaded course contexts
$accessdata['time'] = time();
$accessdata['rsw'] = array();
function get_user_accessdata($userid, $preloadonly=false) {
global $CFG, $ACCESSLIB_PRIVATE, $USER;
- if (!empty($USER->access['rdef']) and empty($ACCESSLIB_PRIVATE->rolepermissions)) {
- // share rdef from USER session with rolepermissions cache in order to conserve memory
- foreach ($USER->access['rdef'] as $k=>$v) {
- $ACCESSLIB_PRIVATE->rolepermissions[$k] =& $USER->access['rdef'][$k];
- }
+ if (isset($USER->access)) {
$ACCESSLIB_PRIVATE->accessdatabyuser[$USER->id] = $USER->access;
}
}
} else {
- $accessdata = get_user_access_sitewide($userid); // includes default role and frontpage role
+ // Includes default role and frontpage role.
+ $accessdata = get_user_roles_sitewide_accessdata($userid);
}
$ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
}
}
-/**
- * Try to minimise the size of $USER->access by eliminating duplicate override storage,
- * this function looks for contexts with the same overrides and shares them.
- *
- * @access private
- * @return void
- */
-function dedupe_user_access() {
- global $USER;
-
- if (CLI_SCRIPT) {
- // no session in CLI --> no compression necessary
- return;
- }
-
- if (empty($USER->access['rdef_count'])) {
- // weird, this should not happen
- return;
- }
-
- // the rdef is growing only, we never remove stuff from it, the rdef_lcc helps us to detect new stuff in rdef
- if ($USER->access['rdef_count'] - $USER->access['rdef_lcc'] > 10) {
- // do not compress after each change, wait till there is more stuff to be done
- return;
- }
-
- $hashmap = array();
- foreach ($USER->access['rdef'] as $k=>$def) {
- $hash = sha1(serialize($def));
- if (isset($hashmap[$hash])) {
- $USER->access['rdef'][$k] =& $hashmap[$hash];
- } else {
- $hashmap[$hash] =& $USER->access['rdef'][$k];
- }
- }
-
- $USER->access['rdef_lcc'] = $USER->access['rdef_count'];
-}
-
/**
* A convenience function to completely load all the capabilities
* for the current user. It is called from has_capability() and functions change permissions.
unset($USER->access);
$USER->access = get_user_accessdata($USER->id);
- // deduplicate the overrides to minimize session size
- dedupe_user_access();
-
// Clear to force a refresh
unset($USER->mycourses);
return;
}
- // load course stuff first
- load_course_context($USER->id, $coursecontext, $USER->access);
-
$USER->access['ra'][$coursecontext->path][(int)$roleid] = (int)$roleid;
-
- load_role_access_by_context($roleid, $coursecontext, $USER->access);
}
/**
$event->add_record_snapshot('role', $role);
$event->trigger();
+ // Reset any cache of this role, including MUC.
+ accesslib_clear_role_cache($roleid);
+
return true;
}
$DB->insert_record('role_capabilities', $cap);
}
}
+
+ // Reset any cache of this role, including MUC.
+ accesslib_clear_role_cache($roleid);
+
return true;
}
} else {
$DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
}
+
+ // Reset any cache of this role, including MUC.
+ accesslib_clear_role_cache($roleid);
+
return true;
}
assign_capability($cap, $permission, $roleid, $systemcontext->id);
}
+ // Reset any cache of this role, including MUC.
+ accesslib_clear_role_cache($roleid);
+
// Mark the system context dirty.
context_system::instance()->mark_dirty();
}
function role_switch($roleid, context $context) {
global $USER;
- //
- // Plan of action
- //
- // - Add the ghost RA to $USER->access
- // as $USER->access['rsw'][$path] = $roleid
- //
- // - Make sure $USER->access['rdef'] has the roledefs
- // it needs to honour the switcherole
- //
- // Roledefs will get loaded "deep" here - down to the last child
- // context. Note that
- //
- // - When visiting subcontexts, our selective accessdata loading
- // will still work fine - though those ra/rdefs will be ignored
- // appropriately while the switch is in place
- //
- // - If a switcherole happens at a category with tons of courses
- // (that have many overrides for switched-to role), the session
- // will get... quite large. Sometimes you just can't win.
- //
- // To un-switch just unset($USER->access['rsw'][$path])
+ // Add the ghost RA to $USER->access as $USER->access['rsw'][$path] = $roleid.
+ // To un-switch just unset($USER->access['rsw'][$path]).
//
// Note: it is not possible to switch to roles that do not have course:view
load_all_capabilities();
}
-
// Add the switch RA
if ($roleid == 0) {
unset($USER->access['rsw'][$context->path]);
$USER->access['rsw'][$context->path] = $roleid;
- // Load roledefs
- load_role_access_by_context($roleid, $context, $USER->access);
-
return true;
}
$cap->roleid = $targetrole;
$DB->insert_record('role_capabilities', $cap);
}
+
+ // Reset any cache of this role, including MUC.
+ accesslib_clear_role_cache($targetrole);
}
/**
require_once($CFG->dirroot.'/grade/grading/lib.php');
grading_manager::delete_all_for_context($this->_id);
+ $ids = $DB->get_fieldset_select('role_capabilities', 'DISTINCT roleid', 'contextid = ?', array($this->_id));
+
// now delete stuff from role related tables, role_unassign_all
// and unenrol should be called earlier to do proper cleanup
$DB->delete_records('role_assignments', array('contextid'=>$this->_id));
$DB->delete_records('role_capabilities', array('contextid'=>$this->_id));
$DB->delete_records('role_names', array('contextid'=>$this->_id));
+
+ if ($ids) {
+ // Reset any cache of these roles, including MUC.
+ accesslib_clear_role_cache($ids);
+ }
}
/**
return true;
}
}
+
+
+/**
+ * Administration setting to define a list of file types.
+ *
+ * @copyright 2016 Jonathon Fowler <fowlerj@usq.edu.au>
+ * @copyright 2017 David Mudrák <david@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class admin_setting_filetypes extends admin_setting_configtext {
+
+ /** @var array Allow selection from these file types only. */
+ protected $onlytypes = [];
+
+ /** @var bool Allow selection of 'All file types' (will be stored as '*'). */
+ protected $allowall = true;
+
+ /** @var core_form\filetypes_util instance to use as a helper. */
+ protected $util = null;
+
+ /**
+ * Constructor.
+ *
+ * @param string $name Unique ascii name like 'mycoresetting' or 'myplugin/mysetting'
+ * @param string $visiblename Localised label of the setting
+ * @param string $description Localised description of the setting
+ * @param string $defaultsetting Default setting value.
+ * @param array $options Setting widget options, an array with optional keys:
+ * 'onlytypes' => array Allow selection from these file types only; for example ['onlytypes' => ['web_image']].
+ * 'allowall' => bool Allow to select 'All file types', defaults to true. Does not apply if onlytypes are set.
+ */
+ public function __construct($name, $visiblename, $description, $defaultsetting = '', array $options = []) {
+
+ parent::__construct($name, $visiblename, $description, $defaultsetting, PARAM_RAW);
+
+ if (array_key_exists('onlytypes', $options) && is_array($options['onlytypes'])) {
+ $this->onlytypes = $options['onlytypes'];
+ }
+
+ if (!$this->onlytypes && array_key_exists('allowall', $options)) {
+ $this->allowall = (bool)$options['allowall'];
+ }
+
+ $this->util = new \core_form\filetypes_util();
+ }
+
+ /**
+ * Normalize the user's input and write it to the database as comma separated list.
+ *
+ * Comma separated list as a text representation of the array was chosen to
+ * make this compatible with how the $CFG->courseoverviewfilesext values are stored.
+ *
+ * @param string $data Value submitted by the admin.
+ * @return string Epty string if all good, error message otherwise.
+ */
+ public function write_setting($data) {
+ return parent::write_setting(implode(',', $this->util->normalize_file_types($data)));
+ }
+
+ /**
+ * Validate data before storage
+ *
+ * @param string $data The setting values provided by the admin
+ * @return bool|string True if ok, the string if error found
+ */
+ public function validate($data) {
+
+ // No need to call parent's validation here as we are PARAM_RAW.
+
+ if ($this->util->is_whitelisted($data, $this->onlytypes)) {
+ return true;
+
+ } else {
+ $troublemakers = $this->util->get_not_whitelisted($data, $this->onlytypes);
+ return get_string('filetypesnotwhitelisted', 'core_form', implode(' ', $troublemakers));
+ }
+ }
+
+ /**
+ * Return an HTML string for the setting element.
+ *
+ * @param string $data The current setting value
+ * @param string $query Admin search query to be highlighted
+ * @return string HTML to be displayed
+ */
+ public function output_html($data, $query='') {
+ global $OUTPUT, $PAGE;
+
+ $default = $this->get_defaultsetting();
+ $context = (object) [
+ 'id' => $this->get_id(),
+ 'name' => $this->get_full_name(),
+ 'value' => $data,
+ 'descriptions' => $this->util->describe_file_types($data),
+ ];
+ $element = $OUTPUT->render_from_template('core_admin/setting_filetypes', $context);
+
+ $PAGE->requires->js_call_amd('core_form/filetypes', 'init', [
+ $this->get_id(),
+ $this->visiblename,
+ $this->onlytypes,
+ $this->allowall,
+ ]);
+
+ return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
+ }
+
+ /**
+ * Should the values be always displayed in LTR mode?
+ *
+ * We always return true here because these values are not RTL compatible.
+ *
+ * @return bool True because these values are not RTL compatible.
+ */
+ public function get_force_ltr() {
+ return true;
+ }
+}
});
}
- // Ajax stuff.
- var deferred = $.Deferred();
-
- var promises = ajax.call([{
+ return ajax.call([{
methodname: 'core_get_fragment',
args: {
component: component,
contextid: contextid,
args: formattedparams
}
- }], false);
-
- promises[0].done(function(data) {
- deferred.resolve(data);
- }).fail(function(ex) {
- deferred.reject(ex);
- });
- return deferred.promise();
+ }])[0];
};
return /** @alias module:core/fragment */{
*/
loadFragment: function(component, callback, contextid, params) {
var promise = $.Deferred();
- $.when(loadFragment(component, callback, contextid, params)).then(function(data) {
+ loadFragment(component, callback, contextid, params).then(function(data) {
var jsNodes = $(data.javascript);
var allScript = '';
jsNodes.each(function(index, scriptNode) {
}
});
promise.resolve(data.html, allScript);
+ return;
}).fail(function(ex) {
promise.reject(ex);
});
ready.reject('Invalid icon system specified' + config.iconsystemmodule);
} else {
iconSystem = system;
- system.init().then(ready.resolve);
+ system.init().then(ready.resolve).catch(notification.exception);
}
});
ready.reject('Invalid icon system specified' + config.iconsystem);
} else {
iconSystem = system;
- system.init().then(ready.resolve);
+ system.init().then(ready.resolve).catch(notification.exception);
}
});
addToLocalStorage(key, value);
date.deferred.resolve(value);
});
+ return;
})
- .fail(function(ex) {
+ .catch(function(ex) {
// If we failed to retrieve the dates then reject the date's
// deferred objects to make sure they don't hang.
dates.forEach(function(date) {
array('component' => new external_value(PARAM_COMPONENT, 'component containing the template'),
'template' => new external_value(PARAM_ALPHANUMEXT, 'name of the template'),
'themename' => new external_value(PARAM_ALPHANUMEXT, 'The current theme.'),
+ 'includecomments' => new external_value(PARAM_BOOL, 'Include comments or not', VALUE_DEFAULT, false)
)
);
}
+ /**
+ * Remove comments from mustache template.
+ * @param string $templatestr
+ * @return mixed
+ */
+ protected static function strip_template_comments($templatestr) {
+ return preg_replace('/(?={{!)(.*)(}})/sU', '', $templatestr);
+ }
+
/**
* Return a mustache template, and all the strings it requires.
*
* @param string $themename The name of the current theme.
* @return string the template
*/
- public static function load_template($component, $template, $themename) {
+ public static function load_template($component, $template, $themename, $includecomments = false) {
global $DB, $CFG, $PAGE;
$params = self::validate_parameters(self::load_template_parameters(),
array('component' => $component,
'template' => $template,
- 'themename' => $themename));
+ 'themename' => $themename,
+ 'includecomments' => $includecomments));
$component = $params['component'];
$template = $params['template'];
$themename = $params['themename'];
+ $includecomments = $params['includecomments'];
$templatename = $component . '/' . $template;
$filename = mustache_template_finder::get_template_filepath($templatename, $themename);
$templatestr = file_get_contents($filename);
+ // Remove comments from template.
+ if (!$includecomments) {
+ $templatestr = self::strip_template_comments($templatestr);
+ }
+
return $templatestr;
}
}
$component = $task->get_component();
if ($plugininfo = core_plugin_manager::instance()->get_plugin_info($component)) {
- if (!$plugininfo->is_enabled() && !$task->get_run_if_component_disabled()) {
+ if ($plugininfo->is_enabled() === false && !$task->get_run_if_component_disabled()) {
echo "Component is not enabled ($component).\n";
return false;
}
'simpledata' => true,
),
+ // Cache system-wide role definitions.
+ 'roledefs' => array(
+ 'mode' => cache_store::MODE_APPLICATION,
+ 'simplekeys' => true,
+ 'simpledata' => true,
+ 'staticacceleration' => true,
+ 'staticaccelerationsize' => 30,
+ ),
+
// Caches plugins existing functions by function name and file.
// Set static acceleration size to 5 to load a few functions.
'plugin_functions' => array(
<FIELD NAME="sequence" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="visible" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
<FIELD NAME="availability" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Availability restrictions for viewing this section, in JSON format. Null if no restrictions."/>
+ <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time at which the course section was last changed."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
'classname' => 'core_course_external',
'methodname' => 'duplicate_course',
'classpath' => 'course/externallib.php',
- 'description' => 'Duplicate an existing course (creating a new one), without user data',
+ 'description' => 'Duplicate an existing course (creating a new one).',
'type' => 'write',
'capabilities' => 'moodle/backup:backupcourse, moodle/restore:restorecourse, moodle/course:create'
),
'type' => 'write',
'classpath' => 'files/externallib.php',
),
+ 'core_form_get_filetypes_browser_data' => array(
+ 'classname' => 'core_form\external',
+ 'methodname' => 'get_filetypes_browser_data',
+ 'classpath' => '',
+ 'description' => 'Provides data for the filetypes element browser.',
+ 'type' => 'read',
+ 'loginrequired' => false,
+ 'ajax' => true,
+ ),
'core_get_component_strings' => array(
'classname' => 'core_external',
'methodname' => 'get_component_strings',
// Automatically generated Moodle v3.3.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2017061201.00) {
+ $table = new xmldb_table('course_sections');
+ $field = new xmldb_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'availability');
+
+ // Define a field 'timemodified' in the 'course_sections' table, to background deletion tasks.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ upgrade_main_savepoint(true, 2017061201.00);
+ }
+
return true;
}
/* === Apis deprecated since Moodle 3.0 === */
/**
- * Returns the URL of the HTTP_REFERER, less the querystring portion if required.
- *
* @deprecated since Moodle 3.0 MDL-49360 - please do not use this function any more.
- * @todo MDL-50265 Remove this function in Moodle 3.4.
- * @param boolean $stripquery if true, also removes the query part of the url.
- * @return string The resulting referer or empty string.
*/
function get_referer($stripquery = true) {
- debugging('get_referer() is deprecated. Please use get_local_referer() instead.', DEBUG_DEVELOPER);
- if (isset($_SERVER['HTTP_REFERER'])) {
- if ($stripquery) {
- return strip_querystring($_SERVER['HTTP_REFERER']);
- } else {
- return $_SERVER['HTTP_REFERER'];
- }
- } else {
- return '';
- }
+ throw new coding_exception('get_referer() can not be used any more. Please use get_local_referer() instead.');
}
/**
- * Checks if current user is a web crawler.
- *
- * This list can not be made complete, this is not a security
- * restriction, we make the list only to help these sites
- * especially when automatic guest login is disabled.
- *
- * If admin needs security they should enable forcelogin
- * and disable guest access!!
- *
- * @return bool
* @deprecated since Moodle 3.0 use \core_useragent::is_web_crawler instead.
*/
function is_web_crawler() {
- debugging('is_web_crawler() has been deprecated, please use core_useragent::is_web_crawler() instead.', DEBUG_DEVELOPER);
- return core_useragent::is_web_crawler();
+ throw new coding_exception('is_web_crawler() can not be used any more. Please use core_useragent::is_web_crawler() instead.');
}
/**
- * Update user's course completion statuses
- *
- * First update all criteria completions, then aggregate all criteria completions
- * and update overall course completions.
- *
* @deprecated since Moodle 3.0 MDL-50287 - please do not use this function any more.
- * @todo Remove this function in Moodle 3.2 MDL-51226.
*/
function completion_cron() {
- global $CFG;
- require_once($CFG->dirroot.'/completion/cron.php');
-
- debugging('completion_cron() is deprecated. Functionality has been moved to scheduled tasks.', DEBUG_DEVELOPER);
- completion_cron_mark_started();
-
- completion_cron_criteria();
-
- completion_cron_completions();
+ throw new coding_exception('completion_cron() can not be used any more. Functionality has been moved to scheduled tasks.');
}
/**
- * Returns an ordered array of tags associated with visible courses
- * (boosted replacement of get_all_tags() allowing association with user and tagtype).
- *
* @deprecated since 3.0
- * @package core_tag
- * @category tag
- * @param int $courseid A course id. Passing 0 will return all distinct tags for all visible courses
- * @param int $userid (optional) the user id, a default of 0 will return all users tags for the course
- * @param string $tagtype (optional) The type of tag, empty string returns all types. Currently (Moodle 2.2) there are two
- * types of tags which are used within Moodle, they are 'official' and 'default'.
- * @param int $numtags (optional) number of tags to display, default of 80 is set in the block, 0 returns all
- * @param string $unused (optional) was selected sorting, moved to tag_print_cloud()
- * @return array
*/
function coursetag_get_tags($courseid, $userid=0, $tagtype='', $numtags=0, $unused = '') {
- debugging('Function coursetag_get_tags() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
-
- global $CFG, $DB;
-
- // get visible course ids
- $courselist = array();
- if ($courseid === 0) {
- if ($courses = $DB->get_records_select('course', 'visible=1 AND category>0', null, '', 'id')) {
- foreach ($courses as $key => $value) {
- $courselist[] = $key;
- }
- }
- }
-
- // get tags from the db ordered by highest count first
- $params = array();
- $sql = "SELECT id as tkey, name, id, isstandard, rawname, f.timemodified, flag, count
- FROM {tag} t,
- (SELECT tagid, MAX(timemodified) as timemodified, COUNT(id) as count
- FROM {tag_instance}
- WHERE itemtype = 'course' ";
-
- if ($courseid > 0) {
- $sql .= " AND itemid = :courseid ";
- $params['courseid'] = $courseid;
- } else {
- if (!empty($courselist)) {
- list($usql, $uparams) = $DB->get_in_or_equal($courselist, SQL_PARAMS_NAMED);
- $sql .= "AND itemid $usql ";
- $params = $params + $uparams;
- }
- }
-
- if ($userid > 0) {
- $sql .= " AND tiuserid = :userid ";
- $params['userid'] = $userid;
- }
-
- $sql .= " GROUP BY tagid) f
- WHERE t.id = f.tagid ";
- if ($tagtype != '') {
- $sql .= "AND isstandard = :isstandard ";
- $params['isstandard'] = ($tagtype === 'official') ? 1 : 0;
- }
- $sql .= "ORDER BY count DESC, name ASC";
-
- // limit the number of tags for output
- if ($numtags == 0) {
- $tags = $DB->get_records_sql($sql, $params);
- } else {
- $tags = $DB->get_records_sql($sql, $params, 0, $numtags);
- }
-
- // prepare the return
- $return = array();
- if ($tags) {
- // avoid print_tag_cloud()'s ksort upsetting ordering by setting the key here
- foreach ($tags as $value) {
- $return[] = $value;
- }
- }
-
- return $return;
-
+ throw new coding_exception('Function coursetag_get_tags() can not be used any more. Userid is no longer used for tagging courses.');
}
/**
- * Returns an ordered array of tags
- * (replaces popular_tags_count() allowing sorting).
- *
* @deprecated since 3.0
- * @package core_tag
- * @category tag
- * @param string $unused (optional) was selected sorting - moved to tag_print_cloud()
- * @param int $numtags (optional) number of tags to display, default of 20 is set in the block, 0 returns all
- * @return array
*/
function coursetag_get_all_tags($unused='', $numtags=0) {
- debugging('Function coursetag_get_all_tag() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
-
- global $CFG, $DB;
-
- // note that this selects all tags except for courses that are not visible
- $sql = "SELECT id, name, isstandard, rawname, f.timemodified, flag, count
- FROM {tag} t,
- (SELECT tagid, MAX(timemodified) as timemodified, COUNT(id) as count
- FROM {tag_instance} WHERE tagid NOT IN
- (SELECT tagid FROM {tag_instance} ti, {course} c
- WHERE c.visible = 0
- AND ti.itemtype = 'course'
- AND ti.itemid = c.id)
- GROUP BY tagid) f
- WHERE t.id = f.tagid
- ORDER BY count DESC, name ASC";
- if ($numtags == 0) {
- $tags = $DB->get_records_sql($sql);
- } else {
- $tags = $DB->get_records_sql($sql, null, 0, $numtags);
- }
-
- $return = array();
- if ($tags) {
- foreach ($tags as $value) {
- $return[] = $value;
- }
- }
-
- return $return;
+ throw new coding_exception('Function coursetag_get_all_tag() can not be used any more. Userid is no longer used for tagging courses.');
}
/**
- * Returns javascript for use in tags block and supporting pages
- *
* @deprecated since 3.0
- * @package core_tag
- * @category tag
- * @return null
*/
function coursetag_get_jscript() {
- debugging('Function coursetag_get_jscript() is deprecated and obsolete.', DEBUG_DEVELOPER);
- return '';
+ throw new coding_exception('Function coursetag_get_jscript() can not be used any more and is obsolete.');
}
/**
- * Returns javascript to create the links in the tag block footer.
- *
* @deprecated since 3.0
- * @package core_tag
- * @category tag
- * @param string $elementid the element to attach the footer to
- * @param array $coursetagslinks links arrays each consisting of 'title', 'onclick' and 'text' elements
- * @return string always returns a blank string
*/
function coursetag_get_jscript_links($elementid, $coursetagslinks) {
- debugging('Function coursetag_get_jscript_links() is deprecated and obsolete.', DEBUG_DEVELOPER);
- return '';
+ throw new coding_exception('Function coursetag_get_jscript_links() can not be used any more and is obsolete.');
}
/**
- * Returns all tags created by a user for a course
- *
* @deprecated since 3.0
- * @package core_tag
- * @category tag
- * @param int $courseid tags are returned for the course that has this courseid
- * @param int $userid return tags which were created by this user
*/
function coursetag_get_records($courseid, $userid) {
- debugging('Function coursetag_get_records() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
-
- global $CFG, $DB;
-
- $sql = "SELECT t.id, name, rawname
- FROM {tag} t, {tag_instance} ti
- WHERE t.id = ti.tagid
- AND ti.tiuserid = :userid
- AND ti.itemid = :courseid
- ORDER BY name ASC";
-
- return $DB->get_records_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid));
+ throw new coding_exception('Function coursetag_get_records() can not be used any more. Userid is no longer used for tagging courses.');
}
/**
- * Stores a tag for a course for a user
- *
* @deprecated since 3.0
- * @package core_tag
- * @category tag
- * @param array $tags simple array of keywords to be stored
- * @param int $courseid the id of the course we wish to store a tag for
- * @param int $userid the id of the user we wish to store a tag for
- * @param string $tagtype official or default only
- * @param string $myurl (optional) for logging creation of course tags
*/
function coursetag_store_keywords($tags, $courseid, $userid=0, $tagtype='official', $myurl='') {
- debugging('Function coursetag_store_keywords() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
-
- global $CFG;
-
- if (is_array($tags) and !empty($tags)) {
- if ($tagtype === 'official') {
- $tagcoll = core_tag_area::get_collection('core', 'course');
- // We don't normally need to create tags, they are created automatically when added to items. but we do here because we want them to be official.
- core_tag_tag::create_if_missing($tagcoll, $tags, true);
- }
- foreach ($tags as $tag) {
- $tag = trim($tag);
- if (strlen($tag) > 0) {
- core_tag_tag::add_item_tag('core', 'course', $courseid, context_course::instance($courseid), $tag, $userid);
- }
- }
- }
-
+ throw new coding_exception('Function coursetag_store_keywords() can not be used any more. Userid is no longer used for tagging courses.');
}
/**
- * Deletes a personal tag for a user for a course.
- *
* @deprecated since 3.0
- * @package core_tag
- * @category tag
- * @param int $tagid the tag we wish to delete
- * @param int $userid the user that the tag is associated with
- * @param int $courseid the course that the tag is associated with
*/
function coursetag_delete_keyword($tagid, $userid, $courseid) {
- debugging('Function coursetag_delete_keyword() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
-
- $tag = core_tag_tag::get($tagid);
- core_tag_tag::remove_item_tag('core', 'course', $courseid, $tag->rawname, $userid);
+ throw new coding_exception('Function coursetag_delete_keyword() can not be used any more. Userid is no longer used for tagging courses.');
}
/**
- * Get courses tagged with a tag
- *
* @deprecated since 3.0
- * @package core_tag
- * @category tag
- * @param int $tagid
- * @return array of course objects
*/
function coursetag_get_tagged_courses($tagid) {
- debugging('Function coursetag_get_tagged_courses() is deprecated. Userid is no longer used for tagging courses.', DEBUG_DEVELOPER);
-
- global $DB;
-
- $courses = array();
-
- $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
-
- $sql = "SELECT c.*, $ctxselect
- FROM {course} c
- JOIN {tag_instance} t ON t.itemid = c.id
- JOIN {context} ctx ON ctx.instanceid = c.id
- WHERE t.tagid = :tagid AND
- t.itemtype = 'course' AND
- ctx.contextlevel = :contextlevel
- ORDER BY c.sortorder ASC";
- $params = array('tagid' => $tagid, 'contextlevel' => CONTEXT_COURSE);
- $rs = $DB->get_recordset_sql($sql, $params);
- foreach ($rs as $course) {
- context_helper::preload_from_record($course);
- if ($course->visible == 1 || has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) {
- $courses[$course->id] = $course;
- }
- }
- return $courses;
+ throw new coding_exception('Function coursetag_get_tagged_courses() can not be used any more. Userid is no longer used for tagging courses.');
}
/**
- * Course tagging function used only during the deletion of a course (called by lib/moodlelib.php) to clean up associated tags
- *
- * @package core_tag
* @deprecated since 3.0
- * @param int $courseid the course we wish to delete tag instances from
- * @param bool $showfeedback if we should output a notification of the delete to the end user
*/
function coursetag_delete_course_tags($courseid, $showfeedback=false) {
- debugging('Function coursetag_delete_course_tags() is deprecated. Use core_tag_tag::remove_all_item_tags().', DEBUG_DEVELOPER);
-
- global $OUTPUT;
- core_tag_tag::remove_all_item_tags('core', 'course', $courseid);
-
- if ($showfeedback) {
- echo $OUTPUT->notification(get_string('deletedcoursetags', 'tag'), 'notifysuccess');
- }
+ throw new coding_exception('Function coursetag_delete_course_tags() is deprecated. Use core_tag_tag::remove_all_item_tags().');
}
/**
return true;
}
+
+/**
+ * Previous internal API, it was not supposed to be used anywhere.
+ *
+ * @access private
+ * @deprecated since Moodle 3.4 and removed immediately. MDL-49398.
+ * @param int $userid the id of the user
+ * @param context_course $coursecontext course context
+ * @param array $accessdata accessdata array (modified)
+ * @return void modifies $accessdata parameter
+ */
+function load_course_context($userid, context_course $coursecontext, &$accessdata) {
+ throw new coding_exception('load_course_context() is removed. Do not use private functions or data structures.');
+}
+
+/**
+ * Previous internal API, it was not supposed to be used anywhere.
+ *
+ * @access private
+ * @deprecated since Moodle 3.4 and removed immediately. MDL-49398.
+ * @param int $roleid the id of the user
+ * @param context $context needs path!
+ * @param array $accessdata accessdata array (is modified)
+ * @return array
+ */
+function load_role_access_by_context($roleid, context $context, &$accessdata) {
+ throw new coding_exception('load_role_access_by_context() is removed. Do not use private functions or data structures.');
+}
+
+/**
+ * Previous internal API, it was not supposed to be used anywhere.
+ *
+ * @access private
+ * @deprecated since Moodle 3.4 and removed immediately. MDL-49398.
+ * @return void
+ */
+function dedupe_user_access() {
+ throw new coding_exception('dedupe_user_access() is removed. Do not use private functions or data structures.');
+}
+
+/**
+ * Previous internal API, it was not supposed to be used anywhere.
+ * Return a nested array showing role assignments
+ * and all relevant role capabilities for the user.
+ *
+ * [ra] => [/path][roleid]=roleid
+ * [rdef] => ["$contextpath:$roleid"][capability]=permission
+ *
+ * @access private
+ * @deprecated since Moodle 3.4. MDL-49398.
+ * @param int $userid - the id of the user
+ * @return array access info array
+ */
+function get_user_access_sitewide($userid) {
+ debugging('get_user_access_sitewide() is deprecated. Do not use private functions or data structures.', DEBUG_DEVELOPER);
+
+ $accessdata = get_user_accessdata($userid);
+ $accessdata['rdef'] = array();
+ $roles = array();
+
+ foreach ($accessdata['ra'] as $path => $pathroles) {
+ $roles = array_merge($pathroles, $roles);
+ }
+
+ $rdefs = get_role_definitions($roles);
+
+ foreach ($rdefs as $roleid => $rdef) {
+ foreach ($rdef as $path => $caps) {
+ $accessdata['rdef']["$path:$roleid"] = $caps;
+ }
+ }
+
+ return $accessdata;
+}
--- /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/>.
+
+/**
+ * This module allows to enhance the form elements MoodleQuickForm_filetypes
+ *
+ * @module core_form/filetypes
+ * @package core_form
+ * @copyright 2017 David Mudrak <david@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since 3.3
+ */
+define(['jquery', 'core/log', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/ajax',
+ 'core/templates', 'core/tree'],
+ function($, Log, Str, ModalFactory, ModalEvents, Ajax, Templates, Tree) {
+
+ "use strict";
+
+ /**
+ * Constructor of the FileTypes instances.
+ *
+ * @constructor
+ * @param {String} elementId The id of the form element to enhance
+ * @param {String} elementLabel The label of the form element used as the modal selector title
+ * @param {String} onlyTypes Limit the list of offered types to this
+ * @param {Bool} allowAll Allow presence of the "All file types" item
+ */
+ var FileTypes = function(elementId, elementLabel, onlyTypes, allowAll) {
+
+ this.elementId = elementId;
+ this.elementLabel = elementLabel;
+ this.onlyTypes = onlyTypes;
+ this.allowAll = allowAll;
+
+ this.inputField = $('#' + elementId);
+ this.wrapperBrowserTrigger = $('[data-filetypesbrowser="' + elementId + '"]');
+ this.wrapperDescriptions = $('[data-filetypesdescriptions="' + elementId + '"]');
+
+ if (!this.wrapperBrowserTrigger.length) {
+ // This is a valid case. Most probably the element is frozen and
+ // the filetypes browser should not be available.
+ return;
+ }
+
+ if (!this.inputField.length || !this.wrapperDescriptions.length) {
+ Log.error('core_form/filetypes: Unexpected DOM structure, unable to enhance filetypes field ' + elementId);
+ return;
+ }
+
+ this.prepareBrowserTrigger()
+ .then(function() {
+ return this.prepareBrowserModal();
+ }.bind(this))
+
+ .then(function() {
+ return this.prepareBrowserTree();
+ }.bind(this));
+ };
+
+ /**
+ * Create and set the browser trigger widget (this.browserTrigger).
+ *
+ * @method prepareBrowserTrigger
+ * @returns {Promise}
+ */
+ FileTypes.prototype.prepareBrowserTrigger = function() {
+ return Templates.render('core_form/filetypes-trigger', {})
+ .then(function(html) {
+ this.wrapperBrowserTrigger.html(html);
+ this.browserTrigger = this.wrapperBrowserTrigger.find('[data-filetypeswidget="browsertrigger"]');
+ }.bind(this));
+ };
+
+ /**
+ * Create and set the modal for displaying the browser (this.browserModal).
+ *
+ * @method prepareBrowserModal
+ * @returns {Promise}
+ */
+ FileTypes.prototype.prepareBrowserModal = function() {
+ return ModalFactory.create({
+ type: ModalFactory.types.SAVE_CANCEL,
+ title: this.elementLabel
+
+ }).then(function(modal) {
+ this.browserModal = modal;
+ }.bind(this))
+
+ .then(function() {
+ // Because we have custom conditional modal trigger, we need to
+ // handle the focus after closing ourselves, too.
+ this.browserModal.getRoot().on(ModalEvents.hidden, function() {
+ this.browserTrigger.focus();
+ }.bind(this));
+
+ this.browserModal.getRoot().on(ModalEvents.save, function() {
+ this.saveBrowserModal();
+ }.bind(this));
+ }.bind(this));
+
+ };
+
+ /**
+ * Create and set the tree in the browser modal's body.
+ *
+ * @method prepareBrowserTree
+ * @returns {Promise}
+ */
+ FileTypes.prototype.prepareBrowserTree = function() {
+
+ this.browserTrigger.on('click', function(e) {
+ e.preventDefault();
+
+ // We want to display the browser modal only when the associated input
+ // field is not frozen (disabled).
+ if (this.inputField.is('[disabled]')) {
+ return;
+ }
+
+ var bodyContent = this.loadBrowserModalBody();
+
+ bodyContent.then(function() {
+
+ // Turn the list of groups and extensions into the tree.
+ this.browserTree = new Tree(this.browserModal.getBody());
+
+ // Override the behaviour of the Enter and Space keys to toggle our checkbox,
+ // rather than toggle the tree node expansion status.
+ this.browserTree.handleKeyDown = function(item, e) {
+ if (e.keyCode == this.browserTree.keys.enter || e.keyCode == this.browserTree.keys.space) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.toggleCheckbox(item.attr('data-filetypesbrowserkey'));
+ } else {
+ Tree.prototype.handleKeyDown.call(this.browserTree, item, e);
+ }
+ }.bind(this);
+
+ if (this.allowAll) {
+ // Hide all other items if "All file types" is enabled.
+ this.hideOrShowItemsDependingOnAllowAll(this.browserModal.getRoot()
+ .find('input[type="checkbox"][data-filetypesbrowserkey="*"]:first'));
+ // And do the same whenever we click that checkbox.
+ this.browserModal.getRoot().on('change', 'input[type="checkbox"][data-filetypesbrowserkey="*"]', function(e) {
+ this.hideOrShowItemsDependingOnAllowAll($(e.currentTarget));
+ }.bind(this));
+ }
+
+ // Synchronize checked status if the file extension is present in multiple groups.
+ this.browserModal.getRoot().on('change', 'input[type="checkbox"][data-filetypesbrowserkey]', function(e) {
+ var checkbox = $(e.currentTarget);
+ var key = checkbox.attr('data-filetypesbrowserkey');
+ this.browserModal.getRoot().find('input[type="checkbox"][data-filetypesbrowserkey="' + key + '"]')
+ .prop('checked', checkbox.prop('checked'));
+ }.bind(this));
+
+ }.bind(this))
+
+ .then(function() {
+ this.browserModal.show();
+ }.bind(this));
+
+ this.browserModal.setBody(bodyContent);
+
+ }.bind(this));
+
+ // Return a resolved promise.
+ return $.when();
+ };
+
+ /**
+ * Load the browser modal body contents.
+ *
+ * @returns {Promise}
+ */
+ FileTypes.prototype.loadBrowserModalBody = function() {
+
+ var args = {
+ onlytypes: this.onlyTypes.join(),
+ allowall: this.allowAll,
+ current: this.inputField.val()
+ };
+
+ return Ajax.call([{
+ methodname: 'core_form_get_filetypes_browser_data',
+ args: args
+
+ }])[0].then(function(browserData) {
+ return Templates.render('core_form/filetypes-browser', {
+ elementid: this.elementId,
+ groups: browserData.groups
+ });
+ }.bind(this));
+ };
+
+ /**
+ * Change the checked status of the given file type (group or extension).
+ *
+ * @method toggleCheckbox
+ * @param {String} key
+ */
+ FileTypes.prototype.toggleCheckbox = function(key) {
+
+ var checkbox = this.browserModal.getRoot().find('input[type="checkbox"][data-filetypesbrowserkey="' + key + '"]:first');
+
+ checkbox.prop('checked', !checkbox.prop('checked'));
+ };
+
+ /**
+ * Update the associated input field with selected file types.
+ *
+ * @method saveBrowserModal
+ */
+ FileTypes.prototype.saveBrowserModal = function() {
+
+ // Check the "All file types" first.
+ if (this.allowAll) {
+ var allcheckbox = this.browserModal.getRoot().find('input[type="checkbox"][data-filetypesbrowserkey="*"]');
+ if (allcheckbox.length && allcheckbox.prop('checked')) {
+ this.inputField.val('*');
+ this.updateDescriptions(['*']);
+ return;
+ }
+ }
+
+ // Iterate over all checked boxes and populate the list.
+ var newvalue = [];
+
+ this.browserModal.getRoot().find('input[type="checkbox"]').each(/** @this represents the checkbox */ function() {
+ var checkbox = $(this);
+ var key = checkbox.attr('data-filetypesbrowserkey');
+
+ if (checkbox.prop('checked')) {
+ newvalue.push(key);
+ }
+ });
+
+ // Remove duplicates (e.g. file types present in multiple groups).
+ newvalue = newvalue.filter(function(x, i, a) {
+ return a.indexOf(x) == i;
+ });
+
+ this.inputField.val(newvalue.join(' '));
+ this.updateDescriptions(newvalue);
+ };
+
+ /**
+ * Describe the selected filetypes in the form when saving the browser.
+ *
+ * @param {Array} keys List of keys to describe
+ * @returns {Promise}
+ */
+ FileTypes.prototype.updateDescriptions = function(keys) {
+
+ var descriptions = [];
+
+ keys.forEach(function(key) {
+ descriptions.push({
+ description: this.browserModal.getRoot().find('[data-filetypesname="' + key + '"]:first').text().trim(),
+ extensions: this.browserModal.getRoot().find('[data-filetypesextensions="' + key + '"]:first').text().trim()
+ });
+ }.bind(this));
+
+ var templatedata = {
+ hasdescriptions: (descriptions.length > 0),
+ descriptions: descriptions
+ };
+
+ return Templates.render('core_form/filetypes-descriptions', templatedata)
+ .then(function(html) {
+ this.wrapperDescriptions.html(html);
+ }.bind(this));
+ };
+
+ /**
+ * If "All file types" is checked, all other browser items are made hidden, and vice versa.
+ *
+ * @param {jQuery} allcheckbox The "All file types" checkbox.
+ */
+ FileTypes.prototype.hideOrShowItemsDependingOnAllowAll = function(allcheckbox) {
+ var others = this.browserModal.getRoot().find('[role="treeitem"][data-filetypesbrowserkey!="*"]');
+ if (allcheckbox.prop('checked')) {
+ others.hide();
+ } else {
+ others.show();
+ }
+ };
+
+ return {
+ init: function(elementId, elementLabel, onlyTypes, allowAll) {
+ new FileTypes(elementId, elementLabel, onlyTypes, allowAll);
+ }
+ };
+});
--- /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/>.
+
+/**
+ * Provides the {@link core_form\external} class.
+ *
+ * @package core_form
+ * @category external
+ * @copyright 2017 David Mudrák <david@moodle.com>
+ * @copyright 2016 Jonathon Fowler <fowlerj@usq.edu.au>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_form;
+
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/externallib.php');
+
+/**
+ * Implements the external functions provided by the core_form subsystem.
+ *
+ * @copyright 2017 David Mudrak <david@moodle.com>
+ * @copyright 2016 Jonathon Fowler <fowlerj@usq.edu.au>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class external extends external_api {
+
+ /**
+ * Describes the input paramaters of the get_filetypes_browser_data external function.
+ *
+ * @return external_description
+ */
+ public static function get_filetypes_browser_data_parameters() {
+ return new external_function_parameters([
+ 'onlytypes' => new external_value(PARAM_RAW, 'Limit the browser to the given groups and extensions', VALUE_DEFAULT, ''),
+ 'allowall' => new external_value(PARAM_BOOL, 'Allows to select All file types, does not apply with onlytypes are set.',
+ VALUE_DEFAULT, true),
+ 'current' => new external_value(PARAM_RAW, 'Current types that should be selected.', VALUE_DEFAULT, ''),
+ ]);
+ }
+
+ /**
+ * Implements the get_filetypes_browser_data external function.
+ *
+ * @param string $onlytypes Allow selection from these file types only; for example 'web_image'.
+ * @param bool $allowall Allow to select 'All file types'. Does not apply if onlytypes is set.
+ * @param string $current Current values that should be selected.
+ * @return object
+ */
+ public static function get_filetypes_browser_data($onlytypes, $allowall, $current) {
+
+ $params = self::validate_parameters(self::get_filetypes_browser_data_parameters(),
+ compact('onlytypes', 'allowall', 'current'));
+
+ $util = new filetypes_util();
+
+ return ['groups' => $util->data_for_browser($params['onlytypes'], $params['allowall'], $params['current'])];
+ }
+
+ /**
+ * Describes the output of the get_filetypes_browser_data external function.
+ *
+ * @return external_description
+ */
+ public static function get_filetypes_browser_data_returns() {
+
+ $type = new external_single_structure([
+ 'key' => new external_value(PARAM_RAW, 'The file type identifier'),
+ 'name' => new external_value(PARAM_RAW, 'The file type name'),
+ 'selected' => new external_value(PARAM_BOOL, 'Should it be marked as selected'),
+ 'ext' => new external_value(PARAM_RAW, 'The file extension associated with the file type'),
+ ]);
+
+ $group = new external_single_structure([
+ 'key' => new external_value(PARAM_RAW, 'The file type group identifier'),
+ 'name' => new external_value(PARAM_RAW, 'The file type group name'),
+ 'selectable' => new external_value(PARAM_BOOL, 'Can it be marked as selected'),
+ 'selected' => new external_value(PARAM_BOOL, 'Should it be marked as selected'),
+ 'ext' => new external_value(PARAM_RAW, 'The list of file extensions associated with the group'),
+ 'expanded' => new external_value(PARAM_BOOL, 'Should the group start as expanded or collapsed'),
+ 'types' => new external_multiple_structure($type, 'List of file types in the group'),
+ ]);
+
+ return new external_single_structure([
+ 'groups' => new external_multiple_structure($group, 'List of file type groups'),
+ ]);
+ }
+}
--- /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/>.
+
+/**
+ * Provides the {@link core_form\filetypes_util} class.
+ *
+ * @package core_form
+ * @copyright 2017 David Mudrák <david@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_form;
+
+use core_collator;
+use core_filetypes;
+use core_text;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Utility class for handling with file types in the forms.
+ *
+ * This class is supposed to serve as a helper class for {@link MoodleQuickForm_filetypes}
+ * and {@link admin_setting_filetypes} classes.
+ *
+ * The file types can be specified in a syntax compatible with what filepicker
+ * and filemanager support via the "accepted_types" option: a list of extensions
+ * (e.g. ".doc"), mimetypes ("image/png") or groups ("audio").
+ *
+ * @copyright 2017 David Mudrak <david@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class filetypes_util {
+
+ /** @var array Cache of all file type groups for the {@link self::get_groups_info()}. */
+ protected $cachegroups = null;
+
+ /**
+ * Converts the argument into an array (list) of file types.
+ *
+ * The list can be separated by whitespace, end of lines, commas, colons and semicolons.
+ * Empty values are not returned. Values are converted to lowercase.
+ * Duplicates are removed. Glob evaluation is not supported.
+ *
+ * The return value can be used as the accepted_types option for the filepicker.
+ *
+ * @param string|array $types List of file extensions, groups or mimetypes
+ * @return array of strings
+ */
+ public function normalize_file_types($types) {
+
+ if ($types === '') {
+ return [];
+ }
+
+ // Turn string into a list.
+ if (!is_array($types)) {
+ $types = preg_split('/[\s,;:"\']+/', $types, null, PREG_SPLIT_NO_EMPTY);
+ }
+
+ // Fix whitespace and normalize the syntax a bit.
+ foreach ($types as $i => $type) {
+ $type = str_replace('*.', '.', $type);
+ $type = core_text::strtolower($type);
+ $type = trim($type);
+
+ if ($type === '*') {
+ return ['*'];
+ }
+
+ $types[$i] = $type;
+ }
+
+ // Do not make the user think that globs (like ".doc?") would work.
+ foreach ($types as $i => $type) {
+ if (strpos($type, '*') !== false or strpos($type, '?') !== false) {
+ unset($types[$i]);
+ }
+ }
+
+ foreach ($types as $i => $type) {
+ if (substr($type, 0, 1) === '.') {
+ // It looks like an extension.
+ $type = '.'.ltrim($type, '.');
+ $types[$i] = clean_param($type, PARAM_FILE);
+ } else if ($this->looks_like_mimetype($type)) {
+ // All good, it looks like a mimetype.
+ continue;
+ } else if ($this->is_filetype_group($type)) {
+ // All good, it is a known type group.
+ continue;
+ } else {
+ // We assume the user typed something like "png" so we consider
+ // it an extension.
+ $types[$i] = '.'.$type;
+ }
+ }
+
+ $types = array_filter($types, 'strlen');
+ $types = array_keys(array_flip($types));
+
+ return $types;
+ }
+
+ /**
+ * Does the given file type looks like a valid MIME type?
+ *
+ * This does not check of the MIME type is actually registered here/known.
+ *
+ * @param string $type
+ * @return bool
+ */
+ public function looks_like_mimetype($type) {
+ return (bool)preg_match('~^[-\.a-z0-9]+/[a-z0-9]+([-\.\+][a-z0-9]+)*$~', $type);
+ }
+
+ /**
+ * Is the given string a known filetype group?
+ *
+ * @param string $type
+ * @return bool|object false or the group info
+ */
+ public function is_filetype_group($type) {
+
+ $info = $this->get_groups_info();
+
+ if (isset($info[$type])) {
+ return $info[$type];
+
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Provides a list of all known file type groups and their properties.
+ *
+ * @return array
+ */
+ public function get_groups_info() {
+
+ if ($this->cachegroups !== null) {
+ return $this->cachegroups;
+ }
+
+ $groups = [];
+
+ foreach (core_filetypes::get_types() as $ext => $info) {
+ if (isset($info['groups']) && is_array($info['groups'])) {
+ foreach ($info['groups'] as $group) {
+ if (!isset($groups[$group])) {
+ $groups[$group] = (object) [
+ 'extensions' => [],
+ 'mimetypes' => [],
+ ];
+ }
+ $groups[$group]->extensions['.'.$ext] = true;
+ if (isset($info['type'])) {
+ $groups[$group]->mimetypes[$info['type']] = true;
+ }
+ }
+ }
+ }
+
+ foreach ($groups as $group => $info) {
+ $info->extensions = array_keys($info->extensions);
+ $info->mimetypes = array_keys($info->mimetypes);
+ }
+
+ $this->cachegroups = $groups;
+ return $this->cachegroups;
+ }
+
+ /**
+ * Return a human readable name of the filetype group.
+ *
+ * @param string $group
+ * @return string
+ */
+ public function get_group_description($group) {
+
+ if (get_string_manager()->string_exists('group:'.$group, 'core_mimetypes')) {
+ return get_string('group:'.$group, 'core_mimetypes');
+ } else {
+ return s($group);
+ }
+ }
+
+ /**
+ * Describe the list of file types for human user.
+ *
+ * Given the list of file types, return a list of human readable
+ * descriptive names of relevant groups, types or file formats.
+ *
+ * @param string|array $types
+ * @return object
+ */
+ public function describe_file_types($types) {
+
+ $descriptions = [];
+ $types = $this->normalize_file_types($types);
+
+ foreach ($types as $type) {
+ if ($type === '*') {
+ $desc = get_string('filetypesany', 'core_form');
+ $descriptions[$desc] = [];
+ } else if ($group = $this->is_filetype_group($type)) {
+ $desc = $this->get_group_description($type);
+ $descriptions[$desc] = $group->extensions;
+
+ } else if ($this->looks_like_mimetype($type)) {
+ $desc = get_mimetype_description($type);
+ $descriptions[$desc] = file_get_typegroup('extension', [$type]);
+
+ } else {
+ $desc = get_mimetype_description(['filename' => 'fakefile'.$type]);
+ if (isset($descriptions[$desc])) {
+ $descriptions[$desc][] = $type;
+ } else {
+ $descriptions[$desc] = [$type];
+ }
+ }
+ }
+
+ $data = [];
+
+ foreach ($descriptions as $desc => $exts) {
+ sort($exts);
+ $data[] = (object)[
+ 'description' => $desc,
+ 'extensions' => join(' ', $exts),
+ ];
+ }
+
+ core_collator::asort_objects_by_property($data, 'description', core_collator::SORT_NATURAL);
+
+ return (object)[
+ 'hasdescriptions' => !empty($data),
+ 'descriptions' => array_values($data),
+ ];
+ }
+
+ /**
+ * Prepares data for the filetypes-browser.mustache
+ *
+ * @param string|array $onlytypes Allow selection from these file types only; for example 'web_image'.
+ * @param bool $allowall Allow to select 'All file types'. Does not apply with onlytypes are set.
+ * @param string|array $current Current values that should be selected.
+ * @return object
+ */
+ public function data_for_browser($onlytypes=null, $allowall=true, $current=null) {
+
+ $groups = [];
+ $current = $this->normalize_file_types($current);
+
+ // Firstly populate the tree of extensions categorized into groups.
+
+ foreach ($this->get_groups_info() as $groupkey => $groupinfo) {
+ if (empty($groupinfo->extensions)) {
+ continue;
+ }
+
+ $group = (object) [
+ 'key' => $groupkey,
+ 'name' => $this->get_group_description($groupkey),
+ 'selectable' => true,
+ 'selected' => in_array($groupkey, $current),
+ 'ext' => implode(' ', $groupinfo->extensions),
+ 'expanded' => false,
+ ];
+
+ $types = [];
+
+ foreach ($groupinfo->extensions as $extension) {
+ if ($onlytypes && !$this->is_whitelisted($extension, $onlytypes)) {
+ $group->selectable = false;
+ $group->expanded = true;
+ $group->ext = '';
+ continue;
+ }
+
+ $desc = get_mimetype_description(['filename' => 'fakefile'.$extension]);
+
+ if ($selected = in_array($extension, $current)) {
+ $group->expanded = true;
+ }
+
+ $types[] = (object) [
+ 'key' => $extension,
+ 'name' => get_mimetype_description(['filename' => 'fakefile'.$extension]),
+ 'selected' => $selected,
+ 'ext' => $extension,
+ ];
+ }
+
+ if (empty($types)) {
+ continue;
+ }
+
+ core_collator::asort_objects_by_property($types, 'name', core_collator::SORT_NATURAL);
+
+ $group->types = array_values($types);
+ $groups[] = $group;
+ }
+
+ core_collator::asort_objects_by_property($groups, 'name', core_collator::SORT_NATURAL);
+
+ // Append all other uncategorized extensions.
+
+ $others = [];
+
+ foreach (core_filetypes::get_types() as $extension => $info) {
+ $extension = '.'.$extension;
+ if ($onlytypes && !$this->is_whitelisted($extension, $onlytypes)) {
+ continue;
+ }
+ if (!isset($info['groups']) || empty($info['groups'])) {
+ $others[] = (object) [
+ 'key' => $extension,
+ 'name' => get_mimetype_description(['filename' => 'fakefile'.$extension]),
+ 'selected' => in_array($extension, $current),
+ 'ext' => $extension,
+ ];
+ }
+ }
+
+ core_collator::asort_objects_by_property($others, 'name', core_collator::SORT_NATURAL);
+
+ if (!empty($others)) {
+ $groups[] = (object) [
+ 'key' => '',
+ 'name' => get_string('filetypesothers', 'core_form'),
+ 'selectable' => false,
+ 'selected' => false,
+ 'ext' => '',
+ 'types' => array_values($others),
+ 'expanded' => true,
+ ];
+ }
+
+ if (empty($onlytypes) and $allowall) {
+ array_unshift($groups, (object) [
+ 'key' => '*',
+ 'name' => get_string('filetypesany', 'core_form'),
+ 'selectable' => true,
+ 'selected' => in_array('*', $current),
+ 'ext' => null,
+ 'types' => [],
+ 'expanded' => false,
+ ]);
+ }
+
+ $groups = array_values($groups);
+
+ return $groups;
+ }
+
+ /**
+ * Expands the file types into the list of file extensions.
+ *
+ * The groups and mimetypes are expanded into the list of their associated file
+ * extensions. Depending on the $keepgroups and $keepmimetypes, the groups
+ * and mimetypes themselves are either kept in the list or removed.
+ *
+ * @param string|array $types
+ * @param bool $keepgroups Keep the group item in the list after expansion
+ * @param bool $keepmimetypes Keep the mimetype item in the list after expansion
+ * @return array list of extensions and eventually groups and types
+ */
+ public function expand($types, $keepgroups=false, $keepmimetypes=false) {
+
+ $expanded = [];
+
+ foreach ($this->normalize_file_types($types) as $type) {
+ if ($group = $this->is_filetype_group($type)) {
+ foreach ($group->extensions as $ext) {
+ $expanded[$ext] = true;
+ }
+ if ($keepgroups) {
+ $expanded[$type] = true;
+ }
+
+ } else if ($this->looks_like_mimetype($type)) {
+ // A mime type expands to the associated extensions.
+ foreach (file_get_typegroup('extension', [$type]) as $ext) {
+ $expanded[$ext] = true;
+ }
+ if ($keepmimetypes) {
+ $expanded[$type] = true;
+ }
+
+ } else {
+ // Single extension expands to itself.
+ $expanded[$type] = true;
+ }
+ }
+
+ return array_keys($expanded);
+ }
+
+ /**
+ * Should the given file type be considered as a part of the given whitelist.
+ *
+ * If multiple types are provided, all of them must be part of the
+ * whitelist. Empty type is part of any whitelist. Any type is part of an
+ * empty whitelist.
+ *
+ * @param string|array $types File types to be checked
+ * @param string|array $whitelist An array or string of whitelisted types
+ * @return boolean
+ */
+ public function is_whitelisted($types, $whitelist) {
+ return empty($this->get_not_whitelisted($types, $whitelist));
+ }
+
+ /**
+ * Returns all types that are not part of the give whitelist.
+ *
+ * This is similar check to the {@link self::is_whitelisted()} but this one
+ * actually returns the extra types.
+ *
+ * @param string|array $types File types to be checked
+ * @param string|array $whitelist An array or string of whitelisted types
+ * @return array Types not present in the whitelist
+ */
+ public function get_not_whitelisted($types, $whitelist) {
+
+ $whitelistedtypes = $this->expand($whitelist, true, true);
+
+ if (empty($whitelistedtypes) || $whitelistedtypes == ['*']) {
+ return [];
+ }
+
+ $giventypes = $this->normalize_file_types($types);
+
+ if (empty($giventypes)) {
+ return [];
+ }
+
+ return array_diff($giventypes, $whitelistedtypes);
+ }
+
+ /**
+ * Is the given filename of an allowed file type?
+ *
+ * Empty whitelist is interpretted as "any file type is allowed" rather
+ * than "no file can be uploaded".
+ *
+ * @param string $filename the file name
+ * @param string|array $whitelist list of allowed file extensions
+ * @return boolean True if the file type is allowed, false if not
+ */
+ public function is_allowed_file_type($filename, $whitelist) {
+
+ $allowedextensions = $this->expand($whitelist);
+
+ if (empty($allowedextensions) || $allowedextensions == ['*']) {
+ return true;
+ }
+
+ $haystack = strrev(trim(core_text::strtolower($filename)));
+
+ foreach ($allowedextensions as $extension) {
+ if (strpos($haystack, strrev($extension)) === 0) {
+ // The file name ends with the extension.
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns file types from the list that are not recognized
+ *
+ * @param string|array $types list of user-defined file types
+ * @return array A list of unknown file types.
+ */
+ public function get_unknown_file_types($types) {
+ $unknown = [];
+
+ foreach ($this->normalize_file_types($types) as $type) {
+ if ($this->is_filetype_group($type)) {
+ // The type is a group that exists.
+ continue;
+ } else if ($this->looks_like_mimetype($type)) {
+ // If there's no extension associated with that mimetype, we consider it unknown.
+ if (empty(file_get_typegroup('extension', [$type]))) {
+ $unknown[$type] = true;
+ }
+ } else {
+ $coretypes = core_filetypes::get_types();
+ $typecleaned = str_replace(".", "", $type);
+ if (empty($coretypes[$typecleaned])) {
+ // If there's no extension, it doesn't exist.
+ $unknown[$type] = true;
+ }
+ }
+ }
+
+ return array_keys($unknown);
+ }
+}
--- /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/>.
+
+/**
+ * Provides the {@link MoodleQuickForm_filetypes} class.
+ *
+ * @package core_form
+ * @copyright 2016 Jonathon Fowler <fowlerj@usq.edu.au>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use core_form\filetypes_util;
+
+defined('MOODLE_INTERNAL') || die;
+
+global $CFG;
+require_once($CFG->dirroot.'/lib/form/group.php');
+
+/**
+ * File types and type groups selection form element.
+ *
+ * @package core_form
+ * @category form
+ * @copyright 2016 Jonathon Fowler <fowlerj@usq.edu.au>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class MoodleQuickForm_filetypes extends MoodleQuickForm_group {
+
+ /** @var array Allow selection from these file types only. */
+ protected $onlytypes = [];
+
+ /** @var bool Allow selection of 'All file types' (will be stored as '*'). */
+ protected $allowall = true;
+
+ /** @var core_form\filetypes_util instance to use as a helper. */
+ protected $util = null;
+
+ /**
+ * Constructor
+ *
+ * @param string $elementname Element's name
+ * @param string $elementlabel Label(s) for an element
+ * @param array $options element options:
+ * 'onlytypes': Allow selection from these file types only; for example ['onlytypes' => ['web_image']].
+ * 'allowall': Allow to select 'All file types', defaults to true. Does not apply with onlytypes are set.
+ * @param array|string $attributes Either a typical HTML attribute string or an associative array
+ */
+ public function __construct($elementname = null, $elementlabel = null, $options = null, $attributes = null) {
+
+ parent::__construct($elementname, $elementlabel);
+ $this->_type = 'filetypes';
+
+ // Hard-frozen elements do not get the name populated automatically,
+ // which leads to PHP notice. Add it explicitly here.
+ $this->setAttributes(array('name' => $elementname));
+ $this->updateAttributes($attributes);
+
+ if (is_array($options) && $options) {
+ if (array_key_exists('onlytypes', $options) && is_array($options['onlytypes'])) {
+ $this->onlytypes = $options['onlytypes'];
+ }
+ if (!$this->onlytypes && array_key_exists('allowall', $options)) {
+ $this->allowall = (bool)$options['allowall'];
+ }
+ }
+
+ $this->util = new filetypes_util();
+ }
+
+ /**
+ * Assemble the elements of the form control.
+ */
+ public function _createElements() {
+
+ $this->_generateId();
+
+ $this->setElements([
+ $this->createFormElement('text', 'filetypes', '', [
+ 'id' => $this->getAttribute('id'),
+ ]),
+
+ $this->createFormElement('static', 'browser', null,
+ '<span data-filetypesbrowser="'.$this->getAttribute('id').'"></span>'),
+
+ $this->createFormElement('static', 'descriptions', null,
+ '<div data-filetypesdescriptions="'.$this->getAttribute('id').'"></div>')
+ ]);
+ }
+
+ /**
+ * Return the selected file types.
+ *
+ * @param array $submitted submitted values
+ * @param bool $assoc if true the retured value is associated array
+ * @return array
+ */
+ public function exportValue(&$submitted, $assoc = false) {
+
+ $value = '';
+ $filetypeselement = null;
+
+ foreach ($this->_elements as $key => $element) {
+ if ($element->_attributes['name'] === 'filetypes') {
+ $filetypeselement = $this->_elements[$key];
+ }
+ }
+
+ if ($filetypeselement) {
+ $formval = $filetypeselement->exportValue($submitted[$this->getName()], false);
+ if ($formval) {
+ $value = $this->util->normalize_file_types($formval);
+ if ($value === ['*'] && !$this->allowall) {
+ $value = [];
+ }
+ $value = implode(',', $value);
+ }
+ }
+
+ return $this->_prepareValue($value, $assoc);
+ }
+
+ /**
+ * Accepts a renderer (called shortly before the renderer's toHtml() method).
+ *
+ * @param HTML_QuickForm_Renderer $renderer An HTML_QuickForm_Renderer object
+ * @param bool $required Whether a group is required
+ * @param string $error An error message associated with a group
+ */
+ public function accept(&$renderer, $required = false, $error = null) {
+ global $PAGE;
+
+ $PAGE->requires->js_call_amd('core_form/filetypes', 'init', [
+ $this->getAttribute('id'),
+ $this->getLabel(),
+ $this->onlytypes,
+ $this->allowall,
+ ]);
+
+ if ($this->isFrozen()) {
+ // Don't render the choose button if the control is frozen.
+ foreach ($this->_elements as $key => $element) {
+ if ($element->_attributes['name'] === 'browser') {
+ unset($this->_elements[$key]);
+ }
+ }
+ }
+
+ parent::accept($renderer, $required, $error);
+ }
+
+ /**
+ * Called by HTML_QuickForm whenever form event is made on this element
+ *
+ * @param string $event Name of event
+ * @param mixed $arg event arguments
+ * @param object $caller calling object
+ * @return bool
+ */
+ public function onQuickFormEvent($event, $arg, &$caller) {
+ global $OUTPUT;
+
+ switch ($event) {
+ case 'updateValue':
+ $value = $this->_findValue($caller->_constantValues);
+ if (null === $value) {
+ if ($caller->isSubmitted()) {
+ $value = $this->_findValue($caller->_submitValues);
+ } else {
+ $value = (string)$this->_findValue($caller->_defaultValues);
+ }
+ }
+ if (!is_array($value)) {
+ $value = array('filetypes' => $value);
+ }
+ if ($value['filetypes'] !== null) {
+ $filetypes = $this->util->normalize_file_types($value['filetypes']);
+ if ($filetypes === ['*'] && !$this->allowall) {
+ $filetypes = [];
+ }
+ $value['descriptions'] = '<div data-filetypesdescriptions="'.$this->getAttribute('id').'">' .
+ $OUTPUT->render_from_template('core_form/filetypes-descriptions',
+ $this->util->describe_file_types($filetypes)).'</div>';
+ }
+ $this->setValue($value);
+ return true;
+ }
+
+ return parent::onQuickFormEvent($event, $arg, $caller);
+ }
+}
--- /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/>.
+}}
+{{!
+ @template core_form/filetypes-browser
+
+ The template is used to render the content of the UI widget allowing the
+ user to select individual file types or their groups.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * groups
+ * key
+ * name
+ * selectable
+ * expanded
+ * ext
+ * types
+ * key
+ * name
+ * selected
+ * expanded
+ * ext
+
+ Example context (json):
+ {
+ "groups": [
+ {
+ "key": "web_file",
+ "name": "Web files",
+ "selectable": true,
+ "selected": false,
+ "ext": ".css .html .xhtml .htm .js",
+ "types": [
+ {
+ "key": ".css",
+ "name": "Cascading Style-Sheet",
+ "selected": true,
+ "ext": ".css"
+ },
+ {
+ "key": ".htm",
+ "name": "HTML text",
+ "selected": false,
+ "ext": ".htm .xhtml .html"
+ }
+ ]
+ },
+ {
+ "key": "web_audio",
+ "name": "Web-safe audio files",
+ "selectable": false,
+ "selected": false,
+ "ext": null,
+ "types": [
+ {
+ "key": "audio/mp3",
+ "name": "Audio file (MP3)",
+ "selected": false,
+ "ext": ".mp3"
+ }
+ ]
+ }
+ ]
+ }
+}}
+<div data-filetypesbrowserbody="{{elementid}}" role="tree">
+{{#groups}}
+ <div data-filetypesbrowserkey="{{key}}" role="treeitem" aria-expanded="{{#expanded}}true{{/expanded}}{{^expanded}}false{{/expanded}}">
+ <label>
+ {{#selectable}}
+ <input data-filetypesbrowserkey="{{key}}" type="checkbox" {{#selected}}checked{{/selected}}>
+ <strong data-filetypesname="{{key}}">{{name}}</strong>
+ <small class="muted" data-filetypesextensions="{{key}}">
+ {{ext}}
+ </small>
+ {{/selectable}}
+ {{^selectable}}
+ <strong>{{name}}</strong>
+ {{/selectable}}
+ </label>
+ {{#types.0}}
+ {{! The tree provides its own accessibility controls, these two links are here for mouse users.
+ For that reason, we hide them from assistive technologies. }}
+ <small aria-hidden="true" data-filetypesbrowserfeature="hideifexpanded" class="pull-right float-right"><a href="#">{{#str}}expand{{/str}}</a></small>
+ <small aria-hidden="true" data-filetypesbrowserfeature="hideifcollapsed" class="pull-right float-right"><a href="#">{{#str}}collapse{{/str}}</a></small>
+ {{/types.0}}
+ <ul class="unstyled list-unstyled" role="group">
+ {{#types}}
+ <li data-filetypesbrowserkey="{{key}}" style="margin-left: 2em" role="treeitem">
+ <label>
+ <input data-filetypesbrowserkey="{{key}}" type="checkbox" {{#selected}}checked{{/selected}}>
+ <span data-filetypesname="{{key}}">{{name}}</span>
+ <small class="muted" data-filetypesextensions="{{key}}">
+ {{ext}}
+ </small>
+ </label>
+ </li>
+ {{/types}}
+ </ul>
+ <hr style="clear:both">
+ </div>
+{{/groups}}
+</div>
--- /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/>.
+}}
+{{!
+ @template core_form/filetypes-descriptions
+
+ Template to describe chosen file types.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * hasdescriptions (bool)
+ * descriptions (array)
+ * description (string)
+ * extensions (string)
+
+ Example context (json):
+ {
+ "hasdescriptions": true,
+ "descriptions": [
+ {
+ "description": "Image (JPEG)",
+ "extensions": ".jpeg .jpe .jpg"
+ },
+ {
+ "description": "Image (GIF)",
+ "extensions": ".gif"
+ }
+ ]
+ }
+}}
+<div class="form-filetypes-descriptions">
+{{#hasdescriptions}}
+ <ul class="list-unstyled unstyled">
+ {{#descriptions}}
+ <li>{{description}} <small class="text-muted muted">{{extensions}}</small></li>
+ {{/descriptions}}
+ </ul>
+{{/hasdescriptions}}
+{{^hasdescriptions}}
+ <p>{{#str}}noselection, form{{/str}}</p>
+{{/hasdescriptions}}
+</div>
--- /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/>.
+}}
+{{!
+ @template core_form/filetypes-trigger
+
+ Renders the button to open the file types browser element.
+
+ This template uses <input> element of the type "button" as that one seems
+ to be well styled by default (especially when compared to alternatives like
+ <button> that are not aligned nicely with the input field.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * filetypeswidget - set to "browsertrigger" to mark the actual clickable element
+
+ Context variables required for this template:
+ * none
+
+ Example context (json):
+ {
+ }
+}}
+<input type="button" class="btn btn-secondary" data-filetypeswidget="browsertrigger" value="{{#str}} choose, core {{/str}}" />
--- /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/>.
+
+/**
+ * Provides the {@link core_form\external_testcase} class.
+ *
+ * @package core_form
+ * @category test
+ * @copyright 2017 David Mudrák <david@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_form;
+
+use advanced_testcase;
+use external_api;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+/**
+ * Test cases for the {@link core_form\external} class.
+ *
+ * @copyright 2017 David Mudrak <david@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class external_testcase extends advanced_testcase {
+
+ /**
+ * Test the core_form_get_filetypes_browser_data external function
+ */
+ public function test_get_filetypes_browser_data() {
+
+ $data = external::get_filetypes_browser_data('', true, '');
+ $data = external_api::clean_returnvalue(external::get_filetypes_browser_data_returns(), $data);
+ $data = json_decode(json_encode($data));
+
+ // The actual data are tested in filetypes_util_test.php, here we just
+ // make sure that the external function wrapper seems to work.
+ $this->assertInternalType('object', $data);
+ $this->assertInternalType('array', $data->groups);
+ }
+}
--- /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/>.
+
+/**
+ * Provides the {@link core_form\filetypes_util_testcase} class.
+ *
+ * @package core_form
+ * @category test
+ * @copyright 2017 David Mudrák <david@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_form;
+
+use advanced_testcase;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+/**
+ * Test cases for the {@link core_form\filetypes_util} class.
+ *
+ * @copyright 2017 David Mudrak <david@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class filetypes_util_testcase extends advanced_testcase {
+
+ /**
+ * Test normalizing list of extensions.
+ */
+ public function test_normalize_file_types() {
+
+ $this->resetAfterTest(true);
+ $util = new filetypes_util();
+
+ $this->assertSame(['.odt'], $util->normalize_file_types('.odt'));
+ $this->assertSame(['.odt'], $util->normalize_file_types('odt'));
+ $this->assertSame(['.odt'], $util->normalize_file_types('.ODT'));
+ $this->assertSame(['.doc', '.jpg', '.mp3'], $util->normalize_file_types('doc, jpg, mp3'));
+ $this->assertSame(['.doc', '.jpg', '.mp3'], $util->normalize_file_types(['.doc', '.jpg', '.mp3']));
+ $this->assertSame(['.doc', '.jpg', '.mp3'], $util->normalize_file_types('doc, *.jpg, mp3'));
+ $this->assertSame(['.doc', '.jpg', '.mp3'], $util->normalize_file_types(['doc ', ' JPG ', '.mp3']));
+ $this->assertSame(['.rtf', '.pdf', '.docx'],
+ $util->normalize_file_types("RTF,.pdf\n...DocX,,,;\rPDF\trtf ...Rtf"));
+ $this->assertSame(['.tgz', '.tar.gz'], $util->normalize_file_types('tgz,TAR.GZ tar.gz .tar.gz tgz TGZ'));
+ $this->assertSame(['.notebook'], $util->normalize_file_types('"Notebook":notebook;NOTEBOOK;,\'NoTeBook\''));
+ $this->assertSame([], $util->normalize_file_types(''));
+ $this->assertSame([], $util->normalize_file_types([]));
+ $this->assertSame(['.0'], $util->normalize_file_types(0));
+ $this->assertSame(['.0'], $util->normalize_file_types('0'));
+ $this->assertSame(['.odt'], $util->normalize_file_types('*.odt'));
+ $this->assertSame([], $util->normalize_file_types('.'));
+ $this->assertSame(['.foo'], $util->normalize_file_types('. foo'));
+ $this->assertSame(['*'], $util->normalize_file_types('*'));
+ $this->assertSame([], $util->normalize_file_types('*~'));
+ $this->assertSame(['.pdf', '.ps'], $util->normalize_file_types('pdf *.ps foo* *bar .r??'));
+ $this->assertSame(['*'], $util->normalize_file_types('pdf *.ps foo* * *bar .r??'));
+ }
+
+ /**
+ * Test MIME type formal recognition.
+ */
+ public function test_looks_like_mimetype() {
+
+ $this->resetAfterTest(true);
+ $util = new filetypes_util();
+
+ $this->assertTrue($util->looks_like_mimetype('type/subtype'));
+ $this->assertTrue($util->looks_like_mimetype('type/x-subtype'));
+ $this->assertTrue($util->looks_like_mimetype('type/x-subtype+xml'));
+ $this->assertTrue($util->looks_like_mimetype('type/vnd.subtype.xml'));
+ $this->assertTrue($util->looks_like_mimetype('type/vnd.subtype+xml'));
+
+ $this->assertFalse($util->looks_like_mimetype('.gif'));
+ $this->assertFalse($util->looks_like_mimetype('audio'));
+ $this->assertFalse($util->looks_like_mimetype('foo/bar/baz'));
+ }
+
+ /**
+ * Test getting/checking group.
+ */
+ public function test_is_filetype_group() {
+
+ $this->resetAfterTest(true);
+ $util = new filetypes_util();
+
+ $audio = $util->is_filetype_group('audio');
+ $this->assertNotFalse($audio);
+ $this->assertInternalType('array', $audio->extensions);
+ $this->assertInternalType('array', $audio->mimetypes);
+
+ $this->assertFalse($util->is_filetype_group('.gif'));
+ $this->assertFalse($util->is_filetype_group('somethingveryunlikelytoeverexist'));
+ }
+
+
+ /**
+ * Test describing list of extensions.
+ */
+ public function test_describe_file_types() {
+
+ $this->resetAfterTest(true);
+ $util = new filetypes_util();
+
+ force_current_language('en');
+
+ // Check that it is able to describe individual file extensions.
+ $desc = $util->describe_file_types('jpg .jpeg *.jpe PNG;.gif, mudrd8mz');
+ $this->assertTrue($desc->hasdescriptions);
+
+ $desc = $desc->descriptions;
+ $this->assertEquals(4, count($desc));
+
+ $this->assertEquals('File', $desc[0]->description);
+ $this->assertEquals('.mudrd8mz', $desc[0]->extensions);
+
+ $this->assertEquals('Image (JPEG)', $desc[2]->description);
+ $this->assertContains('.jpg', $desc[2]->extensions);
+ $this->assertContains('.jpeg', $desc[2]->extensions);
+ $this->assertContains('.jpe', $desc[2]->extensions);
+
+ // Check that it can describe groups and mimetypes too.
+ $desc = $util->describe_file_types('audio text/plain');
+ $this->assertTrue($desc->hasdescriptions);
+
+ $desc = $desc->descriptions;
+ $this->assertEquals(2, count($desc));
+
+ $this->assertEquals('Audio files', $desc[0]->description);
+ $this->assertContains('.mp3', $desc[0]->extensions);
+ $this->assertContains('.wav', $desc[0]->extensions);
+ $this->assertContains('.ogg', $desc[0]->extensions);
+
+ $this->assertEquals('Text file', $desc[1]->description);
+ $this->assertContains('.txt', $desc[1]->extensions);
+
+ // Empty.
+ $desc = $util->describe_file_types('');
+ $this->assertFalse($desc->hasdescriptions);
+ $this->assertEmpty($desc->descriptions);
+
+ // Any.
+ $desc = $util->describe_file_types('*');
+ $this->assertTrue($desc->hasdescriptions);
+ $this->assertNotEmpty($desc->descriptions[0]->description);
+ $this->assertEmpty($desc->descriptions[0]->extensions);
+
+ // Unknown mimetype.
+ $desc = $util->describe_file_types('application/x-something-really-unlikely-ever-exist');
+ $this->assertTrue($desc->hasdescriptions);
+ $this->assertEquals('application/x-something-really-unlikely-ever-exist', $desc->descriptions[0]->description);
+ $this->assertEmpty($desc->descriptions[0]->extensions);
+ }
+
+ /**
+ * Test expanding mime types into extensions.
+ */
+ public function test_expand() {
+
+ $this->resetAfterTest(true);
+ $util = new filetypes_util();
+
+ $this->assertSame([], $util->expand(''));
+
+ $expanded = $util->expand('document .cdr text/plain');
+ $this->assertNotContains('document', $expanded);
+ $this->assertNotContains('text/plain', $expanded);
+ $this->assertContains('.doc', $expanded);
+ $this->assertContains('.odt', $expanded);
+ $this->assertContains('.txt', $expanded);
+ $this->assertContains('.cdr', $expanded);
+
+ $expanded = $util->expand('document .cdr text/plain', true, false);
+ $this->assertContains('document', $expanded);
+ $this->assertNotContains('text/plain', $expanded);
+ $this->assertContains('.doc', $expanded);
+ $this->assertContains('.odt', $expanded);
+ $this->assertContains('.txt', $expanded);
+ $this->assertContains('.cdr', $expanded);
+
+ $expanded = $util->expand('document .cdr text/plain', false, true);
+ $this->assertNotContains('document', $expanded);
+ $this->assertContains('text/plain', $expanded);
+ $this->assertContains('.doc', $expanded);
+ $this->assertContains('.odt', $expanded);
+ $this->assertContains('.txt', $expanded);
+ $this->assertContains('.cdr', $expanded);
+
+ $this->assertSame([], $util->expand('foo/bar', true, false));
+ $this->assertSame(['foo/bar'], $util->expand('foo/bar', true, true));
+ }
+
+ /**
+ * Test checking that a type is among others.
+ */
+ public function test_is_whitelisted() {
+
+ $this->resetAfterTest(true);
+ $util = new filetypes_util();
+
+ // These should be intuitively true.
+ $this->assertTrue($util->is_whitelisted('txt', 'text/plain'));
+ $this->assertTrue($util->is_whitelisted('txt', 'doc txt rtf'));
+ $this->assertTrue($util->is_whitelisted('.txt', '.doc;.txt;.rtf'));
+ $this->assertTrue($util->is_whitelisted('audio', 'text/plain audio video'));
+ $this->assertTrue($util->is_whitelisted('text/plain', 'text/plain audio video'));
+ $this->assertTrue($util->is_whitelisted('jpg jpe jpeg', 'image/jpeg'));
+ $this->assertTrue($util->is_whitelisted(['jpg', 'jpe', '.png'], 'image'));
+
+ // These should be intuitively false.
+ $this->assertFalse($util->is_whitelisted('.gif', 'text/plain'));
+
+ // Not all text/plain formats are in the document group.
+ $this->assertFalse($util->is_whitelisted('text/plain', 'document'));
+
+ // Not all documents (and also the group itself) is not a plain text.
+ $this->assertFalse($util->is_whitelisted('document', 'text/plain'));
+
+ // This may look wrong at the first sight as you might expect that the
+ // mimetype should simply map to an extension ...
+ $this->assertFalse($util->is_whitelisted('image/jpeg', '.jpg'));
+
+ // But it is principally same situation as this (there is no 1:1 mapping).
+ $this->assertFalse($util->is_whitelisted('.c', '.txt'));
+ $this->assertTrue($util->is_whitelisted('.txt .c', 'text/plain'));
+ $this->assertFalse($util->is_whitelisted('text/plain', '.c'));
+
+ // Any type is included if the filter is empty.
+ $this->assertTrue($util->is_whitelisted('txt', ''));
+ $this->assertTrue($util->is_whitelisted('txt', '*'));
+
+ // Empty value is part of any whitelist.
+ $this->assertTrue($util->is_whitelisted('', '.txt'));
+ }
+
+ /**
+ * Test getting types not present in a whitelist.
+ */
+ public function test_get_not_whitelisted() {
+
+ $this->resetAfterTest(true);
+ $util = new filetypes_util();
+
+ $this->assertEmpty($util->get_not_whitelisted('txt', 'text/plain'));
+ $this->assertEmpty($util->get_not_whitelisted('txt', '.doc .txt .rtf'));
+ $this->assertEmpty($util->get_not_whitelisted('txt', 'text/plain'));
+ $this->assertEmpty($util->get_not_whitelisted(['jpg', 'jpe', 'jpeg'], 'image/jpeg'));
+ $this->assertEmpty($util->get_not_whitelisted('', 'foo/bar'));
+ $this->assertEmpty($util->get_not_whitelisted('.foobar', ''));
+ $this->assertEmpty($util->get_not_whitelisted('.foobar', '*'));
+
+ // Returned list is normalized so extensions have the dot added.
+ $this->assertContains('.exe', $util->get_not_whitelisted('exe', '.c .h'));
+
+ // If this looks wrong to you, see {@link test_is_whitelisted()} for more details on this behaviour.
+ $this->assertContains('image/jpeg', $util->get_not_whitelisted('image/jpeg', '.jpg .jpeg'));
+ }
+
+ /**
+ * Test populating the tree for the browser.
+ */
+ public function test_data_for_browser() {
+
+ $this->resetAfterTest(true);
+ $util = new filetypes_util();
+
+ $data = $util->data_for_browser();
+ $this->assertContainsOnly('object', $data);
+ foreach ($data as $group) {
+ $this->assertObjectHasAttribute('key', $group);
+ $this->assertObjectHasAttribute('types', $group);
+ if ($group->key !== '') {
+ $this->assertTrue($group->selectable);
+ }
+ }
+
+ // All these three files are in both "image" and also "web_image"
+ // groups. We display both groups.
+ $data = $util->data_for_browser('jpg png gif', true, '.gif');
+ $this->assertEquals(2, count($data));
+ $this->assertTrue($data[0]->key !== $data[1]->key);
+ foreach ($data as $group) {
+ $this->assertTrue(($group->key === 'image' || $group->key === 'web_image'));
+ $this->assertEquals(3, count($group->types));
+ $this->assertFalse($group->selectable);
+ foreach ($group->types as $ext) {
+ if ($ext->key === '.gif') {
+ $this->assertTrue($ext->selected);
+ } else {
+ $this->assertFalse($ext->selected);
+ }
+ }
+ }
+
+ // There is a group web_image which is a subset of the group image. The
+ // file extensions that fall into both groups will be displayed twice.
+ $data = $util->data_for_browser('web_image');
+ foreach ($data as $group) {
+ $this->assertTrue(($group->key === 'image' || $group->key === 'web_image'));
+ }
+
+ // Check that "All file types" are displayed first.
+ $data = $util->data_for_browser();
+ $group = array_shift($data);
+ $this->assertEquals('*', $group->key);
+
+ // Check that "All file types" is not displayed if should not.
+ $data = $util->data_for_browser(null, false);
+ $group = array_shift($data);
+ $this->assertNotEquals('*', $group->key);
+
+ // Groups with an extension selected start expanded. The "Other files"
+ // starts expanded. The rest start collapsed.
+ $data = $util->data_for_browser(null, false, '.png');
+ foreach ($data as $group) {
+ if ($group->key === 'document') {
+ $this->assertfalse($group->expanded);
+ } else if ($group->key === '') {
+ $this->assertTrue($group->expanded);
+ }
+ foreach ($group->types as $ext) {
+ foreach ($group->types as $ext) {
+ if ($ext->key === '.png') {
+ $this->assertTrue($ext->selected);
+ $this->assertTrue($group->expanded);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Data provider for testing test_is_allowed_file_type.
+ *
+ * @return array
+ */
+ public function is_allowed_file_type_provider() {
+ return [
+ 'Filetype not in extension whitelist' => [
+ 'filename' => 'test.xml',
+ 'whitelist' => '.png .jpg',
+ 'expected' => false
+ ],
+ 'Filetype not in mimetype whitelist' => [
+ 'filename' => 'test.xml',
+ 'whitelist' => 'image/png',
+ 'expected' => false
+ ],
+ 'Filetype not in group whitelist' => [
+ 'filename' => 'test.xml',
+ 'whitelist' => 'web_file',
+ 'expected' => false
+ ],
+ 'Filetype in whitelist as extension' => [
+ 'filename' => 'test.xml',
+ 'whitelist' => 'xml',
+ 'expected' => true
+ ],
+ 'Empty whitelist should allow all' => [
+ 'filename' => 'test.xml',
+ 'whitelist' => '',
+ 'expected' => true
+ ],
+ 'Filetype in whitelist but later on' => [
+ 'filename' => 'test.xml',
+ 'whitelist' => 'gif;jpeg,image/png xml xlsx',
+ 'expected' => true
+ ],
+ 'Filetype in whitelist as mimetype' => [
+ 'filename' => 'test.xml',
+ 'whitelist' => 'image/png application/xml',
+ 'expected' => true
+ ],
+ 'Filetype in whitelist as group' => [
+ 'filename' => 'test.html',
+ 'whitelist' => 'video,web_file',
+ 'expected' => true
+ ],
+ ];
+ }
+
+ /**
+ * Test is_allowed_file_type().
+ * @dataProvider is_allowed_file_type_provider
+ * @param string $filename The filename to check
+ * @param string $whitelist The space , or ; separated list of types supported
+ * @param boolean $expected The expected result. True if the file is allowed, false if not.
+ */
+ public function test_is_allowed_file_type($filename, $whitelist, $expected) {
+ $util = new filetypes_util();
+ $this->assertSame($expected, $util->is_allowed_file_type($filename, $whitelist));
+ }
+
+ /**
+ * Data provider for testing test_get_unknown_file_types.
+ *
+ * @return array
+ */
+ public function get_unknown_file_types_provider() {
+ return [
+ 'Unknown extension' => [
+ 'filetypes' => '.rat',
+ 'expected' => ['.rat']
+ ],
+ 'Multiple unknown extensions' => [
+ 'filetypes' => '.ricefield .rat',
+ 'expected' => ['.ricefield', '.rat']
+ ],
+ 'Existant extension' => [
+ 'filetypes' => '.xml',
+ 'expected' => []
+ ],
+ 'Existant group' => [
+ 'filetypes' => 'web_file',
+ 'expected' => []
+ ],
+ 'Nonexistant mimetypes' => [
+ 'filetypes' => 'ricefield/rat',
+ 'expected' => ['ricefield/rat']
+ ],
+ 'Existant mimetype' => [
+ 'filetypes' => 'application/xml',
+ 'expected' => []
+ ],
+ 'Multiple unknown mimetypes' => [
+ 'filetypes' => 'ricefield/rat cam/ball',
+ 'expected' => ['ricefield/rat', 'cam/ball']
+ ],
+ 'Strange characters in unknown extension/group' => [
+ 'filetypes' => '©ç√√ß∂å√©åß©√',
+ 'expected' => ['.©ç√√ß∂å√©åß©√']
+ ],
+ 'Some existant some not' => [
+ 'filetypes' => '.txt application/xml web_file ©ç√√ß∂å√©åß©√ .png ricefield/rat document',
+ 'expected' => ['.©ç√√ß∂å√©åß©√', 'ricefield/rat']
+ ],
+ ];
+ }
+
+ /**
+ * Test get_unknown_file_types().
+ * @dataProvider get_unknown_file_types_provider
+ * @param string $filetypes The filetypes to check
+ * @param array $expected The expected result. The list of non existant file types.
+ */
+ public function test_get_unknown_file_types($filetypes, $expected) {
+ $util = new filetypes_util();
+ $this->assertSame($expected, $util->get_unknown_file_types($filetypes));
+ }
+}
MoodleQuickForm::registerElementType('editor', "$CFG->libdir/form/editor.php", 'MoodleQuickForm_editor');
MoodleQuickForm::registerElementType('filemanager', "$CFG->libdir/form/filemanager.php", 'MoodleQuickForm_filemanager');
MoodleQuickForm::registerElementType('filepicker', "$CFG->libdir/form/filepicker.php", 'MoodleQuickForm_filepicker');
+MoodleQuickForm::registerElementType('filetypes', "$CFG->libdir/form/filetypes.php", 'MoodleQuickForm_filetypes');
MoodleQuickForm::registerElementType('grading', "$CFG->libdir/form/grading.php", 'MoodleQuickForm_grading');
MoodleQuickForm::registerElementType('group', "$CFG->libdir/form/group.php", 'MoodleQuickForm_group');
MoodleQuickForm::registerElementType('header', "$CFG->libdir/form/header.php", 'MoodleQuickForm_header');
$context->cookieshelpiconformatted = $this->help_icon('cookiesenabled');
$context->errorformatted = $this->error_text($context->error);
- return $this->render_from_template('core/login', $context);
+ return $this->render_from_template('core/loginform', $context);
}
/**
if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
return $this->consume(1);
}
- $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u');
+ $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
if ($this->strlen($sUnicode) < 6) {
//Consume whitespace after incomplete unicode escape
if (preg_match('/\\s/isSu', $this->peek())) {
}
}
- private function consumeExpression($mExpression) {
+ private function consumeExpression($mExpression, $iMaxLength = null) {
$aMatches = null;
- if (preg_match($mExpression, $this->inputLeft(), $aMatches, PREG_OFFSET_CAPTURE) === 1) {
+ $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
+ if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
return $this->consume($aMatches[0][0]);
}
throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
PHP CSS Parser
--------------
-Import git ref: 50a802f562e71236140e2a8903b097c28d6101de
+Import git ref: c3b01ef0a85824e86fd86a74a8154d8d5c34b0ff
(master)
Downloaded from: https://github.com/sabberworm/PHP-CSS-Parser
// If you enable db debugging and exception is thrown, the print footer prints a lot of rubbish
$DB->set_debug(0);
}
- echo $OUTPUT->fatal_error($info->message, $info->moreinfourl, $info->link, $info->backtrace, $info->debuginfo,
+ if (AJAX_SCRIPT) {
+ // If we are in an AJAX script we don't want to use PREFERRED_RENDERER_TARGET.
+ // Because we know we will want to use ajax format.
+ $renderer = $PAGE->get_renderer('core', null, 'ajax');
+ } else {
+ $renderer = $OUTPUT;
+ }
+ echo $renderer->fatal_error($info->message, $info->moreinfourl, $info->link, $info->backtrace, $info->debuginfo,
$info->errorcode);
} catch (Exception $e) {
$out_ex = $e;
// (That is, the Moodle server uses http, with an external box translating everything to https).
if (empty($CFG->sslproxy)) {
if ($rurl['scheme'] === 'http' and $wwwroot['scheme'] === 'https') {
- print_error('sslonlyaccess', 'error');
+ if (defined('REQUIRE_CORRECT_ACCESS') && REQUIRE_CORRECT_ACCESS) {
+ print_error('sslonlyaccess', 'error');
+ } else {
+ redirect($CFG->wwwroot, get_string('wwwrootmismatch', 'error', $CFG->wwwroot), 3);
+ }
}
} else {
if ($wwwroot['scheme'] !== 'https') {
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
- @template core/login
+ @template core/loginform
Moodle template for the login page.
$this->setAdminUser();
load_all_capabilities();
- $this->assertNotEmpty($ACCESSLIB_PRIVATE->rolepermissions);
- $this->assertNotEmpty($ACCESSLIB_PRIVATE->rolepermissions);
$this->assertNotEmpty($ACCESSLIB_PRIVATE->accessdatabyuser);
accesslib_clear_all_caches_for_unit_testing();
- $this->assertEmpty($ACCESSLIB_PRIVATE->rolepermissions);
- $this->assertEmpty($ACCESSLIB_PRIVATE->rolepermissions);
$this->assertEmpty($ACCESSLIB_PRIVATE->dirtycontexts);
$this->assertEmpty($ACCESSLIB_PRIVATE->accessdatabyuser);
}
$this->assertTrue(is_array($access));
$this->assertTrue(is_array($access['ra']));
- $this->assertTrue(is_array($access['rdef']));
- $this->assertTrue(isset($access['rdef_count']));
- $this->assertTrue(is_array($access['loaded']));
+ $this->assertFalse(isset($access['rdef']));
+ $this->assertFalse(isset($access['rdef_count']));
+ $this->assertFalse(isset($access['loaded']));
$this->assertTrue(isset($access['time']));
$this->assertTrue(is_array($access['rsw']));
}
--- /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/>.
+
+/**
+ * Unit tests for lib/classes/output/external.php
+ * @author Guy Thomas <gthomas@moodlerooms.com>
+ * @copyright Copyright (c) 2017 Blackboard Inc.
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\output\external;
+
+require_once(__DIR__.'/../../lib/externallib.php');
+require_once(__DIR__.'/../../lib/mustache/src/Mustache/Tokenizer.php');
+require_once(__DIR__.'/../../lib/mustache/src/Mustache/Parser.php');
+
+/**
+ * Class core_output_external_testcase - test \core\output\external class.
+ * @package core
+ * @author Guy Thomas <gthomas@moodlerooms.com>
+ * @copyright Copyright (c) 2017 Blackboard Inc.
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_output_external_testcase extends base_testcase {
+
+ /**
+ * Ensure that stripping comments from templates does not mutilate the template body.
+ */
+ public function test_strip_template_comments() {
+
+ $templatebody = <<<'TBD'
+ <h1>{{# str }} pluginname, mod_lemmings {{/ str }}</h1>
+ <div>{{test}}</div>
+ <div>{{{unescapedtest}}}</div>
+ {{#lemmings}}
+ <div>
+ <h2>{{name}}</h2>
+ {{> mod_lemmings/lemmingprofile }}
+ {{# pix }} t/edit, core, Edit Lemming {{/ pix }}
+ </div>
+ {{/lemmings}}
+ {{^lemmings}}Sorry, no lemmings today{{/lemmings}}
+ <div id="{{ uniqid }}-tab-container">
+ {{# tabheader }}
+ <ul role="tablist" class="nav nav-tabs">
+ {{# iconlist }}
+ {{# icons }}
+ {{> core/pix_icon }}
+ {{/ icons }}
+ {{/ iconlist }}
+ </ul>
+ {{/ tabheader }}
+ {{# tabbody }}
+ <div class="tab-content">
+ {{# tabcontent }}
+ {{# tabs }}
+ {{> core/notification_info}}
+ {{/ tabs }}
+ {{/ tabcontent }}
+ </div>
+ {{/ tabbody }}
+ </div>
+ {{#js}}
+ require(['jquery','core/tabs'], function($, tabs) {
+
+ var container = $("#{{ uniqid }}-tab-container");
+ tabs.create(container);
+ });
+ {{/js}}
+TBD;
+ $templatewithcomment = <<<TBC
+ {{!
+ 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/>.
+ }}
+ {{!
+ @template mod_lemmings/lemmings
+
+ Lemmings template.
+
+ The purpose of this template is to render a lot of lemmings.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * attributes Array of name / value pairs.
+
+ Example context (json):
+ {
+ "lemmings": [
+ { "name": "Lemmy Winks", "age" : 1, "size" : "big" },
+ { "name": "Rocky", "age" : 2, "size" : "small" }
+ ]
+ }
+
+ }}
+ $templatebody
+ {{!
+ Here's some more comment text
+ Note, there is no need to test bracketed variables inside comments as gherkin does not support that!
+ See this issue: https://github.com/mustache/spec/issues/8
+ }}
+TBC;
+
+ // Ensure that the template when stripped of comments just includes the body.
+ $stripped = phpunit_util::call_internal_method(null, 'strip_template_comments',
+ [$templatewithcomment], 'core\output\external');
+ $this->assertEquals(trim($templatebody), trim($stripped));
+
+ $tokenizer = new Mustache_Tokenizer();
+ $tokens = $tokenizer->scan($templatebody);
+ $parser = new Mustache_Parser();
+ $tree = $parser->parse($tokens);
+ $this->assertNotEmpty($tree);
+ }
+}
$expectation = isset($tests['is_web_crawler']) ? $tests['is_web_crawler'] : false;
$this->assertSame($expectation, core_useragent::is_web_crawler());
}
-
- /**
- * Regression tests for the deprecated is_web_crawler() function
- */
- public function test_deprecated_is_web_crawler() {
-
- $browsers = array(
- 'Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))',
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:18.0) Gecko/18.0 Firefox/18.0',
- 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412 (KHTML, like Gecko) Safari/412',
- 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10',
- 'Opera/9.0 (Windows NT 5.1; U; en)',
- 'Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17 –Nexus',
- 'Mozilla/5.0 (iPad; U; CPU OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5',
- );
- $crawlers = array(
- // Google.
- 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
- 'Googlebot/2.1 (+http://www.googlebot.com/bot.html)',
- 'Googlebot-Image/1.0',
- // Yahoo.
- 'Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)',
- // Bing.
- 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)',
- 'Mozilla/5.0 (compatible; bingbot/2.0 +http://www.bing.com/bingbot.htm)',
- // MSN.
- 'msnbot/2.1',
- // Yandex.
- 'Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)',
- 'Mozilla/5.0 (compatible; YandexImages/3.0; +http://yandex.com/bots)',
- // AltaVista.
- 'AltaVista V2.0B crawler@evreka.com',
- // ZoomSpider.
- 'ZoomSpider - wrensoft.com [ZSEBOT]',
- // Baidu.
- 'Baiduspider+(+http://www.baidu.com/search/spider_jp.html)',
- 'Baiduspider+(+http://www.baidu.com/search/spider.htm)',
- 'BaiDuSpider',
- // Ask.com.
- 'User-Agent: Mozilla/2.0 (compatible; Ask Jeeves/Teoma)',
- );
-
- foreach ($browsers as $agent) {
- core_useragent::instance(true, $agent);
- $this->assertSame($agent, core_useragent::get_user_agent_string());
- $this->assertFalse(is_web_crawler());
- $this->assertDebuggingCalled('is_web_crawler() has been deprecated, please use core_useragent::is_web_crawler() instead.',
- DEBUG_DEVELOPER);
- }
- foreach ($crawlers as $agent) {
- core_useragent::instance(true, $agent);
- $this->assertSame($agent, core_useragent::get_user_agent_string());
- $this->assertTrue(is_web_crawler(), "$agent should be considered a search engine");
- $this->assertDebuggingCalled('is_web_crawler() has been deprecated, please use core_useragent::is_web_crawler() instead.',
- DEBUG_DEVELOPER);
- }
- }
}