Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 402 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  namespace core\moodlenet;
  18  
  19  use cm_info;
  20  use core\event\moodlenet_resource_exported;
  21  use core\oauth2\client;
  22  use moodle_exception;
  23  use stdClass;
  24  use stored_file;
  25  
  26  /**
  27   * API for sharing Moodle LMS activities to MoodleNet instances.
  28   *
  29   * @package   core
  30   * @copyright 2023 Michael Hawkins <michaelh@moodle.com>
  31   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  32   */
  33  class activity_sender {
  34      /**
  35       * @var int Backup share format - the content is being shared as a Moodle backup file.
  36       */
  37      public const SHARE_FORMAT_BACKUP = 0;
  38  
  39      /**
  40       * @var int Maximum upload file size (1.07 GB).
  41       */
  42      public const MAX_FILESIZE = 1070000000;
  43  
  44      /**
  45       * @var cm_info The context module info object for the activity being shared.
  46       */
  47      protected cm_info $cminfo;
  48  
  49      /**
  50       * @var stdClass The course where the activity is located.
  51       */
  52      protected stdClass $course;
  53  
  54      /**
  55       * Class constructor.
  56       *
  57       * @param int $cmid The course module ID of the activity being shared.
  58       * @param int $userid The user ID who is sharing the activity.
  59       * @param moodlenet_client $moodlenetclient The moodlenet_client object used to perform the share.
  60       * @param client $oauthclient The OAuth 2 client for the MoodleNet instance.
  61       * @param int $shareformat The data format to share in. Defaults to a Moodle backup (SHARE_FORMAT_BACKUP).
  62       * @throws moodle_exception
  63       */
  64      public function __construct(
  65          int $cmid,
  66          protected int $userid,
  67          protected moodlenet_client $moodlenetclient,
  68          protected client $oauthclient,
  69          protected int $shareformat = self::SHARE_FORMAT_BACKUP,
  70      ) {
  71          [$this->course, $this->cminfo] = get_course_and_cm_from_cmid($cmid);
  72  
  73          if (!in_array($shareformat, $this->get_allowed_share_formats())) {
  74              throw new moodle_exception('moodlenet:invalidshareformat');
  75          }
  76      }
  77  
  78      /**
  79       * Share an activity/resource to MoodleNet.
  80       *
  81       * @return array The HTTP response code from MoodleNet and the MoodleNet draft resource URL (URL empty string on fail).
  82       *               Format: ['responsecode' => 201, 'drafturl' => 'https://draft.mnurl/here']
  83       * @throws moodle_exception
  84       */
  85      public function share_activity(): array {
  86          global $DB;
  87  
  88          $accesstoken = '';
  89          $resourceurl = '';
  90          $issuer = $this->oauthclient->get_issuer();
  91  
  92          // Check user can share to the requested MoodleNet instance.
  93          $coursecontext = \core\context\course::instance($this->course->id);
  94          $usercanshare = utilities::can_user_share($coursecontext, $this->userid);
  95  
  96          if ($usercanshare && utilities::is_valid_instance($issuer) && $this->oauthclient->is_logged_in()) {
  97              $accesstoken = $this->oauthclient->get_accesstoken()->token;
  98          }
  99  
 100          // Throw an exception if the user is not currently set up to be able to share to MoodleNet.
 101          if (!$accesstoken) {
 102              throw new moodle_exception('moodlenet:usernotconfigured');
 103          }
 104  
 105          // Attempt to prepare and send the resource if validation has passed and we have an OAuth 2 token.
 106  
 107          // Prepare file in requested format.
 108          $filedata = $this->prepare_share_contents();
 109  
 110          // If we have successfully prepared a file to share of permitted size, share it to MoodleNet.
 111          if (!empty($filedata)) {
 112              // Avoid sending a file larger than the defined limit.
 113              $filesize = $filedata->get_filesize();
 114              if ($filesize > self::MAX_FILESIZE) {
 115                  $filedata->delete();
 116                  throw new moodle_exception('moodlenet:sharefilesizelimitexceeded', 'core', '', [
 117                      'filesize' => $filesize,
 118                      'filesizelimit' => self::MAX_FILESIZE,
 119                  ]);
 120              }
 121  
 122              // MoodleNet only accept plaintext descriptions.
 123              $resourcedescription = $this->get_resource_description($coursecontext);
 124  
 125              $response = $this->moodlenetclient->create_resource_from_stored_file(
 126                  $filedata,
 127                  $this->cminfo->name,
 128                  $resourcedescription,
 129              );
 130              $responsecode = $response->getStatusCode();
 131  
 132              $responsebody = json_decode($response->getBody());
 133              $resourceurl = $responsebody->homepage ?? '';
 134  
 135              // TODO: Store consumable information about completed share - to be completed in MDL-77296.
 136  
 137              // Delete the generated file now it is no longer required.
 138              // (It has either been sent, or failed - retries not currently supported).
 139              $filedata->delete();
 140          }
 141  
 142          // Log every attempt to share (and whether or not it was successful).
 143          $this->log_event($coursecontext, $this->cminfo->id, $resourceurl, $responsecode);
 144  
 145          return [
 146              'responsecode' => $responsecode,
 147              'drafturl' => $resourceurl,
 148          ];
 149      }
 150  
 151      /**
 152       * Prepare the data for sharing, in the format specified.
 153       *
 154       * @return stored_file
 155       */
 156      protected function prepare_share_contents(): stored_file {
 157          switch ($this->shareformat) {
 158              case self::SHARE_FORMAT_BACKUP:
 159                  // If sharing the activity as a backup, prepare the packaged backup.
 160                  $packager = new activity_packager($this->cminfo, $this->userid);
 161                  return $packager->get_package();
 162              default:
 163                  throw new \coding_exception("Unknown share format: {$this->shareformat}'");
 164          };
 165      }
 166  
 167      /**
 168       * Log an event to the admin logs for an outbound share attempt.
 169       *
 170       * @param \context $coursecontext The course context being shared from.
 171       * @param int $cmid The CMID of the activity being shared.
 172       * @param string $resourceurl The URL of the draft resource if it was created.
 173       * @param int $responsecode The HTTP response code describing the outcome of the attempt.
 174       * @return void
 175       */
 176      protected function log_event(
 177          \core\context $coursecontext,
 178          int $cmid,
 179          string $resourceurl,
 180          int $responsecode,
 181      ): void {
 182          $event = moodlenet_resource_exported::create([
 183              'context' => $coursecontext,
 184              'other' => [
 185                  'cmids' => [$cmid],
 186                  'resourceurl' => $resourceurl,
 187                  'success' => ($responsecode == 201),
 188              ],
 189          ]);
 190          $event->trigger();
 191      }
 192  
 193      /**
 194       * Return the list of supported share formats.
 195       *
 196       * @return array Array of supported share format values.
 197       */
 198      protected function get_allowed_share_formats(): array {
 199          return [
 200              self::SHARE_FORMAT_BACKUP,
 201          ];
 202      }
 203  
 204      /**
 205       * Fetch the description for the resource being created, in a supported text format.
 206       *
 207       * @param \context $coursecontext The course context being shared from.
 208       * @return string Converted activity description.
 209       */
 210      protected function get_resource_description(
 211          \context $coursecontext,
 212      ): string {
 213          global $PAGE, $DB;
 214          // We need to set the page context here because content_to_text and format_text will need the page context to work.
 215          $PAGE->set_context($coursecontext);
 216  
 217          $intro = $DB->get_record($this->cminfo->modname, ['id' => $this->cminfo->instance], 'intro, introformat', MUST_EXIST);
 218          $processeddescription = strip_tags($intro->intro);
 219          $processeddescription = content_to_text(format_text(
 220              $processeddescription,
 221              $intro->introformat,
 222          ), $intro->introformat);
 223  
 224          return $processeddescription;
 225      }
 226  }