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.

Differences Between: [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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  declare(strict_types=1);
  18  
  19  namespace core_reportbuilder\local\helpers;
  20  
  21  use cache;
  22  use context;
  23  use context_system;
  24  use core_collator;
  25  use core_component;
  26  use core_reportbuilder\local\audiences\base;
  27  use core_reportbuilder\local\models\{audience as audience_model, schedule};
  28  
  29  /**
  30   * Class containing report audience helper methods
  31   *
  32   * @package     core_reportbuilder
  33   * @copyright   2021 David Matamoros <davidmc@moodle.com>
  34   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class audience {
  37  
  38      /**
  39       * Return audience instances for a given report. Note that any records pointing to invalid audience types will be excluded
  40       *
  41       * @param int $reportid
  42       * @return base[]
  43       */
  44      public static function get_base_records(int $reportid): array {
  45          $records = audience_model::get_records(['reportid' => $reportid], 'id');
  46  
  47          $instances = array_map(static function(audience_model $audience): ?base {
  48              return base::instance(0, $audience->to_record());
  49          }, $records);
  50  
  51          // Filter and remove null elements (invalid audience types).
  52          return array_filter($instances);
  53      }
  54  
  55      /**
  56       * Returns list of report IDs that the specified user can access, based on audience configuration. This can be expensive if the
  57       * site has lots of reports, with lots of audiences, so we cache the result for the duration of the users session
  58       *
  59       * @param int|null $userid User ID to check, or the current user if omitted
  60       * @return int[]
  61       */
  62      public static function get_allowed_reports(?int $userid = null): array {
  63          global $USER, $DB;
  64  
  65          $userid = $userid ?: (int) $USER->id;
  66  
  67          // Prepare cache, if we previously stored the users allowed reports then return that.
  68          $cache = cache::make('core', 'reportbuilder_allowed_reports');
  69          $cachedreports = $cache->get($userid);
  70          if ($cachedreports !== false) {
  71              return $cachedreports;
  72          }
  73  
  74          $allowedreports = [];
  75          $reportaudiences = [];
  76  
  77          // Retrieve all audiences and group them by report for convenience.
  78          $audiences = audience_model::get_records();
  79          foreach ($audiences as $audience) {
  80              $reportaudiences[$audience->get('reportid')][] = $audience;
  81          }
  82  
  83          foreach ($reportaudiences as $reportid => $audiences) {
  84  
  85              // Generate audience SQL based on those for the current report.
  86              [$wheres, $params] = self::user_audience_sql($audiences);
  87              if (count($wheres) === 0) {
  88                  continue;
  89              }
  90  
  91              $paramuserid = database::generate_param_name();
  92              $params[$paramuserid] = $userid;
  93  
  94              $sql = "SELECT DISTINCT(u.id)
  95                        FROM {user} u
  96                       WHERE (" . implode(' OR ', $wheres) . ")
  97                         AND u.deleted = 0
  98                         AND u.id = :{$paramuserid}";
  99  
 100              // If we have a matching record, user can view the report.
 101              if ($DB->record_exists_sql($sql, $params)) {
 102                  $allowedreports[] = $reportid;
 103              }
 104          }
 105  
 106          // Store users allowed reports in cache.
 107          $cache->set($userid, $allowedreports);
 108  
 109          return $allowedreports;
 110      }
 111  
 112      /**
 113       * Purge the audience cache of allowed reports
 114       */
 115      public static function purge_caches(): void {
 116          cache::make('core', 'reportbuilder_allowed_reports')->purge();
 117      }
 118  
 119      /**
 120       * Generate SQL select clause and params for selecting reports specified user can access, based on audience configuration
 121       *
 122       * @param string $reporttablealias
 123       * @param int|null $userid User ID to check, or the current user if omitted
 124       * @return array
 125       */
 126      public static function user_reports_list_sql(string $reporttablealias, ?int $userid = null): array {
 127          global $DB;
 128  
 129          $allowedreports = self::get_allowed_reports($userid);
 130  
 131          if (empty($allowedreports)) {
 132              return ['1=0', []];
 133          }
 134  
 135          // Get all sql audiences.
 136          $prefix = database::generate_param_name() . '_';
 137          [$select, $params] = $DB->get_in_or_equal($allowedreports, SQL_PARAMS_NAMED, $prefix);
 138          $sql = "{$reporttablealias}.id {$select}";
 139  
 140          return [$sql, $params];
 141      }
 142  
 143      /**
 144       * Return list of report ID's specified user can access, based on audience configuration
 145       *
 146       * @param int|null $userid User ID to check, or the current user if omitted
 147       * @return int[]
 148       */
 149      public static function user_reports_list(?int $userid = null): array {
 150          global $DB;
 151  
 152          [$select, $params] = self::user_reports_list_sql('rb', $userid);
 153          $sql = "SELECT rb.id
 154                    FROM {reportbuilder_report} rb
 155                   WHERE {$select}";
 156  
 157          return $DB->get_fieldset_sql($sql, $params);
 158      }
 159  
 160      /**
 161       * Returns SQL to limit the list of reports to those that the given user has access to
 162       *
 163       * - A user with 'editall' capability will have access to all reports
 164       * - A user with 'edit' capability will have access to:
 165       *      - Those reports this user has created
 166       *      - Those reports this user is in audience of
 167       * - A user with 'view' capability will have access to:
 168       *      - Those reports this user is in audience of
 169       *
 170       * @param string $reporttablealias
 171       * @param int|null $userid User ID to check, or the current user if omitted
 172       * @param context|null $context
 173       * @return array
 174       */
 175      public static function user_reports_list_access_sql(
 176          string $reporttablealias,
 177          ?int $userid = null,
 178          ?context $context = null
 179      ): array {
 180          global $DB, $USER;
 181  
 182          if ($context === null) {
 183              $context = context_system::instance();
 184          }
 185  
 186          // If user can't view all reports, limit the returned list to those reports they can see.
 187          if (!has_capability('moodle/reportbuilder:editall', $context, $userid)) {
 188              $reports = self::user_reports_list($userid);
 189  
 190              [$paramprefix, $paramuserid] = database::generate_param_names(2);
 191              [$reportselect, $params] = $DB->get_in_or_equal($reports, SQL_PARAMS_NAMED, "{$paramprefix}_", true, null);
 192  
 193              $where = "{$reporttablealias}.id {$reportselect}";
 194  
 195              // User can also see any reports that they can edit.
 196              if (has_capability('moodle/reportbuilder:edit', $context, $userid)) {
 197                  $where = "({$reporttablealias}.usercreated = :{$paramuserid} OR {$where})";
 198                  $params[$paramuserid] = $userid ?? $USER->id;
 199              }
 200  
 201              return [$where, $params];
 202          }
 203  
 204          return ['1=1', []];
 205      }
 206  
 207      /**
 208       * Return appropriate list of where clauses and params for given audiences
 209       *
 210       * @param audience_model[] $audiences
 211       * @param string $usertablealias
 212       * @return array[] [$wheres, $params]
 213       */
 214      public static function user_audience_sql(array $audiences, string $usertablealias = 'u'): array {
 215          $wheres = $params = [];
 216  
 217          foreach ($audiences as $audience) {
 218              if ($instance = base::instance(0, $audience->to_record())) {
 219                  $instancetablealias = database::generate_alias();
 220                  [$instancejoin, $instancewhere, $instanceparams] = $instance->get_sql($instancetablealias);
 221  
 222                  $wheres[] = "{$usertablealias}.id IN (
 223                      SELECT {$instancetablealias}.id
 224                        FROM {user} {$instancetablealias}
 225                             {$instancejoin}
 226                       WHERE {$instancewhere}
 227                       )";
 228                  $params += $instanceparams;
 229              }
 230          }
 231  
 232          return [$wheres, $params];
 233      }
 234  
 235      /**
 236       * Return a list of audiences that are used by any schedule of the given report
 237       *
 238       * @param int $reportid
 239       * @return int[] Array of audience IDs
 240       */
 241      public static function get_audiences_for_report_schedules(int $reportid): array {
 242          global $DB;
 243  
 244          $audiences = $DB->get_fieldset_select(schedule::TABLE, 'audiences', 'reportid = ?', [$reportid]);
 245  
 246          // Reduce JSON encoded audience data of each schedule to an array of audience IDs.
 247          $audienceids = array_reduce($audiences, static function(array $carry, string $audience): array {
 248              return array_merge($carry, (array) json_decode($audience));
 249          }, []);
 250  
 251          return array_unique($audienceids, SORT_NUMERIC);
 252      }
 253  
 254      /**
 255       * Returns the list of audiences types in the system.
 256       *
 257       * @return array
 258       */
 259      private static function get_audience_types(): array {
 260          $sources = [];
 261  
 262          $audiences = core_component::get_component_classes_in_namespace(null, 'reportbuilder\\audience');
 263          foreach ($audiences as $class => $path) {
 264              $audienceclass = $class::instance();
 265              if (is_subclass_of($class, base::class) && $audienceclass->user_can_add()) {
 266                  $componentname = $audienceclass->get_component_displayname();
 267                  $sources[$componentname][$class] = $audienceclass->get_name();
 268              }
 269          }
 270  
 271          return $sources;
 272      }
 273  
 274      /**
 275       * Get all the audiences types the current user can add to, organised by categories.
 276       *
 277       * @return array
 278       *
 279       * @deprecated since Moodle 4.1 - please do not use this function any more, {@see custom_report_audience_cards_exporter}
 280       */
 281      public static function get_all_audiences_menu_types(): array {
 282          debugging('The function ' . __FUNCTION__ . '() is deprecated, please do not use it any more. ' .
 283              'See \'custom_report_audience_cards_exporter\' class for replacement', DEBUG_DEVELOPER);
 284  
 285          $menucardsarray = [];
 286          $notavailablestr = get_string('notavailable', 'moodle');
 287  
 288          $audiencetypes = self::get_audience_types();
 289          $audiencetypeindex = 0;
 290          foreach ($audiencetypes as $categoryname => $audience) {
 291              $menucards = [
 292                  'name' => $categoryname,
 293                  'key' => 'index' . ++$audiencetypeindex,
 294              ];
 295  
 296              foreach ($audience as $classname => $name) {
 297                  $class = $classname::instance();
 298                  $title = $class->is_available() ? get_string('addaudience', 'core_reportbuilder', $class->get_name()) :
 299                      $notavailablestr;
 300                  $menucard['title'] = $title;
 301                  $menucard['name'] = $class->get_name();
 302                  $menucard['disabled'] = !$class->is_available();
 303                  $menucard['identifier'] = get_class($class);
 304                  $menucard['action'] = 'add-audience';
 305                  $menucards['items'][] = $menucard;
 306              }
 307  
 308              // Order audience types on each category alphabetically.
 309              core_collator::asort_array_of_arrays_by_key($menucards['items'], 'name');
 310              $menucards['items'] = array_values($menucards['items']);
 311  
 312              $menucardsarray[] = $menucards;
 313          }
 314  
 315          return $menucardsarray;
 316      }
 317  }