diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php
index 0c20d6b4159..3d50eb33317 100644
--- a/lib/classes/plugin_manager.php
+++ b/lib/classes/plugin_manager.php
@@ -1939,7 +1939,7 @@ class core_plugin_manager {
),
'qbank' => [
- 'deletequestion',
+ 'deletequestion', 'importquestions',
],
'qbehaviour' => array(
diff --git a/question/bank/importquestions/classes/form/import_form.php b/question/bank/importquestions/classes/form/import_form.php
new file mode 100644
index 00000000000..28bab21acad
--- /dev/null
+++ b/question/bank/importquestions/classes/form/import_form.php
@@ -0,0 +1,179 @@
+.
+
+/**
+ * Defines the export questions form.
+ *
+ * @package qbank_importquestions
+ * @copyright 2007 Jamie Pratt me@jamiep.org
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace qbank_importquestions\form;
+
+defined('MOODLE_INTERNAL') || die();
+
+use moodle_exception;
+use moodleform;
+use stdClass;
+
+require_once($CFG->libdir . '/formslib.php');
+
+/**
+ * Form to import questions into the question bank.
+ *
+ * @copyright 2007 Jamie Pratt me@jamiep.org
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_import_form extends moodleform {
+
+ /**
+ * Build the form definition.
+ *
+ * This adds all the form fields that the manage categories feature needs.
+ * @throws \coding_exception
+ */
+ protected function definition() {
+ global $OUTPUT;
+
+ $mform = $this->_form;
+
+ $defaultcategory = $this->_customdata['defaultcategory'];
+ $contexts = $this->_customdata['contexts'];
+
+ // Choice of import format, with help icons.
+ $mform->addElement('header', 'fileformat', get_string('fileformat', 'question'));
+
+ $fileformatnames = get_import_export_formats('import');
+ $radioarray = [];
+ $separators = [];
+ foreach ($fileformatnames as $shortname => $fileformatname) {
+ $radioarray[] = $mform->createElement('radio', 'format', '', $fileformatname, $shortname);
+
+ $separator = '';
+ if (get_string_manager()->string_exists('pluginname_help', 'qformat_' . $shortname)) {
+ $separator .= $OUTPUT->help_icon('pluginname', 'qformat_' . $shortname);
+ }
+ $separator .= '
';
+ $separators[] = $separator;
+ }
+
+ $radioarray[] = $mform->createElement('static', 'makelasthelpiconshowup', '');
+ $mform->addGroup($radioarray, "formatchoices", '', $separators, false);
+ $mform->addRule("formatchoices", null, 'required', null, 'client');
+
+ // Import options.
+ $mform->addElement('header', 'general', get_string('general', 'form'));
+
+ $mform->addElement('questioncategory', 'category', get_string('importcategory', 'question'), compact('contexts'));
+ $mform->setDefault('category', $defaultcategory);
+ $mform->addHelpButton('category', 'importcategory', 'question');
+
+ $categorygroup = [];
+ $categorygroup[] = $mform->createElement('checkbox', 'catfromfile', '', get_string('getcategoryfromfile', 'question'));
+ $categorygroup[] = $mform->createElement('checkbox', 'contextfromfile', '', get_string('getcontextfromfile', 'question'));
+ $mform->addGroup($categorygroup, 'categorygroup', '', '', false);
+ $mform->disabledIf('categorygroup', 'catfromfile', 'notchecked');
+ $mform->setDefault('catfromfile', 1);
+ $mform->setDefault('contextfromfile', 1);
+
+ $matchgrades = [];
+ $matchgrades['error'] = get_string('matchgradeserror', 'question');
+ $matchgrades['nearest'] = get_string('matchgradesnearest', 'question');
+ $mform->addElement('select', 'matchgrades', get_string('matchgrades', 'question'), $matchgrades);
+ $mform->addHelpButton('matchgrades', 'matchgrades', 'question');
+ $mform->setDefault('matchgrades', 'error');
+
+ $mform->addElement('selectyesno', 'stoponerror', get_string('stoponerror', 'question'));
+ $mform->setDefault('stoponerror', 1);
+ $mform->addHelpButton('stoponerror', 'stoponerror', 'question');
+
+ // The file to import.
+ $mform->addElement('header', 'importfileupload', get_string('importquestions', 'question'));
+
+ $mform->addElement('filepicker', 'newfile', get_string('import'));
+ $mform->addRule('newfile', null, 'required', null, 'client');
+
+ // Submit button.
+ $mform->addElement('submit', 'submitbutton', get_string('import'));
+
+ // Set a template for the format select elements.
+ $renderer = $mform->defaultRenderer();
+ $template = "{help} {element}\n";
+ $renderer->setGroupElementTemplate($template, 'format');
+ }
+
+ /**
+ * Checks that a file has been uploaded, and that it is of a plausible type.
+ * @param array $data the submitted data.
+ * @param array $errors the errors so far.
+ * @return array the updated errors.
+ * @throws moodle_exception
+ */
+ protected function validate_uploaded_file($data, $errors) {
+ global $CFG;
+
+ if (empty($data['newfile'])) {
+ $errors['newfile'] = get_string('required');
+ return $errors;
+ }
+
+ $files = $this->get_draft_files('newfile');
+ if (!is_array($files) || count($files) < 1) {
+ $errors['newfile'] = get_string('required');
+ return $errors;
+ }
+
+ if (empty($data['format'])) {
+ $errors['format'] = get_string('required');
+ return $errors;
+ }
+
+ $formatfile = $CFG->dirroot . '/question/format/' . $data['format'] . '/format.php';
+ if (!is_readable($formatfile)) {
+ throw new moodle_exception('formatnotfound', 'question', '', $data['format']);
+ }
+
+ require_once($formatfile);
+
+ $classname = 'qformat_' . $data['format'];
+ $qformat = new $classname();
+
+ $file = reset($files);
+ if (!$qformat->can_import_file($file)) {
+ $a = new stdClass();
+ $a->actualtype = $file->get_mimetype();
+ $a->expectedtype = $qformat->mime_type();
+ $errors['newfile'] = get_string('importwrongfiletype', 'question', $a);
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Validation.
+ *
+ * @param array $data
+ * @param array $files
+ * @return array the errors that were found
+ * @throws \dml_exception|\coding_exception|moodle_exception
+ */
+ public function validation($data, $files) {
+ $errors = parent::validation($data, $files);
+ $errors = $this->validate_uploaded_file($data, $errors);
+ return $errors;
+ }
+}
diff --git a/question/bank/importquestions/classes/navigation.php b/question/bank/importquestions/classes/navigation.php
new file mode 100644
index 00000000000..7f24eaecf1a
--- /dev/null
+++ b/question/bank/importquestions/classes/navigation.php
@@ -0,0 +1,50 @@
+.
+
+/**
+ * Plugin entrypoint for navigation.
+ *
+ * @package qbank_importquestions
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace qbank_importquestions;
+
+/**
+ * Class navigation.
+ *
+ * @package qbank_importquestions
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class navigation extends \core_question\local\bank\navigation_node_base {
+
+ public function get_navigation_title(): string {
+ return get_string('import', 'question');
+ }
+
+ public function get_navigation_key(): string {
+ return 'import';
+ }
+
+ public function get_navigation_url(): \moodle_url {
+ return new \moodle_url('/question/bank/importquestions/import.php');
+ }
+
+}
diff --git a/question/bank/importquestions/classes/plugin_feature.php b/question/bank/importquestions/classes/plugin_feature.php
new file mode 100644
index 00000000000..fc876200366
--- /dev/null
+++ b/question/bank/importquestions/classes/plugin_feature.php
@@ -0,0 +1,41 @@
+.
+
+/**
+ * Plugin entrypoint for qbank.
+ *
+ * @package qbank_importquestions
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace qbank_importquestions;
+
+/**
+ * Class plugin_feature.
+ *
+ * @package qbank_importquestions
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class plugin_feature extends \core_question\local\bank\plugin_features_base {
+
+ public function get_navigation_node(): ?object {
+ return new navigation();
+ }
+}
diff --git a/question/bank/importquestions/classes/privacy/provider.php b/question/bank/importquestions/classes/privacy/provider.php
new file mode 100644
index 00000000000..cbf9a49ed34
--- /dev/null
+++ b/question/bank/importquestions/classes/privacy/provider.php
@@ -0,0 +1,38 @@
+.
+
+namespace qbank_importquestions\privacy;
+
+/**
+ * Privacy Subsystem implementation for qbank_importquestions.
+ *
+ * @package qbank_importquestions
+ * @category privacy
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Marc-Alexandre Ghaly
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+ /**
+ * Get the language string identifier with the component's language
+ * file to explain why this plugin stores no data.
+ *
+ * @return string
+ */
+ public static function get_reason(): string {
+ return 'privacy:metadata';
+ }
+}
diff --git a/question/bank/importquestions/import.php b/question/bank/importquestions/import.php
new file mode 100644
index 00000000000..91eab596d0e
--- /dev/null
+++ b/question/bank/importquestions/import.php
@@ -0,0 +1,150 @@
+.
+
+/**
+ * Defines the import questions form.
+ *
+ * @package qbank_importquestions
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->dirroot . '/question/editlib.php');
+require_once($CFG->dirroot . '/question/bank/importquestions/classes/form/import_form.php');
+require_once($CFG->dirroot . '/question/format.php');
+require_once($CFG->dirroot . '/question/renderer.php');
+
+use qbank_importquestions\form\question_import_form;
+
+require_login();
+core_question\local\bank\helper::require_plugin_enabled('qbank_importquestions');
+list($thispageurl, $contexts, $cmid, $cm, $module, $pagevars) =
+ question_edit_setup('import', '/question/bank/importquestions/import.php');
+
+// Get display strings.
+$txt = new stdClass();
+$txt->importerror = get_string('importerror', 'question');
+$txt->importquestions = get_string('importquestions', 'question');
+
+list($catid, $catcontext) = explode(',', $pagevars['cat']);
+if (!$category = $DB->get_record("question_categories", ['id' => $catid])) {
+ throw new moodle_exception('nocategory', 'question');
+}
+
+$categorycontext = context::instance_by_id($category->contextid);
+$category->context = $categorycontext;
+// This page can be called without courseid or cmid in which case.
+// We get the context from the category object.
+if ($contexts === null) { // Need to get the course from the chosen category.
+ $contexts = new question_edit_contexts($categorycontext);
+ $thiscontext = $contexts->lowest();
+ if ($thiscontext->contextlevel == CONTEXT_COURSE) {
+ require_login($thiscontext->instanceid, false);
+ } else if ($thiscontext->contextlevel == CONTEXT_MODULE) {
+ list($module, $cm) = get_module_from_cmid($thiscontext->instanceid);
+ require_login($cm->course, false, $cm);
+ }
+ $contexts->require_one_edit_tab_cap($edittab);
+}
+
+$PAGE->set_url($thispageurl);
+
+$importform = new question_import_form($thispageurl, ['contexts' => $contexts->having_one_edit_tab_cap('import'),
+ 'defaultcategory' => $pagevars['cat']]);
+
+if ($importform->is_cancelled()) {
+ redirect($thispageurl);
+}
+// Page header.
+$PAGE->set_title($txt->importquestions);
+$PAGE->set_heading($COURSE->fullname);
+echo $OUTPUT->header();
+
+// Print horizontal nav if needed.
+$renderer = $PAGE->get_renderer('core_question', 'bank');
+echo $renderer->extra_horizontal_navigation();
+
+// File upload form submitted.
+if ($form = $importform->get_data()) {
+
+ // File checks out ok.
+ $fileisgood = false;
+
+ // Work out if this is an uploaded file.
+ // Or one from the filesarea.
+ $realfilename = $importform->get_new_filename('newfile');
+ $importfile = make_request_directory() . "/{$realfilename}";
+ if (!$result = $importform->save_file('newfile', $importfile, true)) {
+ throw new moodle_exception('uploadproblem');
+ }
+
+ $formatfile = $CFG->dirroot . '/question/format/' . $form->format . '/format.php';
+ if (!is_readable($formatfile)) {
+ throw new moodle_exception('formatnotfound', 'question', '', $form->format);
+ }
+
+ require_once($formatfile);
+
+ $classname = 'qformat_' . $form->format;
+ $qformat = new $classname();
+
+ // Load data into class.
+ $qformat->setCategory($category);
+ $qformat->setContexts($contexts->having_one_edit_tab_cap('import'));
+ $qformat->setCourse($COURSE);
+ $qformat->setFilename($importfile);
+ $qformat->setRealfilename($realfilename);
+ $qformat->setMatchgrades($form->matchgrades);
+ $qformat->setCatfromfile(!empty($form->catfromfile));
+ $qformat->setContextfromfile(!empty($form->contextfromfile));
+ $qformat->setStoponerror($form->stoponerror);
+
+ // Do anything before that we need to.
+ if (!$qformat->importpreprocess()) {
+ throw new moodle_exception('cannotimport', '', $thispageurl->out());
+ }
+
+ // Process the uploaded file.
+ if (!$qformat->importprocess()) {
+ throw new moodle_exception('cannotimport', '', $thispageurl->out());
+ }
+
+ // In case anything needs to be done after.
+ if (!$qformat->importpostprocess()) {
+ throw new moodle_exception('cannotimport', '', $thispageurl->out());
+ }
+
+ // Log the import into this category.
+ $eventparams = [
+ 'contextid' => $qformat->category->contextid,
+ 'other' => ['format' => $form->format, 'categoryid' => $qformat->category->id],
+ ];
+ $event = \core\event\questions_imported::create($eventparams);
+ $event->trigger();
+
+ $params = $thispageurl->params() + ['category' => $qformat->category->id . ',' . $qformat->category->contextid];
+ echo $OUTPUT->continue_button(new moodle_url('/question/edit.php', $params));
+ echo $OUTPUT->footer();
+ exit;
+}
+
+echo $OUTPUT->heading_with_help($txt->importquestions, 'importquestions', 'question');
+
+// Print upload form.
+$importform->display();
+echo $OUTPUT->footer();
diff --git a/question/bank/importquestions/lang/en/qbank_importquestions.php b/question/bank/importquestions/lang/en/qbank_importquestions.php
new file mode 100644
index 00000000000..0fb0cfe8e08
--- /dev/null
+++ b/question/bank/importquestions/lang/en/qbank_importquestions.php
@@ -0,0 +1,27 @@
+.
+
+/**
+ * Strings for component 'qbank_importquestions', language 'en'
+ *
+ * @package qbank_importquestions
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Marc-Alexandre Ghaly
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pluginname'] = 'Import question bank feature';
+$string['privacy:metadata'] = 'The import question bank will import questions from a file according to the selected file format.';
diff --git a/question/bank/importquestions/version.php b/question/bank/importquestions/version.php
new file mode 100644
index 00000000000..660d8bdbbf7
--- /dev/null
+++ b/question/bank/importquestions/version.php
@@ -0,0 +1,31 @@
+.
+
+/**
+ * Plugin version and other meta-data are defined here.
+ *
+ * @package qbank_importquestions
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Marc-Alexandre Ghaly
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->component = 'qbank_importquestions';
+$plugin->version = 2021070700;
+$plugin->requires = 2021052500;
+$plugin->maturity = MATURITY_STABLE;