8befe48a6a6543e84b060d8cd1f29a084b62743e
[moodle.git] / course / classes / local / repository / content_item_readonly_repository.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 class content_item_repository, for fetching content_items.
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\repository;
27 defined('MOODLE_INTERNAL') || die();
29 use core_course\local\entity\content_item;
30 use core_course\local\entity\lang_string_title;
31 use core_course\local\entity\string_title;
33 /**
34  * The class content_item_repository, for reading content_items.
35  *
36  * @copyright  2020 Jake Dallimore <jrhdallimore@gmail.com>
37  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class content_item_readonly_repository implements content_item_readonly_repository_interface {
40     /**
41      * Get the help string for content items representing core modules.
42      *
43      * @param string $modname the module name.
44      * @return string the help string, including help link.
45      */
46     private function get_core_module_help_string(string $modname): string {
47         global $OUTPUT;
49         $help = '';
50         $sm = get_string_manager();
51         if ($sm->string_exists('modulename_help', $modname)) {
52             $help = get_string('modulename_help', $modname);
53             if ($sm->string_exists('modulename_link', $modname)) { // Link to further info in Moodle docs.
54                 $link = get_string('modulename_link', $modname);
55                 $linktext = get_string('morehelp');
56                 $help .= \html_writer::tag('div', $OUTPUT->doc_link($link, $linktext, true), ['class' => 'helpdoclink']);
57             }
58         }
59         return $help;
60     }
62     /**
63      * Create a content_item object based on legacy data returned from the get_shortcuts hook implementations.
64      *
65      * @param \stdClass $item the stdClass of legacy data.
66      * @return content_item a content item object.
67      */
68     private function content_item_from_legacy_data(\stdClass $item): content_item {
69         global $OUTPUT;
71         // Make sure the legacy data results in a content_item with id = 0.
72         // Even with an id, we can't uniquely identify the item, because we can't guarantee what component it came from.
73         // An id of -1, signifies this.
74         $item->id = -1;
76         // If the module provides the helplink property, append it to the help text to match the look and feel
77         // of the default course modules.
78         if (isset($item->help) && isset($item->helplink)) {
79             $linktext = get_string('morehelp');
80             $item->help .= \html_writer::tag('div',
81                 $OUTPUT->doc_link($item->helplink, $linktext, true), ['class' => 'helpdoclink']);
82         }
84         if (is_string($item->title)) {
85             $item->title = new string_title($item->title);
86         } else if ($item->title instanceof \lang_string) {
87             $item->title = new lang_string_title($item->title->get_identifier(), $item->title->get_component());
88         }
90         // Legacy items had names which are in one of 2 forms:
91         // modname, i.e. 'assign' or
92         // modname:link, i.e. lti:http://etc...
93         // We need to grab the module name out to create the componentname.
94         $modname = (strpos($item->name, ':') !== false) ? explode(':', $item->name)[0] : $item->name;
96         return new content_item($item->id, $item->name, $item->title, $item->link, $item->icon, $item->help ?? '',
97             $item->archetype, 'mod_' . $modname);
98     }
100     /**
101      * Create a stdClass type object based on a content_item instance.
102      *
103      * @param content_item $contentitem
104      * @return \stdClass the legacy data.
105      */
106     private function content_item_to_legacy_data(content_item $contentitem): \stdClass {
107         $item = new \stdClass();
108         $item->id = $contentitem->get_id();
109         $item->name = $contentitem->get_name();
110         $item->title = $contentitem->get_title();
111         $item->link = $contentitem->get_link();
112         $item->icon = $contentitem->get_icon();
113         $item->help = $contentitem->get_help();
114         $item->archetype = $contentitem->get_archetype();
115         $item->componentname = $contentitem->get_component_name();
116         return $item;
117     }
119     /**
120      * Helper to get the contentitems from all subplugin hooks for a given module plugin.
121      *
122      * @param string $parentpluginname the name of the module plugin to check subplugins for.
123      * @param content_item $modulecontentitem the content item of the module plugin, to pass to the hooks.
124      * @param \stdClass $user the user object to pass to subplugins.
125      * @return array the array of content items.
126      */
127     private function get_subplugin_course_content_items(string $parentpluginname, content_item $modulecontentitem,
128             \stdClass $user): array {
130         $contentitems = [];
131         $pluginmanager = \core_plugin_manager::instance();
132         foreach ($pluginmanager->get_subplugins_of_plugin($parentpluginname) as $subpluginname => $subplugin) {
133             // Call the hook, but with a copy of the module content item data.
134             $spcontentitems = component_callback($subpluginname, 'get_course_content_items', [$modulecontentitem, $user], null);
135             if (!is_null($spcontentitems)) {
136                 foreach ($spcontentitems as $spcontentitem) {
137                     $contentitems[] = $spcontentitem;
138                 }
139             }
140         }
141         return $contentitems;
142     }
144     /**
145      * Helper to make sure any legacy items have certain properties, which, if missing are inherited from the parent module item.
146      *
147      * @param \stdClass $legacyitem the legacy information, a stdClass coming from get_shortcuts() hook.
148      * @param content_item $modulecontentitem The module's content item information, to inherit if needed.
149      * @return \stdClass the updated legacy item stdClass
150      */
151     private function legacy_item_inherit_missing(\stdClass $legacyitem, content_item $modulecontentitem): \stdClass {
152         // Fall back to the plugin parent value if the subtype didn't provide anything.
153         $legacyitem->archetype = $legacyitem->archetype ?? $modulecontentitem->get_archetype();
154         $legacyitem->icon = $legacyitem->icon ?? $modulecontentitem->get_icon();
155         return $legacyitem;
156     }
158     /**
159      * Get the list of potential content items for the given course.
160      *
161      * @param \stdClass $course the course
162      * @param \stdClass $user the user, to pass to plugins implementing callbacks.
163      * @return array the array of content_item objects
164      */
165     public function find_all_for_course(\stdClass $course, \stdClass $user): array {
166         global $OUTPUT, $DB;
168         // Get all modules so we know which plugins are enabled and able to add content.
169         // Only module plugins may add content items.
170         $modules = $DB->get_records('modules', ['visible' => 1]);
171         $return = [];
173         // A moodle_url is expected and required by modules in their implementation of the hook 'get_shortcuts'.
174         $urlbase = new \moodle_url('/course/mod.php', ['id' => $course->id]);
176         // Now, generate the content_items.
177         foreach ($modules as $modid => $mod) {
179             // Create the content item for the module itself.
180             // If the module chooses to implement the hook, this may be thrown away.
181             $help = $this->get_core_module_help_string($mod->name);
182             $archetype = plugin_supports('mod', $mod->name, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
184             $contentitem = new content_item(
185                 $mod->id,
186                 $mod->name,
187                 new lang_string_title("modulename", $mod->name),
188                 new \moodle_url($urlbase, ['add' => $mod->name]),
189                 $OUTPUT->pix_icon('icon', '', $mod->name, ['class' => 'icon']),
190                 $help,
191                 $archetype,
192                 'mod_' . $mod->name
193             );
195             // Legacy vs new hooks.
196             // If the new hook is found for a module plugin, use that path (calling mod plugins and their subplugins directly)
197             // If not, check the legacy hook. This won't provide us with enough information to identify items uniquely within their
198             // component (lti + lti source being an example), but we can still list these items.
199             $modcontentitemreference = clone($contentitem);
201             if (component_callback_exists('mod_' . $mod->name, 'get_course_content_items')) {
202                 // Call the module hooks for this module.
203                 $plugincontentitems = component_callback('mod_' . $mod->name, 'get_course_content_items',
204                     [$modcontentitemreference, $user, $course], []);
205                 if (!empty($plugincontentitems)) {
206                     array_push($return, ...$plugincontentitems);
207                 }
209                 // Now, get those for subplugins of the module.
210                 $subpluginitems = $this->get_subplugin_course_content_items('mod_' . $mod->name, $modcontentitemreference, $user);
211                 if (!empty($subpluginitems)) {
212                     array_push($return, ...$subpluginitems);
213                 }
215             } else if (component_callback_exists('mod_' . $mod->name, 'get_shortcuts')) {
216                 // TODO: MDL-68011 this block needs to be removed in 4.3.
217                 debugging('The callback get_shortcuts has been deprecated. Please use get_course_content_items and
218                     get_all_content_items instead. Some features of the activity chooser, such as favourites and recommendations
219                     are not supported when providing content items via the deprecated callback.');
221                 // If get_shortcuts() callback is defined, the default module action is not added.
222                 // It is a responsibility of the callback to add it to the return value unless it is not needed.
223                 // The legacy hook, get_shortcuts, expects a stdClass representation of the core module content_item entry.
224                 $modcontentitemreference = $this->content_item_to_legacy_data($contentitem);
226                 $legacyitems = component_callback($mod->name, 'get_shortcuts', [$modcontentitemreference], null);
227                 if (!is_null($legacyitems)) {
228                     foreach ($legacyitems as $legacyitem) {
230                         $legacyitem = $this->legacy_item_inherit_missing($legacyitem, $contentitem);
232                         // All items must have different links, use them as a key in the return array.
233                         // If plugin returned the only one item with the same link as default item - keep $modname,
234                         // otherwise append the link url to the module name.
235                         $legacyitem->name = (count($legacyitems) == 1 &&
236                             $legacyitem->link->out() === $contentitem->get_link()->out()) ? $mod->name : $mod->name . ':' .
237                                 $legacyitem->link;
239                         $plugincontentitem = $this->content_item_from_legacy_data($legacyitem);
241                         $return[] = $plugincontentitem;
242                     }
243                 }
244             } else {
245                 // Neither callback was found, so just use the default module content item.
246                 $return[] = $contentitem;
247             }
248         }
250         return $return;
251     }