diff --git a/.github/ISSUE_TEMPLATE/bugreport.yml b/.github/ISSUE_TEMPLATE/bugreport.yml new file mode 100644 index 0000000..14c7cba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bugreport.yml @@ -0,0 +1,164 @@ +name: Bug Report +description: File a bug report +title: "[Bug]: " +body: + - type: markdown + attributes: + value: | + --- + + Thanks for submitting a bug report 👍 Please fill in the applicable fields. + Additional information or images can always be added later as comments. + + --- + + - type: markdown + attributes: + value: | + # Environment + + - type: input + id: quiz_archiver_version + attributes: + label: Quiz archiver moodle plugin version + description: Which version of the **quiz_archiver Moodle plugin** are you using? + placeholder: ex. v1.2.3 / develop + validations: + required: true + + - type: input + id: quiz_archive_worker_version + attributes: + label: Quiz archiver worker version + description: Which version of the **quiz archive worker service** are you using? + placeholder: ex. v1.2.3 / develop + validations: + required: true + + - type: dropdown + id: quiz_archive_worker_deployment_method + attributes: + label: Worker service deployment method + description: How did you deploy the quiz archive worker service? + options: + - Docker + - Docker compose + - Manual + - Other, please specify below + default: 1 + validations: + required: false + + - type: input + id: moodle_version + attributes: + label: Moodle Version + description: Which version of Moodle are you using? + placeholder: ex. Moodle 4.3.2+ + validations: + required: false + + - type: input + id: php_version + attributes: + label: PHP Version + description: Which PHP version do you run? + placeholder: ex. PHP 8.3 + validations: + required: false + + - type: dropdown + id: database_type + attributes: + label: Database + description: Which database are you using? + options: + - PostgreSQL + - MySQL + - MariaDB + - Microsoft SQL Server + - Oracle Database + - Other, please specify below + default: 0 + validations: + required: false + + - type: input + id: os + attributes: + label: Operating system + description: Which operating system do your Moodle and the quiz archive worker service run on? + placeholder: ex. Debian 12 + validations: + required: false + + + - type: markdown + attributes: + value: | + # Issue + + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Please tell us what you expected to happen and what happend instead. + value: | + Expected behavior: + + Actual behavior: + + Additional information: + validations: + required: true + + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to reproduce + description: Please list the stepts one needs to perform to reproduce the issue. + placeholder: | + 1. Navigate to the archiver page of a quiz + 2. Click X, Y, and Z + 3. Find the above described error + validations: + required: true + + + - type: markdown + attributes: + value: | + # Additional Information + + - type: textarea + id: moodle_logs + attributes: + label: Relevant Moodle / PHP log output (if applicable) + description: | + Please copy and paste any relevant log output from Moodle or PHP. + See [Moodle Wiki -> Debugging](https://docs.moodle.org/403/en/Debugging) for more details. + render: text + validations: + required: false + + - type: textarea + id: quiz_archive_worker_logs + attributes: + label: Relevant quiz archive worker service log output (if applicable) + description: | + Please copy and paste any relevant log output from the quiz archive worker service. Ideadly set your `LOG_LEVEL` to `DEBUG` when capturing the logs. + See [Quiz Archive Worker -> Configuration](https://github.com/ngandrass/moodle-quiz-archive-worker?tab=readme-ov-file#configuration) for more details. + render: text + validations: + required: false + + - type: markdown + attributes: + value: --- + + - type: markdown + attributes: + value: | + Thanks for filing this bug report! You made it 🎉 + + Be aware, that you can always provide additional information or images as comments, once this issue is created. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..4aa1a8e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Anything Else + url: https://github.com/ngandrass/moodle-quiz_archiver/issues/new + about: For anything else feel free to create a blank issue + - name: Read the Documentation + url: https://github.com/ngandrass/moodle-quiz_archiver/blob/master/README.md + about: Have you already tried to find an answer to your question or problem in the docs? diff --git a/.github/ISSUE_TEMPLATE/suggestion.yml b/.github/ISSUE_TEMPLATE/suggestion.yml new file mode 100644 index 0000000..996ebc3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/suggestion.yml @@ -0,0 +1,28 @@ +name: Feature Request / Suggestion / Improvement +description: File a feature request, provide a suggestion, or discuss an improvement +title: "[Request]: " +body: + + - type: textarea + id: feature-request + attributes: + label: Description + description: Please tell us what you would like to see in the near future, provide your suggestion, or stat an open discussion. + placeholder: Can you please try to make unicorns dance on rainbows with the next release? Thanks! :) + validations: + required: true + + - type: textarea + id: additional-resources + attributes: + label: Additional resources and references + description: List all additional links, material or other media here + validations: + required: false + + - type: markdown + attributes: + value: | + Thanks for contributing your valuble input! You made it 🎉 + + Be aware, that you can always provide additional information or images as comments, once this issue is created. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f66fe1..5b76ff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog + +## Version 1.2.4 (2024021901) + +- Fix image inlining for Moodle instances that reside in subdirectories (e.g., `https://your.domain/moodle`) + - Thanks a lot to @500gLychee for extensive testing and reporting! +- Fix inlining of miscellaneous local images that do not fall into any specific link type category +- Detect quizzes without attempts and prevent archive creation until at least one attempt was registered +- Create GitHub issue template forms for bug reports and feature requests + - Found a bug? Please report it here: https://github.com/ngandrass/moodle-quiz_archiver/issues + + ## Version 1.2.3 (2024011200) - Fix job setting storage issues with MySQL / MariaDB diff --git a/classes/Report.php b/classes/Report.php index 45a39b8..bc749ce 100644 --- a/classes/Report.php +++ b/classes/Report.php @@ -622,26 +622,29 @@ public function generate_full_page(int $attemptid, array $sections, bool $fix_re } /** @var string Regex for URLs of qtype_stack plots */ - const REGEX_MOODLE_URL_STACKPLOT = '/^(https?:\/\/[^\/]+)?(\/question\/type\/stack\/plot\.php\/)(?P[^\/\#\?\&]+\.(png|svg))/m'; + const REGEX_MOODLE_URL_STACKPLOT = '/^(?Phttps?:\/\/.+)?(\/question\/type\/stack\/plot\.php\/)(?P[^\/\#\?\&]+\.(png|svg))$/m'; /** @var string Regex for Moodle file API URLs */ - const REGEX_MOODLE_URL_PLUGINFILE = '/^(https?:\/\/[^\/]+)?(\/pluginfile\.php)(?P\/(?P[^\/]+)\/(?P[^\/]+)\/(?P[^\/]+)(\/(?P\d+))?\/(?P.*)?\/(?P[^\/\?\&\#]+))/m'; + const REGEX_MOODLE_URL_PLUGINFILE = '/^(?Phttps?:\/\/.+)?(\/pluginfile\.php)(?P\/(?P[^\/]+)\/(?P[^\/]+)\/(?P[^\/]+)(\/(?P\d+))?\/(?P.*)?\/(?P[^\/\?\&\#]+))$/m'; /** @var string Regex for Moodle file API URLs of specific types: component=(question|qtype_.*) */ - const REGEX_MOODLE_URL_PLUGINFILE_QUESTION_AND_QTYPE = '/^(https?:\/\/[^\/]+)?(\/pluginfile\.php)(?P\/(?P[^\/]+)\/(?P[^\/]+)\/(?P[^\/]+)\/(?P[^\/]+)\/(?P[^\/]+)\/(?P\d+)\/(?P[^\/\?\&\#]+))/m'; + const REGEX_MOODLE_URL_PLUGINFILE_QUESTION_AND_QTYPE = '/^(?Phttps?:\/\/.+)?(\/pluginfile\.php)(?P\/(?P[^\/]+)\/(?P[^\/]+)\/(?P[^\/]+)\/(?P[^\/]+)\/(?P[^\/]+)\/(?P\d+)\/(?P[^\/\?\&\#]+))$/m'; + + /** @var string Regex for Moodle theme image files */ + const REGEX_MOODLE_URL_THEME_IMAGE = '/^(?Phttps?:\/\/.+)?(\/theme\/image\.php\/)(?P[^\/]+)\/(?P[^\/]+)\/(?P[^\/]+)\/(?P.+)$/m'; /** @var string[] Mapping of file extensions to file types that are allowed to process */ const ALLOWED_IMAGE_TYPES = [ - 'png' => 'image/png', - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'svg' => 'image/svg+xml', - 'gif' => 'image/gif', - 'webp' => 'image/webp', - 'bmp' => 'image/bmp', - 'ico' => 'image/x-icon', - 'tiff' => 'image/tiff' - ]; + 'png' => 'image/png', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'svg' => 'image/svg+xml', + 'gif' => 'image/gif', + 'webp' => 'image/webp', + 'bmp' => 'image/bmp', + 'ico' => 'image/x-icon', + 'tiff' => 'image/tiff' + ]; /** * Tries to download and inline images of tags with src attributes as base64 encoded strings. Replacement @@ -656,7 +659,10 @@ protected function convert_image_to_base64(\DOMElement $img): bool { // Only process images with src attribute if (!$img->getAttribute('src')) { + $img->setAttribute('x-debug-notice', 'no source present'); return false; + } else { + $img->setAttribute('x-original-source', $img->getAttribute('src')); } // Remove any parameters and anchors from URL @@ -672,61 +678,98 @@ protected function convert_image_to_base64(\DOMElement $img): bool { # Make sure to only process web URLs and nothing that somehow remained a valid local filepath if (!substr($img_src_url, 0, 4) === "http") { // Yes, this includes https as well ;) + $img->setAttribute('x-debug-notice', 'not a web URL'); return false; } // Only process allowed image types $img_ext = pathinfo($img_src_url, PATHINFO_EXTENSION); if (!array_key_exists($img_ext, self::ALLOWED_IMAGE_TYPES)) { - return false; + // Edge case: Moodle theme images must not always contain extensions + if (!preg_match(self::REGEX_MOODLE_URL_THEME_IMAGE, $img_src_url)) { + $img->setAttribute('x-debug-notice', 'image type not allowed'); + return false; + } } // Try to get image content based on link type $regex_matches = null; $img_data = null; - if (preg_match(self::REGEX_MOODLE_URL_PLUGINFILE, $img_src_url, $regex_matches)) { - // ### Link type: Moodle pluginfile URL ### // - // Edge case detection: question / qtype files follow another pattern, inserting questionbank_id and question_slot after filearea ... - if ($regex_matches['component'] == 'question' || strpos($regex_matches['component'], 'qtype_') === 0) { - $regex_matches = null; - if (!preg_match(self::REGEX_MOODLE_URL_PLUGINFILE_QUESTION_AND_QTYPE, $img_src_url, $regex_matches)) return false; - } - // Get file content via Moodle File API - $fs = get_file_storage(); - $file = $fs->get_file( - $regex_matches['contextid'], - $regex_matches['component'], - $regex_matches['filearea'], - !empty($regex_matches['itemid']) ? $regex_matches['itemid'] : 0, - '/', // Dirty simplification but works for now *sigh* - $regex_matches['filename'] - ); - - if (!$file) { - return false; + // Handle special internal URLs first + $is_internal_url = substr($img_src_url, 0, strlen($moodle_baseurl)) === $moodle_baseurl; + if ($is_internal_url) { + if (preg_match(self::REGEX_MOODLE_URL_PLUGINFILE, $img_src_url, $regex_matches)) { + // ### Link type: Moodle pluginfile URL ### + $img->setAttribute('x-url-type', 'MOODLE_URL_PLUGINFILE'); + + // Edge case detection: question / qtype files follow another pattern, inserting questionbank_id and question_slot after filearea ... + if ($regex_matches['component'] == 'question' || strpos($regex_matches['component'], 'qtype_') === 0) { + $regex_matches = null; + if (!preg_match(self::REGEX_MOODLE_URL_PLUGINFILE_QUESTION_AND_QTYPE, $img_src_url, $regex_matches)) { + $img->setAttribute('x-url-type', 'MOODLE_URL_PLUGINFILE_QUESTION_AND_QTYPE'); + return false; + } + } + + // Get file content via Moodle File API + $fs = get_file_storage(); + $file = $fs->get_file( + $regex_matches['contextid'], + $regex_matches['component'], + $regex_matches['filearea'], + !empty($regex_matches['itemid']) ? $regex_matches['itemid'] : 0, + '/', // Dirty simplification but works for now *sigh* + $regex_matches['filename'] + ); + + if (!$file) { + $img->setAttribute('x-debug-notice', 'moodledata file not found'); + return false; + } + $img_data = $file->get_content(); + } else if (preg_match(self::REGEX_MOODLE_URL_STACKPLOT, $img_src_url, $regex_matches)) { + // ### Link type: qtype_stack plotfile ### + $img->setAttribute('x-url-type', 'MOODLE_URL_STACKPLOT'); + + // Get STACK plot file from disk + $filename = $CFG->dataroot . '/stack/plots/' . clean_filename($regex_matches['filename']); + if (!is_readable($filename)) { + $img->setAttribute('x-debug-notice', 'stack plot file not readable'); + return false; + } + $img_data = file_get_contents($filename); + } else { + $img->setAttribute('x-debug-internal-url-without-handler', ''); } - $img_data = $file->get_content(); - } else if (preg_match(self::REGEX_MOODLE_URL_STACKPLOT, $img_src_url, $regex_matches)) { - // ### Link type: qtype_stack plotfile ### // - // Get STACK plot file from disk - $filename = $CFG->dataroot . '/stack/plots/' . clean_filename($regex_matches['filename']); - if (!is_readable($filename)) { - return false; + } + + // Fall back to generic URL handling if image data not already set by internal handling routines + if ($img_data === null) { + if (preg_match(self::REGEX_MOODLE_URL_THEME_IMAGE, $img_src_url)) { + // ### Link type: Moodle theme image ### + // We should be able to download there images using a simple HTTP request + // Accessing them directly from disk is a little more complicated due to caching and other logic (see: /theme/image.php). + // Let's try to keep it this way until we encounter explicit problems. + $img->setAttribute('x-url-type', 'MOODLE_URL_THEME_IMAGE'); + } else { + // ### Link type: Generic ### + $img->setAttribute('x-url-type', 'GENERIC'); } - $img_data = file_get_contents($filename); - } else { - // ### Link type: Generic ### // + // No special local file access. Try to download via HTTP request - $c = new curl(); + $c = new curl(['ignoresecurity' => $is_internal_url]); $img_data = $c->get($img_src_url); // Curl handle automatically closed if ($c->get_info()['http_code'] !== 200 || $img_data === false) { + $img->setAttribute('x-debug-more', $img_data); + $img->setAttribute('x-debug-notice', 'HTTP request failed'); return false; } } // Encode and replace image if present if (!$img_data) { + $img->setAttribute('x-debug-notice', 'no image data'); return false; } $img_base64 = base64_encode($img_data); diff --git a/report.php b/report.php index 69e8a26..1708ef4 100644 --- a/report.php +++ b/report.php @@ -189,54 +189,62 @@ public function display($quiz, $cm, $course): bool { // Determine page to display if (!quiz_has_questions($quiz->id)) { - $tplCtx['quizHasNoQuestionsWarning'] = quiz_no_questions_message($quiz, $cm, $this->context); - echo $OUTPUT->render_from_template('quiz_archiver/overview', $tplCtx); - return false; + $tplCtx['quizMissingSomethingWarning'] = quiz_no_questions_message($quiz, $cm, $this->context); + } else { + if (!quiz_has_attempts($quiz->id)) { + $tplCtx['quizMissingSomethingWarning'] = $OUTPUT->notification( + get_string('noattempts', 'quiz'), + \core\output\notification::NOTIFY_ERROR, + false + ); + } } // Archive quiz form - $archive_quiz_form = new archive_quiz_form( - $this->quiz->name, - count($this->report->get_attempts()) - ); - if ($archive_quiz_form->is_submitted()) { - $job = null; - try { - if (!$archive_quiz_form->is_validated()) { - throw new RuntimeException(get_string('error_archive_quiz_form_validation_failed', 'quiz_archiver')); - } + if (!array_key_exists('quizMissingSomethingWarning', $tplCtx)) { + $archive_quiz_form = new archive_quiz_form( + $this->quiz->name, + count($this->report->get_attempts()) + ); + if ($archive_quiz_form->is_submitted()) { + $job = null; + try { + if (!$archive_quiz_form->is_validated()) { + throw new RuntimeException(get_string('error_archive_quiz_form_validation_failed', 'quiz_archiver')); + } - $formdata = $archive_quiz_form->get_data(); - $job = $this->initiate_archive_job( - $formdata->export_attempts, - Report::build_report_sections_from_formdata($formdata), - $formdata->export_attempts_keep_html_files, - $formdata->export_attempts_paper_format, - $formdata->export_quiz_backup, - $formdata->export_course_backup, - $formdata->archive_filename_pattern, - $formdata->export_attempts_filename_pattern, - $formdata->archive_autodelete ? $formdata->archive_retention_time : null, - ); - $tplCtx['jobInitiationStatusAlert'] = [ - "color" => "success", - "message" => get_string('job_created_successfully', 'quiz_archiver', $job->get_jobid()), - "returnMessage" => get_string('continue'), - ]; - } catch (RuntimeException $e) { - $tplCtx['jobInitiationStatusAlert'] = [ - "color" => "danger", - "message" => $e->getMessage(), - "returnMessage" => get_string('retry'), - ]; - } + $formdata = $archive_quiz_form->get_data(); + $job = $this->initiate_archive_job( + $formdata->export_attempts, + Report::build_report_sections_from_formdata($formdata), + $formdata->export_attempts_keep_html_files, + $formdata->export_attempts_paper_format, + $formdata->export_quiz_backup, + $formdata->export_course_backup, + $formdata->archive_filename_pattern, + $formdata->export_attempts_filename_pattern, + $formdata->archive_autodelete ? $formdata->archive_retention_time : null, + ); + $tplCtx['jobInitiationStatusAlert'] = [ + "color" => "success", + "message" => get_string('job_created_successfully', 'quiz_archiver', $job->get_jobid()), + "returnMessage" => get_string('continue'), + ]; + } catch (RuntimeException $e) { + $tplCtx['jobInitiationStatusAlert'] = [ + "color" => "danger", + "message" => $e->getMessage(), + "returnMessage" => get_string('retry'), + ]; + } - // Do not print job overview table if job creation failed - if ($job == null) { - unset($tplCtx['jobOverviewTable']); + // Do not print job overview table if job creation failed + if ($job == null) { + unset($tplCtx['jobOverviewTable']); + } + } else { + $tplCtx['jobInitiationForm'] = $archive_quiz_form->render(); } - } else { - $tplCtx['jobInitiationForm'] = $archive_quiz_form->render(); } // Job overview table diff --git a/res/backup-moodle2-course-qa-ref.mbz b/res/backup-moodle2-course-qa-ref.mbz index 9342b40..e82f38b 100644 Binary files a/res/backup-moodle2-course-qa-ref.mbz and b/res/backup-moodle2-course-qa-ref.mbz differ diff --git a/templates/overview.mustache b/templates/overview.mustache index ee77c24..c4d37b3 100644 --- a/templates/overview.mustache +++ b/templates/overview.mustache @@ -40,10 +40,13 @@ "json": "{...}" } ], - "quizHasNoQuestionsWarning": "
[...]
" + "quizMissingSomethingWarning": "
[...]
" } }} -{{{quizHasNoQuestionsWarning}}} +{{#quizMissingSomethingWarning}} + {{{quizMissingSomethingWarning}}} +

+{{/quizMissingSomethingWarning}} {{! Archive quiz form }}
diff --git a/version.php b/version.php index 97928d5..4a65d25 100644 --- a/version.php +++ b/version.php @@ -25,8 +25,8 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'quiz_archiver'; -$plugin->release = '1.2.3'; -$plugin->version = 2024012000; +$plugin->release = '1.2.4'; +$plugin->version = 2024021901; $plugin->requires = 2022112800; $plugin->supported = [401, 403]; //$plugin->incompatible = 402;