MDL-67585 core_course: add hook get_all_content_items
[moodle.git] / course / classes / local / repository / content_item_readonly_repository.php
CommitLineData
e843336e
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 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 */
25namespace core_course\local\repository;
26
27defined('MOODLE_INTERNAL') || die();
28
29use core_course\local\entity\content_item;
30use core_course\local\entity\lang_string_title;
31use core_course\local\entity\string_title;
32
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 */
39class 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;
48
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 }
61
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 {
8fc7d83f
JD
69 global $OUTPUT;
70
e843336e
JD
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;
75
8fc7d83f
JD
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 }
83
e843336e
JD
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 }
89
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;
95
96 return new content_item($item->id, $item->name, $item->title, $item->link, $item->icon, $item->help ?? '',
97 $item->archetype, 'mod_' . $modname);
98 }
99
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 }
118
8fc7d83f
JD
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 {
129
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 }
143
57dfcf95
JD
144 /**
145 * Get all the content items for a subplugin.
146 *
147 * @param string $parentpluginname
148 * @param content_item $modulecontentitem
149 * @return array
150 */
151 private function get_subplugin_all_content_items(string $parentpluginname, content_item $modulecontentitem): array {
152 $contentitems = [];
153 $pluginmanager = \core_plugin_manager::instance();
154 foreach ($pluginmanager->get_subplugins_of_plugin($parentpluginname) as $subpluginname => $subplugin) {
155 // Call the hook, but with a copy of the module content item data.
156 $spcontentitems = component_callback($subpluginname, 'get_all_content_items', [$modulecontentitem], null);
157 if (!is_null($spcontentitems)) {
158 foreach ($spcontentitems as $spcontentitem) {
159 $contentitems[] = $spcontentitem;
160 }
161 }
162 }
163 return $contentitems;
164 }
165
8fc7d83f
JD
166 /**
167 * Helper to make sure any legacy items have certain properties, which, if missing are inherited from the parent module item.
168 *
169 * @param \stdClass $legacyitem the legacy information, a stdClass coming from get_shortcuts() hook.
170 * @param content_item $modulecontentitem The module's content item information, to inherit if needed.
171 * @return \stdClass the updated legacy item stdClass
172 */
173 private function legacy_item_inherit_missing(\stdClass $legacyitem, content_item $modulecontentitem): \stdClass {
174 // Fall back to the plugin parent value if the subtype didn't provide anything.
175 $legacyitem->archetype = $legacyitem->archetype ?? $modulecontentitem->get_archetype();
176 $legacyitem->icon = $legacyitem->icon ?? $modulecontentitem->get_icon();
177 return $legacyitem;
178 }
179
57dfcf95
JD
180 /**
181 * Find all the available content items, not restricted to course or user.
182 *
183 * @return array the array of content items.
184 */
185 public function find_all(): array {
186 global $OUTPUT, $DB;
187
188 // Get all modules so we know which plugins are enabled and able to add content.
189 // Only module plugins may add content items.
190 $modules = $DB->get_records('modules', ['visible' => 1]);
191 $return = [];
192
193 // Now, generate the content_items.
194 foreach ($modules as $modid => $mod) {
195 // Create the content item for the module itself.
196 // If the module chooses to implement the hook, this may be thrown away.
197 $help = $this->get_core_module_help_string($mod->name);
198 $archetype = plugin_supports('mod', $mod->name, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
199
200 $contentitem = new content_item(
201 $mod->id,
202 $mod->name,
203 new lang_string_title("modulename", $mod->name),
204 new \moodle_url(''), // No course scope, so just an empty link.
205 $OUTPUT->pix_icon('icon', '', $mod->name, ['class' => 'icon']),
206 $help,
207 $archetype,
208 'mod_' . $mod->name
209 );
210
211 $modcontentitemreference = clone($contentitem);
212
213 if (component_callback_exists('mod_' . $mod->name, 'get_all_content_items')) {
214 // Call the module hooks for this module.
215 $plugincontentitems = component_callback('mod_' . $mod->name, 'get_all_content_items',
216 [$modcontentitemreference], []);
217 if (!empty($plugincontentitems)) {
218 array_push($return, ...$plugincontentitems);
219 }
220
221 // Now, get those for subplugins of the module.
222 $subplugincontentitems = $this->get_subplugin_all_content_items('mod_' . $mod->name, $modcontentitemreference);
223 if (!empty($subplugincontentitems)) {
224 array_push($return, ...$subplugincontentitems);
225 }
226 } else {
227 // Neither callback was found, so just use the default module content item.
228 $return[] = $contentitem;
229 }
230 }
231 return $return;
232 }
233
e843336e
JD
234 /**
235 * Get the list of potential content items for the given course.
236 *
237 * @param \stdClass $course the course
238 * @param \stdClass $user the user, to pass to plugins implementing callbacks.
239 * @return array the array of content_item objects
240 */
241 public function find_all_for_course(\stdClass $course, \stdClass $user): array {
242 global $OUTPUT, $DB;
243
244 // Get all modules so we know which plugins are enabled and able to add content.
245 // Only module plugins may add content items.
246 $modules = $DB->get_records('modules', ['visible' => 1]);
247 $return = [];
248
249 // A moodle_url is expected and required by modules in their implementation of the hook 'get_shortcuts'.
250 $urlbase = new \moodle_url('/course/mod.php', ['id' => $course->id]);
251
252 // Now, generate the content_items.
253 foreach ($modules as $modid => $mod) {
254
255 // Create the content item for the module itself.
256 // If the module chooses to implement the hook, this may be thrown away.
257 $help = $this->get_core_module_help_string($mod->name);
258 $archetype = plugin_supports('mod', $mod->name, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
259
260 $contentitem = new content_item(
261 $mod->id,
262 $mod->name,
263 new lang_string_title("modulename", $mod->name),
264 new \moodle_url($urlbase, ['add' => $mod->name]),
265 $OUTPUT->pix_icon('icon', '', $mod->name, ['class' => 'icon']),
266 $help,
267 $archetype,
268 'mod_' . $mod->name
269 );
270
8fc7d83f
JD
271 // Legacy vs new hooks.
272 // If the new hook is found for a module plugin, use that path (calling mod plugins and their subplugins directly)
273 // If not, check the legacy hook. This won't provide us with enough information to identify items uniquely within their
274 // component (lti + lti source being an example), but we can still list these items.
275 $modcontentitemreference = clone($contentitem);
276
277 if (component_callback_exists('mod_' . $mod->name, 'get_course_content_items')) {
278 // Call the module hooks for this module.
279 $plugincontentitems = component_callback('mod_' . $mod->name, 'get_course_content_items',
280 [$modcontentitemreference, $user, $course], []);
281 if (!empty($plugincontentitems)) {
282 array_push($return, ...$plugincontentitems);
283 }
e843336e 284
8fc7d83f
JD
285 // Now, get those for subplugins of the module.
286 $subpluginitems = $this->get_subplugin_course_content_items('mod_' . $mod->name, $modcontentitemreference, $user);
287 if (!empty($subpluginitems)) {
288 array_push($return, ...$subpluginitems);
e843336e
JD
289 }
290
8fc7d83f 291 } else if (component_callback_exists('mod_' . $mod->name, 'get_shortcuts')) {
520add19
JD
292 // TODO: MDL-68011 this block needs to be removed in 4.3.
293 debugging('The callback get_shortcuts has been deprecated. Please use get_course_content_items and
294 get_all_content_items instead. Some features of the activity chooser, such as favourites and recommendations
295 are not supported when providing content items via the deprecated callback.');
296
e843336e
JD
297 // If get_shortcuts() callback is defined, the default module action is not added.
298 // It is a responsibility of the callback to add it to the return value unless it is not needed.
8fc7d83f
JD
299 // The legacy hook, get_shortcuts, expects a stdClass representation of the core module content_item entry.
300 $modcontentitemreference = $this->content_item_to_legacy_data($contentitem);
301
302 $legacyitems = component_callback($mod->name, 'get_shortcuts', [$modcontentitemreference], null);
303 if (!is_null($legacyitems)) {
304 foreach ($legacyitems as $legacyitem) {
e843336e 305
8fc7d83f
JD
306 $legacyitem = $this->legacy_item_inherit_missing($legacyitem, $contentitem);
307
308 // All items must have different links, use them as a key in the return array.
309 // If plugin returned the only one item with the same link as default item - keep $modname,
310 // otherwise append the link url to the module name.
311 $legacyitem->name = (count($legacyitems) == 1 &&
312 $legacyitem->link->out() === $contentitem->get_link()->out()) ? $mod->name : $mod->name . ':' .
313 $legacyitem->link;
314
315 $plugincontentitem = $this->content_item_from_legacy_data($legacyitem);
316
317 $return[] = $plugincontentitem;
318 }
319 }
320 } else {
321 // Neither callback was found, so just use the default module content item.
322 $return[] = $contentitem;
323 }
e843336e
JD
324 }
325
326 return $return;
327 }
328}