Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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   */
  25  namespace core_course\local\repository;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  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;
  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   */
  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;
  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                  $arialabel = get_string('morehelpaboutmodule', '', get_string('modulename', $modname));
  57                  $doclink = $OUTPUT->doc_link($link, $linktext, true, ['aria-label' => $arialabel]);
  58                  $help .= \html_writer::tag('div', $doclink, ['class' => 'helpdoclink']);
  59              }
  60          }
  61          return $help;
  62      }
  63  
  64      /**
  65       * Create a content_item object based on legacy data returned from the get_shortcuts hook implementations.
  66       *
  67       * @param \stdClass $item the stdClass of legacy data.
  68       * @return content_item a content item object.
  69       */
  70      private function content_item_from_legacy_data(\stdClass $item): content_item {
  71          global $OUTPUT;
  72  
  73          // Make sure the legacy data results in a content_item with id = 0.
  74          // Even with an id, we can't uniquely identify the item, because we can't guarantee what component it came from.
  75          // An id of -1, signifies this.
  76          $item->id = -1;
  77  
  78          // If the module provides the helplink property, append it to the help text to match the look and feel
  79          // of the default course modules.
  80          if (isset($item->help) && isset($item->helplink)) {
  81              $linktext = get_string('morehelp');
  82              $item->help .= \html_writer::tag('div',
  83                  $OUTPUT->doc_link($item->helplink, $linktext, true), ['class' => 'helpdoclink']);
  84          }
  85  
  86          if (is_string($item->title)) {
  87              $item->title = new string_title($item->title);
  88          } else if ($item->title instanceof \lang_string) {
  89              $item->title = new lang_string_title($item->title->get_identifier(), $item->title->get_component());
  90          }
  91  
  92          // Legacy items had names which are in one of 2 forms:
  93          // modname, i.e. 'assign' or
  94          // modname:link, i.e. lti:http://etc...
  95          // We need to grab the module name out to create the componentname.
  96          $modname = (strpos($item->name, ':') !== false) ? explode(':', $item->name)[0] : $item->name;
  97  
  98          return new content_item($item->id, $item->name, $item->title, $item->link, $item->icon, $item->help ?? '',
  99              $item->archetype, 'mod_' . $modname);
 100      }
 101  
 102      /**
 103       * Create a stdClass type object based on a content_item instance.
 104       *
 105       * @param content_item $contentitem
 106       * @return \stdClass the legacy data.
 107       */
 108      private function content_item_to_legacy_data(content_item $contentitem): \stdClass {
 109          $item = new \stdClass();
 110          $item->id = $contentitem->get_id();
 111          $item->name = $contentitem->get_name();
 112          $item->title = $contentitem->get_title();
 113          $item->link = $contentitem->get_link();
 114          $item->icon = $contentitem->get_icon();
 115          $item->help = $contentitem->get_help();
 116          $item->archetype = $contentitem->get_archetype();
 117          $item->componentname = $contentitem->get_component_name();
 118          return $item;
 119      }
 120  
 121      /**
 122       * Helper to get the contentitems from all subplugin hooks for a given module plugin.
 123       *
 124       * @param string $parentpluginname the name of the module plugin to check subplugins for.
 125       * @param content_item $modulecontentitem the content item of the module plugin, to pass to the hooks.
 126       * @param \stdClass $user the user object to pass to subplugins.
 127       * @return array the array of content items.
 128       */
 129      private function get_subplugin_course_content_items(string $parentpluginname, content_item $modulecontentitem,
 130              \stdClass $user): array {
 131  
 132          $contentitems = [];
 133          $pluginmanager = \core_plugin_manager::instance();
 134          foreach ($pluginmanager->get_subplugins_of_plugin($parentpluginname) as $subpluginname => $subplugin) {
 135              // Call the hook, but with a copy of the module content item data.
 136              $spcontentitems = component_callback($subpluginname, 'get_course_content_items', [$modulecontentitem, $user], null);
 137              if (!is_null($spcontentitems)) {
 138                  foreach ($spcontentitems as $spcontentitem) {
 139                      $contentitems[] = $spcontentitem;
 140                  }
 141              }
 142          }
 143          return $contentitems;
 144      }
 145  
 146      /**
 147       * Get all the content items for a subplugin.
 148       *
 149       * @param string $parentpluginname
 150       * @param content_item $modulecontentitem
 151       * @return array
 152       */
 153      private function get_subplugin_all_content_items(string $parentpluginname, content_item $modulecontentitem): array {
 154          $contentitems = [];
 155          $pluginmanager = \core_plugin_manager::instance();
 156          foreach ($pluginmanager->get_subplugins_of_plugin($parentpluginname) as $subpluginname => $subplugin) {
 157              // Call the hook, but with a copy of the module content item data.
 158              $spcontentitems = component_callback($subpluginname, 'get_all_content_items', [$modulecontentitem], null);
 159              if (!is_null($spcontentitems)) {
 160                  foreach ($spcontentitems as $spcontentitem) {
 161                      $contentitems[] = $spcontentitem;
 162                  }
 163              }
 164          }
 165          return $contentitems;
 166      }
 167  
 168      /**
 169       * Helper to make sure any legacy items have certain properties, which, if missing are inherited from the parent module item.
 170       *
 171       * @param \stdClass $legacyitem the legacy information, a stdClass coming from get_shortcuts() hook.
 172       * @param content_item $modulecontentitem The module's content item information, to inherit if needed.
 173       * @return \stdClass the updated legacy item stdClass
 174       */
 175      private function legacy_item_inherit_missing(\stdClass $legacyitem, content_item $modulecontentitem): \stdClass {
 176          // Fall back to the plugin parent value if the subtype didn't provide anything.
 177          $legacyitem->archetype = $legacyitem->archetype ?? $modulecontentitem->get_archetype();
 178          $legacyitem->icon = $legacyitem->icon ?? $modulecontentitem->get_icon();
 179          return $legacyitem;
 180      }
 181  
 182      /**
 183       * Find all the available content items, not restricted to course or user.
 184       *
 185       * @return array the array of content items.
 186       */
 187      public function find_all(): array {
 188          global $OUTPUT, $DB, $CFG;
 189  
 190          // Get all modules so we know which plugins are enabled and able to add content.
 191          // Only module plugins may add content items.
 192          $modules = $DB->get_records('modules', ['visible' => 1]);
 193          $return = [];
 194  
 195          // Now, generate the content_items.
 196          foreach ($modules as $modid => $mod) {
 197              // Exclude modules if the code doesn't exist.
 198              if (!file_exists("$CFG->dirroot/mod/$mod->name/lib.php")) {
 199                  continue;
 200              }
 201              // Create the content item for the module itself.
 202              // If the module chooses to implement the hook, this may be thrown away.
 203              $help = $this->get_core_module_help_string($mod->name);
 204              $archetype = plugin_supports('mod', $mod->name, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
 205  
 206              $contentitem = new content_item(
 207                  $mod->id,
 208                  $mod->name,
 209                  new lang_string_title("modulename", $mod->name),
 210                  new \moodle_url(''), // No course scope, so just an empty link.
 211                  $OUTPUT->pix_icon('icon', '', $mod->name, ['class' => 'icon']),
 212                  $help,
 213                  $archetype,
 214                  'mod_' . $mod->name
 215              );
 216  
 217              $modcontentitemreference = clone($contentitem);
 218  
 219              if (component_callback_exists('mod_' . $mod->name, 'get_all_content_items')) {
 220                  // Call the module hooks for this module.
 221                  $plugincontentitems = component_callback('mod_' . $mod->name, 'get_all_content_items',
 222                      [$modcontentitemreference], []);
 223                  if (!empty($plugincontentitems)) {
 224                      array_push($return, ...$plugincontentitems);
 225                  }
 226  
 227                  // Now, get those for subplugins of the module.
 228                  $subplugincontentitems = $this->get_subplugin_all_content_items('mod_' . $mod->name, $modcontentitemreference);
 229                  if (!empty($subplugincontentitems)) {
 230                      array_push($return, ...$subplugincontentitems);
 231                  }
 232              } else {
 233                  // Neither callback was found, so just use the default module content item.
 234                  $return[] = $contentitem;
 235              }
 236          }
 237          return $return;
 238      }
 239  
 240      /**
 241       * Get the list of potential content items for the given course.
 242       *
 243       * @param \stdClass $course the course
 244       * @param \stdClass $user the user, to pass to plugins implementing callbacks.
 245       * @return array the array of content_item objects
 246       */
 247      public function find_all_for_course(\stdClass $course, \stdClass $user): array {
 248          global $OUTPUT, $DB, $CFG;
 249  
 250          // Get all modules so we know which plugins are enabled and able to add content.
 251          // Only module plugins may add content items.
 252          $modules = $DB->get_records('modules', ['visible' => 1]);
 253          $return = [];
 254  
 255          // A moodle_url is expected and required by modules in their implementation of the hook 'get_shortcuts'.
 256          $urlbase = new \moodle_url('/course/mod.php', ['id' => $course->id]);
 257  
 258          // Now, generate the content_items.
 259          foreach ($modules as $modid => $mod) {
 260              // Exclude modules if the code doesn't exist.
 261              if (!file_exists("$CFG->dirroot/mod/$mod->name/lib.php")) {
 262                  continue;
 263              }
 264              // Create the content item for the module itself.
 265              // If the module chooses to implement the hook, this may be thrown away.
 266              $help = $this->get_core_module_help_string($mod->name);
 267              $archetype = plugin_supports('mod', $mod->name, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
 268  
 269              $contentitem = new content_item(
 270                  $mod->id,
 271                  $mod->name,
 272                  new lang_string_title("modulename", $mod->name),
 273                  new \moodle_url($urlbase, ['add' => $mod->name]),
 274                  $OUTPUT->pix_icon('icon', '', $mod->name, ['class' => 'icon']),
 275                  $help,
 276                  $archetype,
 277                  'mod_' . $mod->name
 278              );
 279  
 280              // Legacy vs new hooks.
 281              // If the new hook is found for a module plugin, use that path (calling mod plugins and their subplugins directly)
 282              // If not, check the legacy hook. This won't provide us with enough information to identify items uniquely within their
 283              // component (lti + lti source being an example), but we can still list these items.
 284              $modcontentitemreference = clone($contentitem);
 285  
 286              if (component_callback_exists('mod_' . $mod->name, 'get_course_content_items')) {
 287                  // Call the module hooks for this module.
 288                  $plugincontentitems = component_callback('mod_' . $mod->name, 'get_course_content_items',
 289                      [$modcontentitemreference, $user, $course], []);
 290                  if (!empty($plugincontentitems)) {
 291                      array_push($return, ...$plugincontentitems);
 292                  }
 293  
 294                  // Now, get those for subplugins of the module.
 295                  $subpluginitems = $this->get_subplugin_course_content_items('mod_' . $mod->name, $modcontentitemreference, $user);
 296                  if (!empty($subpluginitems)) {
 297                      array_push($return, ...$subpluginitems);
 298                  }
 299  
 300              } else if (component_callback_exists('mod_' . $mod->name, 'get_shortcuts')) {
 301                  // TODO: MDL-68011 this block needs to be removed in 4.1.
 302                  debugging('The callback get_shortcuts has been deprecated. Please use get_course_content_items and
 303                      get_all_content_items instead. Some features of the activity chooser, such as favourites and recommendations
 304                      are not supported when providing content items via the deprecated callback.');
 305  
 306                  // If get_shortcuts() callback is defined, the default module action is not added.
 307                  // It is a responsibility of the callback to add it to the return value unless it is not needed.
 308                  // The legacy hook, get_shortcuts, expects a stdClass representation of the core module content_item entry.
 309                  $modcontentitemreference = $this->content_item_to_legacy_data($contentitem);
 310  
 311                  $legacyitems = component_callback($mod->name, 'get_shortcuts', [$modcontentitemreference], null);
 312                  if (!is_null($legacyitems)) {
 313                      foreach ($legacyitems as $legacyitem) {
 314  
 315                          $legacyitem = $this->legacy_item_inherit_missing($legacyitem, $contentitem);
 316  
 317                          // All items must have different links, use them as a key in the return array.
 318                          // If plugin returned the only one item with the same link as default item - keep $modname,
 319                          // otherwise append the link url to the module name.
 320                          $legacyitem->name = (count($legacyitems) == 1 &&
 321                              $legacyitem->link->out() === $contentitem->get_link()->out()) ? $mod->name : $mod->name . ':' .
 322                                  $legacyitem->link;
 323  
 324                          $plugincontentitem = $this->content_item_from_legacy_data($legacyitem);
 325  
 326                          $return[] = $plugincontentitem;
 327                      }
 328                  }
 329              } else {
 330                  // Neither callback was found, so just use the default module content item.
 331                  $return[] = $contentitem;
 332              }
 333          }
 334  
 335          return $return;
 336      }
 337  }