See Release Notes
Long Term Support Release
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body