Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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

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