media/player/videojs/amd/src/video-lazy.js
media/player/videojs/amd/src/Youtube-lazy.js
media/player/videojs/videojs/
+media/player/videojs/amd/src/videojs-flash-lazy.js
+media/player/videojs/videojs/video-js.swf
mod/assign/feedback/editpdf/fpdi/
repository/s3/S3.php
theme/boost/scss/bootstrap/
media/player/videojs/amd/src/video-lazy.js
media/player/videojs/amd/src/Youtube-lazy.js
media/player/videojs/videojs/
+media/player/videojs/amd/src/videojs-flash-lazy.js
+media/player/videojs/videojs/video-js.swf
mod/assign/feedback/editpdf/fpdi/
repository/s3/S3.php
theme/boost/scss/bootstrap/
// Check if there are events 1 API handlers.
$eventshandlers = $DB->get_records_sql('SELECT DISTINCT component FROM {events_handlers}');
$themedesignermode = !empty($CFG->themedesignermode);
+$mobileconfigured = !empty($CFG->enablemobilewebservice);
// Check if a directory with development libraries exists.
if (empty($CFG->disabledevlibdirscheck) && (is_dir($CFG->dirroot.'/vendor') || is_dir($CFG->dirroot.'/node_modules'))) {
echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
$maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
- $registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir);
+ $registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir,
+ $mobileconfigured);
* @param array $eventshandlers Events 1 API handlers.
* @param bool $themedesignermode Warn about the theme designer mode.
* @param bool $devlibdir Warn about development libs directory presence.
+ * @param bool $mobileconfigured Whether the mobile web services have been enabled
*
* @return string HTML to output.
*/
public function admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed,
$cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
$buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0,
- $themedesignermode = false, $devlibdir = false) {
+ $themedesignermode = false, $devlibdir = false, $mobileconfigured = false) {
global $CFG;
$output = '';
$output .= $this->cache_warnings($cachewarnings);
$output .= $this->events_handlers($eventshandlers);
$output .= $this->registration_warning($registered);
+ $output .= $this->mobile_configuration_warning($mobileconfigured);
//////////////////////////////////////////////////////////////////////////////////////////////////
//// IT IS ILLEGAL AND A VIOLATION OF THE GPL TO HIDE, REMOVE OR MODIFY THIS COPYRIGHT NOTICE ///
return $this->registration_warning(\core\hub\registration::is_registered());
}
+ /**
+ * Display a warning about the Mobile Web Services being disabled.
+ *
+ * @param boolean $mobileconfigured true if mobile web services are enabled
+ * @return string HTML to output.
+ */
+ protected function mobile_configuration_warning($mobileconfigured) {
+ $output = '';
+ if (!$mobileconfigured) {
+ $settingslink = new moodle_url('/admin/settings.php', ['section' => 'mobilesettings']);
+ $configurebutton = $this->single_button($settingslink, get_string('enablemobilewebservice', 'admin'));
+ $output .= $this->warning(get_string('mobilenotconfiguredwarning', 'admin') . ' ' . $configurebutton);
+ }
+
+ return $output;
+ }
+
/**
* Helper method to render the information about the available Moodle update
*
// Disable web interface evaluation and get predictions.
$settings->add(new admin_setting_configcheckbox('analytics/onlycli', new lang_string('onlycli', 'analytics'),
new lang_string('onlycliinfo', 'analytics'), 1));
+
+ // Training and prediction time limit per model.
+ $settings->add(new admin_setting_configduration('analytics/modeltimelimit', new lang_string('modeltimelimit', 'analytics'),
+ new lang_string('modeltimelimitinfo', 'analytics'), 20 * MINSECS));
+
}
}
--- /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/>.
+
+/**
+ * AMD module for model actions confirmation.
+ *
+ * @module tool_analytics/model
+ * @copyright 2017 David Monllao
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_factory', 'core/modal_events'],
+ function($, Str, log, Notification, ModalFactory, ModalEvents) {
+
+ /**
+ * List of actions that require confirmation and confirmation message.
+ */
+ var actionsList = {
+ clear: {
+ title: {
+ key: 'clearpredictions',
+ component: 'tool_analytics'
+ }, body: {
+ key: 'clearmodelpredictions',
+ component: 'tool_analytics'
+ }
+
+ }
+ };
+
+ /**
+ * Returns the model name.
+ *
+ * @param {Object} actionItem The action item DOM node.
+ * @return {String}
+ */
+ var getModelName = function(actionItem) {
+ return $(actionItem.closest('tr')[0]).find('span.target-name').text();
+ };
+
+ /** @alias module:tool_analytics/model */
+ return {
+
+ /**
+ * Displays a confirm modal window before executing the action.
+ *
+ * @param {String} actionId
+ * @param {String} actionType
+ */
+ confirmAction: function(actionId, actionType) {
+ $('[data-action-id="' + actionId + '"]').on('click', function(ev) {
+ ev.preventDefault();
+
+ var a = $(ev.currentTarget);
+
+ if (typeof actionsList[actionType] === "undefined") {
+ log.error('Action "' + actionType + '" is not allowed.');
+ return;
+ }
+
+ var reqStrings = [
+ actionsList[actionType].title,
+ actionsList[actionType].body
+ ];
+ reqStrings[1].param = getModelName(a);
+
+ var stringsPromise = Str.get_strings(reqStrings);
+ var modalPromise = ModalFactory.create({type: ModalFactory.types.SAVE_CANCEL});
+
+ $.when(stringsPromise, modalPromise).then(function(strings, modal) {
+ modal.setTitle(strings[0]);
+ modal.setBody(strings[1]);
+ modal.setSaveButtonText(strings[0]);
+ modal.getRoot().on(ModalEvents.save, function() {
+ window.location.href = a.attr('href');
+ });
+ modal.show();
+ return modal;
+ }).fail(Notification.exception);
+ });
+ }
+ };
+});
--- /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/>.
+
+/**
+ * Invalid analysables renderable.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics\output;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Invalid analysables renderable.
+ *
+ * @package tool_analytics
+ * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class invalid_analysables implements \renderable, \templatable {
+
+ /**
+ * @var \core_analytics\model
+ */
+ protected $model = null;
+
+ /**
+ * @var int
+ */
+ protected $page = 0;
+
+ /**
+ * @var int
+ */
+ protected $perpage = 0;
+
+ /**
+ * Inits the invalid analysables renderable.
+ *
+ * @param \core_analytics\model $model
+ * @param int $page
+ * @param int $perpage
+ * @return \stdClass
+ */
+ public function __construct(\core_analytics\model $model, $page, $perpage) {
+
+ $this->model = $model;
+ $this->page = $page;
+ $this->perpage = $perpage;
+ }
+
+ /**
+ * Export the data.
+ *
+ * @param \renderer_base $output
+ * @return \stdClass
+ */
+ public function export_for_template(\renderer_base $output) {
+ global $PAGE;
+
+ $offset = $this->page * $this->perpage;
+
+ $analysables = $this->model->get_analyser(['notimesplitting' => true])->get_analysables();
+
+ $skipped = 0;
+ $enoughresults = false;
+ $morepages = false;
+ $results = array();
+ foreach ($analysables as $key => $analysable) {
+
+ $validtraining = $this->model->get_target()->is_valid_analysable($analysable, true);
+ if ($validtraining === true) {
+ if ($this->model->is_static()) {
+ // We still want to show this analysable if it is not valid to get predictions.
+ $validtraining = get_string('notrainingbasedassumptions', 'analytics');
+ } else {
+ // We skip analysables that are valid for training or valid for prediction.
+ continue;
+ }
+ }
+
+ $validprediction = $this->model->get_target()->is_valid_analysable($analysable, false);
+ if ($validprediction === true) {
+ // We skip analysables that are valid for training or valid for prediction.
+ continue;
+ }
+
+ if ($offset && $skipped < $offset) {
+ $skipped++;
+ continue;
+ }
+
+ // Add a new results if we don't have enough yet.
+ if (!$enoughresults) {
+ $results[$analysable->get_id()] = array($analysable, $validtraining, $validprediction);
+ if ($this->perpage && count($results) === $this->perpage) {
+ $enoughresults = true;
+ }
+ } else {
+ // Confirmed that we have results we can not fit into this page.
+ $morepages = true;
+ break;
+ }
+
+ unset($analysables[$key]);
+ }
+
+ // Prepare the context object.
+ $data = new \stdClass();
+ $data->modelname = $this->model->get_target()->get_name();
+
+ if ($this->page > 0) {
+ $prev = clone $PAGE->url;
+ $prev->param('page', $this->page - 1);
+ $button = new \single_button($prev, get_string('previouspage', 'tool_analytics'), 'get');
+ $data->prev = $button->export_for_template($output);
+ }
+ if ($morepages) {
+ $next = clone $PAGE->url;
+ $next->param('page', $this->page + 1);
+ $button = new \single_button($next, get_string('nextpage', 'tool_analytics'), 'get');
+ $data->next = $button->export_for_template($output);
+ }
+
+ $data->analysables = [];
+ foreach ($results as list($analysable, $validtraining, $validprediction)) {
+ $obj = new \stdClass();
+ $obj->url = \html_writer::link($analysable->get_context()->get_url(), $analysable->get_name(),
+ array('target' => '_blank'));
+
+ if ($validtraining !== true) {
+ $obj->validtraining = $validtraining;
+ }
+ if ($validprediction !== true) {
+ $obj->validprediction = $validprediction;
+ }
+ $data->analysables[] = $obj;
+ }
+
+ return $data;
+ }
+}
* @return \stdClass
*/
public function export_for_template(\renderer_base $output) {
+ global $PAGE;
$data = new \stdClass();
}
}
+ // Has this model generated predictions?.
+ $predictioncontexts = $model->get_predictions_contexts();
+
// Model predictions list.
if (!$model->is_enabled()) {
$modeldata->noinsights = get_string('disabledmodel', 'analytics');
} else if ($model->uses_insights()) {
- $predictioncontexts = $model->get_predictions_contexts();
if ($predictioncontexts) {
foreach ($predictioncontexts as $contextid => $unused) {
$actionsmenu->set_owner_selector('model-actions-' . $model->get_id());
$actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
+ $urlparams = ['id' => $model->get_id(), 'sesskey' => sesskey()];
+
+ // Get predictions.
+ if (!$onlycli && $modeldata->enabled && !empty($modeldata->timesplitting)) {
+ $urlparams['action'] = 'getpredictions';
+ $url = new \moodle_url('model.php', $urlparams);
+ $icon = new \action_menu_link_secondary($url, new \pix_icon('i/notifications',
+ get_string('getpredictions', 'tool_analytics')), get_string('getpredictions', 'tool_analytics'));
+ $actionsmenu->add($icon);
+ }
+
+ // Evaluate machine-learning-based models.
+ if (!$onlycli && $model->get_indicators() && !$model->is_static()) {
+ $urlparams['action'] = 'evaluate';
+ $url = new \moodle_url('model.php', $urlparams);
+ $icon = new \action_menu_link_secondary($url, new \pix_icon('i/calc', get_string('evaluate', 'tool_analytics')),
+ get_string('evaluate', 'tool_analytics'));
+ $actionsmenu->add($icon);
+ }
+
+ // Machine-learning-based models evaluation log.
+ if (!$model->is_static()) {
+ $urlparams['action'] = 'log';
+ $url = new \moodle_url('model.php', $urlparams);
+ $icon = new \action_menu_link_secondary($url, new \pix_icon('i/report', get_string('viewlog', 'tool_analytics')),
+ get_string('viewlog', 'tool_analytics'));
+ $actionsmenu->add($icon);
+ }
+
// Edit model.
if (!$model->is_static()) {
- $url = new \moodle_url('model.php', array('action' => 'edit', 'id' => $model->get_id()));
+ $urlparams['action'] = 'edit';
+ $url = new \moodle_url('model.php', $urlparams);
$icon = new \action_menu_link_secondary($url, new \pix_icon('t/edit', get_string('edit')), get_string('edit'));
$actionsmenu->add($icon);
}
$text = get_string('enable');
$icontype = 'i/checked';
}
- $url = new \moodle_url('model.php', array('action' => $action, 'id' => $model->get_id()));
+ $urlparams['action'] = $action;
+ $url = new \moodle_url('model.php', $urlparams);
$icon = new \action_menu_link_secondary($url, new \pix_icon($icontype, $text), $text);
$actionsmenu->add($icon);
- // Evaluate machine-learning-based models.
- if (!$onlycli && $model->get_indicators() && !$model->is_static()) {
- $url = new \moodle_url('model.php', array('action' => 'evaluate', 'id' => $model->get_id()));
- $icon = new \action_menu_link_secondary($url, new \pix_icon('i/calc', get_string('evaluate', 'tool_analytics')),
- get_string('evaluate', 'tool_analytics'));
- $actionsmenu->add($icon);
- }
-
- // Get predictions.
- if (!$onlycli && $modeldata->enabled && !empty($modeldata->timesplitting)) {
- $url = new \moodle_url('model.php', array('action' => 'getpredictions', 'id' => $model->get_id()));
- $icon = new \action_menu_link_secondary($url, new \pix_icon('i/notifications',
- get_string('getpredictions', 'tool_analytics')), get_string('getpredictions', 'tool_analytics'));
+ // Export training data.
+ if (!$model->is_static() && $model->is_trained()) {
+ $urlparams['action'] = 'export';
+ $url = new \moodle_url('model.php', $urlparams);
+ $icon = new \action_menu_link_secondary($url, new \pix_icon('i/export',
+ get_string('exporttrainingdata', 'tool_analytics')), get_string('export', 'tool_analytics'));
$actionsmenu->add($icon);
}
- // Machine-learning-based models evaluation log.
- if (!$model->is_static()) {
- $url = new \moodle_url('model.php', array('action' => 'log', 'id' => $model->get_id()));
- $icon = new \action_menu_link_secondary($url, new \pix_icon('i/report', get_string('viewlog', 'tool_analytics')),
- get_string('viewlog', 'tool_analytics'));
+ // Invalid analysables.
+ $analyser = $model->get_analyser(['notimesplitting' => true]);
+ if (!$analyser instanceof \core_analytics\local\analyser\sitewide) {
+ $urlparams['action'] = 'invalidanalysables';
+ $url = new \moodle_url('model.php', $urlparams);
+ $pix = new \pix_icon('i/report', get_string('invalidanalysables', 'tool_analytics'));
+ $icon = new \action_menu_link_secondary($url, $pix, get_string('invalidanalysables', 'tool_analytics'));
$actionsmenu->add($icon);
}
- // Export training data.
- if (!$model->is_static() && $model->is_trained()) {
- $url = new \moodle_url('model.php', array('action' => 'export', 'id' => $model->get_id()));
- $icon = new \action_menu_link_secondary($url, new \pix_icon('i/export',
- get_string('exporttrainingdata', 'tool_analytics')), get_string('export', 'tool_analytics'));
+ // Clear model.
+ if (!empty($predictioncontexts)) {
+ $actionid = 'clear-' . $model->get_id();
+ $PAGE->requires->js_call_amd('tool_analytics/model', 'confirmAction', [$actionid, 'clear']);
+ $urlparams['action'] = 'clear';
+ $url = new \moodle_url('model.php', $urlparams);
+ $icon = new \action_menu_link_secondary($url, new \pix_icon('e/cleanup_messy_code',
+ get_string('clearpredictions', 'tool_analytics')), get_string('clearpredictions', 'tool_analytics'),
+ ['data-action-id' => $actionid]);
$actionsmenu->add($icon);
}
if ($trainresults->status == 0) {
$output .= $OUTPUT->notification(get_string('trainingprocessfinished', 'tool_analytics'),
\core\output\notification::NOTIFY_SUCCESS);
- } else if ($trainresults->status === \core_analytics\model::NO_DATASET) {
+ } else if ($trainresults->status === \core_analytics\model::NO_DATASET ||
+ $trainresults->status === \core_analytics\model::NOT_ENOUGH_DATA) {
$output .= $OUTPUT->notification(get_string('nodatatotrain', 'tool_analytics'),
\core\output\notification::NOTIFY_WARNING);
} else {
- $output .= $OUTPUT->notification(get_string('generalerror', 'analytics', $trainresults->status),
+ $output .= $OUTPUT->notification(get_string('generalerror', 'tool_analytics', $trainresults->status),
\core\output\notification::NOTIFY_ERROR);
}
}
if ($predictresults->status == 0) {
$output .= $OUTPUT->notification(get_string('predictionprocessfinished', 'tool_analytics'),
\core\output\notification::NOTIFY_SUCCESS);
- } else if ($predictresults->status === \core_analytics\model::NO_DATASET) {
+ } else if ($predictresults->status === \core_analytics\model::NO_DATASET ||
+ $predictresults->status === \core_analytics\model::NOT_ENOUGH_DATA) {
$output .= $OUTPUT->notification(get_string('nodatatopredict', 'tool_analytics'),
\core\output\notification::NOTIFY_WARNING);
} else {
- $output .= $OUTPUT->notification(get_string('generalerror', 'analytics', $predictresults->status),
+ $output .= $OUTPUT->notification(get_string('generalerror', 'tool_analytics', $predictresults->status),
\core\output\notification::NOTIFY_ERROR);
}
}
return $output;
}
+
+ /**
+ * Defer to template.
+ *
+ * @param \tool_analytics\output\invalid_analysables $invalidanalysables
+ * @return string HTML
+ */
+ protected function render_invalid_analysables(\tool_analytics\output\invalid_analysables $invalidanalysables) {
+ $data = $invalidanalysables->export_for_template($this);
+ return parent::render_from_template('tool_analytics/invalid_analysables', $data);
+ }
}
$string['bettercli'] = 'Evaluating models and generating predictions may involve heavy processing. It is recommended to run these actions from the command line.';
$string['cantguessstartdate'] = 'Can\'t guess the start date';
$string['cantguessenddate'] = 'Can\'t guess the end date';
+$string['clearpredictions'] = 'Clear predictions';
+$string['clearmodelpredictions'] = 'Are you sure you want to clear all "{$a}" predictions?';
$string['clienablemodel'] = 'You can enable the model by selecting a time-splitting method by its ID. Note that you can also enable it later using the web interface (\'none\' to exit).';
-$string['clievaluationandpredictions'] = 'A cron task iterates through enabled models and gets predictions. Models evaluation via command line is disabled. You can allow these processes to be executed manually via web interface by enabling <a href="{$a}">\'onlycli\' analytics setting</a>';
+$string['clievaluationandpredictions'] = 'A scheduled task iterates through enabled models and gets predictions. Models evaluation via the web interface is disabled. You can allow these processes to be executed manually via the web interface by disabling the <a href="{$a}">\'onlycli\'</a> analytics setting';
$string['editmodel'] = 'Edit "{$a}" model';
$string['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its time-splitting method will delete its previous predictions and start generating new predictions.';
$string['enabled'] = 'Enabled';
$string['indicators'] = 'Indicators';
$string['info'] = 'Info';
$string['insights'] = 'Insights';
+$string['invalidanalysables'] = 'Invalid site elements';
+$string['invalidanalysablesinfo'] = 'This pages lists this site analysable elements that can not be used by this prediction model. The listed elements can not be used neither to train the prediction model nor the prediction model can get predictions for them.';
+$string['invalidanalysablestable'] = 'Invalid site analysable elements table';
+$string['invalidprediction'] = 'Invalid to get predictions';
+$string['invalidtraining'] = 'Invalid to train the model';
$string['loginfo'] = 'Log extra info';
+$string['modelinvalidanalysables'] = 'Invalid analysable elements for "{$a}" model';
$string['modelresults'] = '{$a} results';
-$string['modelslist'] = 'Models list';
$string['modeltimesplitting'] = 'Time splitting';
+$string['nextpage'] = 'Next page';
$string['nodatatoevaluate'] = 'There is no data to evaluate the model';
$string['nodatatopredict'] = 'No new elements to get predictions for';
$string['nodatatotrain'] = 'There is no new data that can be used for training';
$string['predictmodels'] = 'Predict models';
$string['predictorresultsin'] = 'Predictor logged information in {$a} directory';
$string['predictionprocessfinished'] = 'Prediction process finished';
+$string['previouspage'] = 'Previous page';
$string['samestartdate'] = 'Current start date is good';
$string['sameenddate'] = 'Current end date is good';
$string['target'] = 'Target';
case 'export':
$title = get_string('export', 'tool_analytics');
break;
-
+ case 'clear':
+ $title = get_string('clearpredictions', 'tool_analytics');
+ break;
+ case 'invalidanalysables':
+ $title = get_string('invalidanalysables', 'tool_analytics');
+ break;
default:
throw new moodle_exception('errorunknownaction', 'analytics');
}
switch ($action) {
case 'enable':
+ confirm_sesskey();
+
$model->enable();
redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+ break;
case 'disable':
+ confirm_sesskey();
+
$model->update(0, false, false);
redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+ break;
case 'edit':
+ confirm_sesskey();
if ($model->is_static()) {
echo $OUTPUT->header();
redirect(new \moodle_url('/admin/tool/analytics/index.php'));
} else if ($data = $mform->get_data()) {
- confirm_sesskey();
// Converting option names to class names.
$indicators = array();
break;
case 'evaluate':
+ confirm_sesskey();
+
echo $OUTPUT->header();
if ($model->is_static()) {
break;
case 'getpredictions':
+ confirm_sesskey();
+
echo $OUTPUT->header();
if ($onlycli) {
$filename = 'training-data.' . $model->get_id() . '.' . time() . '.csv';
send_file($file, $filename, null, 0, false, true);
break;
+
+ case 'clear':
+ confirm_sesskey();
+
+ $model->clear();
+ redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+ break;
+
+ case 'invalidanalysables':
+
+ echo $OUTPUT->header();
+
+ $page = optional_param('page', 0, PARAM_INT);
+ // No option in the UI to change this, only for url hackers ;).
+ $perpage = optional_param('perpage', 10, PARAM_INT);
+
+ $renderable = new \tool_analytics\output\invalid_analysables($model, $page, $perpage);
+ $renderer = $PAGE->get_renderer('tool_analytics');
+ echo $renderer->render($renderable);
+
+ break;
}
echo $OUTPUT->footer();
--- /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 tool_analytics/invalid_analysables
+
+ Template for invalid analysables.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * none
+
+ Example context (json):
+ {
+ "modelname": "Not engaging courses",
+ "analysables": [
+ {
+ "url": "<a href=\"#\">Maths</a>",
+ "validtraining": "Ongoing course",
+ "validprediction": "Not enough students activity"
+ }, {
+ "url": "<a href=\"#\">Psichology</a>",
+ "validtraining": "No students",
+ "validprediction": "No students"
+ }
+ ]
+ }
+}}
+
+<div class="box">
+ <h3>{{#str}}modelinvalidanalysables, tool_analytics, {{modelname}}{{/str}}</h3>
+ <div>{{#str}}invalidanalysablesinfo, tool_analytics{{/str}}</div>
+ <div class="m-t-2 m-b-1">
+ <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
+ <span>{{#next}}{{> core/single_button}}{{/next}}</span>
+ </div>
+ <table class="generaltable fullwidth">
+ <caption class="accesshide">{{#str}}invalidanalysablestable, tool_analytics{{/str}}</caption>
+ <thead>
+ <tr>
+ <th scope="col">{{#str}}name{{/str}}</th>
+ <th scope="col">{{#str}}invalidtraining, tool_analytics{{/str}}</th>
+ <th scope="col">{{#str}}invalidprediction, tool_analytics{{/str}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#analysables}}
+ <tr>
+ <td>{{{url}}}</td>
+ <td>{{validtraining}}</td>
+ <td>{{validprediction}}</td>
+ </tr>
+ {{/analysables}}
+ </tbody>
+ </table>
+ <div class="m-t-1 m-b-2">
+ <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
+ <span>{{#next}}{{> core/single_button}}{{/next}}</span>
+ </div>
+</div>
<div class="box">
<table class="generaltable fullwidth">
- <caption>{{#str}}modelslist, tool_analytics{{/str}}</caption>
+ <caption>{{#str}}analyticmodels, tool_analytics{{/str}}</caption>
<thead>
<tr>
<th scope="col">{{#str}}target, tool_analytics{{/str}}</th>
{{#models}}
<tr>
<td>
- {{target}}
+ <span class="target-name">{{target}}</span>
{{#targethelp}}
{{>core/help_icon}}
{{/targethelp}}
--- /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/>.
+
+/**
+ * Site wide http -> https search-replace form.
+ *
+ * @package tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_httpsreplace;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->libdir/formslib.php");
+
+/**
+ * Site wide http -> https search-replace form.
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class form extends \moodleform {
+
+ /**
+ * Define the form.
+ */
+ public function definition() {
+ $mform = $this->_form;
+
+ $mform->addElement('header', 'confirmhdr', get_string('confirm'));
+ $mform->setExpanded('confirmhdr', true);
+ $mform->addElement('checkbox', 'sure', get_string('disclaimer', 'tool_httpsreplace'));
+ $mform->addRule('sure', get_string('required'), 'required', null, 'client');
+ $mform->disable_form_change_checker();
+
+ $this->add_action_buttons(false, get_string('doit', 'tool_httpsreplace'));
+ }
+}
--- /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/>.
+
+/**
+ * url_finder class definition.
+ *
+ * @package tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_httpsreplace;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Examines DB for non-https src or data links
+ *
+ * @package tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class url_finder {
+
+ /**
+ * Returns a hash of what hosts are referred to over http and would need to be changed.
+ *
+ * @param progress_bar $progress Progress bar keeping track of this process.
+ * @return array Hash of domains with number of references as the value.
+ */
+ public function http_link_stats($progress = null) {
+ return $this->process(false, $progress);
+ }
+
+ /**
+ * Changes all resources referred to over http to https.
+ *
+ * @param progress_bar $progress Progress bar keeping track of this process.
+ * @return bool True upon success
+ */
+ public function upgrade_http_links($progress = null) {
+ return $this->process(true, $progress);
+ }
+
+ /**
+ * Replace http domains with https equivalent, with two types of exceptions
+ * for less straightforward swaps.
+ *
+ * @param string $table
+ * @param string $column
+ * @param string $domain
+ * @param string $search search string that has prefix, protocol, domain name and one extra character,
+ * example1: src="http://host.com/
+ * example2: DATA="HTTP://MYDOMAIN.EDU"
+ * example3: src="HTTP://hello.world?
+ * @return void
+ */
+ protected function domain_swap($table, $column, $domain, $search) {
+ global $DB;
+
+ $renames = json_decode(get_config('tool_httpsreplace', 'renames'), true);
+
+ if (isset($renames[$domain])) {
+ $replace = preg_replace('|http://'.preg_quote($domain).'|i', 'https://' . $renames[$domain], $search);
+ } else {
+ $replace = preg_replace('|http://|i', 'https://', $search);
+ }
+ $DB->set_debug(true);
+ $DB->replace_all_text($table, $column, $search, $replace);
+ $DB->set_debug(false);
+ }
+
+ /**
+ * Returns SQL to be used to match embedded http links in the given column
+ *
+ * @param string $columnname name of the column (ready to be used in the SQL query)
+ * @return array
+ */
+ protected function get_select_search_in_column($columnname) {
+ global $DB;
+
+ if ($DB->sql_regex_supported()) {
+ // Database supports regex, use it for better match.
+ $select = $columnname . ' ' . $DB->sql_regex() . ' ?';
+ $params = ["(src|data)\ *=\ *[\\\"\']http://"];
+ } else {
+ // Databases without regex support should use case-insensitive LIKE.
+ // This will have false positive matches and more results than we need, we'll have to filter them in php.
+ $select = $DB->sql_like($columnname, '?', false);
+ $params = ['%=%http://%'];
+ }
+
+ return [$select, $params];
+ }
+
+ /**
+ * Originally forked from core function db_search().
+ * @param bool $replacing Whether or not to replace the found urls.
+ * @param progress_bar $progress Progress bar keeping track of this process.
+ * @return bool|array If $replacing, return true on success. If not, return hash of http urls to number of times used.
+ */
+ protected function process($replacing = false, $progress = null) {
+ global $DB, $CFG;
+
+ require_once($CFG->libdir.'/filelib.php');
+
+ // TODO: block_instances have HTML content as base64, need to decode then
+ // search, currently just skipped. See MDL-60024.
+ $skiptables = array(
+ 'block_instances',
+ 'config',
+ 'config_log',
+ 'config_plugins',
+ 'events_queue',
+ 'files',
+ 'filter_config',
+ 'grade_grades_history',
+ 'grade_items_history',
+ 'log',
+ 'logstore_standard_log',
+ 'repository_instance_config',
+ 'sessions',
+ 'upgrade_log',
+ 'grade_categories_history',
+ '',
+ );
+
+ // Turn off time limits.
+ \core_php_time_limit::raise();
+ if (!$tables = $DB->get_tables() ) { // No tables yet at all.
+ return false;
+ }
+
+ $urls = array();
+
+ $numberoftables = count($tables);
+ $tablenumber = 0;
+ foreach ($tables as $table) {
+ if ($progress) {
+ $progress->update($tablenumber, $numberoftables, get_string('searching', 'tool_httpsreplace', $table));
+ $tablenumber++;
+ }
+ if (in_array($table, $skiptables)) {
+ continue;
+ }
+ if ($columns = $DB->get_columns($table)) {
+ foreach ($columns as $column) {
+
+ // Only convert columns that are either text or long varchar.
+ if ($column->meta_type == 'X' || ($column->meta_type == 'C' && $column->max_length > 255)) {
+ $columnname = $column->name;
+ $columnnamequoted = $DB->get_manager()->generator->getEncQuoted($columnname);
+ list($select, $params) = $this->get_select_search_in_column($columnnamequoted);
+ $rs = $DB->get_recordset_select($table, $select, $params, '', $columnnamequoted);
+
+ $found = array();
+ foreach ($rs as $record) {
+ // Regex to match src=http://etc. and data=http://etc.urls.
+ // Standard warning on expecting regex to perfectly parse HTML
+ // read http://stackoverflow.com/a/1732454 for more info.
+ $regex = '#((src|data)\ *=\ *[\'\"])(http://)([^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))[\'\"]#i';
+ preg_match_all($regex, $record->$columnname, $match);
+ foreach ($match[0] as $i => $fullmatch) {
+ if (strpos($fullmatch, $CFG->wwwroot) !== false) {
+ continue;
+ }
+ $prefix = $match[1][$i];
+ $protocol = $match[3][$i];
+ $url = $protocol . $match[4][$i];
+ $host = \core_text::strtolower(parse_url($url, PHP_URL_HOST));
+ if (empty($host)) {
+ continue;
+ }
+ if ($replacing) {
+ // For replace string use: prefix, protocol, host and one extra character.
+ $found[$prefix . substr($url, 0, strlen($host) + 8)] = $host;
+ } else {
+ $entry["table"] = $table;
+ $entry["columnname"] = $columnname;
+ $entry["url"] = $url;
+ $entry["host"] = $host;
+ $entry["raw"] = $record->$columnname;
+ $entry["ssl"] = '';
+ $urls[] = $entry;
+ }
+ }
+ }
+ $rs->close();
+
+ if ($replacing) {
+ foreach ($found as $search => $domain) {
+ $this->domain_swap($table, $column, $domain, $search);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if ($replacing) {
+ rebuild_course_cache(0, true);
+ purge_all_caches();
+ return true;
+ }
+
+ $domains = array_map(function ($i) {
+ return $i['host'];
+ }, $urls);
+
+ $uniquedomains = array_unique($domains);
+
+ $sslfailures = array();
+
+ foreach ($uniquedomains as $domain) {
+ if (!$this->check_domain_availability("https://$domain/")) {
+ $sslfailures[] = $domain;
+ }
+ }
+
+ $results = array();
+ foreach ($urls as $url) {
+ $host = $url['host'];
+ foreach ($sslfailures as $badhost) {
+ if ($host == $badhost) {
+ if (!isset($results[$host])) {
+ $results[$host] = 1;
+ } else {
+ $results[$host]++;
+ }
+ }
+ }
+ }
+ return $results;
+ }
+
+ /**
+ * Check if url is available (GET request returns 200)
+ *
+ * @param string $url
+ * @return bool
+ */
+ protected function check_domain_availability($url) {
+ $curl = new \curl();
+ $curl->head($url);
+ $info = $curl->get_info();
+ return !empty($info['http_code']) && $info['http_code'] == 200;
+ }
+}
--- /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/>.
+
+/**
+ * url_replace cli script. Examines DB for non-https src or data links, and lists broken ones or replaces all links.
+ *
+ * @package tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+require(__DIR__ . '/../../../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+
+list($options, $unrecognized) = cli_get_params(
+ array(
+ 'help' => false,
+ 'list' => false,
+ 'replace' => false,
+ 'confirm' => false,
+ ),
+ array(
+ 'h' => 'help',
+ 'l' => 'list',
+ 'r' => 'replace',
+ )
+);
+if ($unrecognized) {
+ $unrecognized = implode("\n ", $unrecognized);
+ cli_error(get_string('cliunknowoption', 'admin', $unrecognized), 2);
+}
+if ($options['help'] || (!$options['list'] && !$options['replace'])) {
+ $help = "Examines DB for non-https src or data links, and lists broken links or replaces all links.
+Options:
+-h, --help Print out this help
+-l, --list List of http (not https) urls on a site in the DB that would become broken.
+-r, --replace List of http (not https) urls on a site in the DB that would become broken.
+--confirm Replaces http urls with https across a site's content.
+Example:
+\$ sudo -u www-data /usr/bin/php admin/tool/httpsreplace/cli/url_replace.php --list \n";
+ echo $help;
+ exit(0);
+}
+
+if (!$DB->replace_all_text_supported()) {
+ echo $OUTPUT->notification(get_string('notimplemented', 'tool_httpsreplace'));
+ exit(1);
+}
+
+if (!is_https()) {
+ echo $OUTPUT->notification(get_string('httpwarning', 'tool_httpsreplace'), 'warning');
+ echo "\n";
+}
+
+if ($options['replace']) {
+
+ if ($options['confirm']) {
+
+ $urlfinder = new \tool_httpsreplace\url_finder();
+ $urlfinder->upgrade_http_links();
+ } else {
+ echo "Once this is tool run, changes made can't be reverted. \n" .
+ "A complete backup should be made before running this script. \n\n" .
+ "There is a low risk that the wrong content will be replaced, introducing problems. \n" .
+ "If you are sure you want to continue, add --confirm\n\n";
+ }
+
+} else {
+
+ $urlfinder = new \tool_httpsreplace\url_finder();
+ $results = $urlfinder->http_link_stats();
+ asort($results);
+ $fp = fopen('php://stdout', 'w');
+ fputcsv($fp, ['clientsite', 'httpdomain', 'urlcount']);
+ foreach ($results as $domain => $count) {
+ fputcsv($fp, [$SITE->shortname, $domain, $count]);
+ }
+ fclose($fp);
+}
--- /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/>.
+
+/**
+ * Search and replace http -> https throughout all texts in the whole database
+ *
+ * @package tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir . '/adminlib.php');
+
+admin_externalpage_setup('toolhttpsreplace');
+
+$context = context_system::instance();
+
+require_login();
+require_capability('moodle/site:config', $context);
+
+$PAGE->set_context($context);
+$PAGE->set_url(new moodle_url('/admin/tool/httpsreplace/index.php'));
+$PAGE->set_title(get_string('pageheader', 'tool_httpsreplace'));
+$PAGE->set_pagelayout('admin');
+
+echo $OUTPUT->header();
+
+echo $OUTPUT->heading(get_string('pageheader', 'tool_httpsreplace'));
+
+if (!$DB->replace_all_text_supported()) {
+ echo $OUTPUT->notification(get_string('notimplemented', 'tool_httpsreplace'));
+ echo $OUTPUT->footer();
+ die;
+}
+
+if (!is_https()) {
+ echo $OUTPUT->notification(get_string('httpwarning', 'tool_httpsreplace'), 'warning');
+}
+
+echo '<p>'.get_string('domainexplain', 'tool_httpsreplace').'</p>';
+echo '<p>'.page_doc_link(get_string('doclink', 'tool_httpsreplace')).'</p>';
+
+echo $OUTPUT->continue_button(new moodle_url('/admin/tool/httpsreplace/tool.php'));
+
+echo $OUTPUT->footer();
--- /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/>.
+
+/**
+ * Strings for component 'tool_httpsreplace'
+ *
+ * @package tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['complete'] = 'Completed.';
+$string['count'] = 'Number of embeded content items';
+$string['disclaimer'] = 'I understand the risks of this operation';
+$string['doclink'] = 'Read more documentation on the wiki';
+$string['doit'] = 'Perform replacement';
+$string['domain'] = 'Problematic domain';
+$string['domainexplain'] = 'When an instance is moved from HTTP to HTTPS, all embeded HTTP content will stop working. This tool allows you to automatically convert the HTTP content to HTTPS. Below you can run a report of content that may not work once you run this script. You may want to check each one has HTTPS available or find alternative resources.';
+$string['domainexplainhelp'] = 'These domains are found in your content, but do not appear to support HTTPS content. After switching to HTTPS, the content included from these sites will no longer display within Moodle for users with secure modern browsers. It is possible that these sites are temporarily or permanently unavailable and will not work with either security setting. Proceed only after reviewing these results and determining if this externally hosted content is non-essential. Note: This content would no longer work upon switching to HTTPS anyway.';
+$string['httpwarning'] = 'This instance is still running on HTTP. You can still run this tool and external content will be changed to HTTPS, but internal content will remain on HTTP. You will need to run this script again after switching to HTTPS to convert internal content.';
+$string['notimplemented'] = 'Sorry, this feature is not implemented in your database driver.';
+$string['oktoprocede'] = 'The scan finds no issues with your content. You can proceed to upgrade any HTTP content to use HTTPS.';
+$string['pageheader'] = 'Upgrade externally hosted content urls to HTTPS';
+$string['pluginname'] = 'HTTPS conversion tool';
+$string['replacing'] = 'Replacing HTTP content with HTTPS...';
+$string['searching'] = 'Searching {$a}';
+$string['takeabackupwarning'] = 'Once this is tool run, changes made can\'t be reverted. A complete backup should be made before running this script. There is a low risk that the wrong content will be replaced, introducing problems.';
+$string['toolintro'] = 'If you are planning on converting your site to HTTPS, you can use the <a href="{$a}">HTTPS conversion tool</a> to convert your embeded content to HTTPS.';
--- /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/>.
+
+/**
+ * Link to http -> https replace script.
+ *
+ * @package tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+if ($hassiteconfig) {
+
+ $pluginname = get_string('pluginname', 'tool_httpsreplace');
+ $url = $CFG->wwwroot.'/'.$CFG->admin.'/tool/httpsreplace/index.php';
+ $ADMIN->add('security', new admin_externalpage('toolhttpsreplace', $pluginname, $url, 'moodle/site:config', true));
+
+ $httpsreplaceurl = $CFG->wwwroot.'/'.$CFG->admin.'/tool/httpsreplace/index.php';
+ $ADMIN->locate('httpsecurity')->add(
+ new admin_setting_heading(
+ 'tool_httpsreplaceheader',
+ new lang_string('pluginname', 'tool_httpsreplace'),
+ new lang_string('toolintro', 'tool_httpsreplace', $httpsreplaceurl)
+ )
+ );
+}
--- /dev/null
+@tool @tool_httpsreplace
+Feature: View the httpsreplace report
+ In order to switch to https
+ As an admin
+ I need to be able to automatically replace http links
+
+ Background: Create some http links
+ Given I am on site homepage
+ And the following "courses" exist:
+ | fullname | shortname | category | summary |
+ | Course 1 | C1 | 0 | <img src="http://intentionally.unavailable/test.png"> <img src="http://download.moodle.org/unittest/test.jpg"> |
+ And I log in as "admin"
+
+ @javascript
+ Scenario: Go to the HTTPS replace report screen. Make sure broken domains are reported.
+ When I navigate to "HTTP security" node in "Site administration > Security"
+ And I follow "HTTPS conversion tool"
+ And I press "Continue"
+ Then I should see "intentionally.unavailable"
+
+ @javascript
+ Scenario: Use the find and replace tool.
+ When I navigate to "HTTP security" node in "Site administration > Security"
+ And I follow "HTTPS conversion tool"
+ And I press "Continue"
+ And I set the field "I understand the risks of this operation" to "1"
+ And I press "Perform replacement"
+ Then I should see "intentionally.unavailable"
+ And I should see "download.moodle.org"
--- /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/>.
+
+/**
+ * HTTPS find and replace Tests
+ *
+ * @package tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_httpsreplace\tests;
+
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests the httpsreplace tool.
+ *
+ * @package tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class httpsreplace_test extends \advanced_testcase {
+
+ /**
+ * Data provider for test_upgrade_http_links
+ */
+ public function upgrade_http_links_provider() {
+ global $CFG;
+ // Get the http url, since the default test wwwroot is https.
+ $wwwroothttp = preg_replace('/^https:/', 'http:', $CFG->wwwroot);
+ return [
+ "Test image from another site should be replaced" => [
+ "content" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', false) . '">',
+ "outputregex" => '/UPDATE/',
+ "expectedcontent" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', true) . '">',
+ ],
+ "Test object from another site should be replaced" => [
+ "content" => '<object data="' . $this->getExternalTestFileUrl('/test.swf', false) . '">',
+ "outputregex" => '/UPDATE/',
+ "expectedcontent" => '<object data="' . $this->getExternalTestFileUrl('/test.swf', true) . '">',
+ ],
+ "Test image from a site with international name should be replaced" => [
+ "content" => '<img src="http://中国互联网络信息中心.中国/logosy/201706/W01.png">',
+ "outputregex" => '/UPDATE/',
+ "expectedcontent" => '<img src="https://中国互联网络信息中心.中国/logosy/201706/W01.png">',
+ ],
+ "Link that is from this site should be replaced" => [
+ "content" => '<img src="' . $wwwroothttp . '/logo.png">',
+ "outputregex" => '/UPDATE/',
+ "expectedcontent" => '<img src="' . $CFG->wwwroot . '/logo.png">',
+ ],
+ "Link that is from this site, https new so doesn't need replacing" => [
+ "content" => '<img src="' . $CFG->wwwroot . '/logo.png">',
+ "outputregex" => '/^$/',
+ "expectedcontent" => '<img src="' . $CFG->wwwroot . '/logo.png">',
+ ],
+ "Unavailable image should be replaced" => [
+ "content" => '<img src="http://intentionally.unavailable/link1.jpg">',
+ "outputregex" => '/UPDATE/',
+ "expectedcontent" => '<img src="https://intentionally.unavailable/link1.jpg">',
+ ],
+ "Https content that has an http url as a param should not be replaced" => [
+ "content" => '<img src="https://anothersite.com?param=http://asdf.com">',
+ "outputregex" => '/^$/',
+ "expectedcontent" => '<img src="https://anothersite.com?param=http://asdf.com">',
+ ],
+ "Search for params should be case insensitive" => [
+ "content" => '<object DATA="' . $this->getExternalTestFileUrl('/test.swf', false) . '">',
+ "outputregex" => '/UPDATE/',
+ "expectedcontent" => '<object DATA="' . $this->getExternalTestFileUrl('/test.swf', true) . '">',
+ ],
+ "URL should be case insensitive" => [
+ "content" => '<object data="HTTP://some.site/path?query">',
+ "outputregex" => '/UPDATE/',
+ "expectedcontent" => '<object data="https://some.site/path?query">',
+ ],
+ "More params should not interfere" => [
+ "content" => '<img alt="A picture" src="' . $this->getExternalTestFileUrl('/test.png', false) .
+ '" width="1”><p style="font-size: \'20px\'"></p>',
+ "outputregex" => '/UPDATE/',
+ "expectedcontent" => '<img alt="A picture" src="' . $this->getExternalTestFileUrl('/test.png', true) .
+ '" width="1”><p style="font-size: \'20px\'"></p>',
+ ],
+ "Broken URL should not be changed" => [
+ "content" => '<img src="broken.' . $this->getExternalTestFileUrl('/test.png', false) . '">',
+ "outputregex" => '/^$/',
+ "expectedcontent" => '<img src="broken.' . $this->getExternalTestFileUrl('/test.png', false) . '">',
+ ],
+ "Link URL should not be changed" => [
+ "content" => '<a href="' . $this->getExternalTestFileUrl('/test.png', false) . '">' .
+ $this->getExternalTestFileUrl('/test.png', false) . '</a>',
+ "outputregex" => '/^$/',
+ "expectedcontent" => '<a href="' . $this->getExternalTestFileUrl('/test.png', false) . '">' .
+ $this->getExternalTestFileUrl('/test.png', false) . '</a>',
+ ],
+ "Test image from another site should be replaced but link should not" => [
+ "content" => '<a href="' . $this->getExternalTestFileUrl('/test.png', false) . '"><img src="' .
+ $this->getExternalTestFileUrl('/test.jpg', false) . '"></a>',
+ "outputregex" => '/UPDATE/',
+ "expectedcontent" => '<a href="' . $this->getExternalTestFileUrl('/test.png', false) . '"><img src="' .
+ $this->getExternalTestFileUrl('/test.jpg', true) . '"></a>',
+ ],
+ ];
+ }
+
+ /**
+ * Test upgrade_http_links
+ * @param string $content Example content that we'll attempt to replace.
+ * @param string $ouputregex Regex for what output we expect.
+ * @param string $expectedcontent What content we are expecting afterwards.
+ * @dataProvider upgrade_http_links_provider
+ */
+ public function test_upgrade_http_links($content, $ouputregex, $expectedcontent) {
+ global $DB;
+
+ $this->resetAfterTest();
+ $this->expectOutputRegex($ouputregex);
+
+ $finder = new tool_httpreplace_url_finder_test();
+
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course((object) [
+ 'summary' => $content,
+ ]);
+
+ $finder->upgrade_http_links();
+
+ $summary = $DB->get_field('course', 'summary', ['id' => $course->id]);
+ $this->assertContains($expectedcontent, $summary);
+ }
+
+ /**
+ * Data provider for test_http_link_stats
+ */
+ public function http_link_stats_provider() {
+ global $CFG;
+ // Get the http url, since the default test wwwroot is https.
+ $wwwrootdomain = 'www.example.com';
+ $wwwroothttp = preg_replace('/^https:/', 'http:', $CFG->wwwroot);
+ $testdomain = 'download.moodle.org';
+ return [
+ "Test image from an available site so shouldn't be reported" => [
+ "content" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', false) . '">',
+ "domain" => $testdomain,
+ "expectedcount" => 0,
+ ],
+ "Link that is from this site shouldn't be reported" => [
+ "content" => '<img src="' . $wwwroothttp . '/logo.png">',
+ "domain" => $wwwrootdomain,
+ "expectedcount" => 0,
+ ],
+ "Unavailable, but https shouldn't be reported" => [
+ "content" => '<img src="https://intentionally.unavailable/logo.png">',
+ "domain" => 'intentionally.unavailable',
+ "expectedcount" => 0,
+ ],
+ "Unavailable image should be reported" => [
+ "content" => '<img src="http://intentionally.unavailable/link1.jpg">',
+ "domain" => 'intentionally.unavailable',
+ "expectedcount" => 1,
+ ],
+ "Unavailable object should be reported" => [
+ "content" => '<object data="http://intentionally.unavailable/file.swf">',
+ "domain" => 'intentionally.unavailable',
+ "expectedcount" => 1,
+ ],
+ "Link should not be reported" => [
+ "content" => '<a href="http://intentionally.unavailable/page.php">Link</a>',
+ "domain" => 'intentionally.unavailable',
+ "expectedcount" => 0,
+ ],
+ "Text should not be reported" => [
+ "content" => 'http://intentionally.unavailable/page.php',
+ "domain" => 'intentionally.unavailable',
+ "expectedcount" => 0,
+ ],
+ ];
+ }
+
+ /**
+ * Test http_link_stats
+ * @param string $content Example content that we'll attempt to replace.
+ * @param string $domain The domain we will check was replaced.
+ * @param string $expectedcount Number of urls from that domain that we expect to be replaced.
+ * @dataProvider http_link_stats_provider
+ */
+ public function test_http_link_stats($content, $domain, $expectedcount) {
+ $this->resetAfterTest();
+
+ $finder = new tool_httpreplace_url_finder_test();
+
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course((object) [
+ 'summary' => $content,
+ ]);
+
+ $results = $finder->http_link_stats();
+
+ $this->assertEquals($expectedcount, $results[$domain] ?? 0);
+ }
+
+ /**
+ * Test links and text are not changed
+ */
+ public function test_links_and_text() {
+ global $DB;
+
+ $this->resetAfterTest();
+ $this->expectOutputRegex('/^$/');
+
+ $finder = new tool_httpreplace_url_finder_test();
+
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course((object) [
+ 'summary' => '<a href="http://intentionally.unavailable/page.php">Link</a> http://other.unavailable/page.php',
+ ]);
+
+ $results = $finder->http_link_stats();
+ $this->assertCount(0, $results);
+
+ $finder->upgrade_http_links();
+
+ $results = $finder->http_link_stats();
+ $this->assertCount(0, $results);
+
+ $summary = $DB->get_field('course', 'summary', ['id' => $course->id]);
+ $this->assertContains('http://intentionally.unavailable/page.php', $summary);
+ $this->assertContains('http://other.unavailable/page.php', $summary);
+ $this->assertNotContains('https://intentionally.unavailable', $summary);
+ $this->assertNotContains('https://other.unavailable', $summary);
+ }
+
+ /**
+ * If we have an http wwwroot then we shouldn't report it.
+ */
+ public function test_httpwwwroot() {
+ global $DB, $CFG;
+
+ $this->resetAfterTest();
+ $CFG->wwwroot = preg_replace('/^https:/', 'http:', $CFG->wwwroot);
+ $this->expectOutputRegex('/^$/');
+
+ $finder = new tool_httpreplace_url_finder_test();
+
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course((object) [
+ 'summary' => '<img src="' . $CFG->wwwroot . '/image.png">',
+ ]);
+
+ $results = $finder->http_link_stats();
+ $this->assertCount(0, $results);
+
+ $finder->upgrade_http_links();
+ $summary = $DB->get_field('course', 'summary', ['id' => $course->id]);
+ $this->assertContains($CFG->wwwroot, $summary);
+ }
+
+ /**
+ * Test that links in excluded tables are not replaced
+ */
+ public function test_upgrade_http_links_excluded_tables() {
+ $this->resetAfterTest();
+
+ set_config('test_upgrade_http_links', '<img src="http://somesite/someimage.png" />');
+
+ $finder = new tool_httpreplace_url_finder_test();
+ ob_start();
+ $results = $finder->upgrade_http_links();
+ $output = ob_get_contents();
+ ob_end_clean();
+ $this->assertTrue($results);
+ $this->assertNotContains('https://somesite', $output);
+ $testconf = get_config('core', 'test_upgrade_http_links');
+ $this->assertContains('http://somesite', $testconf);
+ $this->assertNotContains('https://somesite', $testconf);
+ }
+
+ /**
+ * Test renamed domains
+ */
+ public function test_renames() {
+ global $DB, $CFG;
+ $this->resetAfterTest();
+ $this->expectOutputRegex('/UPDATE/');
+
+ $renames = [
+ 'example.com' => 'secure.example.com',
+ ];
+
+ set_config('renames', json_encode($renames), 'tool_httpsreplace');
+
+ $finder = new tool_httpreplace_url_finder_test();
+
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course((object) [
+ 'summary' => '<script src="http://example.com/test.js"><img src="http://EXAMPLE.COM/someimage.png">',
+ ]);
+
+ $results = $finder->http_link_stats();
+ $this->assertCount(0, $results);
+
+ $finder->upgrade_http_links();
+
+ $summary = $DB->get_field('course', 'summary', ['id' => $course->id]);
+ $this->assertContains('https://secure.example.com', $summary);
+ $this->assertNotContains('http://example.com', $summary);
+ $this->assertEquals('<script src="https://secure.example.com/test.js">' .
+ '<img src="https://secure.example.com/someimage.png">', $summary);
+ }
+
+ /**
+ * When there are many different pieces of contents from the same site, we should only run replace once
+ */
+ public function test_multiple() {
+ global $DB;
+ $this->resetAfterTest();
+ $original1 = '';
+ $expected1 = '';
+ $original2 = '';
+ $expected2 = '';
+ for ($i = 0; $i < 15; $i++) {
+ $original1 .= '<img src="http://example.com/image' . $i . '.png">';
+ $expected1 .= '<img src="https://example.com/image' . $i . '.png">';
+ $original2 .= '<img src="http://example.com/image' . ($i + 15 ) . '.png">';
+ $expected2 .= '<img src="https://example.com/image' . ($i + 15) . '.png">';
+ }
+ $finder = new tool_httpreplace_url_finder_test();
+
+ $generator = $this->getDataGenerator();
+ $course1 = $generator->create_course((object) ['summary' => $original1]);
+ $course2 = $generator->create_course((object) ['summary' => $original2]);
+
+ ob_start();
+ $finder->upgrade_http_links();
+ $output = ob_get_contents();
+ ob_end_clean();
+
+ // Make sure everything is replaced.
+ $summary1 = $DB->get_field('course', 'summary', ['id' => $course1->id]);
+ $this->assertEquals($expected1, $summary1);
+ $summary2 = $DB->get_field('course', 'summary', ['id' => $course2->id]);
+ $this->assertEquals($expected2, $summary2);
+
+ // Make sure only one UPDATE statment was called.
+ $this->assertEquals(1, preg_match_all('/UPDATE/', $output));
+ }
+
+ /**
+ * Test the tool when the column name is a reserved word in SQL (in this case 'where')
+ */
+ public function test_reserved_words() {
+ global $DB;
+
+ $this->resetAfterTest();
+ $this->expectOutputRegex('/UPDATE/');
+
+ // Create a table with a field that is a reserved SQL word.
+ $dbman = $DB->get_manager();
+ $table = new \xmldb_table('reserved_words_temp');
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('where', XMLDB_TYPE_TEXT, null, null, null, null, null);
+ $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+ $dbman->create_table($table);
+
+ // Insert a record with an <img> in this table and run tool.
+ $content = '<img src="http://example.com/image.png">';
+ $expectedcontent = '<img src="https://example.com/image.png">';
+ $columnamequoted = $dbman->generator->getEncQuoted('where');
+ $DB->execute("INSERT INTO {reserved_words_temp} ($columnamequoted) VALUES (?)", [$content]);
+
+ $finder = new tool_httpreplace_url_finder_test();
+ $finder->upgrade_http_links();
+
+ $record = $DB->get_record('reserved_words_temp', []);
+ $this->assertContains($expectedcontent, $record->where);
+
+ $dbman->drop_table($table);
+ }
+}
+
+/**
+ * Class tool_httpreplace_url_finder_test for testing replace tool without calling curl
+ *
+ * @package tool_httpsreplace
+ * @copyright 2017 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_httpreplace_url_finder_test extends \tool_httpsreplace\url_finder {
+ /**
+ * Check if url is available (check hardcoded for unittests)
+ *
+ * @param string $url
+ * @return bool
+ */
+ protected function check_domain_availability($url) {
+ return !preg_match('|\.unavailable/$|', $url);
+ }
+}
--- /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/>.
+
+/**
+ * Search and replace http -> https throughout all texts in the whole database
+ *
+ * @package tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_OUTPUT_BUFFERING', true);
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->dirroot . '/course/lib.php');
+require_once($CFG->libdir . '/adminlib.php');
+
+admin_externalpage_setup('toolhttpsreplace');
+
+$context = context_system::instance();
+
+require_login();
+require_capability('moodle/site:config', $context);
+
+$PAGE->set_context($context);
+$PAGE->set_url(new moodle_url('/admin/tool/httpsreplace/index.php'));
+$PAGE->set_title(get_string('pageheader', 'tool_httpsreplace'));
+$PAGE->set_pagelayout('admin');
+
+echo $OUTPUT->header();
+
+echo $OUTPUT->heading(get_string('pageheader', 'tool_httpsreplace'));
+
+if (!$DB->replace_all_text_supported()) {
+ echo $OUTPUT->notification(get_string('notimplemented', 'tool_httpsreplace'));
+ echo $OUTPUT->footer();
+ die;
+}
+
+if (!is_https()) {
+ echo $OUTPUT->notification(get_string('httpwarning', 'tool_httpsreplace'), 'warning');
+}
+
+$form = new \tool_httpsreplace\form();
+
+$finder = new \tool_httpsreplace\url_finder();
+
+$PAGE->set_cacheable(false);
+$progressbar = new progress_bar();
+
+if (!$data = $form->get_data()) {
+
+ echo $progressbar->create();
+
+ $results = $finder->http_link_stats($progressbar);
+
+ $progressbar->update_full(100, get_string('complete', 'tool_httpsreplace'));
+
+ if (empty($results)) {
+ echo '<p>'.get_string('oktoprocede', 'tool_httpsreplace').'</p>';
+ } else {
+ arsort($results);
+ $table = new html_table();
+ $table->id = 'plugins-check';
+ $table->head = array(
+ get_string('domain', 'tool_httpsreplace'),
+ get_string('count', 'tool_httpsreplace'),
+ );
+ $data = array();
+ foreach ($results as $domain => $count) {
+ $cleandomain = format_text($domain, FORMAT_PLAIN);
+ $data[] = [$cleandomain, $count];
+ }
+ $table->data = $data;
+ echo html_writer::table($table);
+ echo get_string('domainexplainhelp', 'tool_httpsreplace');
+ }
+ echo $OUTPUT->notification(get_string('takeabackupwarning', 'tool_httpsreplace'), 'warning');
+ $form->display();
+} else {
+ // Scroll to the end when finished.
+ $PAGE->requires->js_init_code("window.scrollTo(0, document.body.scrollHeight);");
+
+ echo html_writer::tag('p', get_string('replacing', 'tool_httpsreplace'));
+
+ echo $progressbar->create();
+
+ echo $OUTPUT->box_start();
+ $finder->upgrade_http_links($progressbar);
+ echo $OUTPUT->box_end();
+
+ $progressbar->update_full(100, get_string('complete', 'tool_httpsreplace'));
+
+ echo $OUTPUT->continue_button(new moodle_url('/admin/settings.php', ['section' => 'httpsecurity']));
+}
+echo $OUTPUT->footer();
--- /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/>.
+
+/**
+ * Version details.
+ *
+ * @package tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version = 2017082500; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires = 2017082400; // Requires this Moodle version.
+$plugin->component = 'tool_httpsreplace'; // Full name of the plugin (used for diagnostics).
use moodle_url;
use moodle_exception;
use lang_string;
+use curl;
/**
* API exposed by tool_mobile, to be used mostly by external functions and the plugin settings.
return $features;
}
+
+ /**
+ * This function check the current site for potential configuration issues that may prevent the mobile app to work.
+ *
+ * @return array list of potential issues
+ * @since Moodle 3.4
+ */
+ public static function get_potential_config_issues() {
+ global $CFG;
+ require_once($CFG->dirroot . "/lib/filelib.php");
+ require_once($CFG->dirroot . '/message/lib.php');
+
+ $warnings = array();
+
+ $curl = new curl();
+ // Return certificate information and verify the certificate.
+ $curl->setopt(array('CURLOPT_CERTINFO' => 1, 'CURLOPT_SSL_VERIFYPEER' => true));
+ $httpswwwroot = str_replace('http:', 'https:', $CFG->wwwroot); // Force https url.
+ $curl->head($httpswwwroot . "/login/index.php");
+ $info = $curl->get_info();
+
+ // First of all, check the server certificate (if any).
+ if (empty($info['http_code']) or ($info['http_code'] >= 400)) {
+ $warnings[] = ['nohttpsformobilewarning', 'admin'];
+ } else {
+ // Check the certificate is not self-signed or has an untrusted-root.
+ // This may be weak in some scenarios (when the curl SSL verifier is outdated).
+ if (empty($info['certinfo'])) {
+ $warnings[] = ['selfsignedoruntrustedcertificatewarning', 'tool_mobile'];
+ } else {
+ $timenow = time();
+ $expectedissuer = null;
+ foreach ($info['certinfo'] as $cert) {
+ // Check if the signature algorithm is weak (Android won't work with SHA-1).
+ if ($cert['Signature Algorithm'] == 'sha1WithRSAEncryption' || $cert['Signature Algorithm'] == 'sha1WithRSA') {
+ $warnings[] = ['insecurealgorithmwarning', 'tool_mobile'];
+ }
+ // Check certificate start date.
+ if (strtotime($cert['Start date']) > $timenow) {
+ $warnings[] = ['invalidcertificatestartdatewarning', 'tool_mobile'];
+ }
+ // Check certificate end date.
+ if (strtotime($cert['Expire date']) < $timenow) {
+ $warnings[] = ['invalidcertificateexpiredatewarning', 'tool_mobile'];
+ }
+ // Check the chain.
+ if ($expectedissuer !== null) {
+ if ($expectedissuer !== $cert['Subject'] || $cert['Subject'] === $cert['Issuer']) {
+ $warnings[] = ['invalidcertificatechainwarning', 'tool_mobile'];
+ }
+ }
+ $expectedissuer = $cert['Issuer'];
+ }
+ }
+ }
+ // Now check typical configuration problems.
+ if ((int) $CFG->userquota === PHP_INT_MAX) {
+ // In old Moodle version was a text so was possible to have numeric values > PHP_INT_MAX.
+ $warnings[] = ['invaliduserquotawarning', 'tool_mobile'];
+ }
+ // Check ADOdb debug enabled.
+ if (get_config('auth_db', 'debugauthdb') || get_config('enrol_database', 'debugdb')) {
+ $warnings[] = ['adodbdebugwarning', 'tool_mobile'];
+ }
+ // Check display errors on.
+ if (!empty($CFG->debugdisplay)) {
+ $warnings[] = ['displayerrorswarning', 'tool_mobile'];
+ }
+ // Check mobile notifications.
+ $processors = get_message_processors();
+ $enabled = false;
+ foreach ($processors as $processor => $status) {
+ if ($processor == 'airnotifier' && $status->enabled) {
+ $enabled = true;
+ }
+ }
+ if (!$enabled) {
+ $warnings[] = ['mobilenotificationsdisabledwarning', 'tool_mobile'];
+ }
+
+ return $warnings;
+ }
}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+$string['adodbdebugwarning'] = 'ADOdb debugging is enabled. It should be disabled in the external database authentication or external database enrolment plugin settings.';
$string['androidappid'] = 'Android app\'s unique identifier';
$string['androidappid_desc'] = 'This setting may be left as default unless you have a custom Android app.';
$string['autologinkeygenerationlockout'] = 'Auto-login key generation is blocked because of too many requests within an hour.';
</pre>';
$string['disabledfeatures'] = 'Disabled features';
$string['disabledfeatures_desc'] = 'Select here the features you want to disable in the Mobile app for your site. Please note that some features listed here could be already disabled via other site settings. You will have to log out and log in again in the app to see the changes.';
+$string['displayerrorswarning'] = 'Display debug messages (debugdisplay) is enabled. It should be disabled.';
$string['enablesmartappbanners'] = 'Enable App Banners';
$string['enablesmartappbanners_desc'] = 'If enabled, a banner promoting the mobile app will be displayed when accessing the site using a mobile browser.';
$string['forcedurlscheme'] = 'If you want to allow only your custom branded app to be opened via a browser window, then specify its URL scheme here; otherwise leave the field empty.';
$string['forcelogout'] = 'Force log out';
$string['forcelogout_desc'] = 'If enabled, the app option \'Change site\' is replaced by \'Log out\'. This results in the user being completely logged out. They must then re-enter their password the next time they wish to access the site.';
$string['httpsrequired'] = 'HTTPS required';
+$string['insecurealgorithmwarning'] = 'It seems that the HTTPS certificate uses an insecure algorithm for signing (SHA-1). Please try updating the certificate.';
+$string['invalidcertificatechainwarning'] = 'It seems that the certificate chain is invalid.';
+$string['invalidcertificateexpiredatewarning'] = 'It seems that the HTTPS certificate for the site has expired.';
+$string['invalidcertificatestartdatewarning'] = 'It seems that the HTTPS certificate for the site is not yet valid (with a start date in the future).';
$string['invalidprivatetoken'] = 'Invalid private token. Token should not be empty or passed via GET parameter.';
+$string['invaliduserquotawarning'] = 'The user quota (userquota) is set to an invalid number. It should be set to a valid number (an integer value) in Site policies.';
$string['iosappid'] = 'iOS app\'s unique identifier';
$string['iosappid_desc'] = 'This setting may be left as default unless you have a custom iOS app.';
$string['loginintheapp'] = 'Via the app';
$string['mobileauthentication'] = 'Mobile authentication';
$string['mobilecssurl'] = 'CSS';
$string['mobilefeatures'] = 'Mobile features';
+$string['mobilenotificationsdisabledwarning'] = 'Mobile notifications are not enabled. They should be enabled in Manage message outputs.';
$string['mobilesettings'] = 'Mobile settings';
$string['pluginname'] = 'Moodle Mobile tools';
+$string['selfsignedoruntrustedcertificatewarning'] = 'It seems that the HTTPS certificate is self-signed or not trusted. The mobile app will only work with trusted sites.';
$string['setuplink'] = 'App download page';
$string['setuplink_desc'] = 'URL of page with links to download the mobile app from the App Store and Google Play.';
$string['smartappbanners'] = 'App Banners';
$this->assertTimeCurrent($key->validuntil - api::LOGIN_KEY_TTL);
$this->assertEquals('0.0.0.0', $key->iprestriction);
}
+
+ /**
+ * Test get_potential_config_issues.
+ */
+ public function test_get_potential_config_issues() {
+ global $CFG;
+ require_once($CFG->dirroot . '/message/lib.php');
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+
+ $CFG->userquota = '73289234723498234723423489273423497234234';
+ $CFG->debugdisplay = 1;
+ set_config('debugauthdb', 1, 'auth_db');
+ set_config('debugdb', 1, 'enrol_database');
+ $expectedissues = array('nohttpsformobilewarning', 'invaliduserquotawarning', 'adodbdebugwarning', 'displayerrorswarning',
+ 'mobilenotificationsdisabledwarning');
+
+ $processors = get_message_processors();
+ foreach ($processors as $processor => $status) {
+ if ($processor == 'airnotifier' && $status->enabled) {
+ unset($expectedissues['mobilenotificationsdisabledwarning']);
+ }
+ }
+
+ $issues = api::get_potential_config_issues();
+ $this->assertCount(count($expectedissues), $issues);
+ foreach ($issues as $issue) {
+ $this->assertTrue(in_array($issue[0], $expectedissues));
+ }
+ }
}
* @method addResetLink
*/
addResetLink: function() {
- str.get_string('resettouronpage', 'tool_usertours')
- .done(function(s) {
- // Grab the last item in the page of these.
- $('footer, .logininfo')
- .last()
- .append(
- '<div class="usertour">' +
- '<a href="#" data-action="tool_usertours/resetpagetour">' +
- s +
- '</a>' +
- '</div>'
- );
+ var ele;
+ // Append the link to the most suitable place on the page
+ // with fallback to legacy selectors and finally the body
+ // if there is no better place.
+ if ($('.tool_usertours-resettourcontainer').length) {
+ ele = $('.tool_usertours-resettourcontainer');
+ } else if ($('.logininfo').length) {
+ ele = $('.logininfo');
+ } else if ($('footer').length) {
+ ele = $('footer');
+ } else {
+ ele = $('body');
+ }
+ templates.render('tool_usertours/resettour', {})
+ .done(function(html, js) {
+ templates.appendNodeContents(ele, html, js);
});
},
--- /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 tool_usertours/resettour
+
+ Adds the Reset user tour on this page link.
+
+ Example context (json):
+ {}
+}}
+<div class="usertour">
+ <a href="#" data-action="tool_usertours/resetpagetour">{{#str}}resettouronpage, tool_usertours{{/str}}</a>
+</div>
//some plugins may want to have own test client forms
include_once($CFG->dirroot.'/'.$finfo->testclientpath);
}
- $class = $f->name.'_form';
+ $class = $f->name.'_testclient_form';
if (class_exists($class)) {
$functions[$f->name] = $f->name;
continue;
die;
}
-$class = $function.'_form';
+$class = $function.'_testclient_form';
$mform = new $class(null, array('authmethod' => $authmethod));
$mform->set_data(array('function'=>$function, 'protocol'=>$protocol));
// === Test client forms ===
/**
- * Form class for create_categories() web service function test.
+ * Base class for implementations of WS test client forms.
*
* @package core_webservice
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @copyright 2012 Fabio Souto
+ * @copyright 2017 Marina Glancy
*/
-class core_course_create_categories_form extends moodleform {
+abstract class webservice_test_client_base_form extends moodleform {
+
+ /**
+ * Definition of the parameters used by this WS function
+ */
+ protected abstract function test_client_definition();
+
/**
* The form definition.
*/
public function definition() {
- global $CFG;
-
$mform = $this->_form;
$mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
}
$mform->addElement('hidden', 'authmethod', $data['authmethod']);
- $mform->setType('authmethod', core_user::get_property_type('auth'));
- $mform->addElement('text', 'name[0]', 'name[0]');
- $mform->addElement('text', 'parent[0]', 'parent[0]');
- $mform->addElement('text', 'idnumber[0]', 'idnumber[0]');
- $mform->addElement('text', 'description[0]', 'description[0]');
- $mform->addElement('text', 'name[1]', 'name[1]');
- $mform->addElement('text', 'parent[1]', 'parent[1]');
- $mform->addElement('text', 'idnumber[1]', 'idnumber[1]');
- $mform->addElement('text', 'description[1]', 'description[1]');
- $mform->setType('name', core_user::get_property_type('firstname'));
- $mform->setType('parent', core_user::get_property_type('id'));
- $mform->setType('idnumber', core_user::get_property_type('idnumber'));
- $mform->setType('description', core_user::get_property_type('description'));
+ $mform->setType('authmethod', PARAM_ALPHA);
$mform->addElement('hidden', 'function');
$mform->setType('function', PARAM_PLUGIN);
$mform->addElement('hidden', 'protocol');
$mform->setType('protocol', PARAM_ALPHA);
+ $this->test_client_definition();
+
$this->add_action_buttons(true, get_string('execute', 'webservice'));
}
if (!$data = $this->get_data()) {
return null;
}
- // Remove unused from form data.
- unset($data->submitbutton);
- unset($data->protocol);
- unset($data->function);
- unset($data->wsusername);
- unset($data->wspassword);
- unset($data->token);
- unset($data->authmethod);
+ return array_diff_key((array)$data, ['submitbutton' => 1, 'protocol' => 1, 'function' => 1,
+ 'wsusername' => 1, 'wspassword' => 1, 'token' => 1, 'authmethod' => 1]);
+ }
+}
+
+/**
+ * Form class for create_categories() web service function test.
+ *
+ * @package core_webservice
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2012 Fabio Souto
+ */
+class core_course_create_categories_testclient_form extends webservice_test_client_base_form {
+ /**
+ * The form definition.
+ */
+ protected function test_client_definition() {
+ $mform = $this->_form;
+ $mform->addElement('text', 'name[0]', 'name[0]');
+ $mform->addElement('text', 'parent[0]', 'parent[0]');
+ $mform->addElement('text', 'idnumber[0]', 'idnumber[0]');
+ $mform->addElement('text', 'description[0]', 'description[0]');
+ $mform->addElement('text', 'name[1]', 'name[1]');
+ $mform->addElement('text', 'parent[1]', 'parent[1]');
+ $mform->addElement('text', 'idnumber[1]', 'idnumber[1]');
+ $mform->addElement('text', 'description[1]', 'description[1]');
+ $mform->setType('name', PARAM_TEXT);
+ $mform->setType('parent', PARAM_INT);
+ $mform->setType('idnumber', PARAM_RAW);
+ $mform->setType('description', PARAM_RAW);
+ }
+
+ /**
+ * Get the parameters that the user submitted using the form.
+ * @return array|null
+ */
+ public function get_params() {
+ if (!$data = $this->get_data()) {
+ return null;
+ }
$params = array();
$params['categories'] = array();
for ($i=0; $i<10; $i++) {
- if (empty($data->name[$i]) or empty($data->parent[$i])) {
+ if (empty($data->name[$i])) {
continue;
}
$params['categories'][] = array('name'=>$data->name[$i], 'parent'=>$data->parent[$i],
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @copyright 2012 Fabio Souto
*/
-class core_course_delete_categories_form extends moodleform {
+class core_course_delete_categories_testclient_form extends webservice_test_client_base_form {
/**
* The form definition.
*/
- public function definition() {
- global $CFG;
-
+ protected function test_client_definition() {
$mform = $this->_form;
-
- $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
- // Note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters.
- $data = $this->_customdata;
- if ($data['authmethod'] == 'simple') {
- $mform->addElement('text', 'wsusername', 'wsusername');
- $mform->setType('wsusername', core_user::get_property_type('username'));
- $mform->addElement('text', 'wspassword', 'wspassword');
- $mform->setType('wspassword', core_user::get_property_type('password'));
- } else if ($data['authmethod'] == 'token') {
- $mform->addElement('text', 'token', 'token');
- $mform->setType('token', PARAM_RAW_TRIMMED);
- }
-
- $mform->addElement('hidden', 'authmethod', $data['authmethod']);
- $mform->setType('authmethod', core_user::get_property_type('auth'));
$mform->addElement('text', 'id[0]', 'id[0]');
$mform->addElement('text', 'newparent[0]', 'newparent[0]');
$mform->addElement('text', 'recursive[0]', 'recursive[0]');
$mform->addElement('text', 'id[1]', 'id[1]');
$mform->addElement('text', 'newparent[1]', 'newparent[1]');
$mform->addElement('text', 'recursive[1]', 'recursive[1]');
- $mform->setType('id', core_user::get_property_type('id'));
+ $mform->setType('id', PARAM_INT);
$mform->setType('newparent', PARAM_INT);
$mform->setType('recursive', PARAM_BOOL);
-
- $mform->addElement('hidden', 'function');
- $mform->setType('function', PARAM_PLUGIN);
-
- $mform->addElement('hidden', 'protocol');
- $mform->setType('protocol', PARAM_ALPHA);
-
- $this->add_action_buttons(true, get_string('execute', 'webservice'));
}
/**
if (!$data = $this->get_data()) {
return null;
}
- // Remove unused from form data.
- unset($data->submitbutton);
- unset($data->protocol);
- unset($data->function);
- unset($data->wsusername);
- unset($data->wspassword);
- unset($data->token);
- unset($data->authmethod);
-
$params = array();
$params['categories'] = array();
for ($i=0; $i<10; $i++) {
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @copyright 2012 Fabio Souto
*/
-class core_course_update_categories_form extends moodleform {
+class core_course_update_categories_testclient_form extends webservice_test_client_base_form {
/**
* The form definition.
*/
- public function definition() {
- global $CFG;
-
+ protected function test_client_definition() {
$mform = $this->_form;
-
- $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
- // Note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters.
- $data = $this->_customdata;
- if ($data['authmethod'] == 'simple') {
- $mform->addElement('text', 'wsusername', 'wsusername');
- $mform->setType('wsusername', core_user::get_property_type('username'));
- $mform->addElement('text', 'wspassword', 'wspassword');
- $mform->setType('wspassword', core_user::get_property_type('password'));
- } else if ($data['authmethod'] == 'token') {
- $mform->addElement('text', 'token', 'token');
- $mform->setType('token', PARAM_RAW_TRIMMED);
- }
-
- $mform->addElement('hidden', 'authmethod', $data['authmethod']);
- $mform->setType('authmethod', core_user::get_property_type('auth'));
$mform->addElement('text', 'id[0]', 'id[0]');
$mform->addElement('text', 'name[0]', 'name[0]');
$mform->addElement('text', 'parent[0]', 'parent[0]');
$mform->addElement('text', 'parent[1]', 'parent[1]');
$mform->addElement('text', 'idnumber[1]', 'idnumber[1]');
$mform->addElement('text', 'description[1]', 'description[1]');
- $mform->setType('id', core_user::get_property_type('id'));
- $mform->setType('name', core_user::get_property_type('firstname'));
+ $mform->setType('id', PARAM_INT);
+ $mform->setType('name', PARAM_TEXT);
$mform->setType('parent', PARAM_INT);
- $mform->setType('idnumber', core_user::get_property_type('idnumber'));
- $mform->setType('description', core_user::get_property_type('description'));
-
- $mform->addElement('hidden', 'function');
- $mform->setType('function', PARAM_PLUGIN);
-
- $mform->addElement('hidden', 'protocol');
- $mform->setType('protocol', PARAM_ALPHA);
-
- $this->add_action_buttons(true, get_string('execute', 'webservice'));
+ $mform->setType('idnumber', PARAM_RAW);
+ $mform->setType('description', PARAM_RAW);
}
/**
if (!$data = $this->get_data()) {
return null;
}
- // Remove unused from form data.
- unset($data->submitbutton);
- unset($data->protocol);
- unset($data->function);
- unset($data->wsusername);
- unset($data->wspassword);
- unset($data->token);
- unset($data->authmethod);
-
$params = array();
$params['categories'] = array();
for ($i=0; $i<10; $i++) {
}
return $params;
}
-}
\ No newline at end of file
+}
+
+/**
+ * Test class for WS function core_fetch_notifications
+ *
+ * @package core_webservice
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2017 Marina Glancy
+ */
+class core_fetch_notifications_testclient_form extends webservice_test_client_base_form {
+ /**
+ * The form definition.
+ */
+ protected function test_client_definition() {
+ $mform = $this->_form;
+ $mform->addElement('text', 'contextid', 'contextid');
+ $mform->setType('contextid', PARAM_INT);
+ $mform->setDefault('contextid', context_system::instance()->id);
+ }
+}
+
+/**
+ * Test class for WS function get_site_info
+ *
+ * @package core_webservice
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2017 Marina Glancy
+ */
+class core_webservice_get_site_info_testclient_form extends webservice_test_client_base_form {
+ /**
+ * The form definition.
+ */
+ protected function test_client_definition() {
+ }
+}
+
+/**
+ * Test class for WS function core_get_string
+ *
+ * @package core_webservice
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2017 Marina Glancy
+ */
+class core_get_string_testclient_form extends webservice_test_client_base_form {
+ /**
+ * The form definition.
+ */
+ protected function test_client_definition() {
+ $mform = $this->_form;
+ $mform->addElement('text', 'stringid', 'stringid');
+ $mform->setType('stringid', PARAM_STRINGID);
+ $mform->addElement('text', 'component', 'component');
+ $mform->setType('component', PARAM_COMPONENT);
+ $mform->addElement('text', 'lang', 'lang');
+ $mform->setType('lang', PARAM_LANG);
+ $mform->addElement('text', 'stringparams_name[1]', 'Parameter 1 name');
+ $mform->setType('stringparams_name[1]', PARAM_ALPHANUMEXT);
+ $mform->addElement('text', 'stringparams_value[1]', 'Parameter 1 value');
+ $mform->setType('stringparams_value[1]', PARAM_RAW);
+ $mform->addElement('text', 'stringparams_name[2]', 'Parameter 2 name');
+ $mform->setType('stringparams_name[2]', PARAM_ALPHANUMEXT);
+ $mform->addElement('text', 'stringparams_value[2]', 'Parameter 2 value');
+ $mform->setType('stringparams_value[2]', PARAM_RAW);
+ $mform->addElement('text', 'stringparams_name[3]', 'Parameter 3 name');
+ $mform->setType('stringparams_name[3]', PARAM_ALPHANUMEXT);
+ $mform->addElement('text', 'stringparams_value[3]', 'Parameter 3 value');
+ $mform->setType('stringparams_value[3]', PARAM_RAW);
+ $mform->addElement('static', 'paramnote', '', 'If a parameter is not an object, only specify "Parameter 1 value"');
+ }
+
+ /**
+ * Get the parameters that the user submitted using the form.
+ * @return array|null
+ */
+ public function get_params() {
+ $params = parent::get_params();
+ if ($params === null) {
+ return null;
+ }
+
+ $params['stringparams'] = [];
+ for ($idx = 1; $idx <= 3; $idx++) {
+ $name = isset($params['stringparams_name'][$idx]) ? strval($params['stringparams_name'][$idx]) : '';
+ $value = isset($params['stringparams_value'][$idx]) ? strval($params['stringparams_value'][$idx]) : '';
+ if ($name !== '' || $value !== '') {
+ if ($name === '') {
+ $params['stringparams'][] = ['value' => $value];
+ } else {
+ $params['stringparams'][] = ['name' => $name, 'value' => $value];
+ }
+ }
+ }
+ unset($params['stringparams_name']);
+ unset($params['stringparams_value']);
+ return $params;
+ }
+}
return get_string('errorprocessornotready', 'analytics', $isready);
}
+ $currentvalue = get_config('analytics', 'predictionsprocessor');
+ if (!empty($currentvalue) && $currentvalue != str_replace('\\\\', '\\', $data)) {
+ // Clear all models data.
+ $models = \core_analytics\manager::get_all_models();
+ foreach ($models as $model) {
+ $model->clear();
+ }
+ }
+
return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
}
}
*/
public function get_id();
+ /**
+ * The analysable human readable name
+ *
+ * @return string
+ */
+ public function get_name();
+
/**
* The analysable context.
*
foreach ($arrays as $array) {
reset($base);
- while (list($key, $value) = each($array)) {
+ foreach ($array as $key => $value) {
if (is_array($value) && !empty($base[$key]) && is_array($base[$key])) {
$base[$key] = $this->array_merge_recursive_keep_keys($base[$key], $value);
} else {
return $this->course->id;
}
+ /**
+ * The course short name
+ *
+ * @return string
+ */
+ public function get_name() {
+ return format_string($this->course->shortname, true, array('context' => $this->get_context()));
+ }
+
/**
* get_context
*
'/timesplitting/' . self::clean_time_splitting_id($timesplittingid) . '/', self::EVALUATION_FILENAME);
}
+ /**
+ * Gets the list of files that couldn't be previously used for training and prediction.
+ *
+ * @param int $modelid
+ * @param bool $includetarget
+ * @param string[] $timesplittingids
+ * @return null
+ */
+ public static function get_pending_files($modelid, $includetarget, $timesplittingids) {
+ global $DB;
+
+ $fs = get_file_storage();
+
+ if ($includetarget) {
+ $filearea = self::LABELLED_FILEAREA;
+ $usedfileaction = 'trained';
+ } else {
+ $filearea = self::UNLABELLED_FILEAREA;
+ $usedfileaction = 'predicted';
+ }
+
+ $select = 'modelid = :modelid AND action = :action';
+ $params = array('modelid' => $modelid, 'action' => $usedfileaction);
+ $usedfileids = $DB->get_fieldset_select('analytics_used_files', 'fileid', $select, $params);
+
+ // Very likely that we will only have 1 time splitting method here.
+ $filesbytimesplitting = array();
+ foreach ($timesplittingids as $timesplittingid) {
+
+ $filepath = '/timesplitting/' . self::clean_time_splitting_id($timesplittingid) . '/';
+ $files = $fs->get_directory_files(\context_system::instance()->id, 'analytics', $filearea, $modelid, $filepath);
+ foreach ($files as $file) {
+
+ // Discard evaluation files.
+ if ($file->get_filename() === self::EVALUATION_FILENAME) {
+ continue;
+ }
+
+ // No dirs.
+ if ($file->is_directory()) {
+ continue;
+ }
+
+ // Already used for training.
+ if (in_array($file->get_id(), $usedfileids)) {
+ continue;
+ }
+
+ $filesbytimesplitting[$timesplittingid][] = $file;
+ }
+ }
+
+ return $filesbytimesplitting;
+ }
+
/**
* Deletes previous evaluation files of this model.
*
* \core_analytics\local\analyser\by_course and \core_analytics\local\analyser\sitewide are implementing
* this method returning site courses (by_course) and the whole system (sitewide) as analysables.
*
- * @return \core_analytics\analysable[]
+ * @return \core_analytics\analysable[] Array of analysable elements using the analysable id as array key.
*/
abstract public function get_analysables();
* @return \stored_file[]
*/
public function get_analysable_data($includetarget) {
+ global $DB;
+
+ // Time limit control.
+ $modeltimelimit = intval(get_config('analytics', 'modeltimelimit'));
$filesbytimesplitting = array();
- $analysables = $this->get_analysables();
- foreach ($analysables as $analysable) {
+ list($analysables, $processedanalysables) = $this->get_sorted_analysables($includetarget);
+
+ $inittime = time();
+ foreach ($analysables as $key => $analysable) {
$files = $this->process_analysable($analysable, $includetarget);
// Later we will need to aggregate data by time splitting method.
foreach ($files as $timesplittingid => $file) {
- $filesbytimesplitting[$timesplittingid][$analysable->get_id()] = $file;
+ $filesbytimesplitting[$timesplittingid][] = $file;
+ }
+
+ $this->update_analysable_analysed_time($processedanalysables, $analysable->get_id(), $includetarget);
+
+ // Apply time limit.
+ if (!$this->options['evaluation']) {
+ $timespent = time() - $inittime;
+ if ($modeltimelimit <= $timespent) {
+ break;
+ }
+ }
+
+ unset($analysables[$key]);
+ }
+
+ if ($this->options['evaluation'] === false) {
+ // Look for previous training and prediction files we generated and couldn't be used
+ // by machine learning backends because they weren't big enough.
+
+ $pendingfiles = \core_analytics\dataset_manager::get_pending_files($this->modelid, $includetarget,
+ array_keys($filesbytimesplitting));
+ foreach ($pendingfiles as $timesplittingid => $files) {
+ foreach ($files as $file) {
+ $filesbytimesplitting[$timesplittingid][] = $file;
+ }
}
}
// We join the datasets by time splitting method.
$timesplittingfiles = $this->merge_analysable_files($filesbytimesplitting, $includetarget);
+ if (!empty($pendingfiles)) {
+ // We must remove them now as they are already part of another dataset.
+ foreach ($pendingfiles as $timesplittingid => $files) {
+ foreach ($files as $file) {
+ $file->delete();
+ }
+ }
+ }
+
return $timesplittingfiles;
}
$result = $this->analysabletarget->is_valid_analysable($analysable, $includetarget);
if ($result !== true) {
$a = new \stdClass();
- $a->analysableid = $analysable->get_id();
+ $a->analysableid = $analysable->get_name();
$a->result = $result;
$this->add_log(get_string('analysablenotvalidfortarget', 'analytics', $a));
return array();
}
$a = new \stdClass();
- $a->analysableid = $analysable->get_id();
+ $a->analysableid = $analysable->get_name();
$a->errors = implode(', ', $errors);
$this->add_log(get_string('analysablenotused', 'analytics', $a));
}
$data[1][] = $value;
}
}
+
+ /**
+ * Returns the list of analysables sorted in processing priority order.
+ *
+ * It will first return analysables that have never been analysed before
+ * and it will continue with the ones we have already seen by timeanalysed DESC
+ * order.
+ *
+ * @param bool $includetarget
+ * @return array(0 => \core_analytics\analysable[], 1 => \stdClass[])
+ */
+ protected function get_sorted_analysables($includetarget) {
+
+ $analysables = $this->get_analysables();
+
+ // Get the list of analysables that have been already processed.
+ $processedanalysables = $this->get_processed_analysables($includetarget);
+
+ // We want to start processing analysables we have not yet processed and later continue
+ // with analysables that we already processed.
+ $unseen = array_diff_key($analysables, $processedanalysables);
+
+ // Var $processed first as we want to respect its timeanalysed DESC order so analysables that
+ // have recently been processed are on the bottom of the stack.
+ $seen = array_intersect_key($processedanalysables, $analysables);
+ array_walk($seen, function(&$value, $analysableid) use ($analysables) {
+ // We replace the analytics_used_analysables record by the analysable object.
+ $value = $analysables[$analysableid];
+ });
+
+ return array($unseen + $seen, $processedanalysables);
+ }
+
+ /**
+ * Get analysables that have been already processed.
+ *
+ * @param bool $includetarget
+ * @return \stdClass[]
+ */
+ protected function get_processed_analysables($includetarget) {
+ global $DB;
+
+ $params = array('modelid' => $this->modelid);
+ $params['action'] = ($includetarget) ? 'training' : 'prediction';
+ $select = 'modelid = :modelid and action = :action';
+
+ // Weird select fields ordering for performance (analysableid key matching, analysableid is also unique by modelid).
+ return $DB->get_records_select('analytics_used_analysables', $select,
+ $params, 'timeanalysed DESC', 'analysableid, modelid, action, timeanalysed, id AS primarykey');
+ }
+
+ /**
+ * Updates the analysable analysis time.
+ *
+ * @param array $processedanalysables
+ * @param int $analysableid
+ * @param bool $includetarget
+ * @return null
+ */
+ protected function update_analysable_analysed_time($processedanalysables, $analysableid, $includetarget) {
+ global $DB;
+
+ if (!empty($processedanalysables[$analysableid])) {
+ $obj = $processedanalysables[$analysableid];
+
+ $obj->id = $obj->primarykey;
+ unset($obj->primarykey);
+
+ $obj->timeanalysed = time();
+ $DB->update_record('analytics_used_analysables', $obj);
+
+ } else {
+
+ $obj = new \stdClass();
+ $obj->modelid = $this->modelid;
+ $obj->action = ($includetarget) ? 'training' : 'prediction';
+ $obj->analysableid = $analysableid;
+ $obj->timeanalysed = time();
+
+ $DB->insert_record('analytics_used_analysables', $obj);
+ }
+ }
}
foreach ($courses as $course) {
// Skip the frontpage course.
$analysable = \core_analytics\course::instance($course);
- $analysables[] = $analysable;
+ $analysables[$analysable->get_id()] = $analysable;
}
if (empty($analysables)) {
*/
public function get_analysables() {
$analysable = new \core_analytics\site();
- return array($analysable);
+ return array(SYSCONTEXTID => $analysable);
}
}
}
/**
- * Should the model callback be triggered?
+ * This method determines if a prediction is interesing for the model or not.
*
* @param mixed $predictedvalue
* @param float $predictionscore
}
/**
- * Should the model callback be triggered?
+ * This method determines if a prediction is interesing for the model or not.
+ *
+ * This method internally calls ignored_predicted_classes to skip classes
+ * flagged by the target as not important for users.
*
* @param mixed $predictedvalue
* @param float $predictionscore
}
/**
- * Should the model callback be triggered?
+ * This method determines if a prediction is interesing for the model or not.
*
* @param mixed $predictedvalue
* @param float $predictionscore
'\mod_wiki\analytics\indicator\social_breadth',
'\mod_workshop\analytics\indicator\cognitive_depth',
'\mod_workshop\analytics\indicator\social_breadth',
+ '\core_course\analytics\indicator\completion_enabled',
+ '\core_course\analytics\indicator\potential_cognitive_depth',
+ '\core_course\analytics\indicator\potential_social_breadth',
+ '\core\analytics\indicator\any_access_after_end',
+ '\core\analytics\indicator\any_access_before_start',
+ '\core\analytics\indicator\any_write_action',
+ '\core\analytics\indicator\read_actions',
);
$indicators = array();
foreach ($coiindicators as $coiindicator) {
$target = self::get_target('\core\analytics\target\no_teaching');
$timesplittingmethod = '\core\analytics\time_splitting\single_range';
$noteacher = self::get_indicator('\core_course\analytics\indicator\no_teacher');
- $indicators = array($noteacher->get_id() => $noteacher);
+ $nostudent = self::get_indicator('\core_course\analytics\indicator\no_student');
+ $indicators = array($noteacher->get_id() => $noteacher, $nostudent->get_id() => $nostudent);
if (!\core_analytics\model::exists($target, $indicators)) {
\core_analytics\model::create($target, $indicators, $timesplittingmethod);
}
/**
* Model with low prediction accuracy.
*/
- const EVALUATE_LOW_SCORE = 4;
+ const LOW_SCORE = 4;
/**
* Not enough data to evaluate the model properly.
*/
- const EVALUATE_NOT_ENOUGH_DATA = 8;
+ const NOT_ENOUGH_DATA = 8;
/**
* Invalid analysable for the time splitting method.
$this->model->indicators !== $indicatorsstr) {
// Delete generated predictions before changing the model version.
- $this->clear_model();
+ $this->clear();
// It needs to be reset as the version changes.
$this->uniqueid = null;
\core_analytics\manager::check_can_manage_models();
- $this->clear_model();
+ $this->clear();
- // Method self::clear_model is already clearing the current model version.
+ // Method self::clear is already clearing the current model version.
$predictor = \core_analytics\manager::get_predictions_processor();
$predictor->delete_output_dir($this->get_output_dir(array(), true));
$result->status = $predictorresult->status;
$result->info = $predictorresult->info;
+ if ($result->status !== self::OK) {
+ return $result;
+ }
+
$this->flag_file_as_used($samplesfile, 'trained');
// Mark the model as trained if it wasn't.
$samplesfile = $samplesdata[$this->model->timesplitting];
// We need to throw an exception if we are trying to predict stuff that was already predicted.
- $params = array('modelid' => $this->model->id, 'fileid' => $samplesfile->get_id(), 'action' => 'predicted');
+ $params = array('modelid' => $this->model->id, 'action' => 'predicted', 'fileid' => $samplesfile->get_id());
if ($predicted = $DB->get_record('analytics_used_files', $params)) {
throw new \moodle_exception('erroralreadypredict', 'analytics', '', $samplesfile->get_id());
}
$result->predictions = $this->format_predictor_predictions($predictorresult);
}
+ if ($result->status !== self::OK) {
+ return $result;
+ }
+
if ($result->predictions) {
$samplecontexts = $this->execute_prediction_callbacks($result->predictions, $indicatorcalculations);
}
private function format_predictor_predictions($predictorresult) {
$predictions = array();
- if ($predictorresult->predictions) {
+ if (!empty($predictorresult->predictions)) {
foreach ($predictorresult->predictions as $sampleinfo) {
// We parse each prediction.
// Here we will store all predictions' contexts, this will be used to limit which users will see those predictions.
$samplecontexts = array();
+ $records = array();
foreach ($predictions as $uniquesampleid => $prediction) {
- if ($this->get_target()->triggers_callback($prediction->prediction, $prediction->predictionscore)) {
+ // The unique sample id contains both the sampleid and the rangeindex.
+ list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
- // The unique sample id contains both the sampleid and the rangeindex.
- list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
+ if ($this->get_target()->triggers_callback($prediction->prediction, $prediction->predictionscore)) {
- // Store the predicted values.
+ // Prepare the record to store the predicted values.
list($record, $samplecontext) = $this->prepare_prediction_record($sampleid, $rangeindex, $prediction->prediction,
$prediction->predictionscore, json_encode($indicatorcalculations[$uniquesampleid]));
}
}
- $this->save_predictions($records);
+ if (!empty($records)) {
+ $this->save_predictions($records);
+ }
return $samplecontexts;
}
* @return \context
*/
protected function prepare_prediction_record($sampleid, $rangeindex, $prediction, $predictionscore, $calculations) {
- global $DB;
-
$context = $this->get_analyser()->sample_access_context($sampleid);
$record = new \stdClass();
$record->calculations = $calculations;
$record->timecreated = time();
+ $analysable = $this->get_analyser()->get_sample_analysable($sampleid);
+ $timesplitting = $this->get_time_splitting();
+ $timesplitting->set_analysable($analysable);
+ $range = $timesplitting->get_range_by_index($rangeindex);
+ if ($range) {
+ $record->timestart = $range['start'];
+ $record->timeend = $range['end'];
+ }
+
return array($record, $context);
}
* @return void
*/
public function enable($timesplittingid = false) {
- global $DB;
+ global $DB, $USER;
\core_analytics\manager::check_can_manage_models();
}
// Delete generated predictions before changing the model version.
- $this->clear_model();
+ $this->clear();
// It needs to be reset as the version changes.
$this->uniqueid = null;
$this->model->enabled = 1;
$this->model->timemodified = $now;
+ $this->model->usermodified = $USER->id;
// We don't always update timemodified intentionally as we reserve it for target, indicators or timesplitting updates.
$DB->update_record('analytics_models', $this->model);
$outputdir = rtrim($CFG->dataroot, '/') . DIRECTORY_SEPARATOR . 'models';
}
- // Append model id
+ // Append model id.
$outputdir .= DIRECTORY_SEPARATOR . $this->model->id;
if (!$onlymodelid) {
// Append version + subdirs.
*
* @return void
*/
- private function clear_model() {
- global $DB;
+ public function clear() {
+ global $DB, $USER;
+
+ \core_analytics\manager::check_can_manage_models();
// Delete current model version stored stuff.
$predictor = \core_analytics\manager::get_predictions_processor();
$DB->delete_records('analytics_predict_samples', array('modelid' => $this->model->id));
$DB->delete_records('analytics_train_samples', array('modelid' => $this->model->id));
$DB->delete_records('analytics_used_files', array('modelid' => $this->model->id));
+ $DB->delete_records('analytics_used_analysables', array('modelid' => $this->model->id));
// Purge all generated files.
\core_analytics\dataset_manager::clear_model_files($this->model->id);
// We don't expect people to clear models regularly and the cost of filling the cache is
// 1 db read per context.
$this->purge_insights_cache();
+
+ $this->model->trained = 0;
+ $this->model->timemodified = time();
+ $this->model->usermodified = $USER->id;
+ $DB->update_record('analytics_models', $this->model);
}
/**
return SYSCONTEXTID;
}
+ /**
+ * Site.
+ *
+ * @return string
+ */
+ public function get_name() {
+ return get_string('site');
+ }
+
/**
* Analysable context.
*
class dataset_manager_testcase extends advanced_testcase {
/**
- * test_create_dataset
+ * setUp
*
- * @return
+ * @return null
*/
- public function test_create_dataset() {
+ public function setUp() {
$this->resetAfterTest(true);
- $sharedtoprows = array(
+ $this->sharedtoprows = array(
array('var1', 'var2'),
array('value1', 'value2'),
array('header1', 'header2')
);
+ }
+
+ /**
+ * test_create_dataset
+ *
+ * @return null
+ */
+ public function test_create_dataset() {
$dataset1 = new \core_analytics\dataset_manager(1, 1, 'whatever', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
$dataset1->init_process();
- $dataset1data = array_merge($sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
+ $dataset1data = array_merge($this->sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
$f1 = $dataset1->store($dataset1data);
$dataset1->close_process();
/**
* test_merge_datasets
*
- * @return
+ * @return null
*/
public function test_merge_datasets() {
- $this->resetAfterTest(true);
-
- $sharedtoprows = array(
- array('var1', 'var2'),
- array('value1', 'value2'),
- array('header1', 'header2')
- );
$dataset1 = new \core_analytics\dataset_manager(1, 1, 'whatever', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
$dataset1->init_process();
- $dataset1data = array_merge($sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
+ $dataset1data = array_merge($this->sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
$f1 = $dataset1->store($dataset1data);
$dataset1->close_process();
$dataset2 = new \core_analytics\dataset_manager(1, 2, 'whatever', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
$dataset2->init_process();
- $dataset2data = array_merge($sharedtoprows, array(array('no', 'no', 'no')));
+ $dataset2data = array_merge($this->sharedtoprows, array(array('no', 'no', 'no')));
$f2 = $dataset2->store($dataset2data);
$dataset2->close_process();