Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 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   * Contains the content_item_service class.
  19   *
  20   * @package    core
  21   * @subpackage course
  22   * @copyright  2020 Jake Dallimore <jrhdallimore@gmail.com>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  namespace core_course\local\service;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  use core_course\local\exporters\course_content_items_exporter;
  30  use core_course\local\repository\content_item_readonly_repository_interface;
  31  
  32  /**
  33   * The content_item_service class, providing the api for interacting with content items.
  34   *
  35   * @copyright  2020 Jake Dallimore <jrhdallimore@gmail.com>
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class content_item_service {
  39  
  40      /** @var content_item_readonly_repository_interface $repository a repository for content items. */
  41      private $repository;
  42  
  43      /** string the component for this favourite. */
  44      public const COMPONENT = 'core_course';
  45      /** string the favourite prefix itemtype in the favourites table. */
  46      public const FAVOURITE_PREFIX = 'contentitem_';
  47      /** string the recommendation prefix itemtype in the favourites table. */
  48      public const RECOMMENDATION_PREFIX = 'recommend_';
  49      /** string the cache name for recommendations. */
  50      public const RECOMMENDATION_CACHE = 'recommendation_favourite_course_content_items';
  51  
  52      /**
  53       * The content_item_service constructor.
  54       *
  55       * @param content_item_readonly_repository_interface $repository a content item repository.
  56       */
  57      public function __construct(content_item_readonly_repository_interface $repository) {
  58          $this->repository = $repository;
  59      }
  60  
  61      /**
  62       * Returns an array of objects representing favourited content items.
  63       *
  64       * Each object contains the following properties:
  65       * itemtype: a string containing the 'itemtype' key used by the favourites subsystem.
  66       * ids[]: an array of ids, representing the content items within a component.
  67       *
  68       * Since two components can return (via their hook implementation) the same id, the itemtype is used for uniqueness.
  69       *
  70       * @param \stdClass $user
  71       * @return array
  72       */
  73      private function get_favourite_content_items_for_user(\stdClass $user): array {
  74          $favcache = \cache::make('core', 'user_favourite_course_content_items');
  75          $key = $user->id;
  76          $favmods = $favcache->get($key);
  77          if ($favmods !== false) {
  78              return $favmods;
  79          }
  80  
  81          $favourites = $this->get_content_favourites(self::FAVOURITE_PREFIX, \context_user::instance($user->id));
  82  
  83          $favcache->set($key, $favourites);
  84          return $favourites;
  85      }
  86  
  87      /**
  88       * Returns an array of objects representing recommended content items.
  89       *
  90       * Each object contains the following properties:
  91       * itemtype: a string containing the 'itemtype' key used by the favourites subsystem.
  92       * ids[]: an array of ids, representing the content items within a component.
  93       *
  94       * Since two components can return (via their hook implementation) the same id, the itemtype is used for uniqueness.
  95       *
  96       * @return array
  97       */
  98      private function get_recommendations(): array {
  99          global $CFG;
 100  
 101          $recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE);
 102          $key = $CFG->siteguest;
 103          $favmods = $recommendationcache->get($key);
 104          if ($favmods !== false) {
 105              return $favmods;
 106          }
 107  
 108          // Make sure the guest user exists in the database.
 109          if (!\core_user::get_user($CFG->siteguest)) {
 110              throw new \coding_exception('The guest user does not exist in the database.');
 111          }
 112  
 113          // Make sure the guest user context exists.
 114          if (!$guestusercontext = \context_user::instance($CFG->siteguest, false)) {
 115              throw new \coding_exception('The guest user context does not exist.');
 116          }
 117  
 118          $favourites = $this->get_content_favourites(self::RECOMMENDATION_PREFIX, $guestusercontext);
 119  
 120          $recommendationcache->set($CFG->siteguest, $favourites);
 121          return $favourites;
 122      }
 123  
 124      /**
 125       * Gets content favourites from the favourites system depending on the area.
 126       *
 127       * @param  string        $prefix      Prefix for the item type.
 128       * @param  \context_user $usercontext User context for the favourite
 129       * @return array An array of favourite objects.
 130       */
 131      private function get_content_favourites(string $prefix, \context_user $usercontext): array {
 132          // Get all modules and any submodules which implement get_course_content_items() hook.
 133          // This gives us the set of all itemtypes which we'll use to register favourite content items.
 134          // The ids that each plugin returns will be used together with the itemtype to uniquely identify
 135          // each content item for favouriting.
 136          $pluginmanager = \core_plugin_manager::instance();
 137          $plugins = $pluginmanager->get_plugins_of_type('mod');
 138          $itemtypes = [];
 139          foreach ($plugins as $plugin) {
 140              // Add the mod itself.
 141              $itemtypes[] = $prefix . 'mod_' . $plugin->name;
 142  
 143              // Add any subplugins to the list of item types.
 144              $subplugins = $pluginmanager->get_subplugins_of_plugin('mod_' . $plugin->name);
 145              foreach ($subplugins as $subpluginname => $subplugininfo) {
 146                  try {
 147                      if (component_callback_exists($subpluginname, 'get_course_content_items')) {
 148                          $itemtypes[] = $prefix . $subpluginname;
 149                      }
 150                  } catch (\moodle_exception $e) {
 151                      debugging('Cannot get_course_content_items: ' . $e->getMessage(), DEBUG_DEVELOPER);
 152                  }
 153              }
 154          }
 155  
 156          $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
 157          $favourites = [];
 158          $favs = $ufservice->find_all_favourites(self::COMPONENT, $itemtypes);
 159          $favsreduced = array_reduce($favs, function($carry, $item) {
 160              $carry[$item->itemtype][$item->itemid] = 0;
 161              return $carry;
 162          }, []);
 163  
 164          foreach ($itemtypes as $type) {
 165              $favourites[] = (object) [
 166                  'itemtype' => $type,
 167                  'ids' => isset($favsreduced[$type]) ? array_keys($favsreduced[$type]) : []
 168              ];
 169          }
 170          return $favourites;
 171      }
 172  
 173      /**
 174       * Get all content items which may be added to courses, irrespective of course caps, for site admin views, etc.
 175       *
 176       * @param \stdClass $user the user object.
 177       * @return array the array of exported content items.
 178       */
 179      public function get_all_content_items(\stdClass $user): array {
 180          $allcontentitems = $this->repository->find_all();
 181  
 182          return $this->export_content_items($user, $allcontentitems);
 183      }
 184  
 185      /**
 186       * Get content items which name matches a certain pattern and may be added to courses,
 187       * irrespective of course caps, for site admin views, etc.
 188       *
 189       * @param \stdClass $user The user object.
 190       * @param string $pattern The search pattern.
 191       * @return array The array of exported content items.
 192       */
 193      public function get_content_items_by_name_pattern(\stdClass $user, string $pattern): array {
 194          $allcontentitems = $this->repository->find_all();
 195  
 196          $filteredcontentitems = array_filter($allcontentitems, function($contentitem) use ($pattern) {
 197              return preg_match("/$pattern/i", $contentitem->get_title()->get_value());
 198          });
 199  
 200          return $this->export_content_items($user, $filteredcontentitems);
 201      }
 202  
 203      /**
 204       * Export content items.
 205       *
 206       * @param \stdClass $user The user object.
 207       * @param array $contentitems The content items array.
 208       * @return array The array of exported content items.
 209       */
 210      private function export_content_items(\stdClass $user, $contentitems) {
 211          global $PAGE;
 212  
 213          // Export the objects to get the formatted objects for transfer/display.
 214          $favourites = $this->get_favourite_content_items_for_user($user);
 215          $recommendations = $this->get_recommendations();
 216          $ciexporter = new course_content_items_exporter(
 217              $contentitems,
 218              [
 219                  'context' => \context_system::instance(),
 220                  'favouriteitems' => $favourites,
 221                  'recommended' => $recommendations
 222              ]
 223          );
 224          $exported = $ciexporter->export($PAGE->get_renderer('core'));
 225  
 226          // Sort by title for return.
 227          usort($exported->content_items, function($a, $b) {
 228              return $a->title > $b->title;
 229          });
 230  
 231          return $exported->content_items;
 232      }
 233  
 234      /**
 235       * Return a representation of the available content items, for a user in a course.
 236       *
 237       * @param \stdClass $user the user to check access for.
 238       * @param \stdClass $course the course to scope the content items to.
 239       * @param array $linkparams the desired section to return to.
 240       * @return \stdClass[] the content items, scoped to a course.
 241       */
 242      public function get_content_items_for_user_in_course(\stdClass $user, \stdClass $course, array $linkparams = []): array {
 243          global $PAGE;
 244  
 245          if (!has_capability('moodle/course:manageactivities', \context_course::instance($course->id), $user)) {
 246              return [];
 247          }
 248  
 249          // Get all the visible content items.
 250          $allcontentitems = $this->repository->find_all_for_course($course, $user);
 251  
 252          // Content items can only originate from modules or submodules.
 253          $pluginmanager = \core_plugin_manager::instance();
 254          $components = \core_component::get_component_list();
 255          $parents = [];
 256          foreach ($allcontentitems as $contentitem) {
 257              if (!in_array($contentitem->get_component_name(), array_keys($components['mod']))) {
 258                  // It could be a subplugin.
 259                  $info = $pluginmanager->get_plugin_info($contentitem->get_component_name());
 260                  if (!is_null($info)) {
 261                      $parent = $info->get_parent_plugin();
 262                      if ($parent != false) {
 263                          if (in_array($parent, array_keys($components['mod']))) {
 264                              $parents[$contentitem->get_component_name()] = $parent;
 265                              continue;
 266                          }
 267                      }
 268                  }
 269                  throw new \moodle_exception('Only modules and submodules can generate content items. \''
 270                      . $contentitem->get_component_name() . '\' is neither.');
 271              }
 272              $parents[$contentitem->get_component_name()] = $contentitem->get_component_name();
 273          }
 274  
 275          // Now, check access to these items for the user.
 276          $availablecontentitems = array_filter($allcontentitems, function($contentitem) use ($course, $user, $parents) {
 277              // Check the parent module access for the user.
 278              return course_allowed_module($course, explode('_', $parents[$contentitem->get_component_name()])[1], $user);
 279          });
 280  
 281          // Add the link params to the link, if any have been provided.
 282          if (!empty($linkparams)) {
 283              $availablecontentitems = array_map(function ($item) use ($linkparams) {
 284                  $item->get_link()->params($linkparams);
 285                  return $item;
 286              }, $availablecontentitems);
 287          }
 288  
 289          // Export the objects to get the formatted objects for transfer/display.
 290          $favourites = $this->get_favourite_content_items_for_user($user);
 291          $recommended = $this->get_recommendations();
 292          $ciexporter = new course_content_items_exporter(
 293              $availablecontentitems,
 294              [
 295                  'context' => \context_course::instance($course->id),
 296                  'favouriteitems' => $favourites,
 297                  'recommended' => $recommended
 298              ]
 299          );
 300          $exported = $ciexporter->export($PAGE->get_renderer('course'));
 301  
 302          // Sort by title for return.
 303          usort($exported->content_items, function($a, $b) {
 304              return $a->title > $b->title;
 305          });
 306  
 307          return $exported->content_items;
 308      }
 309  
 310      /**
 311       * Add a content item to a user's favourites.
 312       *
 313       * @param \stdClass $user the user whose favourite this is.
 314       * @param string $componentname the name of the component from which the content item originates.
 315       * @param int $contentitemid the id of the content item.
 316       * @return \stdClass the exported content item.
 317       */
 318      public function add_to_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass {
 319          $usercontext = \context_user::instance($user->id);
 320          $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
 321  
 322          // Because each plugin decides its own ids for content items, a combination of
 323          // itemtype and id is used to guarantee uniqueness across all content items.
 324          $itemtype = self::FAVOURITE_PREFIX . $componentname;
 325  
 326          $ufservice->create_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext);
 327  
 328          $favcache = \cache::make('core', 'user_favourite_course_content_items');
 329          $favcache->delete($user->id);
 330  
 331          $items = $this->get_all_content_items($user);
 332          return $items[array_search($contentitemid, array_column($items, 'id'))];
 333      }
 334  
 335      /**
 336       * Remove the content item from a user's favourites.
 337       *
 338       * @param \stdClass $user the user whose favourite this is.
 339       * @param string $componentname the name of the component from which the content item originates.
 340       * @param int $contentitemid the id of the content item.
 341       * @return \stdClass the exported content item.
 342       */
 343      public function remove_from_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass {
 344          $usercontext = \context_user::instance($user->id);
 345          $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
 346  
 347          // Because each plugin decides its own ids for content items, a combination of
 348          // itemtype and id is used to guarantee uniqueness across all content items.
 349          $itemtype = self::FAVOURITE_PREFIX . $componentname;
 350  
 351          $ufservice->delete_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext);
 352  
 353          $favcache = \cache::make('core', 'user_favourite_course_content_items');
 354          $favcache->delete($user->id);
 355  
 356          $items = $this->get_all_content_items($user);
 357          return $items[array_search($contentitemid, array_column($items, 'id'))];
 358      }
 359  
 360      /**
 361       * Toggle an activity to being recommended or not.
 362       *
 363       * @param  string $itemtype The component such as mod_assign, or assignsubmission_file
 364       * @param  int    $itemid   The id related to this component item.
 365       * @return bool True on creating a favourite, false on deleting it.
 366       */
 367      public function toggle_recommendation(string $itemtype, int $itemid): bool {
 368          global $CFG;
 369  
 370          $context = \context_system::instance();
 371  
 372          $itemtype = self::RECOMMENDATION_PREFIX . $itemtype;
 373  
 374          // Favourites are created using a user context. We'll use the site guest user ID as that should not change and there
 375          // can be only one.
 376          $usercontext = \context_user::instance($CFG->siteguest);
 377  
 378          $recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE);
 379  
 380          $favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext);
 381          if ($favouritefactory->favourite_exists(self::COMPONENT, $itemtype, $itemid, $context)) {
 382              $favouritefactory->delete_favourite(self::COMPONENT, $itemtype, $itemid, $context);
 383              $result = $recommendationcache->delete($CFG->siteguest);
 384              return false;
 385          } else {
 386              $favouritefactory->create_favourite(self::COMPONENT, $itemtype, $itemid, $context);
 387              $result = $recommendationcache->delete($CFG->siteguest);
 388              return true;
 389          }
 390      }
 391  }