Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions api/public/cors.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@
$is_question = false;
}

if (strpos($scriptname, '..') !== false
|| strpos($scriptname, '/') !== false
|| strpos($scriptname, '\\') !== false) {
// Give a special exception for sample questions.
if (!($is_question && file_exists('../../samplequestions/' . $scriptname))) {
die("No such script here.");
}
$scriptname = urldecode($_GET['name']);
$corslocation = dirname($CFG->dirroot) . '/corsscripts/';
$questionlocation = dirname($CFG->dirroot) . '/samplequestions/';

if (!str_starts_with(realpath($scriptname), $corslocation) && !str_starts_with(realpath($corslocation . $scriptname), $corslocation)) {
// Give a special exception for sample questions.
if (!($is_question && str_starts_with(realpath($questionlocation . $scriptname), $questionlocation))) {
die("No such script here.");
}
}

if (file_exists('../../corsscripts/' . $scriptname) || $scriptname === 'styles.css'
Expand Down
2 changes: 1 addition & 1 deletion api/public/sample.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
require_once(__DIR__ . '/../../stack/questionlibrary.class.php');
// Required to pass Moodle code check. Uses emulation stub.
require_login();
$files = stack_question_library::get_file_list('../../samplequestions/stackdemo/*');
$files = stack_question_library::get_file_list(realpath(__DIR__ . '/../../samplequestions/stackdemo') . '/*');

$questions = [];
foreach ($files->children as $file) {
Expand Down
2 changes: 1 addition & 1 deletion api/public/stack.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ function setQuestion(question) {
</a>
Choose a STACK sample file:
<?php
$files = stack_question_library::get_file_list('../../samplequestions/stacklibrary/*');
$files = stack_question_library::get_file_list(realpath(__DIR__ . '/../../samplequestions/stacklibrary') . '/*');
function render_directory($dirdetails) {
echo '<div style="margin-left: 30px;">';
foreach ($dirdetails as $file) {
Expand Down
22 changes: 18 additions & 4 deletions classes/library_import.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,26 @@ public static function import_execute($courseid, $category, $filepath, $isfolder
$loadingquiz = false;
$categories = [];

if (str_starts_with($params['filepath'], 'sitelibrary/')) {
$requestedfile = $CFG->dataroot . '/stack/' . $params['filepath'];
$basedir = $CFG->dataroot . '/stack/';
} else {
$requestedfile = $CFG->dirroot . '/question/type/stack/samplequestions/' . $params['filepath'];
$basedir = $CFG->dirroot . '/question/type/stack/samplequestions/';
}
if (
!str_starts_with(realpath($requestedfile), "{$CFG->dataroot}/stack/sitelibrary") &&
!str_starts_with(realpath($requestedfile), "{$CFG->dirroot}/question/type/stack/samplequestions/")
) {
throw new \Exception('Dubious file request.');
}

if (
pathinfo($params['filepath'], PATHINFO_EXTENSION) === 'json'
&& strrpos($params['filepath'], '_quiz.json') !== false
) {
// We've got a quiz file. Load JSON and instantiate.
$quizcontents = file_get_contents($CFG->dirroot . '/question/type/stack/samplequestions/' . $params['filepath']);
$quizcontents = file_get_contents($requestedfile);
$quizdata = json_decode($quizcontents);
// We have to create the quiz, import the questions and then add the questions to the quiz.
// Create quiz and its default category. This is now our target category which we add to the quiz data.
Expand Down Expand Up @@ -142,7 +156,7 @@ public static function import_execute($courseid, $category, $filepath, $isfolder
} else {
// We're importing a folder.
// Full path of supplied question.
$fullpath = $CFG->dirroot . '/question/type/stack/samplequestions/' . $params['filepath'];
$fullpath = $requestedfile;
$reldirname = dirname($params['filepath']);
// List all the files in the same folder.
$files = scandir(dirname($fullpath));
Expand All @@ -166,7 +180,7 @@ public static function import_execute($courseid, $category, $filepath, $isfolder
$qformat->set_display_progress(false);
$qformat->setCategory($thiscategory);
$qformat->setCatfromfile(true);
$qformat->setFilename($CFG->dirroot . '/question/type/stack/samplequestions/' . $category);
$qformat->setFilename($basedir . $category);
$qformat->setContextfromfile(false);
$qformat->setStoponerror(true);
$contexts = new question_edit_contexts($thiscontext);
Expand Down Expand Up @@ -212,7 +226,7 @@ public static function import_execute($courseid, $category, $filepath, $isfolder
}
$qformat->setCatfromfile(false);

$qformat->setFilename($CFG->dirroot . '/question/type/stack/samplequestions/' . $file);
$qformat->setFilename($basedir . $file);
$qformat->setContextfromfile(false);
$qformat->setStoponerror(true);
$contexts = new question_edit_contexts($thiscontext);
Expand Down
17 changes: 15 additions & 2 deletions classes/library_render.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,22 @@ public static function render_execute($category, $filepath) {
$result = $cache->get($params['filepath']);
$isquiz = (pathinfo($params['filepath'], PATHINFO_EXTENSION) === 'json'
&& strrpos($params['filepath'], '_quiz.json') !== false) ? true : false;

if (str_starts_with($params['filepath'], 'sitelibrary/')) {
$requestedfile = $CFG->dataroot . '/stack/' . $params['filepath'];
} else {
$requestedfile = $CFG->dirroot . '/question/type/stack/samplequestions/' . $params['filepath'];
}
if (
!str_starts_with(realpath($requestedfile), "{$CFG->dataroot}/stack/sitelibrary") &&
!str_starts_with(realpath($requestedfile), "{$CFG->dirroot}/question/type/stack/samplequestions/")
) {
throw new \Exception('Dubious file request.');
}

if (!$result && !$isquiz) {
// Get contents of file and run through API question loader to render.
$qcontents = file_get_contents($CFG->dirroot . '/question/type/stack/samplequestions/' . $params['filepath']);
$qcontents = file_get_contents($requestedfile);
try {
$question = StackQuestionLoader::loadxml($qcontents)['question'];
$render = static::call_question_render($question);
Expand Down Expand Up @@ -164,7 +177,7 @@ public static function render_execute($category, $filepath) {
}
}
if (!$result && $isquiz) {
$quizcontents = file_get_contents($CFG->dirroot . '/question/type/stack/samplequestions/' . $params['filepath']);
$quizcontents = file_get_contents($requestedfile);
$json = json_decode($quizcontents);
$quiz = $json->quiz;
$questions = $json->questions;
Expand Down
4 changes: 4 additions & 0 deletions doc/en/STACK_question_admin/Library/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ The following directories are linked to documentation and templates

A significant advantage of using questions from the STACK library is that they are distributed with the source code, and therefore use features which match your version of STACK. Consider using [Gitsync](https://github.com/maths/moodle-qbank_gitsync) to manage large question banks.

# Local site libraries

Additional libraries of STACK questions can be installed on your Moodle server. Once STACK is installed, there will be a `stack/sitelibrary` directory within the Moodle data directory. STACK will interpret each top-level subdirectory of `stack/sitelibrary` as a separate library and allow you to switch between them (and the STACK question library) using a dropdown on the library page displaying subdirectory names. Ask your server administrator to copy a library you wish to use into a suitably named subdirectory in `stack/sitelibrary`. After any update to an installed library, the Moodle MUC cache will need to be cleared.

# Moodle courses released with STACK #

STACK is released with a demonstration course.
Expand Down
1 change: 1 addition & 0 deletions lang/en/qtype_stack.php
Original file line number Diff line number Diff line change
Expand Up @@ -1858,6 +1858,7 @@
$string['stack_library_quiz_course'] = 'The quiz will be imported into course: ';
$string['stack_library_quiz_prefix'] = 'Quiz:';
$string['stack_library_selected'] = 'Displayed question:';
$string['stack_library_select'] = 'Select library:';
$string['stack_library_success'] = 'Successful import of:';
$string['stack_library_not_stack'] = 'This is not a STACK question and so cannot be fully rendered here but you can still import it.';
$string['stack_library_quiz_return'] = 'Return to quiz';
Expand Down
51 changes: 48 additions & 3 deletions questionlibrary.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,30 @@

// Get list of files.
$cache = cache::make('qtype_stack', 'librarycache');
$files = $cache->get('library_file_list');

// Make sure we're only listing contents of STACK library or site library.
$location = optional_param('location', '', PARAM_RAW);
$cacheid = 'library_file_list';
$libraryname = stack_string('stack_library');
if (str_starts_with($location, 'sitelibrary')) {
$libraryname = explode('/', $location)[1];
$cacheid = 'sitelibrary_' . $libraryname . '_file_list';
$location = "{$CFG->dataroot}/stack/{$location}";
if (!str_starts_with(realpath($location), "{$CFG->dataroot}/stack/sitelibrary")) {
$location = __DIR__ . '/samplequestions/stacklibrary/*';
$libraryname = stack_string('stack_library');
$cacheid = 'library_file_list';
} else {
$location .= '/*';
}
} else {
$location = __DIR__ . '/samplequestions/stacklibrary/*';
}

$files = $cache->get($cacheid);
if (!$files) {
$files = stack_question_library::get_file_list('samplequestions/stacklibrary/*');
$cache->set('library_file_list', $files);
$files = stack_question_library::get_file_list($location);
$cache->set($cacheid, $files);
}

$mform = new category_form(null, ['qcontext' => $contexts]);
Expand All @@ -104,6 +124,31 @@
$outputdata->category = $mform->render();
$outputdata->coursename = $coursename;
$outputdata->courseid = $courseid;
$outputdata->libraries = new StdClass();
$outputdata->libraries->items = [];
$outputdata->libraries->hasitems = false;

$libraries = glob("{$CFG->dataroot}/stack/sitelibrary/*");
if ($libraries) {
$libentry = new StdClass();
$libentry->name = stack_string('stack_library');
$urlparams['location'] = "/samplequestions/stacklibrary";
$libentry->url = new moodle_url('/question/type/stack/questionlibrary.php', $urlparams);
$libentry->url = $libentry->url->out();
$libentry->active = ($libentry->name === $libraryname) ? true : false;
$outputdata->libraries->items[] = $libentry;
$outputdata->libraries->hasitems = true;
}
foreach ($libraries as $library) {
$libentry = new StdClass();
$parts = explode('/', $library);
$libentry->name = end($parts);
$urlparams['location'] = "sitelibrary/{$libentry->name}";
$libentry->url = new moodle_url('/question/type/stack/questionlibrary.php', $urlparams);
$libentry->url = $libentry->url->out();
$libentry->active = ($libentry->name === $libraryname) ? true : false;
$outputdata->libraries->items[] = $libentry;
}

echo $OUTPUT->render_from_template('qtype_stack/questionlibrary', $outputdata);

Expand Down
1 change: 1 addition & 0 deletions stack/cas/installhelper.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ public static function create_maximalocal() {
make_upload_directory('stack');
make_upload_directory('stack/logs');
make_upload_directory('stack/plots');
make_upload_directory('stack/sitelibrary');
make_upload_directory('stack/tmp');

if (!file_put_contents(self::maximalocal_location(), self::generate_maximalocal_contents())) {
Expand Down
10 changes: 6 additions & 4 deletions stack/questionlibrary.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,12 @@ public static function render_question(object $question): string {
* Gets the structure of folders and files within a given directory
* See questionfolder.mustache for output and usage.
* We sanitise the structure a bit to remove gitsync files and folders.
* @param string directory within samplequestions to be examined
* @param string sanitised search string e.g. '/srv/stack/samplequestions/stacklibrary/*'
* with the full real path of the folder and search criteria.
* @return object StdClass Representation of the file system
*/
public static function get_file_list(string $dir): object {
global $CFG;
$files = glob($dir);
$results = new stdClass();
$labels = explode('/', $dir);
Expand All @@ -156,9 +158,9 @@ public static function get_file_list(string $dir): object {
|| (pathinfo($path, PATHINFO_EXTENSION) === 'json' && strrpos($path, '_quiz.json') !== false)
) {
$childless = new StdClass();
// Get the path relative to the samplequestions folder.
$pathfromsq = str_replace('samplequestions/', '', $path);
$pathfromsq = str_replace('../', '', $pathfromsq);
// Get the path relative to the samplequestions or stack dataroot folder.
$pathfromsq = str_replace(dirname(__DIR__) . '/samplequestions/', '', $path);
$pathfromsq = str_replace("{$CFG->dataroot}/stack/", '', $pathfromsq);
$childless->path = $pathfromsq;
$labels = explode('/', $path);
$childless->label = end($labels);
Expand Down
17 changes: 17 additions & 0 deletions templates/questionlibrary.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@
{{#str}} stack_library_error, qtype_stack {{/str}}
<span class="stack-library-error-details"></span>
</p>
{{#libraries.hasitems}}
<p>
<span>
{{#str}} stack_library_select, qtype_stack {{/str}}
</span>
<select onChange="document.querySelector('[data-id=\'qa-loading\']').removeAttribute('hidden');window.location.href=this.value;">
{{#libraries.items}}
<option value="{{{url}}}" {{#active}} selected="selected" {{/active}}>
{{name}}
</option>
{{/libraries.items}}
</select>
<span data-id='qa-loading' hidden>
{{#pix}}y/loading, core, {{#str}}loading, tool_lp{{/str}}{{/pix}}
</span>
</p>
{{/libraries.hasitems}}
<div class="alert alert-secondary" style="position: relative; color: black;">
<div class="stack-library-category-holder">
{{#str}} stack_library_destination, qtype_stack {{/str}}
Expand Down
1 change: 1 addition & 0 deletions tests/behat/library.feature
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Feature: Test STACK library
And I click on "STACK question library" "link"
Then I should see "Test questions"
And I should not see "Question variables"
And I should not see "Select library"
And I click on "Calculus-Refresher" "button"
And I click on "CR_Diff_02" "button"
And I click on "CR-Diff-02-linearity-1-b.xml" "button"
Expand Down
Loading