MDL-69704 course: catch exception when calling get_course_content_items
[moodle.git] / course / classes / local / service / content_item_service.php
CommitLineData
dd494a41
JD
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 */
25namespace core_course\local\service;
26
27defined('MOODLE_INTERNAL') || die();
28
29use core_course\local\exporters\course_content_items_exporter;
30use 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 */
38class content_item_service {
39
40 /** @var content_item_readonly_repository_interface $repository a repository for content items. */
41 private $repository;
42
cd09777d
AG
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
dd494a41
JD
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
f09e9b88
JD
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
cd09777d
AG
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
7add3440
MG
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
cd09777d
AG
113 $favourites = $this->get_content_favourites(self::RECOMMENDATION_PREFIX, \context_user::instance($CFG->siteguest));
114
115 $recommendationcache->set($CFG->siteguest, $favourites);
116 return $favourites;
117 }
118
119 /**
120 * Gets content favourites from the favourites system depending on the area.
121 *
122 * @param string $prefix Prefix for the item type.
123 * @param \context_user $usercontext User context for the favourite
124 * @return array An array of favourite objects.
125 */
126 private function get_content_favourites(string $prefix, \context_user $usercontext): array {
f09e9b88
JD
127 // Get all modules and any submodules which implement get_course_content_items() hook.
128 // This gives us the set of all itemtypes which we'll use to register favourite content items.
129 // The ids that each plugin returns will be used together with the itemtype to uniquely identify
130 // each content item for favouriting.
131 $pluginmanager = \core_plugin_manager::instance();
132 $plugins = $pluginmanager->get_plugins_of_type('mod');
133 $itemtypes = [];
134 foreach ($plugins as $plugin) {
135 // Add the mod itself.
cd09777d 136 $itemtypes[] = $prefix . 'mod_' . $plugin->name;
f09e9b88
JD
137
138 // Add any subplugins to the list of item types.
139 $subplugins = $pluginmanager->get_subplugins_of_plugin('mod_' . $plugin->name);
140 foreach ($subplugins as $subpluginname => $subplugininfo) {
d12c1406
SA
141 try {
142 if (component_callback_exists($subpluginname, 'get_course_content_items')) {
143 $itemtypes[] = $prefix . $subpluginname;
144 }
145 } catch (\moodle_exception $e) {
146 debugging('Cannot get_course_content_items: ' . $e->getMessage(), DEBUG_DEVELOPER);
f09e9b88
JD
147 }
148 }
149 }
150
cd09777d 151 $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
f09e9b88 152 $favourites = [];
b3b2d72c
P
153 $favs = $ufservice->find_all_favourites(self::COMPONENT, $itemtypes);
154 $favsreduced = array_reduce($favs, function($carry, $item) {
155 $carry[$item->itemtype][$item->itemid] = 0;
156 return $carry;
157 }, []);
158
159 foreach ($itemtypes as $type) {
160 $favourites[] = (object) [
161 'itemtype' => $type,
162 'ids' => isset($favsreduced[$type]) ? array_keys($favsreduced[$type]) : []
163 ];
f09e9b88 164 }
f09e9b88
JD
165 return $favourites;
166 }
167
57dfcf95
JD
168 /**
169 * Get all content items which may be added to courses, irrespective of course caps, for site admin views, etc.
170 *
f09e9b88 171 * @param \stdClass $user the user object.
57dfcf95
JD
172 * @return array the array of exported content items.
173 */
f09e9b88 174 public function get_all_content_items(\stdClass $user): array {
57dfcf95
JD
175 $allcontentitems = $this->repository->find_all();
176
d379dc9b
MG
177 return $this->export_content_items($user, $allcontentitems);
178 }
179
180 /**
181 * Get content items which name matches a certain pattern and may be added to courses,
182 * irrespective of course caps, for site admin views, etc.
183 *
184 * @param \stdClass $user The user object.
185 * @param string $pattern The search pattern.
186 * @return array The array of exported content items.
187 */
188 public function get_content_items_by_name_pattern(\stdClass $user, string $pattern): array {
189 $allcontentitems = $this->repository->find_all();
190
191 $filteredcontentitems = array_filter($allcontentitems, function($contentitem) use ($pattern) {
192 return preg_match("/$pattern/i", $contentitem->get_title()->get_value());
193 });
194
195 return $this->export_content_items($user, $filteredcontentitems);
196 }
197
198 /**
199 * Export content items.
200 *
201 * @param \stdClass $user The user object.
202 * @param array $contentitems The content items array.
203 * @return array The array of exported content items.
204 */
205 private function export_content_items(\stdClass $user, $contentitems) {
206 global $PAGE;
207
57dfcf95 208 // Export the objects to get the formatted objects for transfer/display.
f09e9b88 209 $favourites = $this->get_favourite_content_items_for_user($user);
cd09777d 210 $recommendations = $this->get_recommendations();
57dfcf95 211 $ciexporter = new course_content_items_exporter(
d379dc9b 212 $contentitems,
f09e9b88
JD
213 [
214 'context' => \context_system::instance(),
cd09777d
AG
215 'favouriteitems' => $favourites,
216 'recommended' => $recommendations
f09e9b88 217 ]
57dfcf95
JD
218 );
219 $exported = $ciexporter->export($PAGE->get_renderer('core'));
220
221 // Sort by title for return.
222 usort($exported->content_items, function($a, $b) {
223 return $a->title > $b->title;
224 });
225
226 return $exported->content_items;
227 }
228
dd494a41
JD
229 /**
230 * Return a representation of the available content items, for a user in a course.
231 *
232 * @param \stdClass $user the user to check access for.
233 * @param \stdClass $course the course to scope the content items to.
234 * @param array $linkparams the desired section to return to.
235 * @return \stdClass[] the content items, scoped to a course.
236 */
237 public function get_content_items_for_user_in_course(\stdClass $user, \stdClass $course, array $linkparams = []): array {
238 global $PAGE;
239
240 if (!has_capability('moodle/course:manageactivities', \context_course::instance($course->id), $user)) {
241 return [];
242 }
243
244 // Get all the visible content items.
245 $allcontentitems = $this->repository->find_all_for_course($course, $user);
246
39b9e293
JD
247 // Content items can only originate from modules or submodules.
248 $pluginmanager = \core_plugin_manager::instance();
249 $components = \core_component::get_component_list();
250 $parents = [];
251 foreach ($allcontentitems as $contentitem) {
252 if (!in_array($contentitem->get_component_name(), array_keys($components['mod']))) {
253 // It could be a subplugin.
254 $info = $pluginmanager->get_plugin_info($contentitem->get_component_name());
255 if (!is_null($info)) {
256 $parent = $info->get_parent_plugin();
257 if ($parent != false) {
258 if (in_array($parent, array_keys($components['mod']))) {
259 $parents[$contentitem->get_component_name()] = $parent;
260 continue;
261 }
262 }
263 }
264 throw new \moodle_exception('Only modules and submodules can generate content items. \''
265 . $contentitem->get_component_name() . '\' is neither.');
266 }
267 $parents[$contentitem->get_component_name()] = $contentitem->get_component_name();
268 }
269
dd494a41 270 // Now, check access to these items for the user.
39b9e293 271 $availablecontentitems = array_filter($allcontentitems, function($contentitem) use ($course, $user, $parents) {
dd494a41 272 // Check the parent module access for the user.
39b9e293 273 return course_allowed_module($course, explode('_', $parents[$contentitem->get_component_name()])[1], $user);
dd494a41
JD
274 });
275
276 // Add the link params to the link, if any have been provided.
277 if (!empty($linkparams)) {
278 $availablecontentitems = array_map(function ($item) use ($linkparams) {
279 $item->get_link()->params($linkparams);
280 return $item;
281 }, $availablecontentitems);
282 }
283
284 // Export the objects to get the formatted objects for transfer/display.
f09e9b88 285 $favourites = $this->get_favourite_content_items_for_user($user);
cd09777d 286 $recommended = $this->get_recommendations();
dd494a41
JD
287 $ciexporter = new course_content_items_exporter(
288 $availablecontentitems,
f09e9b88
JD
289 [
290 'context' => \context_course::instance($course->id),
cd09777d
AG
291 'favouriteitems' => $favourites,
292 'recommended' => $recommended
f09e9b88 293 ]
dd494a41
JD
294 );
295 $exported = $ciexporter->export($PAGE->get_renderer('course'));
296
297 // Sort by title for return.
298 usort($exported->content_items, function($a, $b) {
299 return $a->title > $b->title;
300 });
301
302 return $exported->content_items;
303 }
f09e9b88
JD
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.
cd09777d 319 $itemtype = self::FAVOURITE_PREFIX . $componentname;
f09e9b88 320
cd09777d 321 $ufservice->create_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext);
f09e9b88
JD
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.
cd09777d 344 $itemtype = self::FAVOURITE_PREFIX . $componentname;
f09e9b88 345
cd09777d 346 $ufservice->delete_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext);
f09e9b88
JD
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 }
cd09777d
AG
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 }
dd494a41 386}