Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

   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          \core_collator::asort_objects_by_property($exported->content_items, 'title');
 228          return array_values($exported->content_items);
 229      }
 230  
 231      /**
 232       * Return a representation of the available content items, for a user in a course.
 233       *
 234       * @param \stdClass $user the user to check access for.
 235       * @param \stdClass $course the course to scope the content items to.
 236       * @param array $linkparams the desired section to return to.
 237       * @return \stdClass[] the content items, scoped to a course.
 238       */
 239      public function get_content_items_for_user_in_course(\stdClass $user, \stdClass $course, array $linkparams = []): array {
 240          global $PAGE;
 241  
 242          if (!has_capability('moodle/course:manageactivities', \context_course::instance($course->id), $user)) {
 243              return [];
 244          }
 245  
 246          // Get all the visible content items.
 247          $allcontentitems = $this->repository->find_all_for_course($course, $user);
 248  
 249          // Content items can only originate from modules or submodules.
 250          $pluginmanager = \core_plugin_manager::instance();
 251          $components = \core_component::get_component_list();
 252          $parents = [];
 253          foreach ($allcontentitems as $contentitem) {
 254              if (!in_array($contentitem->get_component_name(), array_keys($components['mod']))) {
 255                  // It could be a subplugin.
 256                  $info = $pluginmanager->get_plugin_info($contentitem->get_component_name());
 257                  if (!is_null($info)) {
 258                      $parent = $info->get_parent_plugin();
 259                      if ($parent != false) {
 260                          if (in_array($parent, array_keys($components['mod']))) {
 261                              $parents[$contentitem->get_component_name()] = $parent;
 262                              continue;
 263                          }
 264                      }
 265                  }
 266                  throw new \moodle_exception('Only modules and submodules can generate content items. \''
 267                      . $contentitem->get_component_name() . '\' is neither.');
 268              }
 269              $parents[$contentitem->get_component_name()] = $contentitem->get_component_name();
 270          }
 271  
 272          // Now, check access to these items for the user.
 273          $availablecontentitems = array_filter($allcontentitems, function($contentitem) use ($course, $user, $parents) {
 274              // Check the parent module access for the user.
 275              return course_allowed_module($course, explode('_', $parents[$contentitem->get_component_name()])[1], $user);
 276          });
 277  
 278          // Add the link params to the link, if any have been provided.
 279          if (!empty($linkparams)) {
 280              $availablecontentitems = array_map(function ($item) use ($linkparams) {
 281                  $item->get_link()->params($linkparams);
 282                  return $item;
 283              }, $availablecontentitems);
 284          }
 285  
 286          // Export the objects to get the formatted objects for transfer/display.
 287          $favourites = $this->get_favourite_content_items_for_user($user);
 288          $recommended = $this->get_recommendations();
 289          $ciexporter = new course_content_items_exporter(
 290              $availablecontentitems,
 291              [
 292                  'context' => \context_course::instance($course->id),
 293                  'favouriteitems' => $favourites,
 294                  'recommended' => $recommended
 295              ]
 296          );
 297          $exported = $ciexporter->export($PAGE->get_renderer('course'));
 298  
 299          // Sort by title for return.
 300          \core_collator::asort_objects_by_property($exported->content_items, 'title');
 301  
 302          return array_values($exported->content_items);
 303      }
 304  
 305      /**
 306       * Add a content item to a user's favourites.
 307       *
 308       * @param \stdClass $user the user whose favourite this is.
 309       * @param string $componentname the name of the component from which the content item originates.
 310       * @param int $contentitemid the id of the content item.
 311       * @return \stdClass the exported content item.
 312       */
 313      public function add_to_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass {
 314          $usercontext = \context_user::instance($user->id);
 315          $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
 316  
 317          // Because each plugin decides its own ids for content items, a combination of
 318          // itemtype and id is used to guarantee uniqueness across all content items.
 319          $itemtype = self::FAVOURITE_PREFIX . $componentname;
 320  
 321          $ufservice->create_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext);
 322  
 323          $favcache = \cache::make('core', 'user_favourite_course_content_items');
 324          $favcache->delete($user->id);
 325  
 326          $items = $this->get_all_content_items($user);
 327          return $items[array_search($contentitemid, array_column($items, 'id'))];
 328      }
 329  
 330      /**
 331       * Remove the content item from a user's favourites.
 332       *
 333       * @param \stdClass $user the user whose favourite this is.
 334       * @param string $componentname the name of the component from which the content item originates.
 335       * @param int $contentitemid the id of the content item.
 336       * @return \stdClass the exported content item.
 337       */
 338      public function remove_from_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass {
 339          $usercontext = \context_user::instance($user->id);
 340          $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
 341  
 342          // Because each plugin decides its own ids for content items, a combination of
 343          // itemtype and id is used to guarantee uniqueness across all content items.
 344          $itemtype = self::FAVOURITE_PREFIX . $componentname;
 345  
 346          $ufservice->delete_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext);
 347  
 348          $favcache = \cache::make('core', 'user_favourite_course_content_items');
 349          $favcache->delete($user->id);
 350  
 351          $items = $this->get_all_content_items($user);
 352          return $items[array_search($contentitemid, array_column($items, 'id'))];
 353      }
 354  
 355      /**
 356       * Toggle an activity to being recommended or not.
 357       *
 358       * @param  string $itemtype The component such as mod_assign, or assignsubmission_file
 359       * @param  int    $itemid   The id related to this component item.
 360       * @return bool True on creating a favourite, false on deleting it.
 361       */
 362      public function toggle_recommendation(string $itemtype, int $itemid): bool {
 363          global $CFG;
 364  
 365          $context = \context_system::instance();
 366  
 367          $itemtype = self::RECOMMENDATION_PREFIX . $itemtype;
 368  
 369          // Favourites are created using a user context. We'll use the site guest user ID as that should not change and there
 370          // can be only one.
 371          $usercontext = \context_user::instance($CFG->siteguest);
 372  
 373          $recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE);
 374  
 375          $favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext);
 376          if ($favouritefactory->favourite_exists(self::COMPONENT, $itemtype, $itemid, $context)) {
 377              $favouritefactory->delete_favourite(self::COMPONENT, $itemtype, $itemid, $context);
 378              $result = $recommendationcache->delete($CFG->siteguest);
 379              return false;
 380          } else {
 381              $favouritefactory->create_favourite(self::COMPONENT, $itemtype, $itemid, $context);
 382              $result = $recommendationcache->delete($CFG->siteguest);
 383              return true;
 384          }
 385      }
 386  }