Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * This file contains the moodle format implementation of the content writer.
  19   *
  20   * @package core_privacy
  21   * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
  22   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  namespace core_privacy\tests\request;
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  /**
  29   * An implementation of the content_writer for use in unit tests.
  30   *
  31   * This implementation does not export any data but instead stores it in
  32   * structures within the instance which can be easily queried for use
  33   * during unit tests.
  34   *
  35   * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
  36   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class content_writer implements \core_privacy\local\request\content_writer {
  39      /**
  40       * @var \context The context currently being exported.
  41       */
  42      protected $context;
  43  
  44      /**
  45       * @var \stdClass The collection of metadata which has been exported.
  46       */
  47      protected $metadata;
  48  
  49      /**
  50       * @var \stdClass The data which has been exported.
  51       */
  52      protected $data;
  53  
  54      /**
  55       * @var \stdClass The related data which has been exported.
  56       */
  57      protected $relateddata;
  58  
  59      /**
  60       * @var \stdClass The list of stored files which have been exported.
  61       */
  62      protected $files;
  63  
  64      /**
  65       * @var \stdClass The custom files which have been exported.
  66       */
  67      protected $customfiles;
  68  
  69      /**
  70       * @var \stdClass The user preferences which have been exported.
  71       */
  72      protected $userprefs;
  73  
  74      /**
  75       * Whether any data has been exported at all within the current context.
  76       *
  77       * @param array $subcontext The location within the current context that this data belongs -
  78       *   in this method it can be partial subcontext path (or none at all to check presence of any data anywhere).
  79       *   User preferences never have subcontext, if $subcontext is specified, user preferences are not checked.
  80       * @return  bool
  81       */
  82      public function has_any_data($subcontext = []) {
  83          if (empty($subcontext)) {
  84              // When subcontext is not specified check presence of user preferences in this context and in system context.
  85              $hasuserprefs = !empty($this->userprefs->{$this->context->id});
  86              $systemcontext = \context_system::instance();
  87              $hasglobaluserprefs = !empty($this->userprefs->{$systemcontext->id});
  88              if ($hasuserprefs || $hasglobaluserprefs) {
  89                  return true;
  90              }
  91          }
  92  
  93          foreach (['data', 'relateddata', 'metadata', 'files', 'customfiles'] as $datatype) {
  94              if (!property_exists($this->$datatype, $this->context->id)) {
  95                  // No data of this type for this context at all. Continue to the next data type.
  96                  continue;
  97              }
  98              $basepath = $this->$datatype->{$this->context->id};
  99              foreach ($subcontext as $subpath) {
 100                  if (!isset($basepath->children->$subpath)) {
 101                      // No data of this type is present for this path. Continue to the next data type.
 102                      continue 2;
 103                  }
 104                  $basepath = $basepath->children->$subpath;
 105              }
 106              if (!empty($basepath)) {
 107                  // Some data found for this type for this subcontext.
 108                  return true;
 109              }
 110          }
 111          return false;
 112      }
 113  
 114      /**
 115       * Whether any data has been exported for any context.
 116       *
 117       * @return  bool
 118       */
 119      public function has_any_data_in_any_context() {
 120          $checkfordata = function($location) {
 121              foreach ($location as $context => $data) {
 122                  if (!empty($data)) {
 123                      return true;
 124                  }
 125              }
 126  
 127              return false;
 128          };
 129  
 130          $hasanydata = $checkfordata($this->data);
 131          $hasanydata = $hasanydata || $checkfordata($this->relateddata);
 132          $hasanydata = $hasanydata || $checkfordata($this->metadata);
 133          $hasanydata = $hasanydata || $checkfordata($this->files);
 134          $hasanydata = $hasanydata || $checkfordata($this->customfiles);
 135          $hasanydata = $hasanydata || $checkfordata($this->userprefs);
 136  
 137          return $hasanydata;
 138      }
 139  
 140      /**
 141       * Constructor for the content writer.
 142       *
 143       * Note: The writer_factory must be passed.
 144       * @param   \core_privacy\local\request\writer          $writer    The writer factory.
 145       */
 146      public function __construct(\core_privacy\local\request\writer $writer) {
 147          $this->data = (object) [];
 148          $this->relateddata = (object) [];
 149          $this->metadata = (object) [];
 150          $this->files = (object) [];
 151          $this->customfiles = (object) [];
 152          $this->userprefs = (object) [];
 153      }
 154  
 155      /**
 156       * Set the context for the current item being processed.
 157       *
 158       * @param   \context        $context    The context to use
 159       */
 160      public function set_context(\context $context) : \core_privacy\local\request\content_writer {
 161          $this->context = $context;
 162  
 163          if (isset($this->data->{$this->context->id}) && empty((array) $this->data->{$this->context->id})) {
 164              $this->data->{$this->context->id} = (object) [
 165                  'children' => (object) [],
 166                  'data' => [],
 167              ];
 168          }
 169  
 170          if (isset($this->relateddata->{$this->context->id}) && empty((array) $this->relateddata->{$this->context->id})) {
 171              $this->relateddata->{$this->context->id} = (object) [
 172                  'children' => (object) [],
 173                  'data' => [],
 174              ];
 175          }
 176  
 177          if (isset($this->metadata->{$this->context->id}) && empty((array) $this->metadata->{$this->context->id})) {
 178              $this->metadata->{$this->context->id} = (object) [
 179                  'children' => (object) [],
 180                  'data' => [],
 181              ];
 182          }
 183  
 184          if (isset($this->files->{$this->context->id}) && empty((array) $this->files->{$this->context->id})) {
 185              $this->files->{$this->context->id} = (object) [
 186                  'children' => (object) [],
 187                  'data' => [],
 188              ];
 189          }
 190  
 191          if (isset($this->customfiles->{$this->context->id}) && empty((array) $this->customfiles->{$this->context->id})) {
 192              $this->customfiles->{$this->context->id} = (object) [
 193                  'children' => (object) [],
 194                  'data' => [],
 195              ];
 196          }
 197  
 198          if (isset($this->userprefs->{$this->context->id}) && empty((array) $this->userprefs->{$this->context->id})) {
 199              $this->userprefs->{$this->context->id} = (object) [
 200                  'children' => (object) [],
 201                  'data' => [],
 202              ];
 203          }
 204  
 205          return $this;
 206      }
 207  
 208      /**
 209       * Return the current context.
 210       *
 211       * @return  \context
 212       */
 213      public function get_current_context() : \context {
 214          return $this->context;
 215      }
 216  
 217      /**
 218       * Export the supplied data within the current context, at the supplied subcontext.
 219       *
 220       * @param   array           $subcontext The location within the current context that this data belongs.
 221       * @param   \stdClass       $data       The data to be exported
 222       */
 223      public function export_data(array $subcontext, \stdClass $data) : \core_privacy\local\request\content_writer {
 224          $current = $this->fetch_root($this->data, $subcontext);
 225          $current->data = $data;
 226  
 227          return $this;
 228      }
 229  
 230      /**
 231       * Get all data within the subcontext.
 232       *
 233       * @param   array           $subcontext The location within the current context that this data belongs.
 234       * @return  array                       The metadata as a series of keys to value + descrition objects.
 235       */
 236      public function get_data(array $subcontext = []) {
 237          return $this->fetch_data_root($this->data, $subcontext);
 238      }
 239  
 240      /**
 241       * Export metadata about the supplied subcontext.
 242       *
 243       * Metadata consists of a key/value pair and a description of the value.
 244       *
 245       * @param   array           $subcontext The location within the current context that this data belongs.
 246       * @param   string          $key        The metadata name.
 247       * @param   string          $value      The metadata value.
 248       * @param   string          $description    The description of the value.
 249       * @return  $this
 250       */
 251      public function export_metadata(array $subcontext, string $key, $value, string $description)
 252              : \core_privacy\local\request\content_writer {
 253          $current = $this->fetch_root($this->metadata, $subcontext);
 254          $current->data[$key] = (object) [
 255                  'value' => $value,
 256                  'description' => $description,
 257              ];
 258  
 259          return $this;
 260      }
 261  
 262      /**
 263       * Get all metadata within the subcontext.
 264       *
 265       * @param   array           $subcontext The location within the current context that this data belongs.
 266       * @return  array                       The metadata as a series of keys to value + descrition objects.
 267       */
 268      public function get_all_metadata(array $subcontext = []) {
 269          return $this->fetch_data_root($this->metadata, $subcontext);
 270      }
 271  
 272      /**
 273       * Get the specified metadata within the subcontext.
 274       *
 275       * @param   array           $subcontext The location within the current context that this data belongs.
 276       * @param   string          $key        The metadata to be fetched within the context + subcontext.
 277       * @param   boolean         $valueonly  Whether to fetch only the value, rather than the value + description.
 278       * @return  array                       The metadata as a series of keys to value + descrition objects.
 279       */
 280      public function get_metadata(array $subcontext, $key, $valueonly = true) {
 281          $keys = $this->get_all_metadata($subcontext);
 282  
 283          if (isset($keys[$key])) {
 284              $metadata = $keys[$key];
 285          } else {
 286              return null;
 287          }
 288  
 289          if ($valueonly) {
 290              return $metadata->value;
 291          } else {
 292              return $metadata;
 293          }
 294      }
 295  
 296      /**
 297       * Export a piece of related data.
 298       *
 299       * @param   array           $subcontext The location within the current context that this data belongs.
 300       * @param   string          $name       The name of the file to be exported.
 301       * @param   \stdClass       $data       The related data to export.
 302       */
 303      public function export_related_data(array $subcontext, $name, $data) : \core_privacy\local\request\content_writer {
 304          $current = $this->fetch_root($this->relateddata, $subcontext);
 305          $current->data[$name] = $data;
 306  
 307          return $this;
 308      }
 309  
 310      /**
 311       * Get all data within the subcontext.
 312       *
 313       * @param   array           $subcontext The location within the current context that this data belongs.
 314       * @param   string          $filename   The name of the intended filename.
 315       * @return  array                       The metadata as a series of keys to value + descrition objects.
 316       */
 317      public function get_related_data(array $subcontext = [], $filename = null) {
 318          $current = $this->fetch_data_root($this->relateddata, $subcontext);
 319  
 320          if (null === $filename) {
 321              return $current;
 322          }
 323  
 324          if (isset($current[$filename])) {
 325              return $current[$filename];
 326          }
 327  
 328          return [];
 329      }
 330  
 331      /**
 332       * Export a piece of data in a custom format.
 333       *
 334       * @param   array           $subcontext The location within the current context that this data belongs.
 335       * @param   string          $filename   The name of the file to be exported.
 336       * @param   string          $filecontent    The content to be exported.
 337       */
 338      public function export_custom_file(array $subcontext, $filename, $filecontent) : \core_privacy\local\request\content_writer {
 339          $filename = clean_param($filename, PARAM_FILE);
 340  
 341          $current = $this->fetch_root($this->customfiles, $subcontext);
 342          $current->data[$filename] = $filecontent;
 343  
 344          return $this;
 345      }
 346  
 347      /**
 348       * Get the specified custom file within the subcontext.
 349       *
 350       * @param   array           $subcontext The location within the current context that this data belongs.
 351       * @param   string          $filename   The name of the file to be fetched within the context + subcontext.
 352       * @return  string                      The content of the file.
 353       */
 354      public function get_custom_file(array $subcontext = [], $filename = null) {
 355          $current = $this->fetch_data_root($this->customfiles, $subcontext);
 356  
 357          if (null === $filename) {
 358              return $current;
 359          }
 360  
 361          if (isset($current[$filename])) {
 362              return $current[$filename];
 363          }
 364  
 365          return null;
 366      }
 367  
 368      /**
 369       * Prepare a text area by processing pluginfile URLs within it.
 370       *
 371       * Note that this method does not implement the pluginfile URL rewriting. Such a job tightly depends on how the
 372       * actual writer exports files so it can be reliably tested only in real writers such as
 373       * {@link core_privacy\local\request\moodle_content_writer}.
 374       *
 375       * However we have to remove @@PLUGINFILE@@ since otherwise {@link format_text()} shows debugging messages
 376       *
 377       * @param   array           $subcontext The location within the current context that this data belongs.
 378       * @param   string          $component  The name of the component that the files belong to.
 379       * @param   string          $filearea   The filearea within that component.
 380       * @param   string          $itemid     Which item those files belong to.
 381       * @param   string          $text       The text to be processed
 382       * @return  string                      The processed string
 383       */
 384      public function rewrite_pluginfile_urls(array $subcontext, $component, $filearea, $itemid, $text) : string {
 385          return str_replace('@@PLUGINFILE@@/', 'files/', $text);
 386      }
 387  
 388      /**
 389       * Export all files within the specified component, filearea, itemid combination.
 390       *
 391       * @param   array           $subcontext The location within the current context that this data belongs.
 392       * @param   string          $component  The name of the component that the files belong to.
 393       * @param   string          $filearea   The filearea within that component.
 394       * @param   string          $itemid     Which item those files belong to.
 395       */
 396      public function export_area_files(array $subcontext, $component, $filearea, $itemid) : \core_privacy\local\request\content_writer  {
 397          $fs = get_file_storage();
 398          $files = $fs->get_area_files($this->context->id, $component, $filearea, $itemid);
 399          foreach ($files as $file) {
 400              $this->export_file($subcontext, $file);
 401          }
 402  
 403          return $this;
 404      }
 405  
 406      /**
 407       * Export the specified file in the target location.
 408       *
 409       * @param   array           $subcontext The location within the current context that this data belongs.
 410       * @param   \stored_file    $file       The file to be exported.
 411       */
 412      public function export_file(array $subcontext, \stored_file $file) : \core_privacy\local\request\content_writer  {
 413          if (!$file->is_directory()) {
 414              $filepath = $file->get_filepath();
 415              // Directory separator in the stored_file class should always be '/'. The following line is just a fail safe.
 416              $filepath = str_replace(DIRECTORY_SEPARATOR, '/', $filepath);
 417              $filepath = explode('/', $filepath);
 418              $filepath[] = $file->get_filename();
 419              $filepath = array_filter($filepath);
 420              $filepath = implode('/', $filepath);
 421              $current = $this->fetch_root($this->files, $subcontext);
 422              $current->data[$filepath] = $file;
 423          }
 424  
 425          return $this;
 426      }
 427  
 428      /**
 429       * Get all files in the specfied subcontext.
 430       *
 431       * @param   array           $subcontext The location within the current context that this data belongs.
 432       * @return  \stored_file[]              The list of stored_files in this context + subcontext.
 433       */
 434      public function get_files(array $subcontext = []) {
 435          return $this->fetch_data_root($this->files, $subcontext);
 436      }
 437  
 438      /**
 439       * Export the specified user preference.
 440       *
 441       * @param   string          $component  The name of the component.
 442       * @param   string          $key        The name of th key to be exported.
 443       * @param   string          $value      The value of the preference
 444       * @param   string          $description    A description of the value
 445       * @return  \core_privacy\local\request\content_writer
 446       */
 447      public function export_user_preference(
 448          string $component,
 449          string $key,
 450          string $value,
 451          string $description
 452      ) : \core_privacy\local\request\content_writer {
 453          $prefs = $this->fetch_root($this->userprefs, []);
 454  
 455          if (!isset($prefs->{$component})) {
 456              $prefs->{$component} = (object) [];
 457          }
 458  
 459          $prefs->{$component}->$key = (object) [
 460              'value' => $value,
 461              'description' => $description,
 462          ];
 463  
 464          return $this;
 465      }
 466  
 467      /**
 468       * Get all user preferences for the specified component.
 469       *
 470       * @param   string          $component  The name of the component.
 471       * @return  \stdClass
 472       */
 473      public function get_user_preferences(string $component) {
 474          $context = \context_system::instance();
 475          $prefs = $this->fetch_root($this->userprefs, [], $context->id);
 476          if (isset($prefs->{$component})) {
 477              return $prefs->{$component};
 478          } else {
 479              return (object) [];
 480          }
 481      }
 482  
 483      /**
 484       * Get all user preferences for the specified component.
 485       *
 486       * @param   string          $component  The name of the component.
 487       * @return  \stdClass
 488       */
 489      public function get_user_context_preferences(string $component) {
 490          $prefs = $this->fetch_root($this->userprefs, []);
 491          if (isset($prefs->{$component})) {
 492              return $prefs->{$component};
 493          } else {
 494              return (object) [];
 495          }
 496      }
 497  
 498      /**
 499       * Perform any required finalisation steps and return the location of the finalised export.
 500       *
 501       * @return  string
 502       */
 503      public function finalise_content() : string {
 504          return 'mock_path';
 505      }
 506  
 507      /**
 508       * Fetch the entire root record at the specified location type, creating it if required.
 509       *
 510       * @param   \stdClass   $base The base to use - e.g. $this->data
 511       * @param   array       $subcontext The subcontext to fetch
 512       * @param   int         $temporarycontextid A temporary context ID to use for the fetch.
 513       * @return  array
 514       */
 515      protected function fetch_root($base, $subcontext, $temporarycontextid = null) {
 516          $contextid = !empty($temporarycontextid) ? $temporarycontextid : $this->context->id;
 517          if (!isset($base->{$contextid})) {
 518              $base->{$contextid} = (object) [
 519                  'children' => (object) [],
 520                  'data' => [],
 521              ];
 522          }
 523  
 524          $current = $base->{$contextid};
 525          foreach ($subcontext as $node) {
 526              if (!isset($current->children->{$node})) {
 527                  $current->children->{$node} = (object) [
 528                      'children' => (object) [],
 529                      'data' => [],
 530                  ];
 531              }
 532              $current = $current->children->{$node};
 533          }
 534  
 535          return $current;
 536      }
 537  
 538      /**
 539       * Fetch the data region of the specified root.
 540       *
 541       * @param   \stdClass   $base The base to use - e.g. $this->data
 542       * @param   array       $subcontext The subcontext to fetch
 543       * @return  array
 544       */
 545      protected function fetch_data_root($base, $subcontext) {
 546          $root = $this->fetch_root($base, $subcontext);
 547  
 548          return $root->data;
 549      }
 550  }