Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   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   * Privacy Fetch Result Set.
  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  
  25  namespace core_privacy\local\request;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Privacy Fetch Result Set.
  31   *
  32   * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  33   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class contextlist extends contextlist_base {
  36  
  37      /**
  38       * Add a set of contexts from  SQL.
  39       *
  40       * The SQL should only return a list of context IDs.
  41       *
  42       * @param   string  $sql    The SQL which will fetch the list of * context IDs
  43       * @param   array   $params The set of SQL parameters
  44       * @return  $this
  45       */
  46      public function add_from_sql(string $sql, array $params) : contextlist {
  47          global $DB;
  48  
  49          $fields = \context_helper::get_preload_record_columns_sql('ctx');
  50          if ($fieldname = $this->guess_id_field_from_sql($sql)) {
  51              if (is_numeric($fieldname)) {
  52                  $wrapper = "
  53                    SELECT {$fields}
  54                      FROM {context} ctx
  55                     WHERE ctx.id = :fieldvalue";
  56                  $params = ['fieldvalue' => $fieldname];
  57              } else {
  58                  // Able to guess a field name.
  59                  $wrapper = "
  60                    SELECT {$fields}
  61                      FROM {context} ctx
  62                      JOIN ({$sql}) target ON ctx.id = target.{$fieldname}";
  63              }
  64          } else {
  65              // No field name available. Fall back on a potentially slower version.
  66              $wrapper = "
  67                SELECT {$fields}
  68                  FROM {context} ctx
  69                 WHERE ctx.id IN ({$sql})";
  70          }
  71          $contexts = $DB->get_recordset_sql($wrapper, $params);
  72  
  73          $contextids = [];
  74          foreach ($contexts as $context) {
  75              $contextids[] = $context->ctxid;
  76              \context_helper::preload_from_record($context);
  77          }
  78          $contexts->close();
  79  
  80          $this->set_contextids(array_merge($this->get_contextids(), $contextids));
  81  
  82          return $this;
  83      }
  84  
  85      /**
  86       * Adds the system context.
  87       *
  88       * @return $this
  89       */
  90      public function add_system_context() : contextlist {
  91          return $this->add_from_sql(SYSCONTEXTID, []);
  92      }
  93  
  94      /**
  95       * Adds the user context for a given user.
  96       *
  97       * @param int $userid
  98       * @return $this
  99       */
 100      public function add_user_context(int $userid) : contextlist {
 101          $sql = "SELECT DISTINCT ctx.id
 102                    FROM {context} ctx
 103                   WHERE ctx.contextlevel = :contextlevel
 104                     AND ctx.instanceid = :instanceid";
 105          return $this->add_from_sql($sql, ['contextlevel' => CONTEXT_USER, 'instanceid' => $userid]);
 106      }
 107  
 108      /**
 109       * Adds the user contexts for given users.
 110       *
 111       * @param array $userids
 112       * @return $this
 113       */
 114      public function add_user_contexts(array $userids) : contextlist {
 115          global $DB;
 116  
 117          list($useridsql, $useridparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
 118          $sql = "SELECT DISTINCT ctx.id
 119                    FROM {context} ctx
 120                   WHERE ctx.contextlevel = :contextlevel
 121                     AND ctx.instanceid $useridsql";
 122          return $this->add_from_sql($sql, ['contextlevel' => CONTEXT_USER] + $useridparams);
 123      }
 124  
 125      /**
 126       * Sets the component for this contextlist.
 127       *
 128       * @param string $component the frankenstyle component name.
 129       */
 130      public function set_component($component) {
 131          parent::set_component($component);
 132      }
 133  
 134      /**
 135       * Guess the name of the contextid field from the supplied SQL.
 136       *
 137       * @param   string  $sql The SQL to guess from
 138       * @return  string  The field name or a numeric value representing the context id
 139       */
 140      protected function guess_id_field_from_sql(string $sql) : string {
 141          // We are not interested in any subquery/view/conditions for the purpose of this method, so
 142          // let's reduce the query to the interesting parts by recursively cleaning all
 143          // contents within parenthesis. If there are problems (null), we keep the text unmodified.
 144          // So just top-level sql will remain after the reduction.
 145          $recursiveregexp = '/\((([^()]*|(?R))*)\)/';
 146          $sql = (preg_replace($recursiveregexp, '', $sql) ?: $sql);
 147          // Get the list of relevant words from the SQL Query.
 148          // We explode the SQL by the space character, then trim any extra whitespace (e.g. newlines), before we filter
 149          // empty value, and finally we re-index the array.
 150          $sql = rtrim($sql, ';');
 151          $words = array_map('trim', preg_split('/\s+/', $sql));
 152          $words = array_filter($words, function($word) {
 153              return $word !== '';
 154          });
 155          $words = array_values($words);
 156          $uwords = array_map('strtoupper', $words); // Uppercase all them.
 157  
 158          // If the query has boolean operators (UNION, it is the only one we support cross-db)
 159          // then we cannot guarantee whats coming after the first query, it can be anything.
 160          if (array_search('UNION', $uwords)) {
 161              return '';
 162          }
 163  
 164          if ($firstfrom = array_search('FROM', $uwords)) {
 165              // Found a FROM keyword.
 166              // Select the previous word.
 167              $fieldname = $words[$firstfrom - 1];
 168              if (is_numeric($fieldname)) {
 169                  return $fieldname;
 170              }
 171  
 172              if ($hasdot = strpos($fieldname, '.')) {
 173                  // This field is against a table alias. Take off the alias.
 174                  $fieldname = substr($fieldname, $hasdot + 1);
 175              }
 176  
 177              return $fieldname;
 178  
 179          } else if ((count($words) == 1) && (is_numeric($words[0]))) {
 180              // Not a real SQL, just a single numerical value - such as one returned by {@link self::add_system_context()}.
 181              return $words[0];
 182  
 183          } else if ((count($words) == 2) && (strtoupper($words[0]) === 'SELECT') && (is_numeric($words[1]))) {
 184              // SQL returning a constant numerical value.
 185              return $words[1];
 186          }
 187  
 188          return '';
 189      }
 190  }