forked from moodle/moodle
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
MDL-75316 core: Add activity sender & moodlenet_client to MoodleNet API
This makes it possible to share a single activity to a MoodleNet instance (which has been enabled for sharing in site admin). This utilises the core\moodlenet\activity_packager to create an activity backup, then sends it using the MoodleNet create resource API specification. Originally implemented as MDL-75932
- Loading branch information
1 parent
9bc769a
commit 77f85ed
Showing
8 changed files
with
748 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
<?php | ||
// This file is part of Moodle - http://moodle.org/ | ||
// | ||
// Moodle is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// Moodle is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU General Public License | ||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
namespace core\event; | ||
|
||
/** | ||
* MoodleNet send attempt event. | ||
* | ||
* @package core | ||
* @copyright 2023 Michael Hawkins <[email protected]> | ||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | ||
*/ | ||
class moodlenet_resource_exported extends \core\event\base { | ||
|
||
/** | ||
* Set basic properties for the event. | ||
* | ||
* @return void | ||
*/ | ||
protected function init(): void { | ||
$this->data['crud'] = 'c'; | ||
|
||
// Used by teachers, but not for direct educational value to their students. | ||
$this->data['edulevel'] = self::LEVEL_OTHER; | ||
} | ||
|
||
/** | ||
* Fetch the localised general event name. | ||
* | ||
* @return string | ||
*/ | ||
public static function get_name(): string { | ||
return get_string('moodlenet:eventresourceexported'); | ||
} | ||
|
||
/** | ||
* Fetch the non-localised event description. | ||
* This description format is designed to work for both single activity and course sharing. | ||
* | ||
* @return string | ||
*/ | ||
public function get_description() { | ||
$outcome = $this->other['success'] ? 'successfully shared' : 'failed to share'; | ||
$cmids = implode("', '", $this->other['cmids']); | ||
|
||
$description = "The user with id '{$this->userid}' {$outcome} activities to MoodleNet with the " . | ||
"following course module ids, from context with id '{$this->data['contextid']}': '{$cmids}'."; | ||
|
||
return rtrim($description, ", '"); | ||
} | ||
|
||
/** | ||
* Returns relevant URL. | ||
* @return \moodle_url | ||
*/ | ||
public function get_url() { | ||
return new \moodle_url($this->other['resourceurl']); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
<?php | ||
// This file is part of Moodle - http://moodle.org/ | ||
// | ||
// Moodle is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// Moodle is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU General Public License | ||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
namespace core\moodlenet; | ||
|
||
use cm_info; | ||
use core\event\moodlenet_resource_exported; | ||
use core\http_client; | ||
use core\oauth2\client; | ||
use moodle_exception; | ||
|
||
/** | ||
* API for sharing Moodle LMS activities to MoodleNet instances. | ||
* | ||
* @package core | ||
* @copyright 2023 Michael Hawkins <[email protected]> | ||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | ||
*/ | ||
class activity_sender { | ||
/** | ||
* @var int Backup share format - the content is being shared as a Moodle backup file. | ||
*/ | ||
public const SHARE_FORMAT_BACKUP = 0; | ||
|
||
/** | ||
* @var int Maximum upload file size (1.07 GB). | ||
*/ | ||
public const MAX_FILESIZE = 1070000000; | ||
|
||
/** | ||
* @var cm_info The context module info object for the activity being shared. | ||
*/ | ||
protected cm_info $cminfo; | ||
|
||
/** | ||
* Class constructor. | ||
* | ||
* @param int $courseid The course ID where the activity is located. | ||
* @param int $cmid The course module ID of the activity being shared. | ||
* @param int $userid The user ID who is sharing the activity. | ||
* @param \core\http_client $httpclient the httpclient object being used to perform the share. | ||
* @param \core\oauth2\client $oauthclient The OAuth 2 client for the MoodleNet instance. | ||
* @param int $shareformat The data format to share in. Defaults to a Moodle backup (SHARE_FORMAT_BACKUP). | ||
*/ | ||
public function __construct( | ||
protected int $courseid, | ||
int $cmid, | ||
protected int $userid, | ||
protected http_client $httpclient, | ||
protected client $oauthclient, | ||
protected int $shareformat = self::SHARE_FORMAT_BACKUP | ||
) { | ||
$this->cminfo = get_fast_modinfo($courseid)->get_cm($cmid); | ||
} | ||
|
||
/** | ||
* Share an activity/resource to MoodleNet. | ||
* | ||
* @return array The HTTP response code from MoodleNet and the MoodleNet draft resource URL (URL empty string on fail). | ||
* Format: ['responsecode' => 201, 'drafturl' => 'https://draft.mnurl/here'] | ||
* @throws moodle_exception | ||
*/ | ||
public function share_activity(): array { | ||
global $DB; | ||
|
||
$accesstoken = ''; | ||
$resourceurl = ''; | ||
$issuer = $this->oauthclient->get_issuer(); | ||
|
||
// Check user can share to the requested MoodleNet instance. | ||
$coursecontext = \context_course::instance($this->courseid); | ||
$usercanshare = utilities::can_user_share($coursecontext, $this->userid); | ||
|
||
if ($usercanshare && utilities::is_valid_instance($issuer) && $this->oauthclient->is_logged_in()) { | ||
$accesstoken = $this->oauthclient->get_accesstoken()->token; | ||
} | ||
|
||
// Throw an exception if the user is not currently set up to be able to share to MoodleNet. | ||
if (!$accesstoken) { | ||
throw new moodle_exception('moodlenet:usernotconfigured', 'core'); | ||
} | ||
|
||
// Attempt to prepare and send the resource if validation has passed and we have an OAuth 2 token. | ||
|
||
// Prepare file in requested format. | ||
$filedata = $this->prepare_share_contents(); | ||
|
||
// If we have successfully prepared a file to share of permitted size, share it to MoodleNet. | ||
if (!empty($filedata['storedfile'])) { | ||
|
||
// Avoid sending a file larger than the defined limit. | ||
$filesize = $filedata['storedfile']->get_filesize(); | ||
if ($filesize > self::MAX_FILESIZE) { | ||
$filedata['storedfile']->delete(); | ||
throw new moodle_exception('moodlenet:sharefilesizelimitexceeded', 'core', '', | ||
[ | ||
'filesize' => $filesize, | ||
'filesizelimit' => self::MAX_FILESIZE, | ||
]); | ||
} | ||
|
||
// MoodleNet only accept plaintext descriptions. | ||
$resourcedescription = $DB->get_field($this->cminfo->modname, 'intro', ['id' => $this->cminfo->instance]); | ||
$resourcedescription = strip_tags($resourcedescription); | ||
$resourcedescription = format_text( | ||
$resourcedescription, | ||
FORMAT_PLAIN, | ||
['context' => $coursecontext] | ||
); | ||
|
||
$moodlenetclient = new moodlenet_client( | ||
$this->httpclient, | ||
$this->oauthclient, | ||
$this->cminfo->name, | ||
$resourcedescription | ||
); | ||
$response = $moodlenetclient->create_resource_from_file($filedata); | ||
$responsecode = $response->getStatusCode(); | ||
|
||
$responsebody = json_decode($response->getBody()); | ||
$resourceurl = $responsebody->homepage ?? ''; | ||
|
||
// TODO: Store consumable information about completed share - to be completed in MDL-77296. | ||
|
||
// Delete the generated file now it is no longer required. | ||
// (It has either been sent, or failed - retries not currently supported). | ||
$filedata['storedfile']->delete(); | ||
} | ||
|
||
// Log every attempt to share (and whether or not it was successful). | ||
$this->log_event($coursecontext, $this->cminfo->id, $resourceurl, $responsecode); | ||
|
||
return [ | ||
'responsecode' => $responsecode, | ||
'drafturl' => $resourceurl, | ||
]; | ||
} | ||
|
||
/** | ||
* Prepare the data for sharing, in the format specified. | ||
* | ||
* @return array Array of metadata about the file, as well as a stored_file object for the file. | ||
*/ | ||
protected function prepare_share_contents(): array { | ||
|
||
switch ($this->shareformat) { | ||
case self::SHARE_FORMAT_BACKUP: | ||
// If sharing the activity as a backup, prepare the packaged backup. | ||
$packager = new activity_packager($this->cminfo, $this->userid); | ||
$filedata = $packager->get_package(); | ||
break; | ||
default: | ||
$filedata = []; | ||
break; | ||
}; | ||
|
||
return $filedata; | ||
} | ||
|
||
/** | ||
* Log an event to the admin logs for an outbound share attempt. | ||
* | ||
* @param \context $coursecontext The course context being shared from. | ||
* @param int $cmid The CMID of the activity being shared. | ||
* @param string $resourceurl The URL of the draft resource if it was created. | ||
* @param int $responsecode The HTTP response code describing the outcome of the attempt. | ||
* @return void | ||
*/ | ||
protected function log_event( | ||
\context $coursecontext, | ||
int $cmid, | ||
string $resourceurl, | ||
int $responsecode | ||
): void { | ||
$event = moodlenet_resource_exported::create([ | ||
'context' => $coursecontext, | ||
'other' => [ | ||
'cmids' => [$cmid], | ||
'resourceurl' => $resourceurl, | ||
'success' => ($responsecode == 201), | ||
], | ||
]); | ||
$event->trigger(); | ||
} | ||
} |
Oops, something went wrong.