By default InnoDB storage table is using legacy Antelope file format
which has major restriction on database row size.
-Use this script to detect and fix database tables with potentail data
+Use this script to detect and fix database tables with potential data
overflow problems.
Options:
</CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
+ <MOODLE version="2.8" requires="2.2">
+ <UNICODE level="required">
+ <FEEDBACK>
+ <ON_ERROR message="unicoderequired" />
+ </FEEDBACK>
+ </UNICODE>
+ <DATABASE level="required">
+ <VENDOR name="mariadb" version="5.5.31" />
+ <VENDOR name="mysql" version="5.5.31" />
+ <VENDOR name="postgres" version="9.1" />
+ <VENDOR name="mssql" version="10.0" />
+ <VENDOR name="oracle" version="10.2" />
+ </DATABASE>
+ <PHP version="5.4.4" level="required">
+ </PHP>
+ <PCREUNICODE level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="pcreunicodewarning" />
+ </FEEDBACK>
+ </PCREUNICODE>
+ <PHP_EXTENSIONS>
+ <PHP_EXTENSION name="iconv" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="iconvrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="mbstring" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="mbstringrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="curl" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="curlrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="openssl" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="opensslrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="tokenizer" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="tokenizerrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="xmlrpc" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="xmlrpcrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="soap" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="soaprecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="ctype" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="ctyperequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="zip" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="ziprequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="zlib" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="gd" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="gdrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="simplexml" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="simplexmlrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="spl" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="splrequired" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="pcre" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="dom" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="xml" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="intl" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="intlrecommended" />
+ </FEEDBACK>
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="json" level="required">
+ </PHP_EXTENSION>
+ <PHP_EXTENSION name="hash" level="required"/>
+ </PHP_EXTENSIONS>
+ <PHP_SETTINGS>
+ <PHP_SETTING name="memory_limit" value="96M" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="settingmemorylimit" />
+ </FEEDBACK>
+ </PHP_SETTING>
+ <PHP_SETTING name="file_uploads" value="1" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="settingfileuploads" />
+ </FEEDBACK>
+ </PHP_SETTING>
+ <PHP_SETTING name="opcache.enable" value="1" level="optional">
+ <FEEDBACK>
+ <ON_CHECK message="opcacherecommended" />
+ </FEEDBACK>
+ </PHP_SETTING>
+ </PHP_SETTINGS>
+ <CUSTOM_CHECKS>
+ <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+ <FEEDBACK>
+ <ON_ERROR message="quizattemptsupgradedmessage" />
+ </FEEDBACK>
+ </CUSTOM_CHECK>
+ </CUSTOM_CHECKS>
+ </MOODLE>
</COMPATIBILITY_MATRIX>
$fields = 'SELECT ' . $this->required_fields_sql('u');
$countfields = 'SELECT COUNT(u.id)';
- $sql = " FROM {user} u
- LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.roleid = :roleid AND ra.contextid = :contextid)
- WHERE u.id IN ($enrolsql)
- $wherecondition
- AND ra.id IS NULL";
+ $sql = " FROM ($enrolsql) enrolled_users_view
+ JOIN {user} u ON u.id = enrolled_users_view.id
+ LEFT JOIN {role_assignments} ra ON (ra.userid = enrolled_users_view.id AND
+ ra.roleid = :roleid AND ra.contextid = :contextid)
+ WHERE ra.id IS NULL
+ $wherecondition";
$params['contextid'] = $this->context->id;
$params['roleid'] = $this->roleid;
GRADE_AGGREGATE_MODE =>new lang_string('aggregatemode', 'grades'),
GRADE_AGGREGATE_SUM =>new lang_string('aggregatesum', 'grades'));
- $defaultvisible = array(GRADE_AGGREGATE_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2,
- GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_MEDIAN, GRADE_AGGREGATE_MIN,
- GRADE_AGGREGATE_MAX, GRADE_AGGREGATE_MODE, GRADE_AGGREGATE_SUM);
+ $defaultvisible = array(GRADE_AGGREGATE_SUM);
- $defaults = array('value'=>GRADE_AGGREGATE_WEIGHTED_MEAN2, 'forced'=>false, 'adv'=>false);
+ $defaults = array('value' => GRADE_AGGREGATE_SUM, 'forced' => false, 'adv' => false);
$temp->add(new admin_setting_gradecat_combo('grade_aggregation', new lang_string('aggregation', 'grades'), new lang_string('aggregation_help', 'grades'), $defaults, $options));
$temp->add(new admin_setting_configmultiselect('grade_aggregations_visible', new lang_string('aggregationsvisible', 'grades'),
| url | Test url name | Test url description | C1 | url1 |
| wiki | Test wiki name | Test wiki description | C1 | wiki1 |
| workshop | Test workshop name | Test workshop description | C1 | workshop1 |
+ And the following "scales" exist:
+ | name | scale |
+ | Test Scale 1 | Disappointing, Good, Very good, Excellent |
+ And the following "activities" exist:
+ | activity | name | intro | course | idnumber | grade |
+ | assign | Test assignment name with scale | Test assignment description | C1 | assign1 | Test Scale 1 |
When I log in as "admin"
And I follow "Course 1"
Then I should see "Test assignment name"
And I should see "Test workshop name"
And I follow "Test assignment name"
And I should see "Test assignment description"
+ And I follow "C1"
+ And I follow "Test assignment name with scale"
+ And I follow "Edit settings"
+ And the field "Type" matches value "Scale"
@javascript
Scenario: Add relations between users and groups
| Course 1 | C1 |
And the following "grade categories" exist:
| fullname | course |
- | Grade category 1 | C1|
+ | Grade category 1 | C1 |
And the following "grade categories" exist:
| fullname | course | gradecategory |
- | Grade sub category 2 | C1 | Grade category 1|
+ | Grade sub category 2 | C1 | Grade category 1 |
When I log in as "admin"
And I follow "Courses"
And I follow "Course 1"
And I navigate to "Grades" node in "Course administration"
Then I should see "Grade category 1"
And I should see "Grade sub category 2"
+
+ Scenario: Add a bunch of grade items
+ Given the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And the following "grade categories" exist:
+ | fullname | course |
+ | Grade category 1 | C1 |
+ And the following "grade categories" exist:
+ | fullname | course | gradecategory |
+ | Grade sub category 2 | C1 | Grade category 1 |
+ And the following "grade items" exist:
+ | itemname | course |
+ | Test Grade Item 1 | C1 |
+ And the following "grade items" exist:
+ | itemname | course | gradecategory |
+ | Test Grade Item 2 | C1 | Grade category 1 |
+ | Test Grade Item 3 | C1 | Grade sub category 2 |
+ When I log in as "admin"
+ And I follow "Course 1"
+ And I follow "Grades"
+ And I expand "Setup" node
+ And I follow "Categories and items"
+ Then I should see "Test Grade Item 1"
+ And I follow "Edit Test Grade Item 1"
+ And I expand all fieldsets
+ And I should see "Course 1"
+ And I press "Cancel"
+ And I should see "Grade category 1"
+ And I should see "Test Grade Item 2"
+ And I follow "Edit Test Grade Item 2"
+ And I expand all fieldsets
+ And I should see "Grade category 1"
+ And I press "Cancel"
+ And I should see "Grade sub category 2"
+ And I should see "Test Grade Item 3"
+ And I follow "Edit Test Grade Item 3"
+ And I expand all fieldsets
+ And I should see "Grade sub category 2"
+ And I press "Cancel"
+
+ Scenario: Add a bunch of scales
+ Given the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And the following "scales" exist:
+ | name | scale |
+ | Test Scale 1 | Disappointing, Good, Very good, Excellent |
+ When I log in as "admin"
+ And I follow "Course 1"
+ And I follow "Grades"
+ And I follow "Scales"
+ Then I should see "Test Scale 1"
+ And I should see "Disappointing, Good, Very good, Excellent"
+
+ Scenario: Add a bunch of outcomes
+ Given the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And the following "scales" exist:
+ | name | scale |
+ | Test Scale 1 | Disappointing, Good, Very good, Excellent |
+ And the following "grade outcomes" exist:
+ | fullname | shortname | scale |
+ | Grade outcome 1 | OT1 | Test Scale 1 |
+ And the following "grade outcomes" exist:
+ | fullname | shortname | course | scale |
+ | Grade outcome 2 | OT2 | C1 | Test Scale 1 |
+ When I log in as "admin"
+ And I set the following administration settings values:
+ | Enable outcomes | 1 |
+ And I follow "Home"
+ And I follow "Course 1"
+ And I follow "Outcomes"
+ Then I should see "Grade outcome 1" in the "#addoutcomes" "css_element"
+ And I should see "Grade outcome 2" in the "#removeoutcomes" "css_element"
+ And I follow "Edit outcomes"
+ And the following should exist in the "generaltable" table:
+ | Full name | Short name | Scale |
+ | Grade outcome 2 | OT2 | Test Scale 1 |
+
+ Scenario: Add a bunch of outcome grade items
+ Given the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And the following "scales" exist:
+ | name | scale |
+ | Test Scale 1 | Disappointing, Good, Very good, Excellent |
+ And the following "grade outcomes" exist:
+ | fullname | shortname | course | scale |
+ | Grade outcome 1 | OT1 | C1 | Test Scale 1 |
+ And the following "grade categories" exist:
+ | fullname | course |
+ | Grade category 1 | C1 |
+ And the following "grade items" exist:
+ | itemname | course | outcome | gradecategory |
+ | Test Outcome Grade Item 1 | C1 | OT1 | Grade category 1 |
+ When I log in as "admin"
+ And I set the following administration settings values:
+ | Enable outcomes | 1 |
+ And I follow "Home"
+ And I follow "Course 1"
+ And I follow "Grades"
+ And I expand "Setup" node
+ And I follow "Categories and items"
+ Then I should see "Test Outcome Grade Item 1"
+ And I follow "Edit Test Outcome Grade Item 1"
+ And the field "Outcome" matches value "Grade outcome 1"
+ And I expand all fieldsets
+ And "//div[contains(@class, 'fitem')]/div[contains(@class, 'fitemtitle')]/div[contains(@class, fstaticlabel) and contains(., 'Grade category')]/../../div[contains(@class, 'felement') and contains(., 'Grade category 1')]" "xpath_element" should exist
+ And I press "Cancel"
And I add a "Lesson" to section "1"
And I set the following fields to these values:
| Name | Test lesson |
+ | Description | Test lesson description |
| available[enabled] | 1 |
And I set the field "deadline[enabled]" to "1"
# Checkbox (AJAX) - Checking "the field matches value" before saving.
--- /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/>.
+
+/**
+ * Lang import controller
+ *
+ * @package tool_langimport
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport;
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir.'/filelib.php');
+require_once($CFG->libdir.'/componentlib.class.php');
+
+/**
+ * Lang import controller
+ *
+ * @package tool_langimport
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class controller {
+ /** @var array list of informational messages */
+ public $info;
+ /** @var array list of error messages */
+ public $errors;
+ /** @var \lang_installer */
+ private $installer;
+ /** @var array languages available on the remote server */
+ public $availablelangs;
+
+ /**
+ * Constructor.
+ */
+ public function __construct() {
+ make_temp_directory('');
+ make_upload_directory('lang');
+
+ $this->info = array();
+ $this->errors = array();
+ $this->installer = new \lang_installer();
+
+ $this->availablelangs = $this->installer->get_remote_list_of_languages();
+ }
+
+ /**
+ * Install language packs provided
+ *
+ * @param string|array $langs array of langcodes or individual langcodes
+ * @param bool $updating true if updating the langpacks
+ * @return int false if an error encountered or
+ * @throws \moodle_exception when error is encountered installing langpack
+ */
+ public function install_languagepacks($langs, $updating = false) {
+ global $CFG;
+
+ $this->installer->set_queue($langs);
+ $results = $this->installer->run();
+
+ $updatedpacks = 0;
+
+ foreach ($results as $langcode => $langstatus) {
+ switch ($langstatus) {
+ case \lang_installer::RESULT_DOWNLOADERROR:
+ $a = new \stdClass();
+ $a->url = $this->installer->lang_pack_url($langcode);
+ $a->dest = $CFG->dataroot.'/lang';
+ $this->errors[] = get_string('remotedownloaderror', 'error', $a);
+ throw new \moodle_exception('remotedownloaderror', 'error', $a);
+ break;
+ case \lang_installer::RESULT_INSTALLED:
+ $updatedpacks++;
+ if ($updating) {
+ event\langpack_updated::event_with_langcode($langcode)->trigger();
+ $this->info[] = get_string('langpackupdated', 'tool_langimport', $langcode);
+ } else {
+ $this->info[] = get_string('langpackinstalled', 'tool_langimport', $langcode);
+ event\langpack_imported::event_with_langcode($langcode)->trigger();
+ }
+ break;
+ case \lang_installer::RESULT_UPTODATE:
+ $this->info[] = get_string('langpackuptodate', 'tool_langimport', $langcode);
+ break;
+ }
+ }
+
+ return $updatedpacks;
+ }
+
+ /**
+ * Uninstall language pack
+ *
+ * @param string $lang language code
+ * @return bool true if language succesfull installed
+ */
+ public function uninstall_language($lang) {
+ global $CFG;
+
+ $dest1 = $CFG->dataroot.'/lang/'.$lang;
+ $dest2 = $CFG->dirroot.'/lang/'.$lang;
+ $rm1 = false;
+ $rm2 = false;
+ if (file_exists($dest1)) {
+ $rm1 = remove_dir($dest1);
+ }
+ if (file_exists($dest2)) {
+ $rm2 = remove_dir($dest2);
+ }
+
+ if ($rm1 or $rm2) {
+ $this->info[] = get_string('langpackremoved', 'tool_langimport', $lang);
+ event\langpack_removed::event_with_langcode($lang)->trigger();
+ return true;
+ } else { // Nothing deleted, possibly due to permission error.
+ $this->errors[] = 'An error has occurred, language pack is not completely uninstalled, please check file permissions';
+ return false;
+ }
+ }
+
+ /**
+ * Updated all install language packs with the latest found on servre
+ *
+ * @return bool true if languages succesfully updated.
+ */
+ public function update_all_installed_languages() {
+ global $CFG;
+
+ if (!$availablelangs = $this->installer->get_remote_list_of_languages()) {
+ $this->errors[] = get_string('cannotdownloadlanguageupdatelist', 'error');
+ return false;
+ }
+
+ $md5array = array(); // Convert to (string)langcode => (string)md5.
+ foreach ($availablelangs as $alang) {
+ $md5array[$alang[0]] = $alang[1];
+ }
+
+ // Filter out unofficial packs.
+ $currentlangs = array_keys(get_string_manager()->get_list_of_translations(true));
+ $updateablelangs = array();
+ foreach ($currentlangs as $clang) {
+ if (!array_key_exists($clang, $md5array)) {
+ $noticeok[] = get_string('langpackupdateskipped', 'tool_langimport', $clang);
+ continue;
+ }
+ $dest1 = $CFG->dataroot.'/lang/'.$clang;
+ $dest2 = $CFG->dirroot.'/lang/'.$clang;
+
+ if (file_exists($dest1.'/langconfig.php') || file_exists($dest2.'/langconfig.php')) {
+ $updateablelangs[] = $clang;
+ }
+ }
+
+ // Filter out packs that have the same md5 key.
+ $neededlangs = array();
+ foreach ($updateablelangs as $ulang) {
+ if (!$this->is_installed_lang($ulang, $md5array[$ulang])) {
+ $neededlangs[] = $ulang;
+ }
+ }
+
+ // Clean-up currently installed versions of the packs.
+ foreach ($neededlangs as $packindex => $pack) {
+ if ($pack == 'en') {
+ continue;
+ }
+
+ // Delete old directories.
+ $dest1 = $CFG->dataroot.'/lang/'.$pack;
+ $dest2 = $CFG->dirroot.'/lang/'.$pack;
+ if (file_exists($dest1)) {
+ if (!remove_dir($dest1)) {
+ $noticeerror[] = 'Could not delete old directory '.$dest1.', update of '.$pack
+ .' failed, please check permissions.';
+ unset($neededlangs[$packindex]);
+ continue;
+ }
+ }
+ if (file_exists($dest2)) {
+ if (!remove_dir($dest2)) {
+ $noticeerror[] = 'Could not delete old directory '.$dest2.', update of '.$pack
+ .' failed, please check permissions.';
+ unset($neededlangs[$packindex]);
+ continue;
+ }
+ }
+ }
+
+ try {
+ $updated = $this->install_languagepacks($neededlangs, true);
+ } catch (\moodle_exception $e) {
+ return false;
+ }
+
+ if ($updated) {
+ $this->info[] = get_string('langupdatecomplete', 'tool_langimport');
+ } else {
+ $this->info[] = get_string('nolangupdateneeded', 'tool_langimport');
+ }
+
+ return true;
+ }
+
+ /**
+ * checks the md5 of the zip file, grabbed from download.moodle.org,
+ * against the md5 of the local language file from last update
+ * @param string $lang language code
+ * @param string $md5check md5 to check
+ * @return bool true if installed
+ */
+ public function is_installed_lang($lang, $md5check) {
+ global $CFG;
+ $md5file = $CFG->dataroot.'/lang/'.$lang.'/'.$lang.'.md5';
+ if (file_exists($md5file)) {
+ return (file_get_contents($md5file) == $md5check);
+ }
+ return false;
+ }
+}
+
+
--- /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/>.
+
+/**
+ * The langimport langpack imported event.
+ *
+ * @package tool_langimport
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_langimport langpack imported event class.
+ *
+ * @property-read array $other {
+ * Extra information about event.
+ *
+ * - string langcode: the langpage pack code.
+ * }
+ *
+ * @package tool_langimport
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class langpack_imported extends \core\event\base {
+ /**
+ * Create instance of event.
+ *
+ * @param string $langcode
+ * @return langpack_updated
+ */
+ public static function event_with_langcode($langcode) {
+ $data = array(
+ 'context' => \context_system::instance(),
+ 'other' => array(
+ 'langcode' => $langcode,
+ )
+ );
+
+ return self::create($data);
+ }
+
+ /**
+ * Init method.
+ *
+ * @return void
+ */
+ protected function init() {
+ $this->data['crud'] = 'c';
+ $this->data['edulevel'] = self::LEVEL_OTHER;
+ }
+
+ /**
+ * Returns description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The language pack '{$this->other['langcode']}' was installed.";
+ }
+
+ /**
+ * Return localised event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('langpackinstalledevent', 'tool_langimport');
+ }
+
+ /**
+ * Returns relevant URL.
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new \moodle_url('/admin/tool/langimport/');
+ }
+
+ /**
+ * Custom validation.
+ *
+ * @throws \coding_exception
+ */
+ protected function validate_data() {
+ parent::validate_data();
+
+ if (!isset($this->other['langcode'])) {
+ throw new \coding_exception('The \'langcode\' value must be set');
+ }
+ // We can't use PARAM_LANG here as the string manager might not be aware of langpack yet.
+ $cleanedlang = clean_param($this->other['langcode'], PARAM_SAFEDIR);
+ if ($cleanedlang !== $this->other['langcode']) {
+ throw new \coding_exception('The \'langcode\' value must be set to a valid language code');
+ }
+ }
+}
--- /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/>.
+
+/**
+ * The langimport langpack removed event.
+ *
+ * @package tool_langimport
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_langimport langpack removed event class.
+ *
+ * @property-read array $other {
+ * Extra information about event.
+ *
+ * - string langcode: the langpage pack code.
+ * }
+ *
+ * @package tool_langimport
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class langpack_removed extends \core\event\base {
+ /**
+ * Create instance of event.
+ *
+ * @param string $langcode
+ * @return langpack_updated
+ */
+ public static function event_with_langcode($langcode) {
+ $data = array(
+ 'context' => \context_system::instance(),
+ 'other' => array(
+ 'langcode' => $langcode,
+ )
+ );
+
+ return self::create($data);
+ }
+
+ /**
+ * Init method.
+ *
+ * @return void
+ */
+ protected function init() {
+ $this->data['crud'] = 'd';
+ $this->data['edulevel'] = self::LEVEL_OTHER;
+ }
+
+ /**
+ * Returns description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The language pack '{$this->other['langcode']}' was removed.";
+ }
+
+ /**
+ * Returns relevant URL.
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new \moodle_url('/admin/tool/langimport/');
+ }
+
+ /**
+ * Return localised event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('langpackremovedevent', 'tool_langimport');
+ }
+
+ /**
+ * Custom validation.
+ *
+ * @throws \coding_exception
+ */
+ protected function validate_data() {
+ parent::validate_data();
+
+ if (!isset($this->other['langcode'])) {
+ throw new \coding_exception('The \'langcode\' value must be set');
+ }
+
+ // We can't use PARAM_LANG here as it queries installed strings.
+ $cleanedlang = clean_param($this->other['langcode'], PARAM_SAFEDIR);
+ if ($cleanedlang !== $this->other['langcode']) {
+ throw new \coding_exception('The \'langcode\' value must be set to a valid language code');
+ }
+ }
+}
--- /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/>.
+
+/**
+ * The langimport langpack updated event.
+ *
+ * @package tool_langimport
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The tool_langimport langpack updated event class.
+ *
+ * @property-read array $other {
+ * Extra information about event.
+ *
+ * - string langcode: the langpage pack code.
+ * }
+ *
+ * @package tool_langimport
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class langpack_updated extends \core\event\base {
+ /**
+ * Create instance of event.
+ *
+ * @param string $langcode
+ * @return langpack_updated
+ */
+ public static function event_with_langcode($langcode) {
+ $data = array(
+ 'context' => \context_system::instance(),
+ 'other' => array(
+ 'langcode' => $langcode,
+ )
+ );
+
+ return self::create($data);
+ }
+
+ /**
+ * Init method.
+ *
+ * @return void
+ */
+ protected function init() {
+ $this->data['crud'] = 'u';
+ $this->data['edulevel'] = self::LEVEL_OTHER;
+ }
+
+ /**
+ * Returns description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The language pack '{$this->other['langcode']}' was updated.";
+ }
+
+ /**
+ * Return localised event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('langpackupdatedevent', 'tool_langimport');
+ }
+
+ /**
+ * Returns relevant URL.
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new \moodle_url('/admin/tool/langimport/');
+ }
+
+ /**
+ * Custom validation.
+ *
+ * @throws \coding_exception
+ */
+ protected function validate_data() {
+ parent::validate_data();
+
+ if (!isset($this->other['langcode'])) {
+ throw new \coding_exception('The \'langcode\' value must be set');
+ }
+
+ $cleanedlang = clean_param($this->other['langcode'], PARAM_LANG);
+ if ($cleanedlang !== $this->other['langcode']) {
+ throw new \coding_exception('The \'langcode\' value must be set to a valid language code');
+ }
+ }
+}
--- /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/>.
+
+/**
+ * A scheduled task for updating langpacks.
+ *
+ * @package tool_langimport
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_langimport\task;
+
+/**
+ * A scheduled task for updating langpacks.
+ *
+ * @package tool_langimport
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class update_langpacks_task extends \core\task\scheduled_task {
+
+ /**
+ * Get a descriptive name for this task (shown to admins).
+ *
+ * @return string
+ */
+ public function get_name() {
+ return get_string('updatelangs', 'tool_langimport');
+ }
+
+ /**
+ * Run langpack update
+ */
+ public function execute() {
+ $controller = new \tool_langimport\controller();
+ if ($controller->update_all_installed_languages()) {
+ foreach ($controller->info as $message) {
+ mtrace($message);
+ }
+ return true;
+ } else {
+ foreach ($controller->errors as $message) {
+ mtrace($message);
+ }
+ return false;
+ }
+
+ }
+
+}
--- /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/>.
+
+/**
+ * Definition of langimport tasks
+ *
+ * @package tool_langimport
+ * @category task
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tasks = array(
+ array(
+ 'classname' => 'tool_langimport\task\update_langpacks_task',
+ 'blocking' => 0,
+ 'minute' => 'R',
+ 'hour' => '4',
+ 'day' => '*',
+ 'month' => '*',
+ 'dayofweek' => '*'
+ )
+);
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-require(dirname(dirname(dirname(dirname(__FILE__)))) . '/config.php');
+require(__DIR__.'/../../../config.php');
require_once($CFG->libdir.'/adminlib.php');
-require_once($CFG->libdir.'/filelib.php');
-require_once($CFG->libdir.'/componentlib.class.php');
admin_externalpage_setup('toollangimport');
get_string_manager()->reset_caches();
-$notice_ok = array();
-$notice_error = array();
+$controller = new tool_langimport\controller();
if (($mode == INSTALLATION_OF_SELECTED_LANG) and confirm_sesskey() and !empty($pack)) {
core_php_time_limit::raise();
- make_temp_directory('');
- make_upload_directory('lang');
-
- $installer = new lang_installer($pack);
- $results = $installer->run();
- foreach ($results as $langcode => $langstatus) {
- switch ($langstatus) {
- case lang_installer::RESULT_DOWNLOADERROR:
- $a = new stdClass();
- $a->url = $installer->lang_pack_url($langcode);
- $a->dest = $CFG->dataroot.'/lang';
- print_error('remotedownloaderror', 'error', 'index.php', $a);
- break;
- case lang_installer::RESULT_INSTALLED:
- $notice_ok[] = get_string('langpackinstalled', 'tool_langimport', $langcode);
- break;
- case lang_installer::RESULT_UPTODATE:
- $notice_ok[] = get_string('langpackuptodate', 'tool_langimport', $langcode);
- break;
- }
- }
+ $controller->install_languagepacks($pack);
}
if ($mode == DELETION_OF_SELECTED_LANG and !empty($uninstalllang)) {
if ($uninstalllang == 'en') {
- $notice_error[] = 'English language pack can not be uninstalled';
+ // TODO.
+ $controller->errors[] = 'English language pack can not be uninstalled';
} else if (!$confirm and confirm_sesskey()) {
echo $OUTPUT->header();
die;
} else if (confirm_sesskey()) {
- $dest1 = $CFG->dataroot.'/lang/'.$uninstalllang;
- $dest2 = $CFG->dirroot.'/lang/'.$uninstalllang;
- $rm1 = false;
- $rm2 = false;
- if (file_exists($dest1)){
- $rm1 = remove_dir($dest1);
- }
- if (file_exists($dest2)){
- $rm2 = remove_dir($dest2);
- }
- if ($rm1 or $rm2) {
- $notice_ok[] = get_string('langpackremoved', 'tool_langimport');
- } else { //nothing deleted, possibly due to permission error
- $notice_error[] = 'An error has occurred, language pack is not completely uninstalled, please check file permissions';
- }
+ $controller->uninstall_language($uninstalllang);
}
}
if ($mode == UPDATE_ALL_LANG) {
core_php_time_limit::raise();
-
- $installer = new lang_installer();
-
- if (!$availablelangs = $installer->get_remote_list_of_languages()) {
- print_error('cannotdownloadlanguageupdatelist', 'error');
- }
- $md5array = array(); // (string)langcode => (string)md5
- foreach ($availablelangs as $alang) {
- $md5array[$alang[0]] = $alang[1];
- }
-
- // filter out unofficial packs
- $currentlangs = array_keys(get_string_manager()->get_list_of_translations(true));
- $updateablelangs = array();
- foreach ($currentlangs as $clang) {
- if (!array_key_exists($clang, $md5array)) {
- $notice_ok[] = get_string('langpackupdateskipped', 'tool_langimport', $clang);
- continue;
- }
- $dest1 = $CFG->dataroot.'/lang/'.$clang;
- $dest2 = $CFG->dirroot.'/lang/'.$clang;
-
- if (file_exists($dest1.'/langconfig.php') || file_exists($dest2.'/langconfig.php')){
- $updateablelangs[] = $clang;
- }
- }
-
- // then filter out packs that have the same md5 key
- $neededlangs = array(); // all the packs that needs updating
- foreach ($updateablelangs as $ulang) {
- if (!is_installed_lang($ulang, $md5array[$ulang])) {
- $neededlangs[] = $ulang;
- }
- }
-
- make_temp_directory('');
- make_upload_directory('lang');
-
- // clean-up currently installed versions of the packs
- foreach ($neededlangs as $packindex => $pack) {
- if ($pack == 'en') {
- continue;
- }
-
- // delete old directories
- $dest1 = $CFG->dataroot.'/lang/'.$pack;
- $dest2 = $CFG->dirroot.'/lang/'.$pack;
- $rm1 = false;
- $rm2 = false;
- if (file_exists($dest1)) {
- if (!remove_dir($dest1)) {
- $notice_error[] = 'Could not delete old directory '.$dest1.', update of '.$pack.' failed, please check permissions.';
- unset($neededlangs[$packindex]);
- continue;
- }
- }
- if (file_exists($dest2)) {
- if (!remove_dir($dest2)) {
- $notice_error[] = 'Could not delete old directory '.$dest2.', update of '.$pack.' failed, please check permissions.';
- unset($neededlangs[$packindex]);
- continue;
- }
- }
- }
-
- // install all needed language packs
- $installer->set_queue($neededlangs);
- $results = $installer->run();
- $updated = false; // any packs updated?
- foreach ($results as $langcode => $langstatus) {
- switch ($langstatus) {
- case lang_installer::RESULT_DOWNLOADERROR:
- $a = new stdClass();
- $a->url = $installer->lang_pack_url($langcode);
- $a->dest = $CFG->dataroot.'/lang';
- print_error('remotedownloaderror', 'error', 'index.php', $a);
- break;
- case lang_installer::RESULT_INSTALLED:
- $updated = true;
- $notice_ok[] = get_string('langpackinstalled', 'tool_langimport', $langcode);
- break;
- case lang_installer::RESULT_UPTODATE:
- $notice_ok[] = get_string('langpackuptodate', 'tool_langimport', $langcode);
- break;
- }
- }
-
- if ($updated) {
- $notice_ok[] = get_string('langupdatecomplete', 'tool_langimport');
- } else {
- $notice_ok[] = get_string('nolangupdateneeded', 'tool_langimport');
- }
-
- unset($installer);
+ $controller->update_all_installed_languages();
}
get_string_manager()->reset_caches();
}
}
-$installer = new lang_installer();
-
-if ($availablelangs = $installer->get_remote_list_of_languages()) {
+if ($availablelangs = $controller->availablelangs) {
$remote = true;
} else {
$remote = false;
echo $OUTPUT->box_end();
}
-if ($notice_ok) {
- $info = implode('<br />', $notice_ok);
+if ($controller->info) {
+ $info = implode('<br />', $controller->info);
echo $OUTPUT->notification($info, 'notifysuccess');
}
-if ($notice_error) {
- $info = implode('<br />', $notice_error);
+if ($controller->errors) {
+ $info = implode('<br />', $controller->errors);
echo $OUTPUT->notification($info, 'notifyproblem');
}
if ($missingparents) {
- foreach ($missingparents as $l=>$parent) {
+ foreach ($missingparents as $l => $parent) {
$a = new stdClass();
$a->lang = $installedlangs[$l];
$a->parent = $parent;
// list of available languages
$options = array();
foreach ($availablelangs as $alang) {
- if (!empty($alang[0]) and trim($alang[0]) !== 'en' and !is_installed_lang($alang[0], $alang[1])) {
+ if (!empty($alang[0]) and trim($alang[0]) !== 'en' and !$controller->is_installed_lang($alang[0], $alang[1])) {
$options[$alang[0]] = $alang[2].' ('.$alang[0].')';
}
}
echo $OUTPUT->box_end();
echo $OUTPUT->footer();
die();
-
-////////////////////////////////////////////////////////////////////////////////
-// Local functions /////////////////////////////////////////////////////////////
-////////////////////////////////////////////////////////////////////////////////
-
-/**
- * checks the md5 of the zip file, grabbed from download.moodle.org,
- * against the md5 of the local language file from last update
- * @param string $lang
- * @param string $md5check
- * @return bool
- */
-function is_installed_lang($lang, $md5check) {
- global $CFG;
- $md5file = $CFG->dataroot.'/lang/'.$lang.'/'.$lang.'.md5';
- if (file_exists($md5file)){
- return (file_get_contents($md5file) == $md5check);
- }
- return false;
-}
$string['installedlangs'] = 'Installed language packs';
$string['langimport'] = 'Language import utility';
$string['langimportdisabled'] = 'Language import feature has been disabled. You have to update your language packs manually at the file-system level. Do not forget to purge string caches after you do so.';
-$string['langpackinstalled'] = 'Language pack {$a} was successfully installed';
-$string['langpackremoved'] = 'Language pack was uninstalled';
-$string['langpackupdateskipped'] = 'Update of {$a} language pack skipped';
-$string['langpackuptodate'] = 'Language pack {$a} is up-to-date';
+$string['langpackinstalled'] = 'Language pack \'{$a}\' was successfully installed';
+$string['langpackinstalledevent'] = 'Language pack installed';
+$string['langpackremoved'] = 'Language pack \'{$a}\' was uninstalled';
+$string['langpackremovedevent'] = 'Language pack uninstalled';
+$string['langpackupdateskipped'] = 'Update of \'{$a}\' language pack skipped';
+$string['langpackuptodate'] = 'Language pack \'{$a}\' is up-to-date';
+$string['langpackupdated'] = 'Language pack \'{$a}\' was successfully updated';
+$string['langpackupdatedevent'] = 'Language pack updated';
$string['langupdatecomplete'] = 'Language pack update completed';
$string['missingcfglangotherroot'] = 'Missing configuration value $CFG->langotherroot';
$string['missinglangparent'] = 'Missing parent language <em>{$a->parent}</em> of <em>{$a->lang}</em>.';
--- /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/>.
+
+/**
+ * Behat steps definitions for Language import tool
+ *
+ * @package tool_langimport
+ * @category test
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+
+use Moodle\BehatExtension\Exception\SkippedException;
+
+/**
+ * Steps definitions related with the Language import tool
+ *
+ * @package tool_langimport
+ * @category test
+ * @copyright 2014 Dan Poltawski <dan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_tool_langimport extends behat_base {
+
+ /**
+ * This step looks to see if the remote language import tests should be run (indicated by
+ * setting TOOL_LANGIMPORT_REMOTE_TESTS in config.php.
+ *
+ * @Given /^remote langimport tests are enabled$/
+ */
+ public function remote_langimport_tests_are_enabled() {
+ if (!defined('TOOL_LANGIMPORT_REMOTE_TESTS')) {
+ throw new SkippedException('To run the remote langimport tests you must '.
+ 'define TOOL_LANGIMPORT_REMOTE_TESTS in config.php');
+ }
+ }
+
+ /**
+ * Downloads a langpack and fakes it being outdated
+ *
+ * @param string $langcode The language code (e.g. en)
+ * @Given /^outdated langpack \'([^\']*)\' is installed$/
+ */
+ public function outdated_langpack_is_installed($langcode) {
+ global $CFG;
+ require_once($CFG->libdir.'/componentlib.class.php');
+
+ // Download the langpack.
+ $dir = make_upload_directory('lang');
+ $installer = new lang_installer($langcode);
+ $result = $installer->run();
+
+ if ($result[$langcode] !== lang_installer::RESULT_INSTALLED) {
+ throw new coding_exception("Failed to install langpack '$langcode'");
+ }
+
+ $path = "$dir/$langcode/$langcode.md5";
+
+ if (!file_exists($path)) {
+ throw new coding_exception("Failed to find '$langcode' checksum");
+ }
+ file_put_contents($path, '000000');
+ }
+}
--- /dev/null
+@tool @tool_langimport
+Feature: Manage language packs
+ In order to support different languages
+ As an administrator
+ I need to be able to add, update and remove language packs
+
+
+ Background:
+ Given remote langimport tests are enabled
+
+ # The pirate language pack is used for testing because its small to download.
+
+ Scenario: Install language pack
+ Given I log in as "admin"
+ And I navigate to "Language packs" node in "Site administration > Language"
+ When I set the field "Available language packs" to "English - Pirate (en_ar)"
+ And I press "Install selected language pack(s)"
+ Then I should see "Language pack 'en_ar' was successfully installed"
+ And the "Installed language packs" select box should contain "English - Pirate (en_ar)"
+ And I navigate to "Live logs" node in "Site administration > Reports"
+ And I should see "The language pack 'en_ar' was installed."
+ And I log out
+
+ Scenario: Update language pack
+ Given outdated langpack 'en_ar' is installed
+ And I log in as "admin"
+ And I navigate to "Language packs" node in "Site administration > Language"
+ When I press "Update all installed language packs"
+ Then I should see "Language pack 'en_ar' was successfully updated"
+ And I should see "Language pack update completed"
+ And I navigate to "Live logs" node in "Site administration > Reports"
+ And I should see "The language pack 'en_ar' was updated."
+ And I log out
+
+ Scenario: Try to uninstall language pack
+ Given I log in as "admin"
+ And I navigate to "Language packs" node in "Site administration > Language"
+ And I set the field "Available language packs" to "English - Pirate (en_ar)"
+ And I press "Install selected language pack(s)"
+ When I set the field "Installed language packs" to "English - Pirate (en_ar)"
+ And I press "Uninstall selected language pack"
+ And I press "Continue"
+ Then I should see "Language pack 'en_ar' was uninstalled"
+ And the "Installed language packs" select box should not contain "English - Pirate (en_ar)"
+ And the "Available language packs" select box should contain "English - Pirate (en_ar)"
+ And I navigate to "Live logs" node in "Site administration > Reports"
+ And I should see "The language pack 'en_ar' was removed."
+ And I should see "Language pack uninstalled"
+ And I log out
+
+ Scenario: Try to uninstall English language pack
+ Given I log in as "admin"
+ And I navigate to "Language packs" node in "Site administration > Language"
+ When I set the field "Installed language packs" to "English (en)"
+ And I press "Uninstall selected language pack"
+ Then I should see "English language pack can not be uninstalled"
+ And I navigate to "Live logs" node in "Site administration > Reports"
+ And I should not see "Language pack uninstalled"
+ And I log out
--- /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/>.
+
+/**
+ * Tests for langimport events.
+ *
+ * @package tool_langimport
+ * @copyright 2014 Dan Poltawski
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Test class for langimport events.
+ *
+ * @package tool_langimport
+ * @copyright 2014 Dan Poltawski
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
+ */
+class tool_langimport_events_testcase extends advanced_testcase {
+
+ /**
+ * Setup testcase.
+ */
+ public function setUp() {
+ $this->setAdminUser();
+ $this->resetAfterTest();
+ }
+
+ public function test_langpack_updated() {
+ global $CFG;
+
+ $event = \tool_langimport\event\langpack_updated::event_with_langcode($CFG->lang);
+
+ // Trigger and capture the event.
+ $sink = $this->redirectEvents();
+ $event->trigger();
+ $events = $sink->get_events();
+ $event = reset($events);
+
+ $this->assertInstanceOf('\tool_langimport\event\langpack_updated', $event);
+ $this->assertEquals(context_system::instance(), $event->get_context());
+ }
+
+ public function test_langpack_updated_validation() {
+ $this->setExpectedException('coding_exception', 'The \'langcode\' value must be set to a valid language code');
+
+ \tool_langimport\event\langpack_updated::event_with_langcode('broken langcode');
+ }
+
+ public function test_langpack_installed() {
+ $event = \tool_langimport\event\langpack_imported::event_with_langcode('fr');
+
+ // Trigger and capture the event.
+ $sink = $this->redirectEvents();
+ $event->trigger();
+ $events = $sink->get_events();
+ $event = reset($events);
+
+ $this->assertInstanceOf('\tool_langimport\event\langpack_imported', $event);
+ $this->assertEquals(context_system::instance(), $event->get_context());
+ }
+
+ public function test_langpack_installed_validation() {
+ $this->setExpectedException('coding_exception', 'The \'langcode\' value must be set to a valid language code');
+
+ \tool_langimport\event\langpack_imported::event_with_langcode('broken langcode');
+ }
+
+ public function test_langpack_removed() {
+ $event = \tool_langimport\event\langpack_removed::event_with_langcode('fr');
+
+ // Trigger and capture the event.
+ $sink = $this->redirectEvents();
+ $event->trigger();
+ $events = $sink->get_events();
+ $event = reset($events);
+
+ $this->assertInstanceOf('\tool_langimport\event\langpack_removed', $event);
+ $this->assertEquals(context_system::instance(), $event->get_context());
+ }
+
+ public function test_langpack_removed_validation() {
+ $this->setExpectedException('coding_exception', 'The \'langcode\' value must be set to a valid language code');
+
+ \tool_langimport\event\langpack_removed::event_with_langcode('broken langcode');
+ }
+}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2014051200; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version = 2014092801; // The current plugin version (Date: YYYYMMDDXX)
$plugin->requires = 2014050800; // Requires this Moodle version
$plugin->component = 'tool_langimport'; // Full name of the plugin (used for diagnostics)
--- /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/>.
+
+/**
+ * Form to edit handlers.
+ *
+ * @package tool_messageinbound
+ * @copyright 2014 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/formslib.php');
+
+/**
+ * Form to edit handlers.
+ *
+ * @copyright 2014 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_messageinbound_edit_handler_form extends moodleform {
+ public function definition() {
+ $mform = $this->_form;
+
+ $handler = $this->_customdata['handler'];
+
+ // Set up the options for formatting text for descriptions, etc.
+ $formatoptions = new stdClass();
+ $formatoptions->trusted = false;
+ $formatoptions->noclean = false;
+ $formatoptions->smiley = false;
+ $formatoptions->filter = false;
+ $formatoptions->para = true;
+ $formatoptions->newlines = false;
+ $formatoptions->overflowdiv = true;
+
+ // General information about the handler.
+ $mform->addElement('header', 'general', get_string('general'));
+ $mform->addElement('static', 'name', get_string('name', 'tool_messageinbound'),
+ $handler->name);
+ $mform->addElement('static', 'classname', get_string('classname', 'tool_messageinbound'));
+
+ $description = format_text($handler->description, FORMAT_MARKDOWN, $formatoptions);
+
+ $mform->addElement('static', 'description', get_string('description', 'tool_messageinbound'),
+ $description);
+
+ // Items which can be configured.
+ $mform->addElement('header', 'configuration', get_string('configuration'));
+
+ $options = array(
+ HOURSECS => get_string('onehour', 'tool_messageinbound'),
+ DAYSECS => get_string('oneday', 'tool_messageinbound'),
+ WEEKSECS => get_string('oneweek', 'tool_messageinbound'),
+ YEARSECS => get_string('oneyear', 'tool_messageinbound'),
+ '' => get_string('noexpiry', 'tool_messageinbound'),
+ );
+ $mform->addElement('select', 'defaultexpiration', get_string('defaultexpiration', 'tool_messageinbound'), $options);
+ $mform->addHelpButton('defaultexpiration', 'defaultexpiration', 'tool_messageinbound');
+
+ if ($handler->can_change_validateaddress()) {
+ $mform->addElement('checkbox', 'validateaddress', get_string('requirevalidation', 'tool_messageinbound'));
+ $mform->addHelpButton('validateaddress', 'validateaddress', 'tool_messageinbound');
+ } else {
+ if ($handler->validateaddress) {
+ $text = get_string('yes');
+ } else {
+ $text = get_string('no');
+ }
+ $mform->addElement('static', 'validateaddress_fake', get_string('requirevalidation', 'tool_messageinbound'), $text);
+ $mform->addElement('hidden', 'validateaddress');
+ $mform->addHelpButton('validateaddress_fake', 'fixedvalidateaddress', 'tool_messageinbound');
+ $mform->setType('validateaddress', PARAM_INT);
+ }
+
+ if ($handler->can_change_enabled()) {
+ $mform->addElement('checkbox', 'enabled', get_string('enabled', 'tool_messageinbound'));
+ } else {
+ if ($handler->enabled) {
+ $text = get_string('yes');
+ } else {
+ $text = get_string('no');
+ }
+ $mform->addElement('static', 'enabled_fake', get_string('enabled', 'tool_messageinbound'), $text);
+ $mform->addHelpButton('enabled', 'fixedenabled', 'tool_messageinbound');
+ $mform->addElement('hidden', 'enabled');
+ $mform->setType('enabled', PARAM_INT);
+ }
+
+ $this->add_action_buttons(true, get_string('savechanges'));
+ }
+}
--- /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/>.
+
+/**
+ * The Mail Pickup Manager.
+ *
+ * @package tool_messageinbound
+ * @copyright 2014 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_messageinbound;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Mail Pickup Manager.
+ *
+ * @copyright 2014 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class manager {
+
+ /**
+ * @var string The main mailbox to check.
+ */
+ const MAILBOX = 'INBOX';
+
+ /**
+ * @var string The mailbox to store messages in when they are awaiting confirmation.
+ */
+ const CONFIRMATIONFOLDER = 'tobeconfirmed';
+
+ /**
+ * @var string The flag for seen/read messages.
+ */
+ const MESSAGE_SEEN = '\seen';
+
+ /**
+ * @var string The flag for flagged messages.
+ */
+ const MESSAGE_FLAGGED = '\flagged';
+
+ /**
+ * @var string The flag for deleted messages.
+ */
+ const MESSAGE_DELETED = '\deleted';
+
+ /**
+ * @var Horde_Imap_Client_Socket A reference to the IMAP client.
+ */
+ protected $client = null;
+
+ /**
+ * @var \core\message\inbound\address_manager A reference to the Inbound Message Address Manager instance.
+ */
+ protected $addressmanager = null;
+
+ /**
+ * @var stdClass The data for the current message being processed.
+ */
+ protected $currentmessagedata = null;
+
+ /**
+ * Retrieve the connection to the IMAP client.
+ *
+ * @return bool Whether a connection was successfully established.
+ */
+ protected function get_imap_client() {
+ global $CFG;
+
+ if (!\core\message\inbound\manager::is_enabled()) {
+ // E-mail processing not set up.
+ mtrace("Inbound Message not fully configured - exiting early.");
+ return false;
+ }
+
+ mtrace("Connecting to {$CFG->messageinbound_host} as {$CFG->messageinbound_hostuser}...");
+
+ $configuration = array(
+ 'username' => $CFG->messageinbound_hostuser,
+ 'password' => $CFG->messageinbound_hostpass,
+ 'hostspec' => $CFG->messageinbound_host,
+ 'secure' => $CFG->messageinbound_hostssl,
+ );
+
+ $this->client = new \Horde_Imap_Client_Socket($configuration);
+
+ try {
+ $this->client->login();
+ mtrace("Connection established.");
+ return true;
+
+ } catch (\Horde_Imap_Client_Exception $e) {
+ $message = $e->getMessage();
+ mtrace("Unable to connect to IMAP server. Failed with '{$message}'");
+
+ return false;
+ }
+ }
+
+ /**
+ * Shutdown and close the connection to the IMAP client.
+ */
+ protected function close_connection() {
+ if ($this->client) {
+ $this->client->close();
+ }
+ $this->client = null;
+ }
+
+ /**
+ * Get the current mailbox information.
+ *
+ * @return \Horde_Imap_Client_Mailbox
+ */
+ protected function get_mailbox() {
+ // Get the current mailbox.
+ $mailbox = $this->client->currentMailbox();
+
+ if (isset($mailbox['mailbox'])) {
+ return $mailbox['mailbox'];
+ } else {
+ throw new \core\message\inbound\processing_failed_exception('couldnotopenmailbox', 'tool_messageinbound');
+ }
+ }
+
+ /**
+ * Execute the main Inbound Message pickup task.
+ */
+ public function pickup_messages() {
+ if (!$this->get_imap_client()) {
+ return false;
+ }
+
+ // Restrict results to messages which are unseen, and have not been flagged.
+ $search = new \Horde_Imap_Client_Search_Query();
+ $search->flag(self::MESSAGE_SEEN, false);
+ $search->flag(self::MESSAGE_FLAGGED, false);
+ mtrace("Searching for Unseen, Unflagged email in the folder '" . self::MAILBOX . "'");
+ $results = $this->client->search(self::MAILBOX, $search);
+
+ // We require the envelope data and structure of each message.
+ $query = new \Horde_Imap_Client_Fetch_Query();
+ $query->envelope();
+ $query->structure();
+
+ // Retrieve the message id.
+ $messages = $this->client->fetch(self::MAILBOX, $query, array('ids' => $results['match']));
+
+ mtrace("Found " . $messages->count() . " messages to parse. Parsing...");
+ $this->addressmanager = new \core\message\inbound\address_manager();
+ foreach ($messages as $message) {
+ $this->process_message($message);
+ }
+
+ // Close the client connection.
+ $this->close_connection();
+
+ return true;
+ }
+
+ /**
+ * Process a message received and validated by the Inbound Message processor.
+ *
+ * @param stdClass $maildata The data retrieved from the database for the current record.
+ * @return bool Whether the message was successfully processed.
+ */
+ public function process_existing_message(\stdClass $maildata) {
+ // Grab the new IMAP client.
+ if (!$this->get_imap_client()) {
+ return false;
+ }
+
+ // Build the search.
+ $search = new \Horde_Imap_Client_Search_Query();
+ // When dealing with Inbound Message messages, we mark them as flagged and seen. Restrict the search to those criterion.
+ $search->flag(self::MESSAGE_SEEN, true);
+ $search->flag(self::MESSAGE_FLAGGED, true);
+ mtrace("Searching for a Seen, Flagged message in the folder '" . self::CONFIRMATIONFOLDER . "'");
+
+ // Match the message ID.
+ $search->headerText('message-id', $maildata->messageid);
+ $search->headerText('to', $maildata->address);
+
+ $results = $this->client->search(self::CONFIRMATIONFOLDER, $search);
+
+ // Build the base query.
+ $query = new \Horde_Imap_Client_Fetch_Query();
+ $query->envelope();
+ $query->structure();
+
+
+ // Fetch the first message from the client.
+ $messages = $this->client->fetch(self::CONFIRMATIONFOLDER, $query, array('ids' => $results['match']));
+ $this->addressmanager = new \core\message\inbound\address_manager();
+ if ($message = $messages->first()) {
+ mtrace("--> Found the message. Passing back to the pickup system.");
+
+ // Process the message.
+ $this->process_message($message, true, true);
+
+ // Close the client connection.
+ $this->close_connection();
+
+ mtrace("============================================================================");
+ return true;
+ } else {
+ // Close the client connection.
+ $this->close_connection();
+
+ mtrace("============================================================================");
+ throw new \core\message\inbound\processing_failed_exception('oldmessagenotfound', 'tool_messageinbound');
+ }
+ }
+
+ /**
+ * Tidy up old messages in the confirmation folder.
+ *
+ * @return bool Whether tidying occurred successfully.
+ */
+ public function tidy_old_messages() {
+ // Grab the new IMAP client.
+ if (!$this->get_imap_client()) {
+ return false;
+ }
+
+ // Open the mailbox.
+ mtrace("Searching for messages older than 24 hours in the '" .
+ self::CONFIRMATIONFOLDER . "' folder.");
+ $this->client->openMailbox(self::CONFIRMATIONFOLDER);
+
+ $mailbox = $this->get_mailbox();
+
+ // Build the search.
+ $search = new \Horde_Imap_Client_Search_Query();
+
+ // Delete messages older than 24 hours old.
+ $search->intervalSearch(DAYSECS, \Horde_Imap_Client_Search_Query::INTERVAL_OLDER);
+
+ $results = $this->client->search($mailbox, $search);
+
+ // Build the base query.
+ $query = new \Horde_Imap_Client_Fetch_Query();
+ $query->envelope();
+
+ // Retrieve the messages and mark them for removal.
+ $messages = $this->client->fetch($mailbox, $query, array('ids' => $results['match']));
+ mtrace("Found " . $messages->count() . " messages for removal.");
+ foreach ($messages as $message) {
+ $this->add_flag_to_message($message->getUid(), self::MESSAGE_DELETED);
+ }
+
+ mtrace("Finished removing messages.");
+ $this->close_connection();
+
+ return true;
+ }
+
+ /**
+ * Process a message and pass it through the Inbound Message handling systems.
+ *
+ * @param Horde_Imap_Client_Data_Fetch $message The message to process
+ * @param bool $viewreadmessages Whether to also look at messages which have been marked as read
+ * @param bool $skipsenderverification Whether to skip the sender verificiation stage
+ */
+ public function process_message(
+ \Horde_Imap_Client_Data_Fetch $message,
+ $viewreadmessages = false,
+ $skipsenderverification = false) {
+ global $USER;
+
+ // We use the Client IDs several times - store them here.
+ $messageid = new \Horde_Imap_Client_Ids($message->getUid());
+
+ mtrace("- Parsing message " . $messageid);
+
+ // First flag this message to prevent another running hitting this message while we look at the headers.
+ $this->add_flag_to_message($messageid, self::MESSAGE_FLAGGED);
+
+ // Record the user that this script is currently being run as. This is important when re-processing existing
+ // messages, as cron_setup_user is called multiple times.
+ $originaluser = $USER;
+
+ $envelope = $message->getEnvelope();
+ $recipients = $envelope->to->bare_addresses;
+ foreach ($recipients as $recipient) {
+ if (!\core\message\inbound\address_manager::is_correct_format($recipient)) {
+ // Message did not contain a subaddress.
+ mtrace("- Recipient '{$recipient}' did not match Inbound Message headers.");
+ continue;
+ }
+
+ // Message contained a match.
+ $senders = $message->getEnvelope()->from->bare_addresses;
+ if (count($senders) !== 1) {
+ mtrace("- Received multiple senders. Only the first sender will be used.");
+ }
+ $sender = array_shift($senders);
+
+ mtrace("-- Subject:\t" . $envelope->subject);
+ mtrace("-- From:\t" . $sender);
+ mtrace("-- Recipient:\t" . $recipient);
+
+ // Grab messagedata including flags.
+ $query = new \Horde_Imap_Client_Fetch_Query();
+ $query->structure();
+ $messagedata = $this->client->fetch($this->get_mailbox(), $query, array(
+ 'ids' => $messageid,
+ ))->first();
+
+ if (!$viewreadmessages && $this->message_has_flag($messageid, self::MESSAGE_SEEN)) {
+ // Something else has already seen this message. Skip it now.
+ mtrace("-- Skipping the message - it has been marked as seen - perhaps by another process.");
+ continue;
+ }
+
+ // Mark it as read to lock the message.
+ $this->add_flag_to_message($messageid, self::MESSAGE_SEEN);
+
+ // Now pass it through the Inbound Message processor.
+ $status = $this->addressmanager->process_envelope($recipient, $sender);
+
+ if (($status & ~ \core\message\inbound\address_manager::VALIDATION_DISABLED_HANDLER) !== $status) {
+ // The handler is disabled.
+ mtrace("-- Skipped message - Handler is disabled. Fail code {$status}");
+ // In order to handle the user error, we need more information about the message being failed.
+ $this->process_message_data($envelope, $messagedata, $messageid);
+ $this->inform_user_of_error(get_string('handlerdisabled', 'tool_messageinbound', $this->currentmessagedata));
+ return;
+ }
+
+ // Check the validation status early. No point processing garbage messages, but we do need to process it
+ // for some validation failure types.
+ if (!$this->passes_key_validation($status, $messageid)) {
+ // None of the above validation failures were found. Skip this message.
+ mtrace("-- Skipped message - it does not appear to relate to a Inbound Message pickup. Fail code {$status}");
+
+ // Remove the seen flag from the message as there may be multiple recipients.
+ $this->remove_flag_from_message($messageid, self::MESSAGE_SEEN);
+
+ // Skip further processing for this recipient.
+ continue;
+ }
+
+ // Process the message as the user.
+ $user = $this->addressmanager->get_data()->user;
+ mtrace("-- Processing the message as user {$user->id} ({$user->username}).");
+ cron_setup_user($user);
+
+ // Process and retrieve the message data for this message.
+ // This includes fetching the full content, as well as all headers, and attachments.
+ $this->process_message_data($envelope, $messagedata, $messageid);
+
+ // When processing validation replies, we need to skip the sender verification phase as this has been
+ // manually completed.
+ if (!$skipsenderverification && $status !== 0) {
+ // Check the validation status for failure types which require confirmation.
+ // The validation result is tested in a bitwise operation.
+ mtrace("-- Message did not meet validation but is possibly recoverable. Fail code {$status}");
+ // This is a recoverable error, but requires user input.
+
+ if ($this->handle_verification_failure($messageid, $recipient)) {
+ mtrace("--- Original message retained on mail server and confirmation message sent to user.");
+ } else {
+ mtrace("--- Invalid Recipient Handler - unable to save. Informing the user of the failure.");
+ $this->inform_user_of_error(get_string('invalidrecipientfinal', 'tool_messageinbound', $this->currentmessagedata));
+ }
+
+ // Returning to normal cron user.
+ mtrace("-- Returning to the original user.");
+ cron_setup_user($originaluser);
+ return;
+ }
+
+ // Add the content and attachment data.
+ mtrace("-- Validation completed. Fetching rest of message content.");
+ $this->process_message_data_body($messagedata, $messageid);
+
+ // The message processor throws exceptions upon failure. These must be caught and notifications sent to
+ // the user here.
+ try {
+ $result = $this->send_to_handler();
+ } catch (\core\message\inbound\processing_failed_exception $e) {
+ // We know about these kinds of errors and they should result in the user being notified of the
+ // failure. Send the user a notification here.
+ $this->inform_user_of_error($e->getMessage());
+
+ // Returning to normal cron user.
+ mtrace("-- Returning to the original user.");
+ cron_setup_user($originaluser);
+ return;
+ } catch (Exception $e) {
+ // An unknown error occurred. The user is not informed, but the administrator is.
+ mtrace("-- Message processing failed. An unexpected exception was thrown. Details follow.");
+ mtrace($e->getMessage());
+
+ // Returning to normal cron user.
+ mtrace("-- Returning to the original user.");
+ cron_setup_user($originaluser);
+ return;
+ }
+
+ if ($result) {
+ // Handle message cleanup. Messages are deleted once fully processed.
+ mtrace("-- Marking the message for removal.");
+ $this->add_flag_to_message($messageid, self::MESSAGE_DELETED);
+ } else {
+ mtrace("-- The Inbound Message processor did not return a success status. Skipping message removal.");
+ }
+
+ // Returning to normal cron user.
+ mtrace("-- Returning to the original user.");
+ cron_setup_user($originaluser);
+
+ mtrace("-- Finished processing " . $message->getUid());
+
+ // Skip the outer loop too. The message has already been processed and it could be possible for there to
+ // be two recipients in the envelope which match somehow.
+ return;
+ }
+ }
+
+ /**
+ * Process a message to retrieve it's header data without body and attachemnts.
+ *
+ * @param Horde_Imap_Client_Data_Envelope $envelope The Envelope of the message
+ * @param Horde_Imap_Client_Data_Fetch $messagedata The structure and part of the message body
+ * @param string|Horde_Imap_Client_Ids $messageid The Hore message Uid
+ * @return \stdClass The current value of the messagedata
+ */
+ private function process_message_data(
+ \Horde_Imap_Client_Data_Envelope $envelope,
+ \Horde_Imap_Client_Data_Fetch $basemessagedata,
+ $messageid) {
+
+ // Get the current mailbox.
+ $mailbox = $this->get_mailbox();
+
+ // We need the structure at various points below.
+ $structure = $basemessagedata->getStructure();
+
+ // Now fetch the rest of the message content.
+ $query = new \Horde_Imap_Client_Fetch_Query();
+ $query->imapDate();
+
+ // Fetch all of the message parts too.
+ $typemap = $structure->contentTypeMap();
+ foreach ($typemap as $part => $type) {
+ // The header.
+ $query->headerText(array(
+ 'id' => $part,
+ ));
+ }
+
+ $messagedata = $this->client->fetch($mailbox, $query, array('ids' => $messageid))->first();
+
+ // Store the data for this message.
+ $headers = '';
+
+ foreach ($typemap as $part => $type) {
+ // Grab all of the header data into a string.
+ $headers .= $messagedata->getHeaderText($part);
+
+ // We don't handle any of the other MIME content at this stage.
+ }
+
+ $data = new \stdClass();
+
+ // The message ID should always be in the first part.
+ $data->messageid = $messagedata->getHeaderText(0, \Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->getValue('Message-ID');
+ $data->subject = $envelope->subject;
+ $data->timestamp = $messagedata->getImapDate()->__toString();
+ $data->envelope = $envelope;
+ $data->data = $this->addressmanager->get_data();
+ $data->headers = $headers;
+
+ $this->currentmessagedata = $data;
+
+ return $this->currentmessagedata;
+ }
+
+ /**
+ * Process a message again to add body and attachment data.
+ *
+ * @param Horde_Imap_Client_Data_Envelope $envelope The Envelope of the message
+ * @param Horde_Imap_Client_Data_Fetch $basemessagedata The structure and part of the message body
+ * @param string|Horde_Imap_Client_Ids $messageid The Hore message Uid
+ * @return \stdClass The current value of the messagedata
+ */
+ private function process_message_data_body(
+ \Horde_Imap_Client_Data_Fetch $basemessagedata,
+ $messageid) {
+ global $CFG;
+
+ // Get the current mailbox.
+ $mailbox = $this->get_mailbox();
+
+ // We need the structure at various points below.
+ $structure = $basemessagedata->getStructure();
+
+ // Now fetch the rest of the message content.
+ $query = new \Horde_Imap_Client_Fetch_Query();
+ $query->fullText();
+
+ // Fetch all of the message parts too.
+ $typemap = $structure->contentTypeMap();
+ foreach ($typemap as $part => $type) {
+ // The body of the part - attempt to decode it on the server.
+ $query->bodyPart($part, array(
+ 'decode' => true,
+ 'peek' => true,
+ ));
+ $query->bodyPartSize($part);
+ }
+
+ $messagedata = $this->client->fetch($mailbox, $query, array('ids' => $messageid))->first();
+
+ // Store the data for this message.
+ $contentplain = '';
+ $contenthtml = '';
+ $attachments = array(
+ 'inline' => array(),
+ 'attachment' => array(),
+ );
+
+ $plainpartid = $structure->findBody('plain');
+ $htmlpartid = $structure->findBody('html');
+
+ foreach ($typemap as $part => $type) {
+ // Get the message data from the body part, and combine it with the structure to give a fully-formed output.
+ $stream = $messagedata->getBodyPart($part, true);
+ $partdata = $structure->getPart($part);
+ $partdata->setContents($stream, array(
+ 'usestream' => true,
+ ));
+
+ if ($part === $plainpartid) {
+ $contentplain = $this->process_message_part_body($messagedata, $partdata, $part);
+
+ } else if ($part === $htmlpartid) {
+ $contenthtml = $this->process_message_part_body($messagedata, $partdata, $part);
+
+ } else if ($filename = $partdata->getName($part)) {
+ if ($attachment = $this->process_message_part_attachment($messagedata, $partdata, $part, $filename)) {
+ // The disposition should be one of 'attachment', 'inline'.
+ // If an empty string is provided, default to 'attachment'.
+ $disposition = $partdata->getDisposition();
+ $disposition = $disposition == 'inline' ? 'inline' : 'attachment';
+ $attachments[$disposition][] = $attachment;
+ }
+ }
+
+ // We don't handle any of the other MIME content at this stage.
+ }
+
+ // The message ID should always be in the first part.
+ $this->currentmessagedata->plain = $contentplain;
+ $this->currentmessagedata->html = $contenthtml;
+ $this->currentmessagedata->attachments = $attachments;
+
+ return $this->currentmessagedata;
+ }
+
+ /**
+ * Process the messagedata and part data to extract the content of this part.
+ *
+ * @param $messagedata The structure and part of the message body
+ * @param $partdata The part data
+ * @param $part The part ID
+ * @return string
+ */
+ private function process_message_part_body($messagedata, $partdata, $part) {
+ // This is a content section for the main body.
+
+ // Get the string version of it.
+ $content = $messagedata->getBodyPart($part);
+ if (!$messagedata->getBodyPartDecode($part)) {
+ // Decode the content.
+ $partdata->setContents($content);
+ $content = $partdata->getContents();
+ }
+
+ // Convert the text from the current encoding to UTF8.
+ $content = \core_text::convert($content, $partdata->getCharset());
+
+ // Fix any invalid UTF8 characters.
+ // Note: XSS cleaning is not the responsibility of this code. It occurs immediately before display when
+ // format_text is called.
+ $content = clean_param($content, PARAM_RAW);
+
+ return $content;
+ }
+
+ /**
+ * Process a message again to add body and attachment data.
+ *
+ * @param $messagedata The structure and part of the message body
+ * @param $partdata The part data
+ * @param $filename The filename of the attachment
+ * @return \stdClass
+ */
+ private function process_message_part_attachment($messagedata, $partdata, $part, $filename) {
+ global $CFG;
+
+ // If a filename is present, assume that this part is an attachment.
+ $attachment = new \stdClass();
+ $attachment->filename = $filename;
+ $attachment->type = $partdata->getType();
+ $attachment->content = $partdata->getContents();
+ $attachment->charset = $partdata->getCharset();
+ $attachment->description = $partdata->getDescription();
+ $attachment->contentid = $partdata->getContentId();
+ $attachment->filesize = $messagedata->getBodyPartSize($part);
+
+ if (empty($CFG->runclamonupload) or empty($CFG->pathtoclam)) {
+ mtrace("--> Attempting virus scan of '{$attachment->filename}'");
+
+ // Store the file on disk - it will need to be virus scanned first.
+ $itemid = rand(1, 999999999);;
+ $directory = make_temp_directory("/messageinbound/{$itemid}", false);
+ $filepath = $directory . "/" . $attachment->filename;
+ if (!$fp = fopen($filepath, "w")) {
+ // Unable to open the temporary file to write this to disk.
+ mtrace("--> Unable to save the file to disk for virus scanning. Check file permissions.");
+
+ throw new \core\message\inbound\processing_failed_exception('attachmentfilepermissionsfailed',
+ 'tool_messageinbound');
+ }
+
+ fwrite($fp, $attachment->content);
+ fclose($fp);
+
+ // Perform a virus scan now.
+ try {
+ \repository::antivir_scan_file($filepath, $attachment->filename, true);
+ } catch (moodle_exception $e) {
+ mtrace("--> A virus was found in the attachment '{$attachment->filename}'.");
+ $this->inform_attachment_virus();
+ return;
+ }
+ }
+
+ return $attachment;
+ }
+
+ /**
+ * Check whether the key provided is valid.
+ *
+ * @param $status The Message to process
+ * @param $messageid The Hore message Uid
+ * @return bool
+ */
+ private function passes_key_validation($status, $messageid) {
+ // The validation result is tested in a bitwise operation.
+ if ((
+ $status & ~ \core\message\inbound\address_manager::VALIDATION_SUCCESS
+ & ~ \core\message\inbound\address_manager::VALIDATION_UNKNOWN_DATAKEY
+ & ~ \core\message\inbound\address_manager::VALIDATION_EXPIRED_DATAKEY
+ & ~ \core\message\inbound\address_manager::VALIDATION_INVALID_HASH
+ & ~ \core\message\inbound\address_manager::VALIDATION_ADDRESS_MISMATCH) !== 0) {
+
+ // One of the above bits was found in the status - fail the validation.
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Add the specified flag to the message.
+ *
+ * @param $messageid
+ * @param string $flag The flag to add
+ */
+ private function add_flag_to_message($messageid, $flag) {
+ // Get the current mailbox.
+ $mailbox = $this->get_mailbox();
+
+ // Mark it as read to lock the message.
+ $this->client->store($mailbox, array(
+ 'ids' => new \Horde_Imap_Client_Ids($messageid),
+ 'add' => $flag,
+ ));
+ }
+
+ /**
+ * Remove the specified flag from the message.
+ *
+ * @param $messageid
+ * @param string $flag The flag to remove
+ */
+ private function remove_flag_from_message($messageid, $flag) {
+ // Get the current mailbox.
+ $mailbox = $this->get_mailbox();
+
+ // Mark it as read to lock the message.
+ $this->client->store($mailbox, array(
+ 'ids' => $messageid,
+ 'delete' => $flag,
+ ));
+ }
+
+ /**
+ * Check whether the message has the specified flag
+ *
+ * @param $messageid
+ * @param string $flag The flag to check
+ * @return bool
+ */
+ private function message_has_flag($messageid, $flag) {
+ // Get the current mailbox.
+ $mailbox = $this->get_mailbox();
+
+ // Grab messagedata including flags.
+ $query = new \Horde_Imap_Client_Fetch_Query();
+ $query->flags();
+ $query->structure();
+ $messagedata = $this->client->fetch($mailbox, $query, array(
+ 'ids' => $messageid,
+ ))->first();
+ $flags = $messagedata->getFlags();
+
+ return in_array($flag, $flags);
+ }
+
+ /**
+ * Send the message to the appropriate handler.
+ *
+ */
+ private function send_to_handler() {
+ try {
+ mtrace("--> Passing to Inbound Message handler {$this->addressmanager->get_handler()->classname}");
+ if ($result = $this->addressmanager->handle_message($this->currentmessagedata)) {
+ $this->inform_user_of_success($this->currentmessagedata, $result);
+ // Request that this message be marked for deletion.
+ return true;
+ }
+
+ } catch (\core\message\inbound\processing_failed_exception $e) {
+ mtrace("-> The Inbound Message handler threw an exception. Unable to process this message. The user has been informed.");
+ mtrace("--> " . $e->getMessage());
+ // Throw the exception again, with additional data.
+ $error = new \stdClass();
+ $error->subject = $this->currentmessagedata->envelope->subject;
+ $error->message = $e->getMessage();
+ throw new \core\message\inbound\processing_failed_exception('messageprocessingfailed', 'tool_messageinbound', $error);
+
+ } catch (Exception $e) {
+ mtrace("-> The Inbound Message handler threw an exception. Unable to process this message. User informed.");
+ mtrace("--> " . $e->getMessage());
+ // An unknown error occurred. Still inform the user but, this time do not include the specific
+ // message information.
+ $error = new \stdClass();
+ $error->subject = $this->currentmessagedata->envelope->subject;
+ throw new \core\message\inbound\processing_failed_exception('messageprocessingfailedunknown',
+ 'tool_messageinbound', $error);
+
+ }
+
+ // Something went wrong and the message was not handled well in the Inbound Message handler.
+ mtrace("-> The Inbound Message handler reported an error. The message may not have been processed.");
+
+ // It is the responsiblity of the handler to throw an appropriate exception if the message was not processed.
+ // Do not inform the user at this point.
+ return false;
+ }
+
+ /**
+ * Handle failure of sender verification.
+ *
+ * This will send a notification to the user identified in the Inbound Message address informing them that a message has been
+ * stored. The message includes a verification link and reply-to address which is handled by the
+ * invalid_recipient_handler.
+ *
+ * @param $recipient The message recipient
+ */
+ private function handle_verification_failure(
+ \Horde_Imap_Client_Ids $messageids,
+ $recipient) {
+ global $DB, $USER;
+
+ if (!$messageid = $this->currentmessagedata->messageid) {
+ mtrace("---> Warning: Unable to determine the Message-ID of the message.");
+ return false;
+ }
+
+ // Move the message into a new mailbox.
+ $this->client->copy(self::MAILBOX, self::CONFIRMATIONFOLDER, array(
+ 'create' => true,
+ 'ids' => $messageids,
+ 'move' => true,
+ ));
+
+ // Store the data from the failed message in the associated table.
+ $record = new \stdClass();
+ $record->messageid = $messageid;
+ $record->userid = $USER->id;
+ $record->address = $recipient;
+ $record->timecreated = time();
+ $record->id = $DB->insert_record('messageinbound_messagelist', $record);
+
+ // Setup the Inbound Message generator for the invalid recipient handler.
+ $addressmanager = new \core\message\inbound\address_manager();
+ $addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
+ $addressmanager->set_data($record->id);
+
+ $eventdata = new \stdClass();
+ $eventdata->component = 'tool_messageinbound';
+ $eventdata->name = 'invalidrecipienthandler';
+
+ $userfrom = clone $USER;
+ $userfrom->customheaders = array();
+ // Adding the In-Reply-To header ensures that it is seen as a reply.
+ $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid;
+
+ // The message will be sent from the intended user.
+ $eventdata->userfrom = \core_user::get_noreply_user();
+ $eventdata->userto = $USER;
+ $eventdata->subject = $this->get_reply_subject($this->currentmessagedata->envelope->subject);
+ $eventdata->fullmessage = get_string('invalidrecipientdescription', 'tool_messageinbound', $this->currentmessagedata);
+ $eventdata->fullmessageformat = FORMAT_PLAIN;
+ $eventdata->fullmessagehtml = get_string('invalidrecipientdescriptionhtml', 'tool_messageinbound', $this->currentmessagedata);
+ $eventdata->smallmessage = $eventdata->fullmessage;
+ $eventdata->notification = 1;
+ $eventdata->replyto = $addressmanager->generate($USER->id);
+
+ mtrace("--> Sending a message to the user to report an verification failure.");
+ if (!message_send($eventdata)) {
+ mtrace("---> Warning: Message could not be sent.");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Inform the identified sender of a processing error.
+ *
+ * @param string $error The error message
+ */
+ private function inform_user_of_error($error) {
+ global $USER;
+
+ // The message will be sent from the intended user.
+ $userfrom = clone $USER;
+ $userfrom->customheaders = array();
+
+ if ($messageid = $this->currentmessagedata->messageid) {
+ // Adding the In-Reply-To header ensures that it is seen as a reply and threading is maintained.
+ $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid;
+ }
+
+ $messagedata = new \stdClass();
+ $messagedata->subject = $this->currentmessagedata->envelope->subject;
+ $messagedata->error = $error;
+
+ $eventdata = new \stdClass();
+ $eventdata->component = 'tool_messageinbound';
+ $eventdata->name = 'messageprocessingerror';
+ $eventdata->userfrom = $userfrom;
+ $eventdata->userto = $USER;
+ $eventdata->subject = self::get_reply_subject($this->currentmessagedata->envelope->subject);
+ $eventdata->fullmessage = get_string('messageprocessingerror', 'tool_messageinbound', $messagedata);
+ $eventdata->fullmessageformat = FORMAT_PLAIN;
+ $eventdata->fullmessagehtml = get_string('messageprocessingerrorhtml', 'tool_messageinbound', $messagedata);
+ $eventdata->smallmessage = $eventdata->fullmessage;
+ $eventdata->notification = 1;
+
+ if (message_send($eventdata)) {
+ mtrace("---> Notification sent to {$USER->email}.");
+ } else {
+ mtrace("---> Unable to send notification.");
+ }
+ }
+
+ /**
+ * Inform the identified sender that message processing was successful.
+ *
+ * @param stdClass $messagedata The data for the current message being processed.
+ * @param mixed $handlerresult The result returned by the handler.
+ */
+ private function inform_user_of_success(\stdClass $messagedata, $handlerresult) {
+ global $USER;
+
+ // Check whether the handler has a success notification.
+ $handler = $this->addressmanager->get_handler();
+ $message = $handler->get_success_message($messagedata, $handlerresult);
+
+ if (!$message) {
+ mtrace("---> Handler has not defined a success notification e-mail.");
+ return false;
+ }
+
+ // Wrap the message in the notification wrapper.
+ $messageparams = new \stdClass();
+ $messageparams->html = $message->html;
+ $messageparams->plain = $message->plain;
+ $messagepreferencesurl = new \moodle_url("/message/edit.php", array('id' => $USER->id));
+ $messageparams->messagepreferencesurl = $messagepreferencesurl->out();
+ $htmlmessage = get_string('messageprocessingsuccesshtml', 'tool_messageinbound', $messageparams);
+ $plainmessage = get_string('messageprocessingsuccess', 'tool_messageinbound', $messageparams);
+
+ // The message will be sent from the intended user.
+ $userfrom = clone $USER;
+ $userfrom->customheaders = array();
+
+ if ($messageid = $this->currentmessagedata->messageid) {
+ // Adding the In-Reply-To header ensures that it is seen as a reply and threading is maintained.
+ $userfrom->customheaders[] = 'In-Reply-To: ' . $messageid;
+ }
+
+ $messagedata = new \stdClass();
+ $messagedata->subject = $this->currentmessagedata->envelope->subject;
+
+ $eventdata = new \stdClass();
+ $eventdata->component = 'tool_messageinbound';
+ $eventdata->name = 'messageprocessingsuccess';
+ $eventdata->userfrom = $userfrom;
+ $eventdata->userto = $USER;
+ $eventdata->subject = self::get_reply_subject($this->currentmessagedata->envelope->subject);
+ $eventdata->fullmessage = $plainmessage;
+ $eventdata->fullmessageformat = FORMAT_PLAIN;
+ $eventdata->fullmessagehtml = $htmlmessage;
+ $eventdata->smallmessage = $eventdata->fullmessage;
+ $eventdata->notification = 1;
+
+ if (message_send($eventdata)) {
+ mtrace("---> Success notification sent to {$USER->email}.");
+ } else {
+ mtrace("---> Unable to send success notification.");
+ }
+ return true;
+ }
+
+ /**
+ * Return a formatted subject line for replies.
+ *
+ * @param $subject string The subject string
+ * @return string The formatted reply subject
+ */
+ private function get_reply_subject($subject) {
+ $prefix = get_string('replysubjectprefix', 'tool_messageinbound');
+ if (!(substr($subject, 0, strlen($prefix)) == $prefix)) {
+ $subject = $prefix . ' ' . $subject;
+ }
+
+ return $subject;
+ }
+}
--- /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/>.
+
+/**
+ * A Handler to re-process messages which previously failed sender
+ * verification.
+ *
+ * @package task_messageinbound
+ * @copyright 2014 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_messageinbound\message\inbound;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/repository/lib.php');
+
+/**
+ * A Handler to re-process messages which previously failed sender
+ * verification.
+ *
+ * This may happen if the user did not use their registerd e-mail address,
+ * the verification hash used had expired, or if some erroneous content was
+ * introduced into the content hash.
+ *
+ * @package task
+ * @copyright 2014 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class invalid_recipient_handler extends \core\message\inbound\handler {
+
+ /**
+ * Do not allow changes to the address validation setting.
+ */
+ public function allow_validateaddress_change() {
+ return false;
+ }
+
+ /**
+ * Return a description for the current handler.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return get_string('invalid_recipient_handler', 'tool_messageinbound');
+ }
+
+ /**
+ * Return a name for the current handler.
+ * This appears in the admin pages as a human-readable name.
+ *
+ * @return string
+ */
+ public function get_name() {
+ return get_string('invalid_recipient_handler_name', 'tool_messageinbound');
+ }
+
+ /**
+ * Process a message received and validated by the Inbound Message processor.
+ *
+ * @param $messagedata The Inbound Message record
+ * @param $messagedata The message data packet.
+ * @return bool Whether the message was successfully processed.
+ */
+ public function process_message(\stdClass $record, \stdClass $data) {
+ global $DB;
+
+ if (!$maildata = $DB->get_record('messageinbound_messagelist', array('id' => $record->datavalue))) {
+ // The message requested couldn't be found. Failing here will alert the user that we failed.
+ throw new \core\message\inbound\processing_failed_exception('oldmessagenotfound', 'tool_messageinbound');
+ }
+
+ mtrace("=== Request to re-process message {$record->datavalue} from server.");
+ mtrace("=== Message-Id:\t{$maildata->messageid}");
+ mtrace("=== Recipient:\t{$maildata->address}");
+
+ $manager = new \tool_messageinbound\manager();
+ return $manager->process_existing_message($maildata);
+ }
+
+}
--- /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/>.
+
+/**
+ * A scheduled task to handle cleanup of old, unconfirmed e-mails.
+ *
+ * @package tool_messageinbound
+ * @copyright 2014 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_messageinbound\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A scheduled task to handle cleanup of old, unconfirmed e-mails.
+ *
+ * @copyright 2014 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cleanup_task extends \core\task\scheduled_task {
+
+ /**
+ * Get a descriptive name for this task (shown to admins).
+ *
+ * @return string
+ */
+ public function get_name() {
+ return get_string('taskcleanup', 'tool_messageinbound');
+ }
+
+ /**
+ * Execute the main Inbound Message pickup task.
+ */
+ public function execute() {
+ $manager = new \tool_messageinbound\manager();
+ return $manager->tidy_old_messages();
+ }
+}
--- /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/>.
+
+/**
+ * A scheduled task to handle Inbound Message e-mail pickup.
+ *
+ * @package tool_messageinbound
+ * @copyright 2014 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_messageinbound\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A scheduled task to handle Inbound Message e-mail pickup.
+ *
+ * @copyright 2014 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class pickup_task extends \core\task\scheduled_task {
+
+ /**
+ * Get a descriptive name for this task (shown to admins).
+ *
+ * @return string
+ */
+ public function get_name() {
+ return get_string('taskpickup', 'tool_messageinbound');
+ }
+
+ /**
+ * Execute the main Inbound Message pickup task.
+ */
+ public function execute() {
+ $manager = new \tool_messageinbound\manager();
+ return $manager->pickup_messages();
+ }
+}
--- /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/>.
+
+/**
+ * Handlers for tool_messageinbound.
+ *
+ * @package task
+ * @category tool_messageinbound
+ * @copyright 2014 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$handlers = array(
+ array(
+ 'classname' => '\tool_messageinbound\message\inbound\invalid_recipient_handler',
+ 'enabled' => true,
+ 'validateaddress' => false,
+ ),
+);
--- /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/>.
+
+/**
+ * Message Providers for task_messageinbound.
+ *
+ * @package task
+ * @category messageinbound
+ * @copyright 2014 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$messageproviders = array (
+ // Invalid recipient handler.
+ 'invalidrecipienthandler' => array(),
+
+ // A generic message processing error.
+ 'messageprocessingerror' => array(),
+
+ // A generic message processing success message.
+ 'messageprocessingsuccess' => array(),
+);
--- /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/>.
+
+/**
+ * The Main Manager tasks.
+ *
+ * @package tool_messageinbound
+ * @copyright 2014 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tasks = array(
+ array(
+ 'classname' => '\tool_messageinbound\task\pickup_task',
+ 'blocking' => 0,
+ 'minute' => '*',
+ 'hour' => '*',
+ 'day' => '*',
+ 'dayofweek' => '*',
+ 'month' => '*'
+ ),
+
+ array(
+ 'classname' => '\tool_messageinbound\task\cleanup_task',
+ 'blocking' => 0,
+ 'minute' => '55',
+ 'hour' => '1',
+ 'day' => '*',
+ 'dayofweek' => '*',
+ 'month' => '*'
+ ),
+);
--- /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/>.
+
+/**
+ * Inbound Message Settings pages.
+ *
+ * @package tool_messageinbound
+ * @copyright 2014 Andrew NIcols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir.'/tablelib.php');
+
+admin_externalpage_setup('messageinbound_handlers');
+
+$classname = optional_param('classname', '', PARAM_RAW);
+
+$pageurl = new moodle_url('/admin/tool/messageinbound/index.php');
+
+if (empty($classname)) {
+ $renderer = $PAGE->get_renderer('tool_messageinbound');
+
+ $records = $DB->get_recordset('messageinbound_handlers', null, 'enabled desc', 'classname');
+ $instances = array();
+ foreach ($records as $record) {
+ $instances[] = \core\message\inbound\manager::get_handler($record->classname);
+ }
+
+ echo $OUTPUT->header();
+ echo $renderer->messageinbound_handlers_table($instances);
+ echo $OUTPUT->footer();
+
+} else {
+ // Retrieve the handler and its record.
+ $handler = \core\message\inbound\manager::get_handler($classname);
+ $record = \core\message\inbound\manager::record_from_handler($handler);
+
+ $formurl = new moodle_url($PAGE->url, array('classname' => $classname));
+ $mform = new tool_messageinbound_edit_handler_form($formurl, array(
+ 'handler' => $handler,
+ ));
+
+ if ($mform->is_cancelled()) {
+ redirect($PAGE->url);
+ } else if ($data = $mform->get_data()) {
+ // Update the record from the form.
+ $record->defaultexpiration = (int) $data->defaultexpiration;
+
+ if ($handler->can_change_validateaddress()) {
+ $record->validateaddress = !empty($data->validateaddress);
+ }
+
+ if ($handler->can_change_enabled()) {
+ $record->enabled = !empty($data->enabled);
+ }
+ $DB->update_record('messageinbound_handlers', $record);
+ redirect($PAGE->url);
+ }
+
+ // Add the breadcrumb.
+ $pageurl->param('classname', $handler->classname);
+ $PAGE->navbar->add($handler->name, $pageurl);
+ echo $OUTPUT->header();
+ echo $OUTPUT->heading(get_string('editinghandler', 'tool_messageinbound', $handler->name));
+ $mform->set_data($record);
+ $mform->display();
+ 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_messageinbound', language 'en'
+ *
+ * @package tool_messageinbound
+ * @copyright 2014 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['classname'] = 'Class name';
+$string['component'] = 'Component';
+$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, you can use the [server]:[port], for example mail.example.com:587 format. If you leave this field blank, Moodle will use the default port for the type of mail server you specify.';
+$string['defaultexpiration'] = 'Default address expiry period';
+$string['defaultexpiration_help'] = 'When an email address is generated by the handler, it can be set to automatically expire after a period of time, so that it can no longer be used. It is advisable to set an expiry period.';
+$string['description'] = 'Description';
+$string['domain'] = 'Email Domain';
+$string['edit'] = 'Edit';
+$string['edithandler'] = 'Edit settings for the {$a} handler';
+$string['editinghandler'] = 'Editing {$a}';
+$string['enabled'] = 'Enabled';