| 
 | 1 | +<?php  | 
 | 2 | +// This file is part of the QuestionPy Moodle plugin - https://questionpy.org  | 
 | 3 | +//  | 
 | 4 | +// Moodle is free software: you can redistribute it and/or modify  | 
 | 5 | +// it under the terms of the GNU General Public License as published by  | 
 | 6 | +// the Free Software Foundation, either version 3 of the License, or  | 
 | 7 | +// (at your option) any later version.  | 
 | 8 | +//  | 
 | 9 | +// Moodle is distributed in the hope that it will be useful,  | 
 | 10 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of  | 
 | 11 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the  | 
 | 12 | +// GNU General Public License for more details.  | 
 | 13 | +//  | 
 | 14 | +// You should have received a copy of the GNU General Public License  | 
 | 15 | +// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.  | 
 | 16 | + | 
 | 17 | +namespace qtype_questionpy\local\attempt_ui;  | 
 | 18 | + | 
 | 19 | +use core\context;  | 
 | 20 | +use core\di;  | 
 | 21 | +use core\exception\coding_exception;  | 
 | 22 | +use core_renderer;  | 
 | 23 | +use DOMDocument;  | 
 | 24 | +use DOMElement;  | 
 | 25 | +use DOMNode;  | 
 | 26 | +use file_exception;  | 
 | 27 | +use form_filemanager;  | 
 | 28 | +use moodle_exception;  | 
 | 29 | +use MoodleQuickForm_editor;  | 
 | 30 | +use qtype_questionpy\local\files\response_file_service;  | 
 | 31 | +use qtype_questionpy\local\files\validatable_upload_limits;  | 
 | 32 | +use qtype_questionpy\utils;  | 
 | 33 | +use qtype_questionpy_renderer;  | 
 | 34 | +use question_attempt;  | 
 | 35 | +use stdClass;  | 
 | 36 | +use stored_file_creation_exception;  | 
 | 37 | + | 
 | 38 | +defined('MOODLE_INTERNAL') || die();  | 
 | 39 | + | 
 | 40 | +global $CFG;  | 
 | 41 | +require_once($CFG->libdir . '/form/editor.php');  | 
 | 42 | + | 
 | 43 | +/**  | 
 | 44 | + * Represents a `<qpy:rich-text-editor/>` element in the question UI XML.  | 
 | 45 | + *  | 
 | 46 | + * @package    qtype_questionpy  | 
 | 47 | + * @author     Maximilian Haye  | 
 | 48 | + * @copyright  2025 TU Berlin, innoCampus {@link https://www.questionpy.org}  | 
 | 49 | + * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later  | 
 | 50 | + */  | 
 | 51 | +class qpy_rich_text_editor implements custom_xhtml_element {  | 
 | 52 | +    // The default in MoodleQuickForm_editor.  | 
 | 53 | +    /** @var int */  | 
 | 54 | +    private const RETURN_TYPES = FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE | FILE_CONTROLLED_LINK;  | 
 | 55 | + | 
 | 56 | +    /**  | 
 | 57 | +     * Trivial private constructor. Use {@see from_element()}.  | 
 | 58 | +     * @param DOMElement $element  | 
 | 59 | +     * @param string $name  | 
 | 60 | +     */  | 
 | 61 | +    private function __construct(  | 
 | 62 | +        /** @var DOMElement */  | 
 | 63 | +        private readonly DOMElement $element,  | 
 | 64 | +        /** @var string */  | 
 | 65 | +        public readonly string $name,  | 
 | 66 | +        /** @var bool */  | 
 | 67 | +        public readonly bool $required,  | 
 | 68 | +    ) {  | 
 | 69 | +    }  | 
 | 70 | + | 
 | 71 | +    /**  | 
 | 72 | +     * Gets the limits that should be validated for the current user in the given context when using this upload field.  | 
 | 73 | +     *  | 
 | 74 | +     * @param context $context  | 
 | 75 | +     * @return validatable_upload_limits  | 
 | 76 | +     */  | 
 | 77 | +    public function get_limits_in(context $context): validatable_upload_limits {  | 
 | 78 | +        global $CFG, $PAGE;  | 
 | 79 | +        require_once($CFG->libdir . '/formslib.php'); // For EDITOR_UNLIMITED_FILES.  | 
 | 80 | + | 
 | 81 | +        $maxfiles = $this->element->getAttribute('max-files');  | 
 | 82 | +        $maxfiles = is_numeric($maxfiles) ? intval($maxfiles) : EDITOR_UNLIMITED_FILES;  | 
 | 83 | + | 
 | 84 | +        $maxbytes = $this->element->getAttribute('max-bytes-per-file');  | 
 | 85 | +        $maxbytes = is_numeric($maxbytes) ? intval($maxbytes) : FILE_AREA_MAX_BYTES_UNLIMITED;  | 
 | 86 | +        $coursemaxbytes = 0;  | 
 | 87 | +        if (!empty($PAGE->course->maxbytes)) {  | 
 | 88 | +            $coursemaxbytes = $PAGE->course->maxbytes;  | 
 | 89 | +        }  | 
 | 90 | +        $maxbytes = get_user_max_upload_file_size($context, $CFG->maxbytes, $coursemaxbytes, $maxbytes);  | 
 | 91 | + | 
 | 92 | +        $areamaxbytes = $this->element->getAttribute('max-bytes-total');  | 
 | 93 | +        $areamaxbytes = is_numeric($areamaxbytes) ? intval($areamaxbytes) : FILE_AREA_MAX_BYTES_UNLIMITED;  | 
 | 94 | + | 
 | 95 | +        return new validatable_upload_limits($maxfiles, $maxbytes, $areamaxbytes);  | 
 | 96 | +    }  | 
 | 97 | + | 
 | 98 | +    /**  | 
 | 99 | +     * Parses the given DOMElement if possible.  | 
 | 100 | +     *  | 
 | 101 | +     * @param DOMElement $element  | 
 | 102 | +     * @return static|null  | 
 | 103 | +     */  | 
 | 104 | +    public static function from_element(DOMElement $element): ?static {  | 
 | 105 | +        $name = $element->getAttribute('name');  | 
 | 106 | +        if (!$name) {  | 
 | 107 | +            debugging('qpy:file-upload without a name');  | 
 | 108 | +            return null;  | 
 | 109 | +        }  | 
 | 110 | + | 
 | 111 | +        $required = $element->hasAttribute('required');  | 
 | 112 | + | 
 | 113 | +        return new static($element, $name, $required);  | 
 | 114 | +    }  | 
 | 115 | + | 
 | 116 | +    /**  | 
 | 117 | +     * Renders this element to a DOMNode.  | 
 | 118 | +     *  | 
 | 119 | +     * @param question_attempt $qa  | 
 | 120 | +     * @param question_ui_renderer $renderer  | 
 | 121 | +     * @return DOMNode  | 
 | 122 | +     * @throws coding_exception  | 
 | 123 | +     * @throws file_exception  | 
 | 124 | +     * @throws moodle_exception  | 
 | 125 | +     * @throws stored_file_creation_exception  | 
 | 126 | +     */  | 
 | 127 | +    public function render(question_attempt $qa, question_ui_renderer $renderer): DOMNode {  | 
 | 128 | +        $id = uniqid('qpy-editor-');  | 
 | 129 | + | 
 | 130 | +        $editor = editors_get_preferred_editor();  | 
 | 131 | + | 
 | 132 | +        $fragment = $this->element->ownerDocument->createDocumentFragment();  | 
 | 133 | + | 
 | 134 | +        $textarea = $this->element->ownerDocument->createElement('textarea');  | 
 | 135 | +        $textarea->setAttribute('name', $this->name);  | 
 | 136 | +        $textarea->setAttribute('id', $id);  | 
 | 137 | +        $textarea->setAttribute('rows', 15);  | 
 | 138 | +        $textarea->setAttribute('cols', 80);  | 
 | 139 | +        $fragment->append($textarea);  | 
 | 140 | + | 
 | 141 | +        $limits = $this->get_limits_in($renderer->options->context);  | 
 | 142 | + | 
 | 143 | +        $fpoptions = [];  | 
 | 144 | +        if ($limits->maxfiles !== 0) {  | 
 | 145 | +            $combinedfilearea = $renderer->prepare_combined_draft_area($qa);  | 
 | 146 | +            $rfs = di::get(response_file_service::class);  | 
 | 147 | +            global $USER;  | 
 | 148 | +            $splitdraftitemid = $rfs->prepare_split_draft_area($this->name, $USER->id, $combinedfilearea);  | 
 | 149 | + | 
 | 150 | +            // This is used to tell the qbehaviour what draft areas to save.  | 
 | 151 | +            $renderer->draftareas[$this->name] = $splitdraftitemid;  | 
 | 152 | + | 
 | 153 | +            $fpoptions = $this->get_fpoptions($limits, $splitdraftitemid, $renderer->options->context);  | 
 | 154 | +        }  | 
 | 155 | + | 
 | 156 | +        // $uploadsoptions = match ($limits->maxfiles) {  | 
 | 157 | +        // 0 => [  | 
 | 158 | +        // 'enable_filemanagement' => false,  | 
 | 159 | +        // ],  | 
 | 160 | +        // default => [  | 
 | 161 | +        // 'subdirs' => false,  | 
 | 162 | +        // 'maxfiles' => $limits->maxbytes,  | 
 | 163 | +        // 'maxbytes' => $limits->maxbytes,  | 
 | 164 | +        // 'areamaxbytes' => $limits->areamaxbytes,  | 
 | 165 | +        // ]  | 
 | 166 | +        // };  | 
 | 167 | +        //  | 
 | 168 | +        // $meditor = new MoodleQuickForm_editor(  | 
 | 169 | +        // elementName: $this->name,  | 
 | 170 | +        // elementLabel: null,  | 
 | 171 | +        // attributes: [  | 
 | 172 | +        // 'data-qpy_editor' => 'true',  | 
 | 173 | +        // ],  | 
 | 174 | +        // options: [  | 
 | 175 | +        // 'context' => $renderer->options->context,  | 
 | 176 | +        // ...$uploadsoptions,  | 
 | 177 | +        // ]  | 
 | 178 | +        // );  | 
 | 179 | +        // $meditor->_generateId();  | 
 | 180 | +        //  | 
 | 181 | +        // $meditor->setValue([  | 
 | 182 | +        // 'text' => '',  | 
 | 183 | +        // 'format' => 1,  | 
 | 184 | +        // 'itemid' => $splitdraftitemid,  | 
 | 185 | +        // ]);  | 
 | 186 | + | 
 | 187 | + | 
 | 188 | +        $editor->use_editor($id, [  | 
 | 189 | +            'context' => $renderer->options->context,  | 
 | 190 | +            'enable_filemanagement' => $limits->maxfiles !== 0,  | 
 | 191 | +            'required' => $this->required,  | 
 | 192 | +            'maxfiles' => $limits->maxfiles,  | 
 | 193 | +            'subdirs' => false,  | 
 | 194 | +            'maxbytes' => $limits->maxbytes,  | 
 | 195 | +            'areamaxbytes' => $limits->areamaxbytes,  | 
 | 196 | +            'return_types' => self::RETURN_TYPES,  | 
 | 197 | +        ], $fpoptions);  | 
 | 198 | + | 
 | 199 | +//        return dom_utils::html_to_fragment($this->element->ownerDocument, $meditor->toHtml());  | 
 | 200 | +        return $fragment;  | 
 | 201 | +    }  | 
 | 202 | + | 
 | 203 | +    /**  | 
 | 204 | +     * @param validatable_upload_limits $limits  | 
 | 205 | +     * @param int $draftitemid  | 
 | 206 | +     * @param context $context  | 
 | 207 | +     * @return array  | 
 | 208 | +     * @throws coding_exception  | 
 | 209 | +     */  | 
 | 210 | +    private function get_fpoptions(validatable_upload_limits $limits, int $draftitemid, context $context): array {  | 
 | 211 | +        // Mostly stolen from MoodleQuickForm_editor.  | 
 | 212 | + | 
 | 213 | +        if ($limits->maxfiles == 0) {  | 
 | 214 | +            return [];  | 
 | 215 | +        }  | 
 | 216 | + | 
 | 217 | +        $fpoptions = [];  | 
 | 218 | + | 
 | 219 | +        $args = new stdClass();  | 
 | 220 | +        // Need these three to filter repositories list.  | 
 | 221 | +        $args->accepted_types = ['web_image'];  | 
 | 222 | +        $args->return_types = self::RETURN_TYPES;  | 
 | 223 | +        $args->context = $context;  | 
 | 224 | +        $args->env = 'filepicker';  | 
 | 225 | + | 
 | 226 | +        // Advimage plugin.  | 
 | 227 | +        $imageoptions = initialise_filepicker($args);  | 
 | 228 | +        $imageoptions->context = $context;  | 
 | 229 | +        $imageoptions->client_id = uniqid();  | 
 | 230 | +        $imageoptions->maxbytes = $limits->maxbytes;  | 
 | 231 | +        $imageoptions->areamaxbytes = $limits->areamaxbytes;  | 
 | 232 | +        $imageoptions->env = 'editor';  | 
 | 233 | +        $imageoptions->itemid = $draftitemid;  | 
 | 234 | + | 
 | 235 | +        // Moodlemedia plugin.  | 
 | 236 | +        $args->accepted_types = ['video', 'audio'];  | 
 | 237 | +        $mediaoptions = initialise_filepicker($args);  | 
 | 238 | +        $mediaoptions->context = $context;  | 
 | 239 | +        $mediaoptions->client_id = uniqid();  | 
 | 240 | +        $mediaoptions->maxbytes  = $limits->maxbytes;  | 
 | 241 | +        $mediaoptions->areamaxbytes  = $limits->areamaxbytes;  | 
 | 242 | +        $mediaoptions->env = 'editor';  | 
 | 243 | +        $mediaoptions->itemid = $draftitemid;  | 
 | 244 | + | 
 | 245 | +        // Advlink plugin.  | 
 | 246 | +        $args->accepted_types = '*';  | 
 | 247 | +        $linkoptions = initialise_filepicker($args);  | 
 | 248 | +        $linkoptions->context = $context;  | 
 | 249 | +        $linkoptions->client_id = uniqid();  | 
 | 250 | +        $linkoptions->maxbytes  = $limits->maxbytes;  | 
 | 251 | +        $linkoptions->areamaxbytes  = $limits->areamaxbytes;  | 
 | 252 | +        $linkoptions->env = 'editor';  | 
 | 253 | +        $linkoptions->itemid = $draftitemid;  | 
 | 254 | + | 
 | 255 | +        // Subtitles, for multimedia.  | 
 | 256 | +        $args->accepted_types = ['.vtt'];  | 
 | 257 | +        $subtitleoptions = initialise_filepicker($args);  | 
 | 258 | +        $subtitleoptions->context = $context;  | 
 | 259 | +        $subtitleoptions->client_id = uniqid();  | 
 | 260 | +        $subtitleoptions->maxbytes  = $limits->maxbytes;  | 
 | 261 | +        $subtitleoptions->areamaxbytes  = $limits->areamaxbytes;  | 
 | 262 | +        $subtitleoptions->env = 'editor';  | 
 | 263 | +        $subtitleoptions->itemid = $draftitemid;  | 
 | 264 | + | 
 | 265 | +        if (has_capability('moodle/h5p:deploy', $context)) {  | 
 | 266 | +            // Only set H5P Plugin settings if the user can deploy new H5P content.  | 
 | 267 | +            // H5P plugin.  | 
 | 268 | +            $args->accepted_types = ['.h5p'];  | 
 | 269 | +            $h5poptions = initialise_filepicker($args);  | 
 | 270 | +            $h5poptions->context = $context;  | 
 | 271 | +            $h5poptions->client_id = uniqid();  | 
 | 272 | +            $h5poptions->maxbytes  = $limits->maxbytes;  | 
 | 273 | +            $h5poptions->areamaxbytes  = $limits->areamaxbytes;  | 
 | 274 | +            $h5poptions->env = 'editor';  | 
 | 275 | +            $h5poptions->itemid = $draftitemid;  | 
 | 276 | +            $fpoptions['h5p'] = $h5poptions;  | 
 | 277 | +        }  | 
 | 278 | + | 
 | 279 | +        $fpoptions['image'] = $imageoptions;  | 
 | 280 | +        $fpoptions['media'] = $mediaoptions;  | 
 | 281 | +        $fpoptions['link'] = $linkoptions;  | 
 | 282 | +        $fpoptions['subtitle'] = $subtitleoptions;  | 
 | 283 | + | 
 | 284 | +        return $fpoptions;  | 
 | 285 | +    }  | 
 | 286 | +}  | 
0 commit comments