| 1 | efrain | 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 | }
 |