MDL-70897 various: uasort callback can not return bool
[moodle.git] / course / classes / local / service / content_item_service.php
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/>.
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;
27 defined('MOODLE_INTERNAL') || die();
29 use core_course\local\exporters\course_content_items_exporter;
30 use core_course\local\repository\content_item_readonly_repository_interface;
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 {
40     /** @var content_item_readonly_repository_interface $repository a repository for content items. */
41     private $repository;
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';
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     }
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         }
81         $favourites = $this->get_content_favourites(self::FAVOURITE_PREFIX, \context_user::instance($user->id));
83         $favcache->set($key, $favourites);
84         return $favourites;
85     }
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;
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         }
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         }
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         }
118         $favourites = $this->get_content_favourites(self::RECOMMENDATION_PREFIX, $guestusercontext);
120         $recommendationcache->set($CFG->siteguest, $favourites);
121         return $favourites;
122     }
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;
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         }
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         }, []);
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     }
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();
182         return $this->export_content_items($user, $allcontentitems);
183     }
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();
196         $filteredcontentitems = array_filter($allcontentitems, function($contentitem) use ($pattern) {
197             return preg_match("/$pattern/i", $contentitem->get_title()->get_value());
198         });
200         return $this->export_content_items($user, $filteredcontentitems);
201     }
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;
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'));
226         // Sort by title for return.
227         usort($exported->content_items, function($a, $b) {
228             return strcmp($a->title, $b->title);
229         });
231         return $exported->content_items;
232     }
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;
245         if (!has_capability('moodle/course:manageactivities', \context_course::instance($course->id), $user)) {
246             return [];
247         }
249         // Get all the visible content items.
250         $allcontentitems = $this->repository->find_all_for_course($course, $user);
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         }
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         });
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         }
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'));
302         // Sort by title for return.
303         usort($exported->content_items, function($a, $b) {
304             return strcmp($a->title, $b->title);
305         });
307         return $exported->content_items;
308     }
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);
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;
326         $ufservice->create_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext);
328         $favcache = \cache::make('core', 'user_favourite_course_content_items');
329         $favcache->delete($user->id);
331         $items = $this->get_all_content_items($user);
332         return $items[array_search($contentitemid, array_column($items, 'id'))];
333     }
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);
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;
351         $ufservice->delete_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext);
353         $favcache = \cache::make('core', 'user_favourite_course_content_items');
354         $favcache->delete($user->id);
356         $items = $this->get_all_content_items($user);
357         return $items[array_search($contentitemid, array_column($items, 'id'))];
358     }
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;
370         $context = \context_system::instance();
372         $itemtype = self::RECOMMENDATION_PREFIX . $itemtype;
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);
378         $recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE);
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     }