' . s($message) . '
' . + '' . + get_string('moreinformation') . '
'; + if (empty($CFG->rolesactive)) { + $message .= '' . get_string('installproblem', 'error') . '
'; + // It is usually not possible to recover from errors triggered during installation, you may need to create a new database or use a different database prefix for new installation. + } + $output .= $this->box($message, 'errorbox alert alert-danger', null, ['data-rel' => 'fatalerror']); + + if ($CFG->debugdeveloper) { + $labelsep = get_string('labelsep', 'langconfig'); + if (!empty($debuginfo)) { + $debuginfo = s($debuginfo); // removes all nasty JS + $debuginfo = str_replace("\n", '+ * <
+ * if (!$this->page->requires->should_create_one_time_item_now($thing)) { + * return ''; + * } + * // Else generate it. + *+ * + * @param string $thing identifier for the bit of content. Should be of the form + * frankenstyle_things, e.g. core_course_modchooser. + * @return bool if true, the caller should generate that bit of output now, otherwise don't. + */ + public function should_create_one_time_item_now($thing) { + if ($this->has_one_time_item_been_created($thing)) { + return false; + } + + $this->set_one_time_item_created($thing); + return true; + } + + /** + * Has a particular bit of HTML that is only required once on this page + * (e.g. the contents of the modchooser) already been generated? + * + * Normally, you can use the {@see should_create_one_time_item_now()} helper + * method rather than calling this method directly. + * + * @param string $thing identifier for the bit of content. Should be of the form + * frankenstyle_things, e.g. core_course_modchooser. + * @return bool whether that bit of output has been created. + */ + public function has_one_time_item_been_created($thing) { + return isset($this->onetimeitemsoutput[$thing]); + } + + /** + * Indicate that a particular bit of HTML that is only required once on this + * page (e.g. the contents of the modchooser) has been generated (or is about to be)? + * + * Normally, you can use the {@see should_create_one_time_item_now()} helper + * method rather than calling this method directly. + * + * @param string $thing identifier for the bit of content. Should be of the form + * frankenstyle_things, e.g. core_course_modchooser. + */ + public function set_one_time_item_created($thing) { + if ($this->has_one_time_item_been_created($thing)) { + throw new coding_exception($thing . ' is only supposed to be ouput ' . + 'once per page, but it seems to be being output again.'); + } + return $this->onetimeitemsoutput[$thing] = true; + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(page_requirements_manager::class, \page_requirements_manager::class); diff --git a/lib/classes/output/requirements/yui.php b/lib/classes/output/requirements/yui.php new file mode 100644 index 0000000000000..58a695191c28b --- /dev/null +++ b/lib/classes/output/requirements/yui.php @@ -0,0 +1,379 @@ +. + +namespace core\output\requirements; + +use cache; +use core_component; +use core_minify; +use core\exception\coding_exception; +use DirectoryIterator; + +/** + * This class represents the YUI configuration. + * + * @copyright 2013 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.5 + * @package core + * @category output + */ +class yui { + /** + * These settings must be public so that when the object is converted to json they are exposed. + * Note: Some of these are camelCase because YUI uses camelCase variable names. + * + * The settings are described and documented in the YUI API at: + * - http://yuilibrary.com/yui/docs/api/classes/config.html + * - http://yuilibrary.com/yui/docs/api/classes/Loader.html + */ + public $debug = false; + public $base; + public $comboBase; + public $combine; + public $filter = null; + public $insertBefore = 'firstthemesheet'; + public $groups = []; + public $modules = []; + /** @var array The log sources that should be not be logged. */ + public $logInclude = []; + /** @var array Tog sources that should be logged. */ + public $logExclude = []; + /** @var string The minimum log level for YUI logging statements. */ + public $logLevel; + + /** + * @var array List of functions used by the YUI Loader group pattern recognition. + */ + protected $jsconfigfunctions = []; + + /** + * Create a new group within the YUI_config system. + * + * @param string $name The name of the group. This must be unique and + * not previously used. + * @param array $config The configuration for this group. + * @return void + */ + public function add_group($name, $config) { + if (isset($this->groups[$name])) { + throw new coding_exception( + "A YUI configuration group for '{$name}' already exists. " . + 'To make changes to this group use YUI_config->update_group().', + ); + } + $this->groups[$name] = $config; + } + + /** + * Update an existing group configuration + * + * Note, any existing configuration for that group will be wiped out. + * This includes module configuration. + * + * @param string $name The name of the group. This must be unique and + * not previously used. + * @param array $config The configuration for this group. + * @return void + */ + public function update_group($name, $config) { + if (!isset($this->groups[$name])) { + throw new coding_exception( + 'The Moodle YUI module does not exist. ' . + 'You must define the moodle module config using YUI_config->add_module_config first.', + ); + } + $this->groups[$name] = $config; + } + + /** + * Set the value of a configuration function used by the YUI Loader's pattern testing. + * + * Only the body of the function should be passed, and not the whole function wrapper. + * + * The JS function your write will be passed a single argument 'name' containing the + * name of the module being loaded. + * + * @param string $function String the body of the JavaScript function. This should be used i + * @return string the name of the function to use in the group pattern configuration. + */ + public function set_config_function($function) { + $configname = 'yui' . (count($this->jsconfigfunctions) + 1) . 'ConfigFn'; + if (isset($this->jsconfigfunctions[$configname])) { + throw new coding_exception( + "A YUI config function with this name already exists. Config function names must be unique.", + ); + } + $this->jsconfigfunctions[$configname] = $function; + return '@' . $configname . '@'; + } + + /** + * Allow setting of the config function described in {@see set_config_function} from a file. + * The contents of this file are then passed to set_config_function. + * + * When jsrev is positive, the function is minified and stored in a MUC cache for subsequent uses. + * + * @param string $file The path to the JavaScript function used for YUI configuration. + * @return string the name of the function to use in the group pattern configuration. + */ + public function set_config_source($file) { + global $CFG; + $cache = cache::make('core', 'yuimodules'); + + // Attempt to get the metadata from the cache. + $keyname = 'configfn_' . $file; + $fullpath = $CFG->dirroot . '/' . $file; + if (!isset($CFG->jsrev) || $CFG->jsrev == -1) { + $cache->delete($keyname); + $configfn = file_get_contents($fullpath); + } else { + $configfn = $cache->get($keyname); + if ($configfn === false) { + require_once($CFG->libdir . '/jslib.php'); + $configfn = core_minify::js_files([$fullpath]); + $cache->set($keyname, $configfn); + } + } + return $this->set_config_function($configfn); + } + + /** + * Retrieve the list of JavaScript functions for YUI_config groups. + * + * @return string The complete set of config functions + */ + public function get_config_functions() { + $configfunctions = ''; + foreach ($this->jsconfigfunctions as $functionname => $function) { + $configfunctions .= "var {$functionname} = function(me) {"; + $configfunctions .= $function; + $configfunctions .= "};\n"; + } + return $configfunctions; + } + + /** + * Update the header JavaScript with any required modification for the YUI Loader. + * + * @param string $js String The JavaScript to manipulate. + * @return string the modified JS string. + */ + public function update_header_js($js) { + // Update the names of the the configFn variables. + // The PHP json_encode function cannot handle literal names so we have to wrap + // them in @ and then replace them with literals of the same function name. + foreach ($this->jsconfigfunctions as $functionname => $function) { + $js = str_replace('"@' . $functionname . '@"', $functionname, $js); + } + return $js; + } + + /** + * Add configuration for a specific module. + * + * @param string $name The name of the module to add configuration for. + * @param array $config The configuration for the specified module. + * @param string $group The name of the group to add configuration for. + * If not specified, then this module is added to the global + * configuration. + * @return void + */ + public function add_module_config($name, $config, $group = null) { + if ($group) { + if (!isset($this->groups[$name])) { + throw new coding_exception( + 'The Moodle YUI module does not exist. ' . + 'You must define the moodle module config using YUI_config->add_module_config first.', + ); + } + if (!isset($this->groups[$group]['modules'])) { + $this->groups[$group]['modules'] = []; + } + $modules = &$this->groups[$group]['modules']; + } else { + $modules = &$this->modules; + } + $modules[$name] = $config; + } + + /** + * Add the moodle YUI module metadata for the moodle group to the YUI_config instance. + * + * If js caching is disabled, metadata will not be served causing YUI to calculate + * module dependencies as each module is loaded. + * + * If metadata does not exist it will be created and stored in a MUC entry. + * + * @return void + */ + public function add_moodle_metadata() { + global $CFG; + if (!isset($this->groups['moodle'])) { + throw new coding_exception( + 'The Moodle YUI module does not exist. ' . + 'You must define the moodle module config using YUI_config->add_module_config first.', + ); + } + + if (!isset($this->groups['moodle']['modules'])) { + $this->groups['moodle']['modules'] = []; + } + + $cache = cache::make('core', 'yuimodules'); + if (!isset($CFG->jsrev) || $CFG->jsrev == -1) { + $metadata = []; + $metadata = $this->get_moodle_metadata(); + $cache->delete('metadata'); + } else { + // Attempt to get the metadata from the cache. + if (!$metadata = $cache->get('metadata')) { + $metadata = $this->get_moodle_metadata(); + $cache->set('metadata', $metadata); + } + } + + // Merge with any metadata added specific to this page which was added manually. + $this->groups['moodle']['modules'] = array_merge( + $this->groups['moodle']['modules'], + $metadata + ); + } + + /** + * Determine the module metadata for all moodle YUI modules. + * + * This works through all modules capable of serving YUI modules, and attempts to get + * metadata for each of those modules. + * + * @return array of module metadata + */ + private function get_moodle_metadata() { + $moodlemodules = []; + // Core isn't a plugin type or subsystem - handle it seperately. + if ($module = $this->get_moodle_path_metadata(core_component::get_component_directory('core'))) { + $moodlemodules = array_merge($moodlemodules, $module); + } + + // Handle other core subsystems. + $subsystems = core_component::get_core_subsystems(); + foreach ($subsystems as $subsystem => $path) { + if (is_null($path)) { + continue; + } + if ($module = $this->get_moodle_path_metadata($path)) { + $moodlemodules = array_merge($moodlemodules, $module); + } + } + + // And finally the plugins. + $plugintypes = core_component::get_plugin_types(); + foreach ($plugintypes as $plugintype => $pathroot) { + $pluginlist = core_component::get_plugin_list($plugintype); + foreach ($pluginlist as $plugin => $path) { + if ($module = $this->get_moodle_path_metadata($path)) { + $moodlemodules = array_merge($moodlemodules, $module); + } + } + } + + return $moodlemodules; + } + + /** + * Helper function process and return the YUI metadata for all of the modules under the specified path. + * + * @param string $path the UNC path to the YUI src directory. + * @return array the complete array for frankenstyle directory. + */ + private function get_moodle_path_metadata($path) { + // Add module metadata is stored in frankenstyle_modname/yui/src/yui_modname/meta/yui_modname.json. + $baseyui = $path . '/yui/src'; + $modules = []; + if (is_dir($baseyui)) { + $items = new DirectoryIterator($baseyui); + foreach ($items as $item) { + if ($item->isDot() || !$item->isDir()) { + continue; + } + $metafile = realpath($baseyui . '/' . $item . '/meta/' . $item . '.json'); + if (!is_readable($metafile)) { + continue; + } + $metadata = file_get_contents($metafile); + $modules = array_merge($modules, (array) json_decode($metadata)); + } + } + return $modules; + } + + /** + * Define YUI modules which we have been required to patch between releases. + * + * We must do this because we aggressively cache content on the browser, and we must also override use of the + * external CDN which will serve the true authoritative copy of the code without our patches. + * + * @param string $combobase The local combobase + * @param string $yuiversion The current YUI version + * @param int $patchlevel The patch level we're working to for YUI + * @param array $patchedmodules An array containing the names of the patched modules + * @return void + */ + public function define_patched_core_modules($combobase, $yuiversion, $patchlevel, $patchedmodules) { + // The version we use is suffixed with a patchlevel so that we can get additional revisions between YUI releases. + $subversion = $yuiversion . '_' . $patchlevel; + + if ($this->comboBase == $combobase) { + // If we are using the local combobase in the loader, we can add a group and still make use of the combo + // loader. We just need to specify a different root which includes a slightly different YUI version number + // to include our patchlevel. + $patterns = []; + $modules = []; + foreach ($patchedmodules as $modulename) { + // We must define the pattern and module here so that the loader uses our group configuration instead of + // the standard module definition. We may lose some metadata provided by upstream but this will be + // loaded when the module is loaded anyway. + $patterns[$modulename] = [ + 'group' => 'yui-patched', + ]; + $modules[$modulename] = []; + } + + // Actually add the patch group here. + $this->add_group('yui-patched', [ + 'combine' => true, + 'root' => $subversion . '/', + 'patterns' => $patterns, + 'modules' => $modules, + ]); + } else { + // The CDN is in use - we need to instead use the local combobase for this module and override the modules + // definition. We cannot use the local base - we must use the combobase because we cannot invalidate the + // local base in browser caches. + $fullpathbase = $combobase . $subversion . '/'; + foreach ($patchedmodules as $modulename) { + $this->modules[$modulename] = [ + 'fullpath' => $fullpathbase . $modulename . '/' . $modulename . '-min.js', + ]; + } + } + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(yui::class, \YUI_config::class); diff --git a/lib/classes/output/select_menu.php b/lib/classes/output/select_menu.php index eb487911d1761..e642c70b6c69c 100644 --- a/lib/classes/output/select_menu.php +++ b/lib/classes/output/select_menu.php @@ -18,8 +18,6 @@ namespace core\output; -use renderer_base; - /** * A single-select combobox widget that is functionally similar to an HTML select element. * @@ -28,7 +26,7 @@ * @copyright 2022 Shamim Rezaie
+ * $THEME->layouts = array( + * // Most pages - if we encounter an unknown or a missing page type, this one is used. + * 'standard' => array( + * 'theme' = 'mytheme', + * 'file' => 'normal.php', + * 'regions' => array('side-pre', 'side-post'), + * 'defaultregion' => 'side-post' + * ), + * // The site home page. + * 'home' => array( + * 'theme' = 'mytheme', + * 'file' => 'home.php', + * 'regions' => array('side-pre', 'side-post'), + * 'defaultregion' => 'side-post' + * ), + * // ... + * ); + *+ * + * 'theme' name of the theme where is the layout located + * 'file' is the layout file to use for this type of page. + * layout files are stored in layout subfolder + * 'regions' This lists the regions on the page where blocks may appear. For + * each region you list here, your layout file must include a call to + *
+ * echo $OUTPUT->blocks_for_region($regionname); + *+ * or equivalent so that the blocks are actually visible. + * + * 'defaultregion' If the list of regions is non-empty, then you must pick + * one of the one of them as 'default'. This has two meanings. First, this is + * where new blocks are added. Second, if there are any blocks associated with + * the page, but in non-existent regions, they appear here. (Imaging, for example, + * that someone added blocks using a different theme that used different region + * names, and then switched to this theme.) + */ + public $layouts = []; + + /** + * @var string Name of the renderer factory class to use. Must implement the + * {@see renderer_factory} interface. + * + * This is an advanced feature. Moodle output is generated by 'renderers', + * you can customise the HTML that is output by writing custom renderers, + * and then you need to specify 'renderer factory' so that Moodle can find + * your renderers. + * + * There are some renderer factories supplied with Moodle. Please follow these + * links to see what they do. + *
There are no more open containers. This suggests there is a nesting problem.
' . + $this->output_log(), DEBUG_DEVELOPER); + return; + } + + $container = array_pop($this->opencontainers); + if ($container->type != $type) { + debugging('The type of container to be closed (' . $container->type . + ') does not match the type of the next open container (' . $type . + '). This suggests there is a nesting problem.
' . + $this->output_log(), DEBUG_DEVELOPER); + } + if ($this->isdebugging) { + $this->log('Close', $type); + } + return $container->closehtml; + } + + /** + * Close all but the last open container. This is useful in places like error + * handling, where you want to close all the open containers (apart from ) + * before outputting the error message. + * + * @param bool $shouldbenone assert that the stack should be empty now - causes a + * developer debug warning if it isn't. + * @return string the HTML required to close any open containers inside . + */ + public function pop_all_but_last($shouldbenone = false) { + if ($shouldbenone && count($this->opencontainers) != 1) { + debugging('Some HTML tags were opened in the body of the page but not closed.
' . + $this->output_log(), DEBUG_DEVELOPER); + } + $output = ''; + while (count($this->opencontainers) > 1) { + $container = array_pop($this->opencontainers); + $output .= $container->closehtml; + } + return $output; + } + + /** + * You can call this function if you want to throw away an instance of this + * class without properly emptying the stack (for example, in a unit test). + * Calling this method stops the destruct method from outputting a developer + * debug warning. After calling this method, the instance can no longer be used. + */ + public function discard() { + $this->opencontainers = null; + } + + /** + * Adds an entry to the log. + * + * @param string $action The name of the action + * @param string $type The type of action + */ + protected function log($action, $type) { + $this->log[] = 'https://moodle.org
" @javascript Scenario: Add and remove auto-link prevention to URLs diff --git a/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js b/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js index 085863073762b..a08c57003f0d1 100644 --- a/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js +++ b/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js @@ -1,3 +1,3 @@ -define("tiny_recordrtc/base_recorder",["exports","core/str","./common","core/pending","./options","editor_tiny/uploader","core/toast","core/modal_events","core/templates","core/notification","core/prefetch","core/local/modal/alert"],(function(_exports,_str,_common,_pending,_options,_uploader,_toast,ModalEvents,Templates,_notification,_prefetch,_alert){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_pending=_interopRequireDefault(_pending),_uploader=_interopRequireDefault(_uploader),ModalEvents=_interopRequireWildcard(ModalEvents),Templates=_interopRequireWildcard(Templates),_alert=_interopRequireDefault(_alert);return _exports.default=class{constructor(editor,modal){var obj,key,value;value=!1,(key="stopRequested")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,this.ready=!1,this.checkAndWarnAboutBrowserCompatibility()&&(this.editor=editor,this.config=(0,_options.getData)(editor).params,this.modal=modal,this.modalRoot=modal.getRoot()[0],this.startStopButton=this.modalRoot.querySelector('button[data-action="startstop"]'),this.uploadButton=this.modalRoot.querySelector('button[data-action="upload"]'),this.setRecordButtonState(!1),this.player=this.configurePlayer(),this.registerEventListeners(),this.ready=!0,this.captureUserMedia(),this.prefetchContent())}isReady(){return this.ready}configurePlayer(){throw new Error("configurePlayer() must be implemented in ".concat(this.constructor.name))}getSupportedTypes(){throw new Error("getSupportedTypes() must be implemented in ".concat(this.constructor.name))}getRecordingOptions(){throw new Error("getRecordingOptions() must be implemented in ".concat(this.constructor.name))}getFileName(prefix){throw new Error("getFileName() must be implemented in ".concat(this.constructor.name))}getMediaConstraints(){throw new Error("getMediaConstraints() must be implemented in ".concat(this.constructor.name))}playOnCapture(){return!1}getTimeLimit(){throw new Error("getTimeLimit() must be implemented in ".concat(this.constructor.name))}getEmbedTemplateName(){throw new Error("getEmbedTemplateName() must be implemented in ".concat(this.constructor.name))}static getModalClass(){throw new Error("getModalClass() must be implemented in ".concat(this.constructor.name))}getParsedRecordingOptions(){const compatTypes=this.getSupportedTypes().reduce(((result,type)=>(result.push(type),result.push(type.replace("=",":")),result)),[]).filter((type=>window.MediaRecorder.isTypeSupported(type))),options=this.getRecordingOptions();return 0!==compatTypes.length&&(options.mimeType=compatTypes[0]),window.console.info("Selected codec ".concat(options.mimeType," from ").concat(compatTypes.length," options."),compatTypes),options}async captureUserMedia(){try{const stream=await navigator.mediaDevices.getUserMedia(this.getMediaConstraints());this.handleCaptureSuccess(stream)}catch(error){this.handleCaptureFailure(error)}}prefetchContent(){(0,_prefetch.prefetchStrings)(_common.component,["uploading","recordagain_title","recordagain_desc","discard_title","discard_desc","confirm_yes","recordinguploaded","maxfilesizehit","maxfilesizehit_title","uploadfailed"]),(0,_prefetch.prefetchTemplates)([this.getEmbedTemplateName(),"tiny_recordrtc/timeremaining"])}async displayAlert(title,content){const pendingPromise=new _pending.default("core/confirm:alert"),modal=await _alert.default.create({title:title,body:content,removeOnClose:!0});return modal.show(),pendingPromise.resolve(),modal}handleCaptureSuccess(stream){this.player.srcObject=stream,this.playOnCapture()&&(this.player.muted=!0,this.player.play()),this.stream=stream,this.setupPlayerSource(),this.setRecordButtonState(!0)}setupPlayerSource(){this.player.srcObject||(this.player.srcObject=this.stream,this.player.muted=!0,this.player.play())}setRecordButtonState(enabled){this.startStopButton.disabled=!enabled}setRecordButtonVisibility(visible){this.getButtonContainer("start-stop").classList.toggle("hide",!visible)}setUploadButtonState(enabled){this.uploadButton.disabled=!enabled}setUploadButtonVisibility(visible){this.getButtonContainer("upload").classList.toggle("hide",!visible)}handleCaptureFailure(error){var subject="gum".concat(error.name.replace("Error","").toLowerCase());this.displayAlert((0,_str.getString)("".concat(subject,"_title"),_common.component),(0,_str.getString)(subject,_common.component))}close(){this.modal.hide()}registerEventListeners(){this.modalRoot.addEventListener("click",this.handleModalClick.bind(this)),this.modal.getRoot().on(ModalEvents.outsideClick,this.outsideClickHandler.bind(this)),this.modal.getRoot().on(ModalEvents.hidden,(()=>{this.cleanupStream(),this.requestRecordingStop()}))}async outsideClickHandler(event){if(this.isRecording())event.preventDefault();else if(this.hasData()){event.preventDefault();try{await(0,_notification.saveCancelPromise)(await(0,_str.getString)("discard_title",_common.component),await(0,_str.getString)("discard_desc",_common.component),await(0,_str.getString)("confirm_yes",_common.component)),this.modal.hide()}catch(error){}}}handleModalClick(event){const button=event.target.closest("button");if(button&&button.dataset.action){const action=button.dataset.action;"startstop"===action&&this.handleRecordingStartStopRequested(),"upload"===action&&this.uploadRecording()}}handleRecordingStartStopRequested(){var _this$mediaRecorder;"recording"===(null===(_this$mediaRecorder=this.mediaRecorder)||void 0===_this$mediaRecorder?void 0:_this$mediaRecorder.state)?this.requestRecordingStop():this.startRecording()}async onMediaStopped(){var _this$getButtonContai;this.blob=new Blob(this.data.chunks,{type:this.mediaRecorder.mimeType}),this.player.srcObject=null,this.player.src=URL.createObjectURL(this.blob),this.setRecordButtonTextFromString("recordagain"),this.player.muted=!1,this.player.controls=!0,null===(_this$getButtonContai=this.getButtonContainer("player"))||void 0===_this$getButtonContai||_this$getButtonContai.classList.toggle("hide",!1),this.setUploadButtonVisibility(!0),this.setUploadButtonState(!0)}async uploadRecording(){if(0===this.data.chunks.length)return void this.displayAlert("norecordingfound");const fileName=this.getFileName((1e3*Math.random()).toString().replace(".",""));try{this.setRecordButtonVisibility(!1),this.setUploadButtonState(!1);const fileURL=await(0,_uploader.default)(this.editor,"media",this.blob,fileName,(progress=>{this.setUploadButtonTextProgress(progress)}));this.insertMedia(fileURL),this.close(),(0,_toast.add)(await(0,_str.getString)("recordinguploaded",_common.component))}catch(error){this.setUploadButtonState(!0),(0,_toast.add)(await(0,_str.getString)("uploadfailed",_common.component,{error:error}),{type:"error"})}}getButtonContainer(purpose){return this.modalRoot.querySelector('[data-purpose="'.concat(purpose,'-container"]'))}static isBrowserCompatible(){return this.checkSecure()&&this.hasUserMedia()}static async display(editor){const ModalClass=this.getModalClass(),modal=await ModalClass.create({templateContext:{},large:!0,removeOnClose:!0});return new this(editor,modal).isReady()&&modal.show(),modal}checkAndWarnAboutBrowserCompatibility(){return this.constructor.checkSecure()?!!this.constructor.hasUserMedia||((0,_str.getStrings)(["nowebrtc_title","nowebrtc"].map((key=>({key:key,component:_common.component})))).then((_ref2=>{let[title,message]=_ref2;return(0,_toast.add)(message,{title:title,type:"error"})})).catch(),!1):((0,_str.getStrings)(["insecurealert_title","insecurealert"].map((key=>({key:key,component:_common.component})))).then((_ref=>{let[title,message]=_ref;return(0,_toast.add)(message,{title:title,type:"error"})})).catch(),!1)}static hasUserMedia(){return navigator.mediaDevices&&window.MediaRecorder}static checkSecure(){return window.isSecureContext}async setStopRecordingButton(){const{html:html,js:js}=await Templates.renderForPromise("tiny_recordrtc/timeremaining",this.getTimeRemaining());Templates.replaceNodeContents(this.startStopButton,html,js),this.buttonTimer=setInterval(this.updateRecordButtonTime.bind(this),500)}updateRecordButtonTime(){const{remaining:remaining,minutes:minutes,seconds:seconds}=this.getTimeRemaining();remaining<0?this.requestRecordingStop():(this.startStopButton.querySelector('[data-type="minutes"]').textContent=minutes,this.startStopButton.querySelector('[data-type="seconds"]').textContent=seconds)}async setRecordButtonTextFromString(string){this.startStopButton.textContent=await(0,_str.getString)(string,_common.component)}async setUploadButtonTextProgress(progress){this.uploadButton.textContent=await(0,_str.getString)("uploading",_common.component,{progress:Math.round(100*progress)/100})}async resetUploadButtonText(){this.uploadButton.textContent=await(0,_str.getString)("upload",_common.component)}clearButtonTimer(){this.buttonTimer&&clearInterval(this.buttonTimer),this.buttonTimer=null}getTimeRemaining(){const now=(new Date).getTime(),remaining=Math.floor(this.getTimeLimit()-(now-this.startTime)/1e3),formatter=new Intl.NumberFormat(navigator.language,{minimumIntegerDigits:2}),seconds=formatter.format(remaining%60);return{remaining:remaining,minutes:formatter.format(Math.floor((remaining-seconds)/60)),seconds:seconds}}getMaxUploadSize(){return this.config.maxrecsize}requestRecordingStop(){this.mediaRecorder&&"inactive"!==this.mediaRecorder.state?this.stopRequested=!0:this.cleanupStream()}stopRecorder(){this.mediaRecorder.stop(),this.player.muted=!1}cleanupStream(){this.stream&&this.stream.getTracks().filter((track=>"ended"!==track.readyState)).forEach((track=>track.stop()))}handleStopped(){this.onMediaStopped(),this.clearButtonTimer()}handleStarted(){this.startTime=(new Date).getTime(),this.setStopRecordingButton()}handleDataAvailable(event){if(this.isRecording()){const newSize=this.data.blobSize+event.data.size;newSize>=this.getMaxUploadSize()?(this.stopRecorder(),this.displayFileLimitHitMessage()):(this.data.chunks.push(event.data),this.data.blobSize=newSize,this.stopRequested&&this.stopRecorder())}}async displayFileLimitHitMessage(){(0,_toast.add)(await(0,_str.getString)("maxfilesizehit",_common.component),{title:await(0,_str.getString)("maxfilesizehit_title",_common.component),type:"error"})}isRecording(){var _this$mediaRecorder2;return"recording"===(null===(_this$mediaRecorder2=this.mediaRecorder)||void 0===_this$mediaRecorder2?void 0:_this$mediaRecorder2.state)}hasData(){var _this$data;return!(null===(_this$data=this.data)||void 0===_this$data||!_this$data.blobSize)}async startRecording(){if(this.mediaRecorder){if(this.isRecording()&&this.mediaRecorder.stop(),this.hasData()){if(!await this.recordAgainConfirmation())return;this.setUploadButtonVisibility(!1),this.stream.active||await this.captureUserMedia()}this.mediaRecorder=null}this.mediaRecorder=new MediaRecorder(this.stream,this.getParsedRecordingOptions()),this.mediaRecorder.addEventListener("dataavailable",this.handleDataAvailable.bind(this)),this.mediaRecorder.addEventListener("stop",this.handleStopped.bind(this)),this.mediaRecorder.addEventListener("start",this.handleStarted.bind(this)),this.data={chunks:[],blobSize:0},this.setupPlayerSource(),this.stopRequested=!1,this.mediaRecorder.start(50)}async recordAgainConfirmation(){try{return await(0,_notification.saveCancelPromise)(await(0,_str.getString)("recordagain_title",_common.component),await(0,_str.getString)("recordagain_desc",_common.component),await(0,_str.getString)("confirm_yes",_common.component)),!0}catch{return!1}}async insertMedia(source){const{html:html}=await Templates.renderForPromise(this.getEmbedTemplateName(),this.getEmbedTemplateContext({source:source}));this.editor.insertContent(html)}getEmbedTemplateContext(templateContext){return templateContext}},_exports.default})); +define("tiny_recordrtc/base_recorder",["exports","core/str","./common","core/pending","./options","editor_tiny/uploader","core/toast","core/modal_events","core/templates","core/notification","core/prefetch","core/local/modal/alert"],(function(_exports,_str,_common,_pending,_options,_uploader,_toast,ModalEvents,Templates,_notification,_prefetch,_alert){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_pending=_interopRequireDefault(_pending),_uploader=_interopRequireDefault(_uploader),ModalEvents=_interopRequireWildcard(ModalEvents),Templates=_interopRequireWildcard(Templates),_alert=_interopRequireDefault(_alert);return _exports.default=class{constructor(editor,modal){_defineProperty(this,"stopRequested",!1),_defineProperty(this,"buttonTimer",null),_defineProperty(this,"pauseTime",null),_defineProperty(this,"startTime",null),this.ready=!1,this.checkAndWarnAboutBrowserCompatibility()&&(this.editor=editor,this.config=(0,_options.getData)(editor).params,this.modal=modal,this.modalRoot=modal.getRoot()[0],this.startStopButton=this.modalRoot.querySelector('button[data-action="startstop"]'),this.uploadButton=this.modalRoot.querySelector('button[data-action="upload"]'),this.pauseResumeButton=this.modalRoot.querySelector('button[data-action="pauseresume"]'),this.setRecordButtonState(!1),this.player=this.configurePlayer(),this.registerEventListeners(),this.ready=!0,this.captureUserMedia(),this.prefetchContent())}isReady(){return this.ready}configurePlayer(){throw new Error("configurePlayer() must be implemented in ".concat(this.constructor.name))}getSupportedTypes(){throw new Error("getSupportedTypes() must be implemented in ".concat(this.constructor.name))}getRecordingOptions(){throw new Error("getRecordingOptions() must be implemented in ".concat(this.constructor.name))}getFileName(prefix){throw new Error("getFileName() must be implemented in ".concat(this.constructor.name))}getMediaConstraints(){throw new Error("getMediaConstraints() must be implemented in ".concat(this.constructor.name))}playOnCapture(){return!1}getTimeLimit(){throw new Error("getTimeLimit() must be implemented in ".concat(this.constructor.name))}getEmbedTemplateName(){throw new Error("getEmbedTemplateName() must be implemented in ".concat(this.constructor.name))}static getModalClass(){throw new Error("getModalClass() must be implemented in ".concat(this.constructor.name))}getParsedRecordingOptions(){const compatTypes=this.getSupportedTypes().reduce(((result,type)=>(result.push(type),result.push(type.replace("=",":")),result)),[]).filter((type=>window.MediaRecorder.isTypeSupported(type))),options=this.getRecordingOptions();return 0!==compatTypes.length&&(options.mimeType=compatTypes[0]),window.console.info("Selected codec ".concat(options.mimeType," from ").concat(compatTypes.length," options."),compatTypes),options}async captureUserMedia(){try{const stream=await navigator.mediaDevices.getUserMedia(this.getMediaConstraints());this.handleCaptureSuccess(stream)}catch(error){this.handleCaptureFailure(error)}}prefetchContent(){(0,_prefetch.prefetchStrings)(_common.component,["uploading","recordagain_title","recordagain_desc","discard_title","discard_desc","confirm_yes","recordinguploaded","maxfilesizehit","maxfilesizehit_title","uploadfailed","pause","resume"]),(0,_prefetch.prefetchTemplates)([this.getEmbedTemplateName(),"tiny_recordrtc/timeremaining"])}async displayAlert(title,content){const pendingPromise=new _pending.default("core/confirm:alert"),modal=await _alert.default.create({title:title,body:content,removeOnClose:!0});return modal.show(),pendingPromise.resolve(),modal}handleCaptureSuccess(stream){this.player.srcObject=stream,this.playOnCapture()&&(this.player.muted=!0,this.player.play()),this.stream=stream,this.setupPlayerSource(),this.setRecordButtonState(!0)}setupPlayerSource(){this.player.srcObject||(this.player.srcObject=this.stream,this.player.muted=!0,this.player.play())}setRecordButtonState(enabled){this.startStopButton.disabled=!enabled}setRecordButtonVisibility(visible){this.getButtonContainer("start-stop").classList.toggle("hide",!visible)}setPauseButtonVisibility(visible){this.pauseResumeButton&&this.pauseResumeButton.classList.toggle("hidden",!visible)}setUploadButtonState(enabled){this.uploadButton.disabled=!enabled}setUploadButtonVisibility(visible){this.getButtonContainer("upload").classList.toggle("hide",!visible)}setPlayerState(state){var _this$getButtonContai;this.player.muted=!state,this.player.controls=state,null===(_this$getButtonContai=this.getButtonContainer("player"))||void 0===_this$getButtonContai||_this$getButtonContai.classList.toggle("hide",!state)}handleCaptureFailure(error){var subject="gum".concat(error.name.replace("Error","").toLowerCase());this.displayAlert((0,_str.getString)("".concat(subject,"_title"),_common.component),(0,_str.getString)(subject,_common.component))}close(){this.modal.hide()}registerEventListeners(){this.modalRoot.addEventListener("click",this.handleModalClick.bind(this)),this.modal.getRoot().on(ModalEvents.outsideClick,this.outsideClickHandler.bind(this)),this.modal.getRoot().on(ModalEvents.hidden,(()=>{this.cleanupStream(),this.requestRecordingStop()}))}async outsideClickHandler(event){if(this.isRecording()||this.isPaused())event.preventDefault();else if(this.hasData()){event.preventDefault();try{await(0,_notification.saveCancelPromise)(await(0,_str.getString)("discard_title",_common.component),await(0,_str.getString)("discard_desc",_common.component),await(0,_str.getString)("confirm_yes",_common.component)),this.modal.hide()}catch(error){}}}handleModalClick(event){const button=event.target.closest("button");if(button&&button.dataset.action){const action=button.dataset.action;"startstop"===action&&this.handleRecordingStartStopRequested(),"upload"===action&&this.uploadRecording(),"pauseresume"===action&&this.handleRecordingPauseResumeRequested()}}handleRecordingStartStopRequested(){this.isRecording()||this.isPaused()?this.requestRecordingStop():this.startRecording()}handleRecordingPauseResumeRequested(){this.isRecording()?this.mediaRecorder.pause():this.isPaused()&&this.mediaRecorder.resume()}async onMediaStopped(){this.blob=new Blob(this.data.chunks,{type:this.mediaRecorder.mimeType}),this.player.srcObject=null,this.player.src=URL.createObjectURL(this.blob),this.setRecordButtonTextFromString("recordagain"),this.setUploadButtonVisibility(!0),this.setPlayerState(!0),this.setUploadButtonState(!0),this.setPauseButtonVisibility(!1),"inactive"===this.mediaRecorder.state&&this.setPauseButtonTextFromString("pause")}async uploadRecording(){if(0===this.data.chunks.length)return void this.displayAlert("norecordingfound");const fileName=this.getFileName((1e3*Math.random()).toString().replace(".",""));try{this.setRecordButtonVisibility(!1),this.setUploadButtonState(!1);const fileURL=await(0,_uploader.default)(this.editor,"media",this.blob,fileName,(progress=>{this.setUploadButtonTextProgress(progress)}));this.insertMedia(fileURL),this.close(),(0,_toast.add)(await(0,_str.getString)("recordinguploaded",_common.component))}catch(error){this.setUploadButtonState(!0),(0,_toast.add)(await(0,_str.getString)("uploadfailed",_common.component,{error:error}),{type:"error"})}}getButtonContainer(purpose){return this.modalRoot.querySelector('[data-purpose="'.concat(purpose,'-container"]'))}static isBrowserCompatible(){return this.checkSecure()&&this.hasUserMedia()}static async display(editor){const ModalClass=this.getModalClass(),modal=await ModalClass.create({templateContext:{isallowedpausing:(0,_options.isPausingAllowed)(editor)},large:!0,removeOnClose:!0});return new this(editor,modal).isReady()&&modal.show(),modal}checkAndWarnAboutBrowserCompatibility(){return this.constructor.checkSecure()?!!this.constructor.hasUserMedia||((0,_str.getStrings)(["nowebrtc_title","nowebrtc"].map((key=>({key:key,component:_common.component})))).then((_ref2=>{let[title,message]=_ref2;return(0,_toast.add)(message,{title:title,type:"error"})})).catch(),!1):((0,_str.getStrings)(["insecurealert_title","insecurealert"].map((key=>({key:key,component:_common.component})))).then((_ref=>{let[title,message]=_ref;return(0,_toast.add)(message,{title:title,type:"error"})})).catch(),!1)}static hasUserMedia(){return navigator.mediaDevices&&window.MediaRecorder}static checkSecure(){return window.isSecureContext}async setStopRecordingButton(){const{html:html,js:js}=await Templates.renderForPromise("tiny_recordrtc/timeremaining",this.getTimeRemaining());Templates.replaceNodeContents(this.startStopButton,html,js),this.startButtonTimer()}updateRecordButtonTime(){const{remaining:remaining,minutes:minutes,seconds:seconds}=this.getTimeRemaining();remaining<0?this.requestRecordingStop():(this.startStopButton.querySelector('[data-type="minutes"]').textContent=minutes,this.startStopButton.querySelector('[data-type="seconds"]').textContent=seconds)}async setRecordButtonTextFromString(string){this.startStopButton.textContent=await(0,_str.getString)(string,_common.component)}async setPauseButtonTextFromString(string){this.pauseResumeButton&&(this.pauseResumeButton.textContent=await(0,_str.getString)(string,_common.component))}async setUploadButtonTextProgress(progress){this.uploadButton.textContent=await(0,_str.getString)("uploading",_common.component,{progress:Math.round(100*progress)/100})}async resetUploadButtonText(){this.uploadButton.textContent=await(0,_str.getString)("upload",_common.component)}clearButtonTimer(){this.buttonTimer&&clearInterval(this.buttonTimer),this.buttonTimer=null,this.pauseTime=null,this.startTime=null}pauseButtonTimer(){this.pauseTime=(new Date).getTime(),this.buttonTimer&&clearInterval(this.buttonTimer)}startButtonTimer(){if(null!==this.pauseTime){const pauseDuration=(new Date).getTime()-this.pauseTime;this.startTime+=pauseDuration,this.pauseTime=null}this.buttonTimer=setInterval(this.updateRecordButtonTime.bind(this),500)}getTimeRemaining(){let now=(new Date).getTime();null!==this.pauseTime&&(now=this.pauseTime);const remaining=Math.floor(this.getTimeLimit()-(now-this.startTime)/1e3),formatter=new Intl.NumberFormat(navigator.language,{minimumIntegerDigits:2}),seconds=formatter.format(remaining%60);return{remaining:remaining,minutes:formatter.format(Math.floor((remaining-seconds)/60)),seconds:seconds}}getMaxUploadSize(){return this.config.maxrecsize}requestRecordingStop(){this.mediaRecorder&&"inactive"!==this.mediaRecorder.state?(this.stopRequested=!0,this.isPaused()&&this.stopRecorder()):this.cleanupStream()}stopRecorder(){this.isPaused()&&(this.pauseTime=null),this.mediaRecorder.stop(),this.player.muted=!1}cleanupStream(){this.stream&&this.stream.getTracks().filter((track=>"ended"!==track.readyState)).forEach((track=>track.stop()))}handleStopped(){this.onMediaStopped(),this.clearButtonTimer()}handleStarted(){this.startTime=(new Date).getTime(),(0,_options.isPausingAllowed)(this.editor)&&!this.isPaused()&&this.setPauseButtonVisibility(!0),this.setStopRecordingButton()}handlePaused(){this.pauseButtonTimer(),this.setPauseButtonTextFromString("resume")}handleResume(){this.startButtonTimer(),this.setPauseButtonTextFromString("pause")}handleDataAvailable(event){if(this.isRecording()||this.isPaused()){const newSize=this.data.blobSize+event.data.size;newSize>=this.getMaxUploadSize()?(this.stopRecorder(),this.displayFileLimitHitMessage()):(this.data.chunks.push(event.data),this.data.blobSize=newSize,this.stopRequested&&this.stopRecorder())}}async displayFileLimitHitMessage(){(0,_toast.add)(await(0,_str.getString)("maxfilesizehit",_common.component),{title:await(0,_str.getString)("maxfilesizehit_title",_common.component),type:"error"})}isRecording(){var _this$mediaRecorder;return"recording"===(null===(_this$mediaRecorder=this.mediaRecorder)||void 0===_this$mediaRecorder?void 0:_this$mediaRecorder.state)}isPaused(){var _this$mediaRecorder2;return"paused"===(null===(_this$mediaRecorder2=this.mediaRecorder)||void 0===_this$mediaRecorder2?void 0:_this$mediaRecorder2.state)}hasData(){var _this$data;return!(null===(_this$data=this.data)||void 0===_this$data||!_this$data.blobSize)}async startRecording(){if(this.mediaRecorder){if((this.isRecording()||this.isPaused())&&this.mediaRecorder.stop(),this.hasData()){if(!await this.recordAgainConfirmation())return;this.setUploadButtonVisibility(!1),this.setPlayerState(!1),this.stream.active||await this.captureUserMedia()}this.mediaRecorder=null}this.mediaRecorder=new MediaRecorder(this.stream,this.getParsedRecordingOptions()),this.mediaRecorder.addEventListener("dataavailable",this.handleDataAvailable.bind(this)),this.mediaRecorder.addEventListener("stop",this.handleStopped.bind(this)),this.mediaRecorder.addEventListener("start",this.handleStarted.bind(this)),this.mediaRecorder.addEventListener("pause",this.handlePaused.bind(this)),this.mediaRecorder.addEventListener("resume",this.handleResume.bind(this)),this.data={chunks:[],blobSize:0},this.setupPlayerSource(),this.stopRequested=!1,this.mediaRecorder.start(50)}async recordAgainConfirmation(){try{return await(0,_notification.saveCancelPromise)(await(0,_str.getString)("recordagain_title",_common.component),await(0,_str.getString)("recordagain_desc",_common.component),await(0,_str.getString)("confirm_yes",_common.component)),!0}catch{return!1}}async insertMedia(source){const{html:html}=await Templates.renderForPromise(this.getEmbedTemplateName(),this.getEmbedTemplateContext({source:source}));this.editor.insertContent(html)}getEmbedTemplateContext(templateContext){return templateContext}},_exports.default})); //# sourceMappingURL=base_recorder.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js.map b/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js.map index a9dbd3cebb052..9eb02d8395575 100644 --- a/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js.map +++ b/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js.map @@ -1 +1 @@ -{"version":3,"file":"base_recorder.min.js","sources":["../src/base_recorder.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see- * if (!$this->page->requires->should_create_one_time_item_now($thing)) { - * return ''; - * } - * // Else generate it. - *- * - * @param string $thing identifier for the bit of content. Should be of the form - * frankenstyle_things, e.g. core_course_modchooser. - * @return bool if true, the caller should generate that bit of output now, otherwise don't. - */ - public function should_create_one_time_item_now($thing) { - if ($this->has_one_time_item_been_created($thing)) { - return false; - } - - $this->set_one_time_item_created($thing); - return true; - } - - /** - * Has a particular bit of HTML that is only required once on this page - * (e.g. the contents of the modchooser) already been generated? - * - * Normally, you can use the {@link should_create_one_time_item_now()} helper - * method rather than calling this method directly. - * - * @param string $thing identifier for the bit of content. Should be of the form - * frankenstyle_things, e.g. core_course_modchooser. - * @return bool whether that bit of output has been created. - */ - public function has_one_time_item_been_created($thing) { - return isset($this->onetimeitemsoutput[$thing]); - } - - /** - * Indicate that a particular bit of HTML that is only required once on this - * page (e.g. the contents of the modchooser) has been generated (or is about to be)? - * - * Normally, you can use the {@link should_create_one_time_item_now()} helper - * method rather than calling this method directly. - * - * @param string $thing identifier for the bit of content. Should be of the form - * frankenstyle_things, e.g. core_course_modchooser. - */ - public function set_one_time_item_created($thing) { - if ($this->has_one_time_item_been_created($thing)) { - throw new coding_exception($thing . ' is only supposed to be ouput ' . - 'once per page, but it seems to be being output again.'); - } - return $this->onetimeitemsoutput[$thing] = true; - } -} - -/** - * This class represents the YUI configuration. - * - * @copyright 2013 Andrew Nicols - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.5 - * @package core - * @category output - */ -class YUI_config { - /** - * These settings must be public so that when the object is converted to json they are exposed. - * Note: Some of these are camelCase because YUI uses camelCase variable names. - * - * The settings are described and documented in the YUI API at: - * - http://yuilibrary.com/yui/docs/api/classes/config.html - * - http://yuilibrary.com/yui/docs/api/classes/Loader.html - */ - public $debug = false; - public $base; - public $comboBase; - public $combine; - public $filter = null; - public $insertBefore = 'firstthemesheet'; - public $groups = array(); - public $modules = array(); - /** @var array The log sources that should be not be logged. */ - public $logInclude = []; - /** @var array Tog sources that should be logged. */ - public $logExclude = []; - /** @var string The minimum log level for YUI logging statements. */ - public $logLevel; - - /** - * @var array List of functions used by the YUI Loader group pattern recognition. - */ - protected $jsconfigfunctions = array(); - - /** - * Create a new group within the YUI_config system. - * - * @param string $name The name of the group. This must be unique and - * not previously used. - * @param array $config The configuration for this group. - * @return void - */ - public function add_group($name, $config) { - if (isset($this->groups[$name])) { - throw new coding_exception("A YUI configuration group for '{$name}' already exists. To make changes to this group use YUI_config->update_group()."); - } - $this->groups[$name] = $config; - } - - /** - * Update an existing group configuration - * - * Note, any existing configuration for that group will be wiped out. - * This includes module configuration. - * - * @param string $name The name of the group. This must be unique and - * not previously used. - * @param array $config The configuration for this group. - * @return void - */ - public function update_group($name, $config) { - if (!isset($this->groups[$name])) { - throw new coding_exception('The Moodle YUI module does not exist. You must define the moodle module config using YUI_config->add_module_config first.'); - } - $this->groups[$name] = $config; - } - - /** - * Set the value of a configuration function used by the YUI Loader's pattern testing. - * - * Only the body of the function should be passed, and not the whole function wrapper. - * - * The JS function your write will be passed a single argument 'name' containing the - * name of the module being loaded. - * - * @param $function String the body of the JavaScript function. This should be used i - * @return string the name of the function to use in the group pattern configuration. - */ - public function set_config_function($function) { - $configname = 'yui' . (count($this->jsconfigfunctions) + 1) . 'ConfigFn'; - if (isset($this->jsconfigfunctions[$configname])) { - throw new coding_exception("A YUI config function with this name already exists. Config function names must be unique."); - } - $this->jsconfigfunctions[$configname] = $function; - return '@' . $configname . '@'; - } - - /** - * Allow setting of the config function described in {@see set_config_function} from a file. - * The contents of this file are then passed to set_config_function. - * - * When jsrev is positive, the function is minified and stored in a MUC cache for subsequent uses. - * - * @param $file The path to the JavaScript function used for YUI configuration. - * @return string the name of the function to use in the group pattern configuration. - */ - public function set_config_source($file) { - global $CFG; - $cache = cache::make('core', 'yuimodules'); - - // Attempt to get the metadata from the cache. - $keyname = 'configfn_' . $file; - $fullpath = $CFG->dirroot . '/' . $file; - if (!isset($CFG->jsrev) || $CFG->jsrev == -1) { - $cache->delete($keyname); - $configfn = file_get_contents($fullpath); - } else { - $configfn = $cache->get($keyname); - if ($configfn === false) { - require_once($CFG->libdir . '/jslib.php'); - $configfn = core_minify::js_files(array($fullpath)); - $cache->set($keyname, $configfn); - } - } - return $this->set_config_function($configfn); - } - - /** - * Retrieve the list of JavaScript functions for YUI_config groups. - * - * @return string The complete set of config functions - */ - public function get_config_functions() { - $configfunctions = ''; - foreach ($this->jsconfigfunctions as $functionname => $function) { - $configfunctions .= "var {$functionname} = function(me) {"; - $configfunctions .= $function; - $configfunctions .= "};\n"; - } - return $configfunctions; - } - - /** - * Update the header JavaScript with any required modification for the YUI Loader. - * - * @param $js String The JavaScript to manipulate. - * @return string the modified JS string. - */ - public function update_header_js($js) { - // Update the names of the the configFn variables. - // The PHP json_encode function cannot handle literal names so we have to wrap - // them in @ and then replace them with literals of the same function name. - foreach ($this->jsconfigfunctions as $functionname => $function) { - $js = str_replace('"@' . $functionname . '@"', $functionname, $js); - } - return $js; - } - - /** - * Add configuration for a specific module. - * - * @param string $name The name of the module to add configuration for. - * @param array $config The configuration for the specified module. - * @param string $group The name of the group to add configuration for. - * If not specified, then this module is added to the global - * configuration. - * @return void - */ - public function add_module_config($name, $config, $group = null) { - if ($group) { - if (!isset($this->groups[$name])) { - throw new coding_exception('The Moodle YUI module does not exist. You must define the moodle module config using YUI_config->add_module_config first.'); - } - if (!isset($this->groups[$group]['modules'])) { - $this->groups[$group]['modules'] = array(); - } - $modules = &$this->groups[$group]['modules']; - } else { - $modules = &$this->modules; - } - $modules[$name] = $config; - } - - /** - * Add the moodle YUI module metadata for the moodle group to the YUI_config instance. - * - * If js caching is disabled, metadata will not be served causing YUI to calculate - * module dependencies as each module is loaded. - * - * If metadata does not exist it will be created and stored in a MUC entry. - * - * @return void - */ - public function add_moodle_metadata() { - global $CFG; - if (!isset($this->groups['moodle'])) { - throw new coding_exception('The Moodle YUI module does not exist. You must define the moodle module config using YUI_config->add_module_config first.'); - } - - if (!isset($this->groups['moodle']['modules'])) { - $this->groups['moodle']['modules'] = array(); - } - - $cache = cache::make('core', 'yuimodules'); - if (!isset($CFG->jsrev) || $CFG->jsrev == -1) { - $metadata = array(); - $metadata = $this->get_moodle_metadata(); - $cache->delete('metadata'); - } else { - // Attempt to get the metadata from the cache. - if (!$metadata = $cache->get('metadata')) { - $metadata = $this->get_moodle_metadata(); - $cache->set('metadata', $metadata); - } - } - - // Merge with any metadata added specific to this page which was added manually. - $this->groups['moodle']['modules'] = array_merge($this->groups['moodle']['modules'], - $metadata); - } - - /** - * Determine the module metadata for all moodle YUI modules. - * - * This works through all modules capable of serving YUI modules, and attempts to get - * metadata for each of those modules. - * - * @return array of module metadata - */ - private function get_moodle_metadata() { - $moodlemodules = array(); - // Core isn't a plugin type or subsystem - handle it seperately. - if ($module = $this->get_moodle_path_metadata(core_component::get_component_directory('core'))) { - $moodlemodules = array_merge($moodlemodules, $module); - } - - // Handle other core subsystems. - $subsystems = core_component::get_core_subsystems(); - foreach ($subsystems as $subsystem => $path) { - if (is_null($path)) { - continue; - } - if ($module = $this->get_moodle_path_metadata($path)) { - $moodlemodules = array_merge($moodlemodules, $module); - } - } - - // And finally the plugins. - $plugintypes = core_component::get_plugin_types(); - foreach ($plugintypes as $plugintype => $pathroot) { - $pluginlist = core_component::get_plugin_list($plugintype); - foreach ($pluginlist as $plugin => $path) { - if ($module = $this->get_moodle_path_metadata($path)) { - $moodlemodules = array_merge($moodlemodules, $module); - } - } - } - - return $moodlemodules; - } - - /** - * Helper function process and return the YUI metadata for all of the modules under the specified path. - * - * @param string $path the UNC path to the YUI src directory. - * @return array the complete array for frankenstyle directory. - */ - private function get_moodle_path_metadata($path) { - // Add module metadata is stored in frankenstyle_modname/yui/src/yui_modname/meta/yui_modname.json. - $baseyui = $path . '/yui/src'; - $modules = array(); - if (is_dir($baseyui)) { - $items = new DirectoryIterator($baseyui); - foreach ($items as $item) { - if ($item->isDot() or !$item->isDir()) { - continue; - } - $metafile = realpath($baseyui . '/' . $item . '/meta/' . $item . '.json'); - if (!is_readable($metafile)) { - continue; - } - $metadata = file_get_contents($metafile); - $modules = array_merge($modules, (array) json_decode($metadata)); - } - } - return $modules; - } - - /** - * Define YUI modules which we have been required to patch between releases. - * - * We must do this because we aggressively cache content on the browser, and we must also override use of the - * external CDN which will serve the true authoritative copy of the code without our patches. - * - * @param string $combobase The local combobase - * @param string $yuiversion The current YUI version - * @param int $patchlevel The patch level we're working to for YUI - * @param array $patchedmodules An array containing the names of the patched modules - * @return void - */ - public function define_patched_core_modules($combobase, $yuiversion, $patchlevel, $patchedmodules) { - // The version we use is suffixed with a patchlevel so that we can get additional revisions between YUI releases. - $subversion = $yuiversion . '_' . $patchlevel; - - if ($this->comboBase == $combobase) { - // If we are using the local combobase in the loader, we can add a group and still make use of the combo - // loader. We just need to specify a different root which includes a slightly different YUI version number - // to include our patchlevel. - $patterns = array(); - $modules = array(); - foreach ($patchedmodules as $modulename) { - // We must define the pattern and module here so that the loader uses our group configuration instead of - // the standard module definition. We may lose some metadata provided by upstream but this will be - // loaded when the module is loaded anyway. - $patterns[$modulename] = array( - 'group' => 'yui-patched', - ); - $modules[$modulename] = array(); - } - - // Actually add the patch group here. - $this->add_group('yui-patched', array( - 'combine' => true, - 'root' => $subversion . '/', - 'patterns' => $patterns, - 'modules' => $modules, - )); - - } else { - // The CDN is in use - we need to instead use the local combobase for this module and override the modules - // definition. We cannot use the local base - we must use the combobase because we cannot invalidate the - // local base in browser caches. - $fullpathbase = $combobase . $subversion . '/'; - foreach ($patchedmodules as $modulename) { - $this->modules[$modulename] = array( - 'fullpath' => $fullpathbase . $modulename . '/' . $modulename . '-min.js' - ); - } - } - } -} - -/** - * Invalidate all server and client side template caches. - */ -function template_reset_all_caches() { - global $CFG; - - $next = time(); - if (isset($CFG->templaterev) and $next <= $CFG->templaterev and $CFG->templaterev - $next < 60 * 60) { - // This resolves problems when reset is requested repeatedly within 1s, - // the < 1h condition prevents accidental switching to future dates - // because we might not recover from it. - $next = $CFG->templaterev + 1; - } - - set_config('templaterev', $next); -} - -/** - * Invalidate all server and client side JS caches. - */ -function js_reset_all_caches() { - global $CFG; - - $next = time(); - if (isset($CFG->jsrev) and $next <= $CFG->jsrev and $CFG->jsrev - $next < 60*60) { - // This resolves problems when reset is requested repeatedly within 1s, - // the < 1h condition prevents accidental switching to future dates - // because we might not recover from it. - $next = $CFG->jsrev+1; - } - - set_config('jsrev', $next); -} +// This file is deprecated, but it should never have been manually included by anything outside of lib/outputlib.php. +// Throwing an exception here should be fine because removing the manual inclusion should have no impact. +throw new \core\exception\coding_exception( + 'This file should not be manually included by any component.', +); diff --git a/lib/pagelib.php b/lib/pagelib.php index cd649a39d7a2f..1c4d00195095f 100644 --- a/lib/pagelib.php +++ b/lib/pagelib.php @@ -31,6 +31,7 @@ use core\navigation\views\secondary; use core\navigation\output\primary as primaryoutput; use core\output\activity_header; +use core\output\xhtml_container_stack; /** * $PAGE is a central store of information about the current page we are @@ -1034,9 +1035,6 @@ public function has_navbar() { * by the get_fragment() web service and not for use elsewhere. */ public function start_collecting_javascript_requirements() { - global $CFG; - require_once($CFG->libdir.'/outputfragmentrequirementslib.php'); - // Check that the requirements manager has not already been switched. if (get_class($this->_requires) == 'fragment_requirements_manager') { throw new coding_exception('JavaScript collection has already been started.'); diff --git a/lib/psr/http-client/CHANGELOG.md b/lib/psr/http-client/CHANGELOG.md index e2dc25f519b39..babba7c7b2ea6 100644 --- a/lib/psr/http-client/CHANGELOG.md +++ b/lib/psr/http-client/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 1.0.3 + +Add `source` link in composer.json. No code changes. + +## 1.0.2 + +Allow PSR-7 (psr/http-message) 2.0. No code changes. + ## 1.0.1 Allow installation with PHP 8. No code changes. diff --git a/lib/psr/http-client/README.md b/lib/psr/http-client/README.md index 6876b8403639a..84af5c55d5fea 100644 --- a/lib/psr/http-client/README.md +++ b/lib/psr/http-client/README.md @@ -7,6 +7,6 @@ Note that this is not a HTTP Client implementation of its own. It is merely abst The installable [package][package-url] and [implementations][implementation-url] are listed on Packagist. -[psr-url]: http://www.php-fig.org/psr/psr-18 +[psr-url]: https://www.php-fig.org/psr/psr-18 [package-url]: https://packagist.org/packages/psr/http-client [implementation-url]: https://packagist.org/providers/psr/http-client-implementation diff --git a/lib/psr/http-client/composer.json b/lib/psr/http-client/composer.json index c195f8ff155ad..6fed350beb874 100644 --- a/lib/psr/http-client/composer.json +++ b/lib/psr/http-client/composer.json @@ -7,12 +7,15 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, "require": { "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "autoload": { "psr-4": { diff --git a/lib/psr/http-client/readme_moodle.txt b/lib/psr/http-client/readme_moodle.txt new file mode 100644 index 0000000000000..eaa9df3c716a0 --- /dev/null +++ b/lib/psr/http-client/readme_moodle.txt @@ -0,0 +1,10 @@ +# PSR-18 HTTP Client + +This is a description for including the PSR-18 Interfaces in Moodle + +## Installation + +1. Visit https://github.com/php-fig/http-client +2. Download the latest release +3. Unzip in this folder +4. Update `thirdpartylibs.xml` diff --git a/lib/psr/http-message/README.md b/lib/psr/http-message/README.md index 28185338f7238..2668be6c30bbb 100644 --- a/lib/psr/http-message/README.md +++ b/lib/psr/http-message/README.md @@ -10,4 +10,7 @@ interface that describes a HTTP message. See the specification for more details. Usage ----- -We'll certainly need some stuff in here. \ No newline at end of file +Before reading the usage guide we recommend reading the PSR-7 interfaces method list: + +* [`PSR-7 Interfaces Method List`](docs/PSR7-Interfaces.md) +* [`PSR-7 Usage Guide`](docs/PSR7-Usage.md) \ No newline at end of file diff --git a/lib/psr/http-message/composer.json b/lib/psr/http-message/composer.json index b0d2937a03a5f..c66e5aba40c97 100644 --- a/lib/psr/http-message/composer.json +++ b/lib/psr/http-message/composer.json @@ -7,11 +7,11 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "autoload": { "psr-4": { @@ -20,7 +20,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } } } diff --git a/lib/psr/http-message/docs/PSR7-Interfaces.md b/lib/psr/http-message/docs/PSR7-Interfaces.md new file mode 100644 index 0000000000000..3a7e7dda69534 --- /dev/null +++ b/lib/psr/http-message/docs/PSR7-Interfaces.md @@ -0,0 +1,130 @@ +# Interfaces + +The purpose of this list is to help in finding the methods when working with PSR-7. This can be considered as a cheatsheet for PSR-7 interfaces. + +The interfaces defined in PSR-7 are the following: + +| Class Name | Description | +|---|---| +| [Psr\Http\Message\MessageInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagemessageinterface) | Representation of a HTTP message | +| [Psr\Http\Message\RequestInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagerequestinterface) | Representation of an outgoing, client-side request. | +| [Psr\Http\Message\ServerRequestInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageserverrequestinterface) | Representation of an incoming, server-side HTTP request. | +| [Psr\Http\Message\ResponseInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageresponseinterface) | Representation of an outgoing, server-side response. | +| [Psr\Http\Message\StreamInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagestreaminterface) | Describes a data stream | +| [Psr\Http\Message\UriInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageuriinterface) | Value object representing a URI. | +| [Psr\Http\Message\UploadedFileInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageuploadedfileinterface) | Value object representing a file uploaded through an HTTP request. | + +## `Psr\Http\Message\MessageInterface` Methods + +| Method Name | Description | Notes | +|------------------------------------| ----------- | ----- | +| `getProtocolVersion()` | Retrieve HTTP protocol version | 1.0 or 1.1 | +| `withProtocolVersion($version)` | Returns new message instance with given HTTP protocol version | | +| `getHeaders()` | Retrieve all HTTP Headers | [Request Header List](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields), [Response Header List](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields) | +| `hasHeader($name)` | Checks if HTTP Header with given name exists | | +| `getHeader($name)` | Retrieves a array with the values for a single header | | +| `getHeaderLine($name)` | Retrieves a comma-separated string of the values for a single header | | +| `withHeader($name, $value)` | Returns new message instance with given HTTP Header | if the header existed in the original instance, replaces the header value from the original message with the value provided when creating the new instance. | +| `withAddedHeader($name, $value)` | Returns new message instance with appended value to given header | If header already exists value will be appended, if not a new header will be created | +| `withoutHeader($name)` | Removes HTTP Header with given name| | +| `getBody()` | Retrieves the HTTP Message Body | Returns object implementing `StreamInterface`| +| `withBody(StreamInterface $body)` | Returns new message instance with given HTTP Message Body | | + + +## `Psr\Http\Message\RequestInterface` Methods + +Same methods as `Psr\Http\Message\MessageInterface` + the following methods: + +| Method Name | Description | Notes | +|------------------------------------| ----------- | ----- | +| `getRequestTarget()` | Retrieves the message's request target | origin-form, absolute-form, authority-form, asterisk-form ([RFC7230](https://www.rfc-editor.org/rfc/rfc7230.txt)) | +| `withRequestTarget($requestTarget)` | Return a new message instance with the specific request-target | | +| `getMethod()` | Retrieves the HTTP method of the request. | GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE (defined in [RFC7231](https://tools.ietf.org/html/rfc7231)), PATCH (defined in [RFC5789](https://tools.ietf.org/html/rfc5789)) | +| `withMethod($method)` | Returns a new message instance with the provided HTTP method | | +| `getUri()` | Retrieves the URI instance | | +| `withUri(UriInterface $uri, $preserveHost = false)` | Returns a new message instance with the provided URI | | + + +## `Psr\Http\Message\ServerRequestInterface` Methods + +Same methods as `Psr\Http\Message\RequestInterface` + the following methods: + +| Method Name | Description | Notes | +|------------------------------------| ----------- | ----- | +| `getServerParams() ` | Retrieve server parameters | Typically derived from `$_SERVER` | +| `getCookieParams()` | Retrieves cookies sent by the client to the server. | Typically derived from `$_COOKIES` | +| `withCookieParams(array $cookies)` | Returns a new request instance with the specified cookies | | +| `withQueryParams(array $query)` | Returns a new request instance with the specified query string arguments | | +| `getUploadedFiles()` | Retrieve normalized file upload data | | +| `withUploadedFiles(array $uploadedFiles)` | Returns a new request instance with the specified uploaded files | | +| `getParsedBody()` | Retrieve any parameters provided in the request body | | +| `withParsedBody($data)` | Returns a new request instance with the specified body parameters | | +| `getAttributes()` | Retrieve attributes derived from the request | | +| `getAttribute($name, $default = null)` | Retrieve a single derived request attribute | | +| `withAttribute($name, $value)` | Returns a new request instance with the specified derived request attribute | | +| `withoutAttribute($name)` | Returns a new request instance that without the specified derived request attribute | | + +## `Psr\Http\Message\ResponseInterface` Methods: + +Same methods as `Psr\Http\Message\MessageInterface` + the following methods: + +| Method Name | Description | Notes | +|------------------------------------| ----------- | ----- | +| `getStatusCode()` | Gets the response status code. | | +| `withStatus($code, $reasonPhrase = '')` | Returns a new response instance with the specified status code and, optionally, reason phrase. | | +| `getReasonPhrase()` | Gets the response reason phrase associated with the status code. | | + +## `Psr\Http\Message\StreamInterface` Methods + +| Method Name | Description | Notes | +|------------------------------------| ----------- | ----- | +| `__toString()` | Reads all data from the stream into a string, from the beginning to end. | | +| `close()` | Closes the stream and any underlying resources. | | +| `detach()` | Separates any underlying resources from the stream. | | +| `getSize()` | Get the size of the stream if known. | | +| `eof()` | Returns true if the stream is at the end of the stream.| | +| `isSeekable()` | Returns whether or not the stream is seekable. | | +| `seek($offset, $whence = SEEK_SET)` | Seek to a position in the stream. | | +| `rewind()` | Seek to the beginning of the stream. | | +| `isWritable()` | Returns whether or not the stream is writable. | | +| `write($string)` | Write data to the stream. | | +| `isReadable()` | Returns whether or not the stream is readable. | | +| `read($length)` | Read data from the stream. | | +| `getContents()` | Returns the remaining contents in a string | | +| `getMetadata($key = null)()` | Get stream metadata as an associative array or retrieve a specific key. | | + +## `Psr\Http\Message\UriInterface` Methods + +| Method Name | Description | Notes | +|------------------------------------| ----------- | ----- | +| `getScheme()` | Retrieve the scheme component of the URI. | | +| `getAuthority()` | Retrieve the authority component of the URI. | | +| `getUserInfo()` | Retrieve the user information component of the URI. | | +| `getHost()` | Retrieve the host component of the URI. | | +| `getPort()` | Retrieve the port component of the URI. | | +| `getPath()` | Retrieve the path component of the URI. | | +| `getQuery()` | Retrieve the query string of the URI. | | +| `getFragment()` | Retrieve the fragment component of the URI. | | +| `withScheme($scheme)` | Return an instance with the specified scheme. | | +| `withUserInfo($user, $password = null)` | Return an instance with the specified user information. | | +| `withHost($host)` | Return an instance with the specified host. | | +| `withPort($port)` | Return an instance with the specified port. | | +| `withPath($path)` | Return an instance with the specified path. | | +| `withQuery($query)` | Return an instance with the specified query string. | | +| `withFragment($fragment)` | Return an instance with the specified URI fragment. | | +| `__toString()` | Return the string representation as a URI reference. | | + +## `Psr\Http\Message\UploadedFileInterface` Methods + +| Method Name | Description | Notes | +|------------------------------------| ----------- | ----- | +| `getStream()` | Retrieve a stream representing the uploaded file. | | +| `moveTo($targetPath)` | Move the uploaded file to a new location. | | +| `getSize()` | Retrieve the file size. | | +| `getError()` | Retrieve the error associated with the uploaded file. | | +| `getClientFilename()` | Retrieve the filename sent by the client. | | +| `getClientMediaType()` | Retrieve the media type sent by the client. | | + +> `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`. +> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered. + diff --git a/lib/psr/http-message/docs/PSR7-Usage.md b/lib/psr/http-message/docs/PSR7-Usage.md new file mode 100644 index 0000000000000..b6d048a341e8c --- /dev/null +++ b/lib/psr/http-message/docs/PSR7-Usage.md @@ -0,0 +1,159 @@ +### PSR-7 Usage + +All PSR-7 applications comply with these interfaces +They were created to establish a standard between middleware implementations. + +> `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`. +> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered. + + +The following examples will illustrate how basic operations are done in PSR-7. + +##### Examples + + +For this examples to work (at least) a PSR-7 implementation package is required. (eg: zendframework/zend-diactoros, guzzlehttp/psr7, slim/slim, etc) +All PSR-7 implementations should have the same behaviour. + +The following will be assumed: +`$request` is an object of `Psr\Http\Message\RequestInterface` and + +`$response` is an object implementing `Psr\Http\Message\RequestInterface` + + +### Working with HTTP Headers + +#### Adding headers to response: + +```php +$response->withHeader('My-Custom-Header', 'My Custom Message'); +``` + +#### Appending values to headers + +```php +$response->withAddedHeader('My-Custom-Header', 'The second message'); +``` + +#### Checking if header exists: + +```php +$request->hasHeader('My-Custom-Header'); // will return false +$response->hasHeader('My-Custom-Header'); // will return true +``` + +> Note: My-Custom-Header was only added in the Response + +#### Getting comma-separated values from a header (also applies to request) + +```php +// getting value from request headers +$request->getHeaderLine('Content-Type'); // will return: "text/html; charset=UTF-8" +// getting value from response headers +$response->getHeaderLine('My-Custom-Header'); // will return: "My Custom Message; The second message" +``` + +#### Getting array of value from a header (also applies to request) +```php +// getting value from request headers +$request->getHeader('Content-Type'); // will return: ["text/html", "charset=UTF-8"] +// getting value from response headers +$response->getHeader('My-Custom-Header'); // will return: ["My Custom Message", "The second message"] +``` + +#### Removing headers from HTTP Messages +```php +// removing a header from Request, removing deprecated "Content-MD5" header +$request->withoutHeader('Content-MD5'); + +// removing a header from Response +// effect: the browser won't know the size of the stream +// the browser will download the stream till it ends +$response->withoutHeader('Content-Length'); +``` + +### Working with HTTP Message Body + +When working with the PSR-7 there are two methods of implementation: +#### 1. Getting the body separately + +> This method makes the body handling easier to understand and is useful when repeatedly calling body methods. (You only call `getBody()` once). Using this method mistakes like `$response->write()` are also prevented. + +```php +$body = $response->getBody(); +// operations on body, eg. read, write, seek +// ... +// replacing the old body +$response->withBody($body); +// this last statement is optional as we working with objects +// in this case the "new" body is same with the "old" one +// the $body variable has the same value as the one in $request, only the reference is passed +``` + +#### 2. Working directly on response + +> This method is useful when only performing few operations as the `$request->getBody()` statement fragment is required + +```php +$response->getBody()->write('hello'); +``` + +### Getting the body contents + +The following snippet gets the contents of a stream contents. +> Note: Streams must be rewinded, if content was written into streams, it will be ignored when calling `getContents()` because the stream pointer is set to the last character, which is `\0` - meaning end of stream. +```php +$body = $response->getBody(); +$body->rewind(); // or $body->seek(0); +$bodyText = $body->getContents(); +``` +> Note: If `$body->seek(1)` is called before `$body->getContents()`, the first character will be ommited as the starting pointer is set to `1`, not `0`. This is why using `$body->rewind()` is recommended. + +### Append to body + +```php +$response->getBody()->write('Hello'); // writing directly +$body = $request->getBody(); // which is a `StreamInterface` +$body->write('xxxxx'); +``` + +### Prepend to body +Prepending is different when it comes to streams. The content must be copied before writing the content to be prepended. +The following example will explain the behaviour of streams. + +```php +// assuming our response is initially empty +$body = $repsonse->getBody(); +// writing the string "abcd" +$body->write('abcd'); + +// seeking to start of stream +$body->seek(0); +// writing 'ef' +$body->write('ef'); // at this point the stream contains "efcd" +``` + +#### Prepending by rewriting separately + +```php +// assuming our response body stream only contains: "abcd" +$body = $response->getBody(); +$body->rewind(); +$contents = $body->getContents(); // abcd +// seeking the stream to beginning +$body->rewind(); +$body->write('ef'); // stream contains "efcd" +$body->write($contents); // stream contains "efabcd" +``` + +> Note: `getContents()` seeks the stream while reading it, therefore if the second `rewind()` method call was not present the stream would have resulted in `abcdefabcd` because the `write()` method appends to stream if not preceeded by `rewind()` or `seek(0)`. + +#### Prepending by using contents as a string +```php +$body = $response->getBody(); +$body->rewind(); +$contents = $body->getContents(); // efabcd +$contents = 'ef'.$contents; +$body->rewind(); +$body->write($contents); +``` diff --git a/lib/psr/http-message/readme_moodle.txt b/lib/psr/http-message/readme_moodle.txt new file mode 100644 index 0000000000000..622d300aa9d89 --- /dev/null +++ b/lib/psr/http-message/readme_moodle.txt @@ -0,0 +1,10 @@ +# PSR-7 HTTP Message + +This is a description for including the PSR-7 Interfaces in Moodle + +## Installation + +1. Visit https://github.com/php-fig/http-message +2. Download the latest release +3. Unzip in this folder +4. Update `thirdpartylibs.xml` diff --git a/lib/psr/http-message/src/MessageInterface.php b/lib/psr/http-message/src/MessageInterface.php index dd46e5ec81eef..a83c98518d549 100644 --- a/lib/psr/http-message/src/MessageInterface.php +++ b/lib/psr/http-message/src/MessageInterface.php @@ -23,7 +23,7 @@ interface MessageInterface * * @return string HTTP protocol version. */ - public function getProtocolVersion(); + public function getProtocolVersion(): string; /** * Return an instance with the specified HTTP protocol version. @@ -38,7 +38,7 @@ public function getProtocolVersion(); * @param string $version HTTP protocol version * @return static */ - public function withProtocolVersion($version); + public function withProtocolVersion(string $version): MessageInterface; /** * Retrieves all message header values. @@ -65,7 +65,7 @@ public function withProtocolVersion($version); * key MUST be a header name, and each value MUST be an array of strings * for that header. */ - public function getHeaders(); + public function getHeaders(): array; /** * Checks if a header exists by the given case-insensitive name. @@ -75,7 +75,7 @@ public function getHeaders(); * name using a case-insensitive string comparison. Returns false if * no matching header name is found in the message. */ - public function hasHeader($name); + public function hasHeader(string $name): bool; /** * Retrieves a message header value by the given case-insensitive name. @@ -91,7 +91,7 @@ public function hasHeader($name); * header. If the header does not appear in the message, this method MUST * return an empty array. */ - public function getHeader($name); + public function getHeader(string $name): array; /** * Retrieves a comma-separated string of the values for a single header. @@ -112,7 +112,7 @@ public function getHeader($name); * concatenated together using a comma. If the header does not appear in * the message, this method MUST return an empty string. */ - public function getHeaderLine($name); + public function getHeaderLine(string $name): string; /** * Return an instance with the provided value replacing the specified header. @@ -129,7 +129,7 @@ public function getHeaderLine($name); * @return static * @throws \InvalidArgumentException for invalid header names or values. */ - public function withHeader($name, $value); + public function withHeader(string $name, $value): MessageInterface; /** * Return an instance with the specified header appended with the given value. @@ -147,7 +147,7 @@ public function withHeader($name, $value); * @return static * @throws \InvalidArgumentException for invalid header names or values. */ - public function withAddedHeader($name, $value); + public function withAddedHeader(string $name, $value): MessageInterface; /** * Return an instance without the specified header. @@ -161,14 +161,14 @@ public function withAddedHeader($name, $value); * @param string $name Case-insensitive header field name to remove. * @return static */ - public function withoutHeader($name); + public function withoutHeader(string $name): MessageInterface; /** * Gets the body of the message. * * @return StreamInterface Returns the body as a stream. */ - public function getBody(); + public function getBody(): StreamInterface; /** * Return an instance with the specified message body. @@ -183,5 +183,5 @@ public function getBody(); * @return static * @throws \InvalidArgumentException When the body is not valid. */ - public function withBody(StreamInterface $body); + public function withBody(StreamInterface $body): MessageInterface; } diff --git a/lib/psr/http-message/src/RequestInterface.php b/lib/psr/http-message/src/RequestInterface.php index a96d4fd636679..33f85e559d024 100644 --- a/lib/psr/http-message/src/RequestInterface.php +++ b/lib/psr/http-message/src/RequestInterface.php @@ -39,7 +39,7 @@ interface RequestInterface extends MessageInterface * * @return string */ - public function getRequestTarget(); + public function getRequestTarget(): string; /** * Return an instance with the specific request-target. @@ -55,17 +55,18 @@ public function getRequestTarget(); * * @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various * request-target forms allowed in request messages) - * @param mixed $requestTarget + * @param string $requestTarget * @return static */ - public function withRequestTarget($requestTarget); + public function withRequestTarget(string $requestTarget): RequestInterface; + /** * Retrieves the HTTP method of the request. * * @return string Returns the request method. */ - public function getMethod(); + public function getMethod(): string; /** * Return an instance with the provided HTTP method. @@ -82,7 +83,7 @@ public function getMethod(); * @return static * @throws \InvalidArgumentException for invalid HTTP methods. */ - public function withMethod($method); + public function withMethod(string $method): RequestInterface; /** * Retrieves the URI instance. @@ -93,7 +94,7 @@ public function withMethod($method); * @return UriInterface Returns a UriInterface instance * representing the URI of the request. */ - public function getUri(); + public function getUri(): UriInterface; /** * Returns an instance with the provided URI. @@ -125,5 +126,5 @@ public function getUri(); * @param bool $preserveHost Preserve the original state of the Host header. * @return static */ - public function withUri(UriInterface $uri, $preserveHost = false); + public function withUri(UriInterface $uri, bool $preserveHost = false): RequestInterface; } diff --git a/lib/psr/http-message/src/ResponseInterface.php b/lib/psr/http-message/src/ResponseInterface.php index c306514e6bbdf..e9299a91443f2 100644 --- a/lib/psr/http-message/src/ResponseInterface.php +++ b/lib/psr/http-message/src/ResponseInterface.php @@ -27,7 +27,7 @@ interface ResponseInterface extends MessageInterface * * @return int Status code. */ - public function getStatusCode(); + public function getStatusCode(): int; /** * Return an instance with the specified status code and, optionally, reason phrase. @@ -49,7 +49,7 @@ public function getStatusCode(); * @return static * @throws \InvalidArgumentException For invalid status code arguments. */ - public function withStatus($code, $reasonPhrase = ''); + public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface; /** * Gets the response reason phrase associated with the status code. @@ -64,5 +64,5 @@ public function withStatus($code, $reasonPhrase = ''); * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml * @return string Reason phrase; must return an empty string if none present. */ - public function getReasonPhrase(); + public function getReasonPhrase(): string; } diff --git a/lib/psr/http-message/src/ServerRequestInterface.php b/lib/psr/http-message/src/ServerRequestInterface.php index 02512340ad853..8625d0e100d93 100644 --- a/lib/psr/http-message/src/ServerRequestInterface.php +++ b/lib/psr/http-message/src/ServerRequestInterface.php @@ -51,7 +51,7 @@ interface ServerRequestInterface extends RequestInterface * * @return array */ - public function getServerParams(); + public function getServerParams(): array; /** * Retrieve cookies. @@ -63,7 +63,7 @@ public function getServerParams(); * * @return array */ - public function getCookieParams(); + public function getCookieParams(): array; /** * Return an instance with the specified cookies. @@ -82,7 +82,7 @@ public function getCookieParams(); * @param array $cookies Array of key/value pairs representing cookies. * @return static */ - public function withCookieParams(array $cookies); + public function withCookieParams(array $cookies): ServerRequestInterface; /** * Retrieve query string arguments. @@ -96,7 +96,7 @@ public function withCookieParams(array $cookies); * * @return array */ - public function getQueryParams(); + public function getQueryParams(): array; /** * Return an instance with the specified query string arguments. @@ -120,7 +120,7 @@ public function getQueryParams(); * $_GET. * @return static */ - public function withQueryParams(array $query); + public function withQueryParams(array $query): ServerRequestInterface; /** * Retrieve normalized file upload data. @@ -134,7 +134,7 @@ public function withQueryParams(array $query); * @return array An array tree of UploadedFileInterface instances; an empty * array MUST be returned if no data is present. */ - public function getUploadedFiles(); + public function getUploadedFiles(): array; /** * Create a new instance with the specified uploaded files. @@ -147,7 +147,7 @@ public function getUploadedFiles(); * @return static * @throws \InvalidArgumentException if an invalid structure is provided. */ - public function withUploadedFiles(array $uploadedFiles); + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface; /** * Retrieve any parameters provided in the request body. @@ -194,7 +194,7 @@ public function getParsedBody(); * @throws \InvalidArgumentException if an unsupported argument type is * provided. */ - public function withParsedBody($data); + public function withParsedBody($data): ServerRequestInterface; /** * Retrieve attributes derived from the request. @@ -207,7 +207,7 @@ public function withParsedBody($data); * * @return array Attributes derived from the request. */ - public function getAttributes(); + public function getAttributes(): array; /** * Retrieve a single derived request attribute. @@ -224,7 +224,7 @@ public function getAttributes(); * @param mixed $default Default value to return if the attribute does not exist. * @return mixed */ - public function getAttribute($name, $default = null); + public function getAttribute(string $name, $default = null); /** * Return an instance with the specified derived request attribute. @@ -241,7 +241,7 @@ public function getAttribute($name, $default = null); * @param mixed $value The value of the attribute. * @return static */ - public function withAttribute($name, $value); + public function withAttribute(string $name, $value): ServerRequestInterface; /** * Return an instance that removes the specified derived request attribute. @@ -257,5 +257,5 @@ public function withAttribute($name, $value); * @param string $name The attribute name. * @return static */ - public function withoutAttribute($name); + public function withoutAttribute(string $name): ServerRequestInterface; } diff --git a/lib/psr/http-message/src/StreamInterface.php b/lib/psr/http-message/src/StreamInterface.php index f68f391269b47..a62aabb8288b8 100644 --- a/lib/psr/http-message/src/StreamInterface.php +++ b/lib/psr/http-message/src/StreamInterface.php @@ -25,14 +25,14 @@ interface StreamInterface * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring * @return string */ - public function __toString(); + public function __toString(): string; /** * Closes the stream and any underlying resources. * * @return void */ - public function close(); + public function close(): void; /** * Separates any underlying resources from the stream. @@ -48,7 +48,7 @@ public function detach(); * * @return int|null Returns the size in bytes if known, or null if unknown. */ - public function getSize(); + public function getSize(): ?int; /** * Returns the current position of the file read/write pointer @@ -56,21 +56,21 @@ public function getSize(); * @return int Position of the file pointer * @throws \RuntimeException on error. */ - public function tell(); + public function tell(): int; /** * Returns true if the stream is at the end of the stream. * * @return bool */ - public function eof(); + public function eof(): bool; /** * Returns whether or not the stream is seekable. * * @return bool */ - public function isSeekable(); + public function isSeekable(): bool; /** * Seek to a position in the stream. @@ -84,7 +84,7 @@ public function isSeekable(); * SEEK_END: Set position to end-of-stream plus offset. * @throws \RuntimeException on failure. */ - public function seek($offset, $whence = SEEK_SET); + public function seek(int $offset, int $whence = SEEK_SET): void; /** * Seek to the beginning of the stream. @@ -96,14 +96,14 @@ public function seek($offset, $whence = SEEK_SET); * @link http://www.php.net/manual/en/function.fseek.php * @throws \RuntimeException on failure. */ - public function rewind(); + public function rewind(): void; /** * Returns whether or not the stream is writable. * * @return bool */ - public function isWritable(); + public function isWritable(): bool; /** * Write data to the stream. @@ -112,14 +112,14 @@ public function isWritable(); * @return int Returns the number of bytes written to the stream. * @throws \RuntimeException on failure. */ - public function write($string); + public function write(string $string): int; /** * Returns whether or not the stream is readable. * * @return bool */ - public function isReadable(); + public function isReadable(): bool; /** * Read data from the stream. @@ -131,7 +131,7 @@ public function isReadable(); * if no bytes are available. * @throws \RuntimeException if an error occurs. */ - public function read($length); + public function read(int $length): string; /** * Returns the remaining contents in a string @@ -140,7 +140,7 @@ public function read($length); * @throws \RuntimeException if unable to read or an error occurs while * reading. */ - public function getContents(); + public function getContents(): string; /** * Get stream metadata as an associative array or retrieve a specific key. @@ -149,10 +149,10 @@ public function getContents(); * stream_get_meta_data() function. * * @link http://php.net/manual/en/function.stream-get-meta-data.php - * @param string $key Specific metadata to retrieve. + * @param string|null $key Specific metadata to retrieve. * @return array|mixed|null Returns an associative array if no key is * provided. Returns a specific key value if a key is provided and the * value is found, or null if the key is not found. */ - public function getMetadata($key = null); + public function getMetadata(?string $key = null); } diff --git a/lib/psr/http-message/src/UploadedFileInterface.php b/lib/psr/http-message/src/UploadedFileInterface.php index f8a6901e01447..dd19d653819f1 100644 --- a/lib/psr/http-message/src/UploadedFileInterface.php +++ b/lib/psr/http-message/src/UploadedFileInterface.php @@ -28,7 +28,7 @@ interface UploadedFileInterface * @throws \RuntimeException in cases when no stream is available or can be * created. */ - public function getStream(); + public function getStream(): StreamInterface; /** * Move the uploaded file to a new location. @@ -62,7 +62,7 @@ public function getStream(); * @throws \RuntimeException on any error during the move operation, or on * the second or subsequent call to the method. */ - public function moveTo($targetPath); + public function moveTo(string $targetPath): void; /** * Retrieve the file size. @@ -73,7 +73,7 @@ public function moveTo($targetPath); * * @return int|null The file size in bytes or null if unknown. */ - public function getSize(); + public function getSize(): ?int; /** * Retrieve the error associated with the uploaded file. @@ -89,7 +89,7 @@ public function getSize(); * @see http://php.net/manual/en/features.file-upload.errors.php * @return int One of PHP's UPLOAD_ERR_XXX constants. */ - public function getError(); + public function getError(): int; /** * Retrieve the filename sent by the client. @@ -104,7 +104,7 @@ public function getError(); * @return string|null The filename sent by the client or null if none * was provided. */ - public function getClientFilename(); + public function getClientFilename(): ?string; /** * Retrieve the media type sent by the client. @@ -119,5 +119,5 @@ public function getClientFilename(); * @return string|null The media type sent by the client or null if none * was provided. */ - public function getClientMediaType(); + public function getClientMediaType(): ?string; } diff --git a/lib/psr/http-message/src/UriInterface.php b/lib/psr/http-message/src/UriInterface.php index 9d7ab9eae8f29..15e2cf2862850 100644 --- a/lib/psr/http-message/src/UriInterface.php +++ b/lib/psr/http-message/src/UriInterface.php @@ -1,4 +1,5 @@ =8.0.2" + "php": ">=8.1" }, "autoload": { "files": [ @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", diff --git a/lib/table/UPGRADING.md b/lib/table/UPGRADING.md new file mode 100644 index 0000000000000..e9eb81afec2e4 --- /dev/null +++ b/lib/table/UPGRADING.md @@ -0,0 +1,11 @@ +# core_table (subsystem) Upgrade notes + +## 4.5dev + +### Added + +- A new `$reponsive` property (defaulting to `true`) has been added to the `core_table\flexible_table` class. + This property allows you to control whether the table is rendered as a responsive table. + + For more information see [MDL-80748](https://tracker.moodle.org/browse/MDL-80748) + diff --git a/lib/table/classes/base_export_format.php b/lib/table/classes/base_export_format.php new file mode 100644 index 0000000000000..35653e4c444f8 --- /dev/null +++ b/lib/table/classes/base_export_format.php @@ -0,0 +1,102 @@ +. + +namespace core_table; + +use flexible_table; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once("{$CFG->libdir}/tablelib.php"); + +/** + * The table base export format. + * + * @package core_table + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class base_export_format { + /** + * @var flexible_table or child class reference pointing to table class object from which to export data. + */ + public $table; + + /** + * @var bool output started. Keeps track of whether any output has been started yet. + */ + public $documentstarted = false; + + /** + * Constructor. + * + * @param flexible_table $table + */ + public function __construct(&$table) { + $this->table =& $table; + } + + public function set_table(&$table) { + $this->table =& $table; + } + + public function add_data($row) { + return false; + } + + public function add_seperator() { + return false; + } + + public function document_started() { + return $this->documentstarted; + } + + /** + * Format the text. + * + * Given text in a variety of format codings, this function returns + * the text as safe HTML or as plain text dependent on what is appropriate + * for the download format. The default removes all tags. + * + * @param string $text + * @param int $format + * @param null|array $options + * @param null|int $courseid + */ + public function format_text($text, $format = FORMAT_MOODLE, $options = null, $courseid = null) { + // Use some whitespace to indicate where there was some line spacing. + $text = str_replace(['', "\n", "\r"], ' ', $text); + return html_entity_decode(strip_tags($text), ENT_COMPAT); + } + + /** + * Format a row of data, removing HTML tags and entities from each of the cells + * + * @param array $row + * @return array + */ + public function format_data(array $row): array { + return array_map([$this, 'format_text'], $row); + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(base_export_format::class, \table_default_export_format_parent::class); diff --git a/lib/table/classes/dataformat_export_format.php b/lib/table/classes/dataformat_export_format.php new file mode 100644 index 0000000000000..fcc84a4a8b5fc --- /dev/null +++ b/lib/table/classes/dataformat_export_format.php @@ -0,0 +1,156 @@ +. + +namespace core_table; + +use core\exception\coding_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once("{$CFG->libdir}/tablelib.php"); + +use core\dataformat; + +/** + * Dataformat exporter + * + * @package core_table + * @subpackage tablelib + * @copyright 2016 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dataformat_export_format extends base_export_format { + /** @var \core\dataformat\base $dataformat */ + protected $dataformat; + + /** @var int $rownum */ + protected $rownum = 0; + + /** @var array $columns */ + protected $columns; + + /** + * Constructor + * + * @param string $table An sql table + * @param string $dataformat type of dataformat for export + */ + public function __construct(&$table, $dataformat) { + parent::__construct($table); + + if (ob_get_length()) { + throw new coding_exception("Output can not be buffered before instantiating table_dataformat_export_format"); + } + + $this->dataformat = dataformat::get_format_instance($dataformat); + + // The dataformat export time to first byte could take a while to generate... + set_time_limit(0); + + // Close the session so that the users other tabs in the same session are not blocked. + \core\session\manager::write_close(); + } + + /** + * Whether the current dataformat supports export of HTML + * + * @return bool + */ + public function supports_html(): bool { + return $this->dataformat->supports_html(); + } + + /** + * Start document + * + * @param string $filename + * @param string $sheettitle + */ + public function start_document($filename, $sheettitle) { + $this->documentstarted = true; + $this->dataformat->set_filename($filename); + $this->dataformat->send_http_headers(); + $this->dataformat->set_sheettitle($sheettitle); + $this->dataformat->start_output(); + } + + /** + * Start export + * + * @param string $sheettitle optional spreadsheet worksheet title + */ + public function start_table($sheettitle) { + $this->dataformat->set_sheettitle($sheettitle); + } + + /** + * Output headers + * + * @param array $headers + */ + public function output_headers($headers) { + $this->columns = $this->format_data($headers); + if (method_exists($this->dataformat, 'write_header')) { + error_log('The function write_header() does not support multiple sheets. In order to support multiple sheets you ' . + 'must implement start_output() and start_sheet() and remove write_header() in your dataformat.'); + $this->dataformat->write_header($this->columns); + } else { + $this->dataformat->start_sheet($this->columns); + } + } + + /** + * Add a row of data + * + * @param array $row One record of data + */ + public function add_data($row) { + if (!$this->supports_html()) { + $row = $this->format_data($row); + } + + $this->dataformat->write_record($row, $this->rownum++); + return true; + } + + /** + * Finish export + */ + public function finish_table() { + if (method_exists($this->dataformat, 'write_footer')) { + error_log('The function write_footer() does not support multiple sheets. In order to support multiple sheets you ' . + 'must implement close_sheet() and close_output() and remove write_footer() in your dataformat.'); + $this->dataformat->write_footer($this->columns); + } else { + $this->dataformat->close_sheet($this->columns); + } + } + + /** + * Finish download + */ + public function finish_document() { + $this->dataformat->close_output(); + exit(); + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(dataformat_export_format::class, \table_dataformat_export_format::class); diff --git a/lib/table/classes/flexible_table.php b/lib/table/classes/flexible_table.php new file mode 100644 index 0000000000000..967006d1ef7a9 --- /dev/null +++ b/lib/table/classes/flexible_table.php @@ -0,0 +1,2031 @@ +. + +namespace core_table; + +use core\context; +use core_table\local\filter\filterset; +use core\exception\coding_exception; +use core\output\renderable; +use html_writer; +use moodle_url; +use paging_bar; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once("{$CFG->libdir}/tablelib.php"); + +// phpcs:disable moodle.NamingConventions.ValidVariableName.MemberNameUnderscore + +/** + * Flexible table implementation. + * + * @package core_table + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class flexible_table { + public $attributes = []; + public $baseurl = null; + + /** @var string The caption of table */ + public $caption; + + /** @var array The caption attributes of table */ + public $captionattributes; + + public $column_class = []; + public $column_nosort = ['userpic']; + public $column_style = []; + public $column_suppress = []; + public $columns = []; + public $currentrow = 0; + public $currpage = 0; + + /** + * Which download plugin to use. Default '' means none - print html table with paging. + * Property set by is_downloading which typically passes in cleaned data from $ + * @var string + */ + public $download = ''; + + /** + * Whether data is downloadable from table. Determines whether to display download buttons. Set by method downloadable(). + * @var bool + */ + public $downloadable = false; + + /** @var dataformat_export_format */ + public $exportclass = null; + + public $headers = []; + public $is_collapsible = false; + public $is_sortable = false; + public $maxsortkeys = 2; + public $pagesize = 30; + public $request = []; + + /** @var bool Stores if setup has already been called on this flixible table. */ + public $setup = false; + + /** @var int[] Array of positions in which to display download controls. */ + public $showdownloadbuttonsat = [TABLE_P_TOP]; + + public $sort_default_column = null; + public $sort_default_order = SORT_ASC; + + /** @var bool Has start output been called yet? */ + public $started_output = false; + + public $totalrows = 0; + public $uniqueid = null; + public $use_initials = false; + public $use_pages = false; + + /** @var string Key of field returned by db query that is the id field of the user table or equivalent. */ + public $useridfield = 'id'; + + /** @var bool Whether to make the table to be scrolled horizontally with ease. Make table responsive across all viewports. */ + public bool $responsive = true; + + /** @var array The sticky attribute of each table column. */ + protected $columnsticky = []; + + /** @var string $filename */ + protected $filename; + + /** + * The currently applied filerset. This is required for dynamic tables, but can be used by other tables too if desired. + * @var filterset + */ + protected $filterset = null; + + /** @var string A column which should be considered as a header column. */ + protected $headercolumn = null; + + /** @var string For create header with help icon. */ + private $helpforheaders = []; + + /** @var array List of hidden columns. */ + protected $hiddencolumns; + + /** @var string The manually set first name initial preference */ + protected $ifirst; + + /** @var string The manually set last name initial preference */ + protected $ilast; + + /** @var bool Whether the table preferences is resetting. */ + protected $resetting; + + /** @var string */ + protected $sheettitle; + + /** @var array The fields to sort. */ + protected $sortdata; + + /** @var string[] Columns that are expected to contain a users fullname. */ + protected $userfullnamecolumns = ['fullname']; + + private $column_textsort = []; + + /** @var array[] Attributes for each column */ + private $columnsattributes = []; + + /** @var int The default per page size for the table. */ + private $defaultperpage = 30; + + /** @var bool Whether to store table properties in the user_preferences table. */ + private $persistent = false; + + /** @var array For storing user-customised table properties in the user_preferences db table. */ + private $prefs = []; + + /** + * Constructor + * @param string $uniqueid all tables have to have a unique id, this is used + * as a key when storing table properties like sort order in the session. + */ + public function __construct($uniqueid) { + $this->uniqueid = $uniqueid; + $this->request = [ + TABLE_VAR_SORT => 'tsort', + TABLE_VAR_HIDE => 'thide', + TABLE_VAR_SHOW => 'tshow', + TABLE_VAR_IFIRST => 'tifirst', + TABLE_VAR_ILAST => 'tilast', + TABLE_VAR_PAGE => 'page', + TABLE_VAR_RESET => 'treset', + TABLE_VAR_DIR => 'tdir', + ]; + } + + /** + * Call this to pass the download type. Use : + * $download = optional_param('download', '', PARAM_ALPHA); + * To get the download type. We assume that if you call this function with + * params that this table's data is downloadable, so we call is_downloadable + * for you (even if the param is '', which means no download this time. + * Also you can call this method with no params to get the current set + * download type. + * @param string|null $download type of dataformat for export. + * @param string $filename filename for downloads without file extension. + * @param string $sheettitle title for downloaded data. + * @return string download dataformat type. + */ + public function is_downloading($download = null, $filename = '', $sheettitle = '') { + if ($download !== null) { + $this->sheettitle = $sheettitle; + $this->is_downloadable(true); + $this->download = $download; + $this->filename = clean_filename($filename); + $this->export_class_instance(); + } + return $this->download; + } + + /** + * Get, and optionally set, the export class. + * @param dataformat_export_format $exportclass (optional) if passed, set the table to use this export class. + * @return dataformat_export_format the export class in use (after any set). + */ + public function export_class_instance($exportclass = null) { + if (!is_null($exportclass)) { + $this->started_output = true; + $this->exportclass = $exportclass; + $this->exportclass->table = $this; + } else if (is_null($this->exportclass) && !empty($this->download)) { + $this->exportclass = new dataformat_export_format($this, $this->download); + if (!$this->exportclass->document_started()) { + $this->exportclass->start_document($this->filename, $this->sheettitle); + } + } + return $this->exportclass; + } + + /** + * Probably don't need to call this directly. Calling is_downloading with a + * param automatically sets table as downloadable. + * + * @param bool $downloadable optional param to set whether data from + * table is downloadable. If ommitted this function can be used to get + * current state of table. + * @return bool whether table data is set to be downloadable. + */ + public function is_downloadable($downloadable = null) { + if ($downloadable !== null) { + $this->downloadable = $downloadable; + } + return $this->downloadable; + } + + /** + * Call with boolean true to store table layout changes in the user_preferences table. + * Note: user_preferences.value has a maximum length of 1333 characters. + * Call with no parameter to get current state of table persistence. + * + * @param bool $persistent Optional parameter to set table layout persistence. + * @return bool Whether or not the table layout preferences will persist. + */ + public function is_persistent($persistent = null) { + if ($persistent == true) { + $this->persistent = true; + } + return $this->persistent; + } + + /** + * Where to show download buttons. + * @param array $showat array of postions in which to show download buttons. + * Containing TABLE_P_TOP and/or TABLE_P_BOTTOM + */ + public function show_download_buttons_at($showat) { + $this->showdownloadbuttonsat = $showat; + } + + /** + * Sets the is_sortable variable to the given boolean, sort_default_column to + * the given string, and the sort_default_order to the given integer. + * @param bool $bool + * @param string $defaultcolumn + * @param int $defaultorder + * @return void + */ + public function sortable($bool, $defaultcolumn = null, $defaultorder = SORT_ASC) { + $this->is_sortable = $bool; + $this->sort_default_column = $defaultcolumn; + $this->sort_default_order = $defaultorder; + } + + /** + * Use text sorting functions for this column (required for text columns with Oracle). + * Be warned that you cannot use this with column aliases. You can only do this + * with real columns. See MDL-40481 for an example. + * @param string column name + */ + public function text_sorting($column) { + $this->column_textsort[] = $column; + } + + /** + * Do not sort using this column + * @param string column name + */ + public function no_sorting($column) { + $this->column_nosort[] = $column; + } + + /** + * Is the column sortable? + * @param string column name, null means table + * @return bool + */ + public function is_sortable($column = null) { + if (empty($column)) { + return $this->is_sortable; + } + if (!$this->is_sortable) { + return false; + } + return !in_array($column, $this->column_nosort); + } + + /** + * Sets the is_collapsible variable to the given boolean. + * @param bool $bool + * @return void + */ + public function collapsible($bool) { + $this->is_collapsible = $bool; + } + + /** + * Sets the use_pages variable to the given boolean. + * @param bool $bool + * @return void + */ + public function pageable($bool) { + $this->use_pages = $bool; + } + + /** + * Sets the use_initials variable to the given boolean. + * @param bool $bool + * @return void + */ + public function initialbars($bool) { + $this->use_initials = $bool; + } + + /** + * Sets the pagesize variable to the given integer, the totalrows variable + * to the given integer, and the use_pages variable to true. + * @param int $perpage + * @param int $total + * @return void + */ + public function pagesize($perpage, $total) { + $this->pagesize = $perpage; + $this->totalrows = $total; + $this->use_pages = true; + } + + /** + * Assigns each given variable in the array to the corresponding index + * in the request class variable. + * @param array $variables + * @return void + */ + public function set_control_variables($variables) { + foreach ($variables as $what => $variable) { + if (isset($this->request[$what])) { + $this->request[$what] = $variable; + } + } + } + + /** + * Gives the given $value to the $attribute index of $this->attributes. + * @param string $attribute + * @param mixed $value + * @return void + */ + public function set_attribute($attribute, $value) { + $this->attributes[$attribute] = $value; + } + + /** + * What this method does is set the column so that if the same data appears in + * consecutive rows, then it is not repeated. + * + * For example, in the quiz overview report, the fullname column is set to be suppressed, so + * that when one student has made multiple attempts, their name is only printed in the row + * for their first attempt. + * @param int $column the index of a column. + */ + public function column_suppress($column) { + if (isset($this->column_suppress[$column])) { + $this->column_suppress[$column] = true; + } + } + + /** + * Sets the given $column index to the given $classname in $this->column_class. + * @param int $column + * @param string $classname + * @return void + */ + public function column_class($column, $classname) { + if (isset($this->column_class[$column])) { + $this->column_class[$column] = ' ' . $classname; // This space needed so that classnames don't run together in the HTML. + } + } + + /** + * Sets the given $column index and $property index to the given $value in $this->column_style. + * @param int $column + * @param string $property + * @param mixed $value + * @return void + */ + public function column_style($column, $property, $value) { + if (isset($this->column_style[$column])) { + $this->column_style[$column][$property] = $value; + } + } + + /** + * Sets a sticky attribute to a column. + * @param string $column Column name + * @param bool $sticky + */ + public function column_sticky(string $column, bool $sticky = true): void { + if (isset($this->columnsticky[$column])) { + $this->columnsticky[$column] = $sticky == true ? ' sticky-column' : ''; + } + } + + /** + * Sets the given $attributes to $this->columnsattributes. + * Column attributes will be added to every cell in the column. + * + * @param array[] $attributes e.g. ['c0_firstname' => ['data-foo' => 'bar']] + */ + public function set_columnsattributes(array $attributes): void { + $this->columnsattributes = $attributes; + } + + /** + * Sets all columns' $propertys to the given $value in $this->column_style. + * @param int $property + * @param string $value + * @return void + */ + public function column_style_all($property, $value) { + foreach (array_keys($this->columns) as $column) { + $this->column_style[$column][$property] = $value; + } + } + + /** + * Sets $this->baseurl. + * @param moodle_url|string $url the url with params needed to call up this page + */ + public function define_baseurl($url) { + $this->baseurl = new moodle_url($url); + } + + /** + * Define the columns for the table. + * + * @param array $columns an array of identifying names for columns. If + * columns are sorted then column names must correspond to a field in sql. + */ + public function define_columns($columns) { + $this->columns = []; + $this->column_style = []; + $this->column_class = []; + $this->columnsticky = []; + $this->columnsattributes = []; + $colnum = 0; + + foreach ($columns as $column) { + $this->columns[$column] = $colnum++; + $this->column_style[$column] = []; + $this->column_class[$column] = ''; + $this->columnsticky[$column] = ''; + $this->columnsattributes[$column] = []; + $this->column_suppress[$column] = false; + } + } + + /** + * Define the headers for the table, replacing any existing header configuration. + * + * @param array $headers numerical keyed array of displayed string titles + * for each column. + */ + public function define_headers($headers) { + $this->headers = $headers; + } + + /** + * Mark a specific column as being a table header using the column name defined in define_columns. + * + * Note: Only one column can be a header, and it will be rendered using a th tag. + * + * @param string $column + */ + public function define_header_column(string $column) { + $this->headercolumn = $column; + } + + /** + * Defines a help icon for the header + * + * Always use this function if you need to create header with sorting and help icon. + * + * @param renderable[] $helpicons An array of renderable objects to be used as help icons + */ + public function define_help_for_headers($helpicons) { + $this->helpforheaders = $helpicons; + } + + /** + * Mark the table preferences to be reset. + */ + public function mark_table_to_reset(): void { + $this->resetting = true; + } + + /** + * Is the table marked for reset preferences? + * + * @return bool True if the table is marked to reset, false otherwise. + */ + protected function is_resetting_preferences(): bool { + if ($this->resetting === null) { + $this->resetting = optional_param($this->request[TABLE_VAR_RESET], false, PARAM_BOOL); + } + + return $this->resetting; + } + + /** + * Must be called after table is defined. Use methods above first. Cannot + * use functions below till after calling this method. + */ + public function setup() { + if (empty($this->columns) || empty($this->uniqueid)) { + return false; + } + + $this->initialise_table_preferences(); + + if (empty($this->baseurl)) { + debugging('You should set baseurl when using flexible_table.'); + global $PAGE; + $this->baseurl = $PAGE->url; + } + + if ($this->currpage == null) { + $this->currpage = optional_param($this->request[TABLE_VAR_PAGE], 0, PARAM_INT); + } + + $this->setup = true; + + // Always introduce the "flexible" class for the table if not specified. + if (empty($this->attributes)) { + $this->attributes['class'] = 'flexible table table-striped table-hover'; + } else if (!isset($this->attributes['class'])) { + $this->attributes['class'] = 'flexible table table-striped table-hover'; + } else if (!in_array('flexible', explode(' ', $this->attributes['class']))) { + $this->attributes['class'] = trim('flexible table table-striped table-hover ' . $this->attributes['class']); + } + } + + /** + * Get the order by clause from the session or user preferences, for the table with id $uniqueid. + * @param string $uniqueid the identifier for a table. + * @return string SQL fragment that can be used in an ORDER BY clause. + */ + public static function get_sort_for_table($uniqueid) { + global $SESSION; + if (isset($SESSION->flextable[$uniqueid])) { + $prefs = $SESSION->flextable[$uniqueid]; + } else if (!$prefs = json_decode(get_user_preferences("flextable_{$uniqueid}", ''), true)) { + return ''; + } + + if (empty($prefs['sortby'])) { + return ''; + } + if (empty($prefs['textsort'])) { + $prefs['textsort'] = []; + } + + return self::construct_order_by($prefs['sortby'], $prefs['textsort']); + } + + /** + * Prepare an an order by clause from the list of columns to be sorted. + * + * @param array $cols column name => SORT_ASC or SORT_DESC + * @return string SQL fragment that can be used in an ORDER BY clause. + */ + public static function construct_order_by($cols, $textsortcols = []) { + global $DB; + $bits = []; + + foreach ($cols as $column => $order) { + if (in_array($column, $textsortcols)) { + $column = $DB->sql_order_by_text($column); + } + if ($order == SORT_ASC) { + $bits[] = $DB->sql_order_by_null($column); + } else { + $bits[] = $DB->sql_order_by_null($column, SORT_DESC); + } + } + + return implode(', ', $bits); + } + + /** + * Get the SQL Sort clause for the table. + * + * @return string SQL fragment that can be used in an ORDER BY clause. + */ + public function get_sql_sort() { + return self::construct_order_by($this->get_sort_columns(), $this->column_textsort); + } + + /** + * Whether the current table contains any fullname columns + * + * @return bool + */ + private function contains_fullname_columns(): bool { + $fullnamecolumns = array_intersect_key($this->columns, array_flip($this->userfullnamecolumns)); + + return !empty($fullnamecolumns); + } + + /** + * Get the columns to sort by, in the form required by {@see construct_order_by()}. + * @return array column name => SORT_... constant. + */ + public function get_sort_columns() { + if (!$this->setup) { + throw new coding_exception('Cannot call get_sort_columns until you have called setup.'); + } + + if (empty($this->prefs['sortby'])) { + return []; + } + foreach ($this->prefs['sortby'] as $column => $notused) { + if (isset($this->columns[$column])) { + continue; // This column is OK. + } + if (in_array($column, \core_user\fields::get_name_fields()) && $this->contains_fullname_columns()) { + continue; // This column is OK. + } + // This column is not OK. + unset($this->prefs['sortby'][$column]); + } + + return $this->prefs['sortby']; + } + + /** + * Get the starting row number for this page. + * + * @return int the offset for LIMIT clause of SQL + */ + public function get_page_start() { + if (!$this->use_pages) { + return ''; + } + return $this->currpage * $this->pagesize; + } + + /** + * @return int the pagesize for LIMIT clause of SQL + */ + public function get_page_size() { + if (!$this->use_pages) { + return ''; + } + return $this->pagesize; + } + + /** + * @return array sql to add to where statement. + */ + public function get_sql_where() { + global $DB; + + $conditions = []; + $params = []; + + if ($this->contains_fullname_columns()) { + static $i = 0; + $i++; + + if (!empty($this->prefs['i_first'])) { + $conditions[] = $DB->sql_like('firstname', ':ifirstc' . $i, false, false); + $params['ifirstc' . $i] = $this->prefs['i_first'] . '%'; + } + if (!empty($this->prefs['i_last'])) { + $conditions[] = $DB->sql_like('lastname', ':ilastc' . $i, false, false); + $params['ilastc' . $i] = $this->prefs['i_last'] . '%'; + } + } + + return [implode(" AND ", $conditions), $params]; + } + + /** + * Add a row of data to the table. This function takes an array or object with + * column names as keys or property names. + * + * It ignores any elements with keys that are not defined as columns. It + * puts in empty strings into the row when there is no element in the passed + * array corresponding to a column in the table. It puts the row elements in + * the proper order (internally row table data is stored by in arrays with + * a numerical index corresponding to the column number). + * + * @param object|array $rowwithkeys array keys or object property names are column names, + * as defined in call to define_columns. + * @param string $classname CSS class name to add to this row's tr tag. + */ + public function add_data_keyed($rowwithkeys, $classname = '') { + $this->add_data($this->get_row_from_keyed($rowwithkeys), $classname); + } + + /** + * Add a number of rows to the table at once. And optionally finish output after they have been added. + * + * @param (object|array|null)[] $rowstoadd Array of rows to add to table, a null value in array adds a separator row. Or a + * object or array is added to table. We expect properties for the row array as would be + * passed to add_data_keyed. + * @param bool $finish + */ + public function format_and_add_array_of_rows($rowstoadd, $finish = true) { + foreach ($rowstoadd as $row) { + if (is_null($row)) { + $this->add_separator(); + } else { + $this->add_data_keyed($this->format_row($row)); + } + } + if ($finish) { + $this->finish_output(!$this->is_downloading()); + } + } + + /** + * Add a seperator line to table. + */ + public function add_separator() { + if (!$this->setup) { + return false; + } + $this->add_data(null); + } + + /** + * This method actually directly echoes the row passed to it now or adds it + * to the download. If this is the first row and start_output has not + * already been called this method also calls start_output to open the table + * or send headers for the downloaded. + * Can be used as before. print_html now calls finish_html to close table. + * + * @param array $row a numerically keyed row of data to add to the table. + * @param string $classname CSS class name to add to this row's tr tag. + * @return bool success. + */ + public function add_data($row, $classname = '') { + if (!$this->setup) { + return false; + } + if (!$this->started_output) { + $this->start_output(); + } + if ($this->exportclass !== null) { + if ($row === null) { + $this->exportclass->add_seperator(); + } else { + $this->exportclass->add_data($row); + } + } else { + $this->print_row($row, $classname); + } + return true; + } + + /** + * You should call this to finish outputting the table data after adding + * data to the table with add_data or add_data_keyed. + * + */ + public function finish_output($closeexportclassdoc = true) { + if ($this->exportclass !== null) { + $this->exportclass->finish_table(); + if ($closeexportclassdoc) { + $this->exportclass->finish_document(); + } + } else { + $this->finish_html(); + } + } + + /** + * Hook that can be overridden in child classes to wrap a table in a form + * for example. Called only when there is data to display and not + * downloading. + */ + public function wrap_html_start() { + } + + /** + * Hook that can be overridden in child classes to wrap a table in a form + * for example. Called only when there is data to display and not + * downloading. + */ + public function wrap_html_finish() { + } + + /** + * Call appropriate methods on this table class to perform any processing on values before displaying in table. + * Takes raw data from the database and process it into human readable format, perhaps also adding html linking when + * displaying table as html, adding a div wrap, etc. + * + * See for example col_fullname below which will be called for a column whose name is 'fullname'. + * + * @param array|object $row row of data from db used to make one row of the table. + * @return array one row for the table, added using add_data_keyed method. + */ + public function format_row($row) { + if (is_array($row)) { + $row = (object)$row; + } + $formattedrow = []; + foreach (array_keys($this->columns) as $column) { + $colmethodname = 'col_' . $column; + if (method_exists($this, $colmethodname)) { + $formattedcolumn = $this->$colmethodname($row); + } else { + $formattedcolumn = $this->other_cols($column, $row); + if ($formattedcolumn === null) { + $formattedcolumn = $row->$column; + } + } + $formattedrow[$column] = $formattedcolumn; + } + return $formattedrow; + } + + /** + * Fullname is treated as a special columname in tablelib and should always + * be treated the same as the fullname of a user. + * @uses $this->useridfield if the userid field is not expected to be id + * then you need to override $this->useridfield to point at the correct + * field for the user id. + * + * @param object $row the data from the db containing all fields from the + * users table necessary to construct the full name of the user in + * current language. + * @return string contents of cell in column 'fullname', for this row. + */ + public function col_fullname($row) { + global $COURSE; + + $name = fullname($row, has_capability('moodle/site:viewfullnames', $this->get_context())); + if ($this->download) { + return $name; + } + + $userid = $row->{$this->useridfield}; + if ($COURSE->id == SITEID) { + $profileurl = new moodle_url('/user/profile.php', ['id' => $userid]); + } else { + $profileurl = new moodle_url( + '/user/view.php', + ['id' => $userid, 'course' => $COURSE->id] + ); + } + return html_writer::link($profileurl, $name); + } + + /** + * You can override this method in a child class. See the description of + * build_table which calls this method. + */ + public function other_cols($column, $row) { + if ( + isset($row->$column) && ($column === 'email' || $column === 'idnumber') && + (!$this->is_downloading() || $this->export_class_instance()->supports_html()) + ) { + // Columns email and idnumber may potentially contain malicious characters, escape them by default. + // This function will not be executed if the child class implements col_email() or col_idnumber(). + return s($row->$column); + } + return null; + } + + /** + * Used from col_* functions when text is to be displayed. Does the + * right thing - either converts text to html or strips any html tags + * depending on if we are downloading and what is the download type. Params + * are the same as format_text function in weblib.php but some default + * options are changed. + */ + public function format_text($text, $format = FORMAT_MOODLE, $options = null, $courseid = null) { + if (!$this->is_downloading()) { + if (is_null($options)) { + $options = new stdClass(); + } + // Some sensible defaults. + if (!isset($options->para)) { + $options->para = false; + } + if (!isset($options->newlines)) { + $options->newlines = false; + } + if (!isset($options->filter)) { + $options->filter = false; + } + return format_text($text, $format, $options); + } else { + $eci = $this->export_class_instance(); + return $eci->format_text($text, $format, $options, $courseid); + } + } + /** + * This method is deprecated although the old api is still supported. + * @deprecated 1.9.2 - Jun 2, 2008 + */ + public function print_html() { + if (!$this->setup) { + return false; + } + $this->finish_html(); + } + + /** + * This function is not part of the public api. + * @return string initial of first name we are currently filtering by + */ + public function get_initial_first() { + if (!$this->use_initials) { + return null; + } + + return $this->prefs['i_first']; + } + + /** + * This function is not part of the public api. + * @return string initial of last name we are currently filtering by + */ + public function get_initial_last() { + if (!$this->use_initials) { + return null; + } + + return $this->prefs['i_last']; + } + + /** + * Helper function, used by {@see print_initials_bar()} to output one initial bar. + * @param array $alpha of letters in the alphabet. + * @param string $current the currently selected letter. + * @param string $class class name to add to this initial bar. + * @param string $title the name to put in front of this initial bar. + * @param string $urlvar URL parameter name for this initial. + * + * @deprecated since Moodle 3.3 + */ + protected function print_one_initials_bar($alpha, $current, $class, $title, $urlvar) { + + debugging('Method print_one_initials_bar() is no longer used and has been deprecated, ' . + 'to print initials bar call print_initials_bar()', DEBUG_DEVELOPER); + + echo html_writer::start_tag('div', ['class' => 'initialbar ' . $class]) . + $title . ' : '; + if ($current) { + echo html_writer::link($this->baseurl->out(false, [$urlvar => '']), get_string('all')); + } else { + echo html_writer::tag('strong', get_string('all')); + } + + foreach ($alpha as $letter) { + if ($letter === $current) { + echo html_writer::tag('strong', $letter); + } else { + echo html_writer::link($this->baseurl->out(false, [$urlvar => $letter]), $letter); + } + } + + echo html_writer::end_tag('div'); + } + + /** + * This function is not part of the public api. + */ + public function print_initials_bar() { + global $OUTPUT; + + $ifirst = $this->get_initial_first(); + $ilast = $this->get_initial_last(); + if (is_null($ifirst)) { + $ifirst = ''; + } + if (is_null($ilast)) { + $ilast = ''; + } + + if ((!empty($ifirst) || !empty($ilast) || $this->use_initials) && $this->contains_fullname_columns()) { + $prefixfirst = $this->request[TABLE_VAR_IFIRST]; + $prefixlast = $this->request[TABLE_VAR_ILAST]; + echo $OUTPUT->initials_bar($ifirst, 'firstinitial', get_string('firstname'), $prefixfirst, $this->baseurl); + echo $OUTPUT->initials_bar($ilast, 'lastinitial', get_string('lastname'), $prefixlast, $this->baseurl); + } + } + + /** + * This function is not part of the public api. + */ + public function print_nothing_to_display() { + global $OUTPUT; + + // Render the dynamic table header. + echo $this->get_dynamic_table_html_start(); + + // Render button to allow user to reset table preferences. + echo $this->render_reset_button(); + + $this->print_initials_bar(); + + echo $OUTPUT->notification(get_string('nothingtodisplay'), 'info', false); + + // Render the dynamic table footer. + echo $this->get_dynamic_table_html_end(); + } + + /** + * This function is not part of the public api. + */ + public function get_row_from_keyed($rowwithkeys) { + if (is_object($rowwithkeys)) { + $rowwithkeys = (array)$rowwithkeys; + } + $row = []; + foreach (array_keys($this->columns) as $column) { + if (isset($rowwithkeys[$column])) { + $row[] = $rowwithkeys[$column]; + } else { + $row[] = ''; + } + } + return $row; + } + + /** + * Get the html for the download buttons + * + * Usually only use internally + */ + public function download_buttons() { + global $OUTPUT; + + if ($this->is_downloadable() && !$this->is_downloading()) { + return $OUTPUT->download_dataformat_selector( + get_string('downloadas', 'table'), + $this->baseurl->out_omit_querystring(), + 'download', + $this->baseurl->params() + ); + } else { + return ''; + } + } + + /** + * This function is not part of the public api. + * You don't normally need to call this. It is called automatically when + * needed when you start adding data to the table. + * + */ + public function start_output() { + $this->started_output = true; + if ($this->exportclass !== null) { + $this->exportclass->start_table($this->sheettitle); + $this->exportclass->output_headers($this->headers); + } else { + $this->start_html(); + $this->print_headers(); + echo html_writer::start_tag('tbody'); + } + } + + /** + * This function is not part of the public api. + */ + public function print_row($row, $classname = '') { + echo $this->get_row_html($row, $classname); + } + + /** + * Generate html code for the passed row. + * + * @param array $row Row data. + * @param string $classname classes to add. + * + * @return string $html html code for the row passed. + */ + public function get_row_html($row, $classname = '') { + static $suppresslastrow = null; + $rowclasses = []; + + if ($classname) { + $rowclasses[] = $classname; + } + + $rowid = $this->uniqueid . '_r' . $this->currentrow; + $html = ''; + + $html .= html_writer::start_tag('tr', ['class' => implode(' ', $rowclasses), 'id' => $rowid]); + + // If we have a separator, print it. + if ($row === null) { + $colcount = count($this->columns); + $html .= html_writer::tag('td', html_writer::tag( + 'div', + '', + ['class' => 'tabledivider'] + ), ['colspan' => $colcount]); + } else { + $html .= $this->get_row_cells_html($rowid, $row, $suppresslastrow); + } + + $html .= html_writer::end_tag('tr'); + + $suppressenabled = array_sum($this->column_suppress); + if ($suppressenabled) { + $suppresslastrow = $row; + } + $this->currentrow++; + return $html; + } + + /** + * Generate html code for the row cells. + * + * @param string $rowid + * @param array $row + * @param array|null $suppresslastrow + * @return string + */ + public function get_row_cells_html(string $rowid, array $row, ?array $suppresslastrow): string { + $html = ''; + $colbyindex = array_flip($this->columns); + foreach ($row as $index => $data) { + $column = $colbyindex[$index]; + + $columnattributes = $this->columnsattributes[$column] ?? []; + if (isset($columnattributes['class'])) { + $this->column_class($column, $columnattributes['class']); + unset($columnattributes['class']); + } + + $attributes = [ + 'class' => "cell c{$index}" . $this->column_class[$column] . $this->columnsticky[$column], + 'id' => "{$rowid}_c{$index}", + 'style' => $this->make_styles_string($this->column_style[$column]), + ]; + + $celltype = 'td'; + if ($this->headercolumn && $column == $this->headercolumn) { + $celltype = 'th'; + $attributes['scope'] = 'row'; + } + + $attributes += $columnattributes; + + if (empty($this->prefs['collapse'][$column])) { + if ($this->column_suppress[$column] && $suppresslastrow !== null && $suppresslastrow[$index] === $data) { + $content = ' '; + } else { + $content = $data; + } + } else { + $content = ' '; + } + + $html .= html_writer::tag($celltype, $content, $attributes); + } + return $html; + } + + /** + * This function is not part of the public api. + */ + public function finish_html() { + global $OUTPUT, $PAGE; + + if (!$this->started_output) { + // No data has been added to the table. + $this->print_nothing_to_display(); + } else { + // Print empty rows to fill the table to the current pagesize. + // This is done so the header aria-controls attributes do not point to + // non existant elements. + $emptyrow = array_fill(0, count($this->columns), ''); + while ($this->currentrow < $this->pagesize) { + $this->print_row($emptyrow, 'emptyrow'); + } + + echo html_writer::end_tag('tbody'); + echo html_writer::end_tag('table'); + if ($this->responsive) { + echo html_writer::end_tag('div'); + } + $this->wrap_html_finish(); + + // Paging bar. + if (in_array(TABLE_P_BOTTOM, $this->showdownloadbuttonsat)) { + echo $this->download_buttons(); + } + + if ($this->use_pages) { + $pagingbar = new paging_bar($this->totalrows, $this->currpage, $this->pagesize, $this->baseurl); + $pagingbar->pagevar = $this->request[TABLE_VAR_PAGE]; + echo $OUTPUT->render($pagingbar); + } + + // Render the dynamic table footer. + echo $this->get_dynamic_table_html_end(); + } + } + + /** + * Generate the HTML for the collapse/uncollapse icon. This is a helper method + * used by {@see print_headers()}. + * @param string $column the column name, index into various names. + * @param int $index numerical index of the column. + * @return string HTML fragment. + */ + protected function show_hide_link($column, $index) { + global $OUTPUT; + // Some headers contain