<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains the content_item_service class.
*
* @package core
* @subpackage course
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\local\service;
defined('MOODLE_INTERNAL') || die();
use core_course\local\exporters\course_content_items_exporter;
use core_course\local\repository\content_item_readonly_repository_interface;
/**
* The content_item_service class, providing the api for interacting with content items.
*
* @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content_item_service {
/** @var content_item_readonly_repository_interface $repository a repository for content items. */
private $repository;
/** string the component for this favourite. */
public const COMPONENT = 'core_course';
/** string the favourite prefix itemtype in the favourites table. */
public const FAVOURITE_PREFIX = 'contentitem_';
/** string the recommendation prefix itemtype in the favourites table. */
public const RECOMMENDATION_PREFIX = 'recommend_';
/** string the cache name for recommendations. */
public const RECOMMENDATION_CACHE = 'recommendation_favourite_course_content_items';
/**
* The content_item_service constructor.
*
* @param content_item_readonly_repository_interface $repository a content item repository.
*/
public function __construct(content_item_readonly_repository_interface $repository) {
$this->repository = $repository;
}
/**
* Returns an array of objects representing favourited content items.
*
* Each object contains the following properties:
* itemtype: a string containing the 'itemtype' key used by the favourites subsystem.
* ids[]: an array of ids, representing the content items within a component.
*
* Since two components can return (via their hook implementation) the same id, the itemtype is used for uniqueness.
*
* @param \stdClass $user
* @return array
*/
private function get_favourite_content_items_for_user(\stdClass $user): array {
$favcache = \cache::make('core', 'user_favourite_course_content_items');
$key = $user->id;
$favmods = $favcache->get($key);
if ($favmods !== false) {
return $favmods;
}
$favourites = $this->get_content_favourites(self::FAVOURITE_PREFIX, \context_user::instance($user->id));
$favcache->set($key, $favourites);
return $favourites;
}
/**
* Returns an array of objects representing recommended content items.
*
* Each object contains the following properties:
* itemtype: a string containing the 'itemtype' key used by the favourites subsystem.
* ids[]: an array of ids, representing the content items within a component.
*
* Since two components can return (via their hook implementation) the same id, the itemtype is used for uniqueness.
*
* @return array
*/
private function get_recommendations(): array {
global $CFG;
$recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE);
$key = $CFG->siteguest;
$favmods = $recommendationcache->get($key);
if ($favmods !== false) {
return $favmods;
}
// Make sure the guest user exists in the database.
if (!\core_user::get_user($CFG->siteguest)) {
throw new \coding_exception('The guest user does not exist in the database.');
}
// Make sure the guest user context exists.
if (!$guestusercontext = \context_user::instance($CFG->siteguest, false)) {
throw new \coding_exception('The guest user context does not exist.');
}
$favourites = $this->get_content_favourites(self::RECOMMENDATION_PREFIX, $guestusercontext);
$recommendationcache->set($CFG->siteguest, $favourites);
return $favourites;
}
/**
* Gets content favourites from the favourites system depending on the area.
*
* @param string $prefix Prefix for the item type.
* @param \context_user $usercontext User context for the favourite
* @return array An array of favourite objects.
*/
private function get_content_favourites(string $prefix, \context_user $usercontext): array {
// Get all modules and any submodules which implement get_course_content_items() hook.
// This gives us the set of all itemtypes which we'll use to register favourite content items.
// The ids that each plugin returns will be used together with the itemtype to uniquely identify
// each content item for favouriting.
$pluginmanager = \core_plugin_manager::instance();
$plugins = $pluginmanager->get_plugins_of_type('mod');
$itemtypes = [];
foreach ($plugins as $plugin) {
// Add the mod itself.
$itemtypes[] = $prefix . 'mod_' . $plugin->name;
// Add any subplugins to the list of item types.
$subplugins = $pluginmanager->get_subplugins_of_plugin('mod_' . $plugin->name);
foreach ($subplugins as $subpluginname => $subplugininfo) {
try {
if (component_callback_exists($subpluginname, 'get_course_content_items')) {
$itemtypes[] = $prefix . $subpluginname;
}
} catch (\moodle_exception $e) {
debugging('Cannot get_course_content_items: ' . $e->getMessage(), DEBUG_DEVELOPER);
}
}
}
$ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
$favourites = [];
$favs = $ufservice->find_all_favourites(self::COMPONENT, $itemtypes);
$favsreduced = array_reduce($favs, function($carry, $item) {
$carry[$item->itemtype][$item->itemid] = 0;
return $carry;
}, []);
foreach ($itemtypes as $type) {
$favourites[] = (object) [
'itemtype' => $type,
'ids' => isset($favsreduced[$type]) ? array_keys($favsreduced[$type]) : []
];
}
return $favourites;
}
/**
* Get all content items which may be added to courses, irrespective of course caps, for site admin views, etc.
*
* @param \stdClass $user the user object.
* @return array the array of exported content items.
*/
public function get_all_content_items(\stdClass $user): array {
$allcontentitems = $this->repository->find_all();
return $this->export_content_items($user, $allcontentitems);
}
/**
* Get content items which name matches a certain pattern and may be added to courses,
* irrespective of course caps, for site admin views, etc.
*
* @param \stdClass $user The user object.
* @param string $pattern The search pattern.
* @return array The array of exported content items.
*/
public function get_content_items_by_name_pattern(\stdClass $user, string $pattern): array {
$allcontentitems = $this->repository->find_all();
$filteredcontentitems = array_filter($allcontentitems, function($contentitem) use ($pattern) {
return preg_match("/$pattern/i", $contentitem->get_title()->get_value());
});
return $this->export_content_items($user, $filteredcontentitems);
}
/**
* Export content items.
*
* @param \stdClass $user The user object.
* @param array $contentitems The content items array.
* @return array The array of exported content items.
*/
private function export_content_items(\stdClass $user, $contentitems) {
global $PAGE;
// Export the objects to get the formatted objects for transfer/display.
$favourites = $this->get_favourite_content_items_for_user($user);
$recommendations = $this->get_recommendations();
$ciexporter = new course_content_items_exporter(
$contentitems,
[
'context' => \context_system::instance(),
'favouriteitems' => $favourites,
'recommended' => $recommendations
]
);
$exported = $ciexporter->export($PAGE->get_renderer('core'));
// Sort by title for return.
< usort($exported->content_items, function($a, $b) {
< return $a->title > $b->title;
< });
<
< return $exported->content_items;
> \core_collator::asort_objects_by_property($exported->content_items, 'title');
> return array_values($exported->content_items);
}
/**
* Return a representation of the available content items, for a user in a course.
*
* @param \stdClass $user the user to check access for.
* @param \stdClass $course the course to scope the content items to.
* @param array $linkparams the desired section to return to.
* @return \stdClass[] the content items, scoped to a course.
*/
public function get_content_items_for_user_in_course(\stdClass $user, \stdClass $course, array $linkparams = []): array {
global $PAGE;
if (!has_capability('moodle/course:manageactivities', \context_course::instance($course->id), $user)) {
return [];
}
// Get all the visible content items.
$allcontentitems = $this->repository->find_all_for_course($course, $user);
// Content items can only originate from modules or submodules.
$pluginmanager = \core_plugin_manager::instance();
$components = \core_component::get_component_list();
$parents = [];
foreach ($allcontentitems as $contentitem) {
if (!in_array($contentitem->get_component_name(), array_keys($components['mod']))) {
// It could be a subplugin.
$info = $pluginmanager->get_plugin_info($contentitem->get_component_name());
if (!is_null($info)) {
$parent = $info->get_parent_plugin();
if ($parent != false) {
if (in_array($parent, array_keys($components['mod']))) {
$parents[$contentitem->get_component_name()] = $parent;
continue;
}
}
}
throw new \moodle_exception('Only modules and submodules can generate content items. \''
. $contentitem->get_component_name() . '\' is neither.');
}
$parents[$contentitem->get_component_name()] = $contentitem->get_component_name();
}
// Now, check access to these items for the user.
$availablecontentitems = array_filter($allcontentitems, function($contentitem) use ($course, $user, $parents) {
// Check the parent module access for the user.
return course_allowed_module($course, explode('_', $parents[$contentitem->get_component_name()])[1], $user);
});
// Add the link params to the link, if any have been provided.
if (!empty($linkparams)) {
$availablecontentitems = array_map(function ($item) use ($linkparams) {
$item->get_link()->params($linkparams);
return $item;
}, $availablecontentitems);
}
// Export the objects to get the formatted objects for transfer/display.
$favourites = $this->get_favourite_content_items_for_user($user);
$recommended = $this->get_recommendations();
$ciexporter = new course_content_items_exporter(
$availablecontentitems,
[
'context' => \context_course::instance($course->id),
'favouriteitems' => $favourites,
'recommended' => $recommended
]
);
$exported = $ciexporter->export($PAGE->get_renderer('course'));
// Sort by title for return.
< usort($exported->content_items, function($a, $b) {
< return $a->title > $b->title;
< });
> \core_collator::asort_objects_by_property($exported->content_items, 'title');
< return $exported->content_items;
> return array_values($exported->content_items);
}
/**
* Add a content item to a user's favourites.
*
* @param \stdClass $user the user whose favourite this is.
* @param string $componentname the name of the component from which the content item originates.
* @param int $contentitemid the id of the content item.
* @return \stdClass the exported content item.
*/
public function add_to_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass {
$usercontext = \context_user::instance($user->id);
$ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
// Because each plugin decides its own ids for content items, a combination of
// itemtype and id is used to guarantee uniqueness across all content items.
$itemtype = self::FAVOURITE_PREFIX . $componentname;
$ufservice->create_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext);
$favcache = \cache::make('core', 'user_favourite_course_content_items');
$favcache->delete($user->id);
$items = $this->get_all_content_items($user);
return $items[array_search($contentitemid, array_column($items, 'id'))];
}
/**
* Remove the content item from a user's favourites.
*
* @param \stdClass $user the user whose favourite this is.
* @param string $componentname the name of the component from which the content item originates.
* @param int $contentitemid the id of the content item.
* @return \stdClass the exported content item.
*/
public function remove_from_user_favourites(\stdClass $user, string $componentname, int $contentitemid): \stdClass {
$usercontext = \context_user::instance($user->id);
$ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
// Because each plugin decides its own ids for content items, a combination of
// itemtype and id is used to guarantee uniqueness across all content items.
$itemtype = self::FAVOURITE_PREFIX . $componentname;
$ufservice->delete_favourite(self::COMPONENT, $itemtype, $contentitemid, $usercontext);
$favcache = \cache::make('core', 'user_favourite_course_content_items');
$favcache->delete($user->id);
$items = $this->get_all_content_items($user);
return $items[array_search($contentitemid, array_column($items, 'id'))];
}
/**
* Toggle an activity to being recommended or not.
*
* @param string $itemtype The component such as mod_assign, or assignsubmission_file
* @param int $itemid The id related to this component item.
* @return bool True on creating a favourite, false on deleting it.
*/
public function toggle_recommendation(string $itemtype, int $itemid): bool {
global $CFG;
$context = \context_system::instance();
$itemtype = self::RECOMMENDATION_PREFIX . $itemtype;
// Favourites are created using a user context. We'll use the site guest user ID as that should not change and there
// can be only one.
$usercontext = \context_user::instance($CFG->siteguest);
$recommendationcache = \cache::make('core', self::RECOMMENDATION_CACHE);
$favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext);
if ($favouritefactory->favourite_exists(self::COMPONENT, $itemtype, $itemid, $context)) {
$favouritefactory->delete_favourite(self::COMPONENT, $itemtype, $itemid, $context);
$result = $recommendationcache->delete($CFG->siteguest);
return false;
} else {
$favouritefactory->create_favourite(self::COMPONENT, $itemtype, $itemid, $context);
$result = $recommendationcache->delete($CFG->siteguest);
return true;
}
}
}