first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,69 @@
<?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 backup;
use backup_controller;
use cm_info;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
/**
* Packager to prepare appropriate backup of an activity to share to MoodleNet.
*
* @package core
* @copyright 2023 Raquel Ortega <raquel.ortega@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_packager extends resource_packager {
/**
* Constructor.
*
* @param cm_info $cminfo context module information about the resource being packaged.
* @param int $userid The ID of the user performing the packaging.
*/
public function __construct(
cm_info $cminfo,
int $userid,
) {
// Check backup/restore support.
if (!plugin_supports('mod', $cminfo->modname , FEATURE_BACKUP_MOODLE2)) {
throw new \coding_exception("Cannot backup module $cminfo->modname. This module doesn't support the backup feature.");
}
parent::__construct($cminfo, $userid, $cminfo->modname);
}
/**
* Get the backup controller for the activity.
*
* @return backup_controller the backup controller for the activity.
*/
protected function get_backup_controller(): backup_controller {
return new backup_controller(
backup::TYPE_1ACTIVITY,
$this->cminfo->id,
backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO,
backup::MODE_GENERAL,
$this->userid
);
}
}
+189
View File
@@ -0,0 +1,189 @@
<?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\oauth2\client;
use moodle_exception;
use stored_file;
/**
* API for sharing Moodle LMS activities to MoodleNet instances.
*
* @package core
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_sender extends resource_sender {
/**
* @var cm_info The context module info object for the activity being shared.
*/
protected cm_info $cminfo;
/**
* Class constructor.
*
* @param int $cmid The course module ID of the activity being shared
* @param int $userid The user ID who is sharing the activity
* @param moodlenet_client $moodlenetclient The moodlenet_client object used to perform the share
* @param 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(
int $cmid,
protected int $userid,
protected moodlenet_client $moodlenetclient,
protected client $oauthclient,
protected int $shareformat = self::SHARE_FORMAT_BACKUP,
) {
parent::__construct($cmid, $userid, $moodlenetclient, $oauthclient, $shareformat);
[$this->course, $this->cminfo] = get_course_and_cm_from_cmid($cmid);
$this->packager = new activity_packager($this->cminfo, $this->userid);
}
/**
* 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']
* @deprecated since Moodle 4.3
* @todo Final deprecation MDL-79086
*/
public function share_activity(): array {
debugging('Method share_activity is deprecated, use share_resource instead.', DEBUG_DEVELOPER);
return $this->share_resource();
}
/**
* 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']
*/
public function share_resource(): array {
$accesstoken = '';
$resourceurl = '';
$issuer = $this->oauthclient->get_issuer();
// Check user can share to the requested MoodleNet instance.
$coursecontext = \core\context\course::instance($this->course->id);
$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');
}
// 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)) {
// Avoid sending a file larger than the defined limit.
$filesize = $filedata->get_filesize();
if ($filesize > self::MAX_FILESIZE) {
$filedata->delete();
throw new moodle_exception('moodlenet:sharefilesizelimitexceeded', 'core', '', [
'filesize' => $filesize,
'filesizelimit' => self::MAX_FILESIZE,
]);
}
// MoodleNet only accept plaintext descriptions.
$resourcedescription = $this->get_resource_description($coursecontext);
$response = $this->moodlenetclient->create_resource_from_stored_file(
$filedata,
$this->cminfo->name,
$resourcedescription,
);
$responsecode = $response->getStatusCode();
$responsebody = json_decode($response->getBody());
$resourceurl = $responsebody->homepage ?? '';
// Delete the generated file now it is no longer required.
// (It has either been sent, or failed - retries not currently supported).
$filedata->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,
];
}
/**
* 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(
\core\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();
}
/**
* Fetch the description for the resource being created, in a supported text format.
*
* @param \context $coursecontext The course context being shared from.
* @return string Converted activity description.
*/
protected function get_resource_description(
\context $coursecontext,
): string {
global $PAGE, $DB;
// We need to set the page context here because content_to_text and format_text will need the page context to work.
$PAGE->set_context($coursecontext);
$intro = $DB->get_record($this->cminfo->modname, ['id' => $this->cminfo->instance], 'intro, introformat', MUST_EXIST);
$processeddescription = strip_tags($intro->intro);
$processeddescription = content_to_text(format_text(
$processeddescription,
$intro->introformat,
), $intro->introformat);
return $processeddescription;
}
}
+64
View File
@@ -0,0 +1,64 @@
<?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 backup;
use backup_controller;
use stdClass;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
/**
* Packager to prepare appropriate backup of a course to share to MoodleNet.
*
* @package core
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_packager extends resource_packager {
/**
* Constructor for course packager.
*
* @param stdClass $course The course to package
* @param int $userid The ID of the user performing the packaging
*/
public function __construct(
stdClass $course,
int $userid,
) {
parent::__construct($course, $userid, $course->shortname);
}
/**
* Get the backup controller for the course.
*
* @return backup_controller the backup controller for the course.
*/
protected function get_backup_controller(): backup_controller {
return new backup_controller(
backup::TYPE_1COURSE,
$this->course->id,
backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO,
backup::MODE_GENERAL,
$this->userid,
);
}
}
@@ -0,0 +1,99 @@
<?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 backup_activity_task;
use backup_controller;
use stdClass;
use stored_file;
/**
* Packager to prepare appropriate backup of a number of activities in a course to share to MoodleNet.
*
* @package core
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_partial_packager extends course_packager {
/**
* @var int[] $cmids List of course module ids of selected activities.
*/
protected array $cmids;
/**
* Constructor for course partial packager.
*
* @param stdClass $course The course to package
* @param array $cmids List of course module id of selected activities.
* @param int $userid The ID of the user performing the packaging
*/
public function __construct(
stdClass $course,
array $cmids,
int $userid,
) {
$this->cmids = $cmids;
parent::__construct($course, $userid);
}
/**
* Package the resource identified by resource id into a new stored_file.
*
* @param backup_controller $controller The backup controller.
* @return stored_file
*/
protected function package(backup_controller $controller): stored_file {
$this->remove_unselected_activities($controller);
return parent::package($controller);
}
/**
* Remove unselected activities in the course backup.
*
* @param backup_controller $controller The backup controller.
*/
protected function remove_unselected_activities(backup_controller $controller): void {
foreach ($this->get_all_activity_tasks($controller) as $task) {
foreach ($task->get_settings() as $setting) {
if (in_array($task->get_moduleid(), $this->cmids) &&
str_contains($setting->get_name(), '_included') !== false) {
$setting->set_value(1);
} else {
$setting->set_value(0);
}
}
}
}
/**
* Get all the activity tasks in the controller.
*
* @param backup_controller $controller The backup controller.
* @return backup_activity_task[] Array of activity tasks.
*/
protected function get_all_activity_tasks(backup_controller $controller): array {
$tasks = [];
foreach ($controller->get_plan()->get_tasks() as $task) {
if (! $task instanceof backup_activity_task) { // Only for activity tasks.
continue;
}
$tasks[] = $task;
}
return $tasks;
}
}
@@ -0,0 +1,100 @@
<?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 core\event\moodlenet_resource_exported;
use core\oauth2\client;
use moodle_exception;
/**
* API for sharing a number of Moodle LMS activities as a course backup to MoodleNet instances.
*
* @package core
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_partial_sender extends course_sender {
/**
* Constructor for course sender.
*
* @param int $courseid The course ID of the course being shared
* @param int $userid The user ID who is sharing the activity
* @param moodlenet_client $moodlenetclient The moodlenet_client object used to perform the share
* @param 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(
int $courseid,
protected int $userid,
protected moodlenet_client $moodlenetclient,
protected client $oauthclient,
protected array $cmids,
protected int $shareformat = self::SHARE_FORMAT_BACKUP,
) {
parent::__construct($courseid, $userid, $moodlenetclient, $oauthclient, $shareformat);
$this->validate_course_module_ids($this->course, $this->cmids);
$this->packager = new course_partial_packager($this->course, $this->cmids, $this->userid);
}
/**
* Log an event to the admin logs for an outbound share attempt.
*
* @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(
string $resourceurl,
int $responsecode,
): void {
$event = moodlenet_resource_exported::create([
'context' => $this->coursecontext,
'other' => [
'cmids' => $this->cmids,
'courseid' => [$this->course->id],
'resourceurl' => $resourceurl,
'success' => ($responsecode === 201),
],
]);
$event->trigger();
}
/**
* Validate the course module ids.
*
* @param \stdClass $course Course object
* @param array $cmids List of course module ids to check
* @return void
*/
protected function validate_course_module_ids(
\stdClass $course,
array $cmids,
): void {
if (empty($cmids)) {
throw new moodle_exception('invalidcoursemodule');
}
$modinfo = get_fast_modinfo($course);
$cms = $modinfo->get_cms();
foreach ($cmids as $cmid) {
if (!array_key_exists($cmid, $cms)) {
throw new moodle_exception('invalidcoursemodule');
}
}
}
}
+180
View File
@@ -0,0 +1,180 @@
<?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 core\event\moodlenet_resource_exported;
use core\oauth2\client;
use moodle_exception;
use stored_file;
/**
* API for sharing Moodle LMS courses to MoodleNet instances.
*
* @package core
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_sender extends resource_sender {
/**
* @var \core\context\course|false The course context.
*/
protected \core\context\course|false $coursecontext;
/**
* Constructor for course sender.
*
* @param int $courseid The course ID of the course being shared
* @param int $userid The user ID who is sharing the activity
* @param moodlenet_client $moodlenetclient The moodlenet_client object used to perform the share
* @param 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(
int $courseid,
protected int $userid,
protected moodlenet_client $moodlenetclient,
protected client $oauthclient,
protected int $shareformat = self::SHARE_FORMAT_BACKUP,
) {
parent::__construct($courseid, $userid, $moodlenetclient, $oauthclient, $shareformat);
$this->course = get_course($courseid);
$this->coursecontext = \core\context\course::instance($courseid);
$this->packager = new course_packager($this->course, $this->userid);
}
/**
* Share a course 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']
*/
public function share_resource(): array {
$accesstoken = '';
$issuer = $this->oauthclient->get_issuer();
// Check user can share to the requested MoodleNet instance.
$usercanshare = utilities::can_user_share($this->coursecontext, $this->userid, 'course');
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');
}
// 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();
// Avoid sending a file larger than the defined limit.
$filesize = $filedata->get_filesize();
if ($filesize > self::MAX_FILESIZE) {
$filedata->delete();
throw new moodle_exception('moodlenet:sharefilesizelimitexceeded', 'core', '', [
'filesize' => $filesize,
'filesizelimit' => self::MAX_FILESIZE,
]);
}
// MoodleNet only accept plaintext descriptions.
$resourcedescription = $this->get_resource_description();
$response = $this->moodlenetclient->create_resource_from_stored_file(
$filedata,
$this->course->fullname,
$resourcedescription,
);
$responsecode = $response->getStatusCode();
$responsebody = json_decode($response->getBody(), false, 512, JSON_THROW_ON_ERROR);
$resourceurl = $responsebody->homepage ?? '';
// Delete the generated file now it is no longer required.
// (It has either been sent, or failed - retries not currently supported).
$filedata->delete();
// Log every attempt to share (and whether it was successful).
$this->log_event($resourceurl, $responsecode);
return [
'responsecode' => $responsecode,
'drafturl' => $resourceurl,
];
}
/**
* Log an event to the admin logs for an outbound share attempt.
*
* @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(
string $resourceurl,
int $responsecode,
): void {
$event = moodlenet_resource_exported::create([
'context' => $this->coursecontext,
'other' => [
'courseid' => [$this->course->id],
'resourceurl' => $resourceurl,
'success' => ($responsecode === 201),
],
]);
$event->trigger();
}
/**
* Return the list of supported share formats.
*
* @return array Array of supported share format values.
*/
protected static function get_allowed_share_formats(): array {
return [
self::SHARE_FORMAT_BACKUP,
];
}
/**
* Fetch the description for the resource being created, in a supported text format.
*
* @return string Converted course description.
*/
protected function get_resource_description(): string {
global $PAGE;
// We need to set the page context here because content_to_text and format_text will need the page context to work.
$PAGE->set_context($this->coursecontext);
$processeddescription = strip_tags($this->course->summary);
$processeddescription = content_to_text
(
format_text(
$processeddescription,
$this->course->summaryformat,
),
$this->course->summaryformat
);
return $processeddescription;
}
}
+139
View File
@@ -0,0 +1,139 @@
<?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 core\http_client;
use core\oauth2\client;
use stored_file;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* MoodleNet client which handles direct outbound communication with MoodleNet instances.
*
* @package core
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class moodlenet_client {
/**
* @var string MoodleNet resource creation endpoint URI.
*/
protected const API_CREATE_RESOURCE_URI = '/.pkg/@moodlenet/ed-resource/basic/v1/create';
/**
* @var string MoodleNet scope for creating resources.
*/
public const API_SCOPE_CREATE_RESOURCE = '@moodlenet/ed-resource:write.own';
/**
* Constructor.
*
* @param http_client $httpclient The httpclient object being used to perform the share.
* @param client $oauthclient The OAuth 2 client for the MoodleNet site being shared to.
*/
public function __construct(
protected http_client $httpclient,
protected client $oauthclient,
) {
// All properties promoted, nothing further required.
}
/**
* Create a resource on MoodleNet which includes a file.
*
* @param stored_file $file The file data to send to MoodleNet.
* @param string $resourcename The name of the resource being shared.
* @param string $resourcedescription A description of the resource being shared.
* @return \Psr\Http\Message\ResponseInterface The HTTP client response from MoodleNet.
*/
public function create_resource_from_stored_file(
stored_file $file,
string $resourcename,
string $resourcedescription,
): ResponseInterface {
// This may take a long time if a lot of data is being shared.
\core_php_time_limit::raise();
$moodleneturl = $this->oauthclient->get_issuer()->get('baseurl');
$apiurl = rtrim($moodleneturl, '/') . self::API_CREATE_RESOURCE_URI;
$stream = $file->get_psr_stream();
$requestdata = $this->prepare_file_share_request_data(
$file->get_filename(),
$file->get_mimetype(),
$stream,
$resourcename,
$resourcedescription,
);
try {
$response = $this->httpclient->request('POST', $apiurl, $requestdata);
} finally {
$stream->close(); // Always close the request stream ASAP. Or it will remain open till shutdown/destruct.
}
return $response;
}
/**
* Prepare the request data required for sharing a file to MoodleNet.
* This creates an array in the format used by \core\httpclient options to send a multipart request.
*
* @param string $filename Name of the file being shared.
* @param string $mimetype Mime type of the file being shared.
* @param StreamInterface $stream Stream of the file being shared.
* @param string $resourcename The name of the resource being shared.
* @param string $resourcedescription A description of the resource being shared.
* @return array Data in the format required to send a file to MoodleNet using \core\httpclient.
*/
protected function prepare_file_share_request_data(
string $filename,
string $mimetype,
StreamInterface $stream,
string $resourcename,
string $resourcedescription,
): array {
return [
'headers' => [
'Authorization' => 'Bearer ' . $this->oauthclient->get_accesstoken()->token,
],
'multipart' => [
[
'name' => 'metadata',
'contents' => json_encode([
'name' => $resourcename,
'description' => $resourcedescription,
]),
'headers' => [
'Content-Disposition' => 'form-data; name="."',
],
],
[
'name' => 'filecontents',
'contents' => $stream,
'headers' => [
'Content-Disposition' => 'form-data; name=".resource"; filename="' . $filename . '"',
'Content-Type' => $mimetype,
'Content-Transfer-Encoding' => 'binary',
],
],
],
];
}
}
+191
View File
@@ -0,0 +1,191 @@
<?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 backup_controller;
use backup_root_task;
use cm_info;
use core\context\user;
use stdClass;
use stored_file;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
/**
* Base packager to prepare appropriate backup of a resource to share to MoodleNet.
*
* @package core
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class resource_packager {
/**
* @var string $resourcefilename The filename for the resource.
*/
protected string $resourcefilename = 'resource';
/**
* @var stdClass $course The course which the resource belongs to.
*/
protected stdClass $course;
/**
* @var cm_info $cminfo The course module which the resource belongs to.
*/
protected cm_info $cminfo;
/**
* @var int $userid The ID of the user performing the packaging.
*/
protected int $userid;
/**
* Constructor for the base packager.
*
* @param stdClass|cm_info $resource The resource object
* @param int $userid The user id
*/
public function __construct(
stdClass|cm_info $resource,
int $userid,
string $resourcefilename,
) {
if ($resource instanceof cm_info) {
$this->cminfo = $resource;
$this->course = $resource->get_course();
} else {
$this->course = $resource;
}
$this->userid = $userid;
$this->resourcefilename = $resourcefilename;
}
/**
* Get the backup controller for the course.
*
* @return backup_controller The backup controller instance that will be used to package the resource.
*/
abstract protected function get_backup_controller(): backup_controller;
/**
* Prepare the backup file using appropriate setting overrides and return relevant information.
*
* @return stored_file
*/
public function get_package(): stored_file {
$controller = $this->get_backup_controller();
$alltasksettings = $this->get_all_task_settings($controller);
// Override relevant settings to remove user data when packaging to share to MoodleNet.
$this->override_task_setting($alltasksettings, 'setting_root_users', 0);
$this->override_task_setting($alltasksettings, 'setting_root_role_assignments', 0);
$this->override_task_setting($alltasksettings, 'setting_root_blocks', 0);
$this->override_task_setting($alltasksettings, 'setting_root_comments', 0);
$this->override_task_setting($alltasksettings, 'setting_root_badges', 0);
$this->override_task_setting($alltasksettings, 'setting_root_userscompletion', 0);
$this->override_task_setting($alltasksettings, 'setting_root_logs', 0);
$this->override_task_setting($alltasksettings, 'setting_root_grade_histories', 0);
$this->override_task_setting($alltasksettings, 'setting_root_groups', 0);
$storedfile = $this->package($controller);
$controller->destroy(); // We are done with the controller, destroy it.
return $storedfile;
}
/**
* Get all backup settings available for override.
*
* @return array the associative array of taskclass => settings instances.
*/
protected function get_all_task_settings(backup_controller $controller): array {
$tasksettings = [];
foreach ($controller->get_plan()->get_tasks() as $task) {
$taskclass = get_class($task);
$tasksettings[$taskclass] = $task->get_settings();
}
return $tasksettings;
}
/**
* Override a backup task setting with a given value.
*
* @param array $alltasksettings All task settings.
* @param string $settingname The name of the setting to be overridden (task class name format).
* @param int $settingvalue Value to be given to the setting.
*/
protected function override_task_setting(array $alltasksettings, string $settingname, int $settingvalue): void {
if (empty($rootsettings = $alltasksettings[backup_root_task::class])) {
return;
}
foreach ($rootsettings as $setting) {
$name = $setting->get_ui_name();
if ($name == $settingname && $settingvalue != $setting->get_value()) {
$setting->set_value($settingvalue);
return;
}
}
}
/**
* Package the resource identified by resource id into a new stored_file.
*
* @param backup_controller $controller The backup controller.
* @return stored_file
*/
protected function package(backup_controller $controller): stored_file {
// Execute the backup and fetch the result.
$controller->execute_plan();
$result = $controller->get_results();
if (!isset($result['backup_destination'])) {
throw new \moodle_exception('Failed to package resource.');
}
$backupfile = $result['backup_destination'];
if (!$backupfile->get_contenthash()) {
throw new \moodle_exception('Failed to package resource (invalid file).');
}
// Create the location we want to copy this file to.
$filerecord = [
'contextid' => user::instance($this->userid)->id,
'userid' => $this->userid,
'component' => 'user',
'filearea' => 'draft',
'itemid' => file_get_unused_draft_itemid(),
'filepath' => '/',
'filename' => $this->resourcefilename . '_backup.mbz',
];
// Create the local file based on the backup.
$fs = get_file_storage();
$file = $fs->create_file_from_storedfile($filerecord, $backupfile);
// Delete the backup now it has been created in the file area.
$backupfile->delete();
return $file;
}
}
+103
View File
@@ -0,0 +1,103 @@
<?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 core\oauth2\client;
use moodle_exception;
use stdClass;
use stored_file;
/**
* API for sharing Moodle LMS resources to MoodleNet instances.
*
* @package core
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class resource_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 stdClass The course where the activity is located.
*/
protected stdClass $course;
/** @var resource_packager Resource packager. */
protected resource_packager $packager;
/**
* Class constructor.
*
* @param int $resourceid The resource ID of the resource being shared.
* @param int $userid The user ID who is sharing the activity.
* @param moodlenet_client $moodlenetclient The moodlenet_client object used to perform the share.
* @param 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).
* @throws moodle_exception
*/
public function __construct(
int $resourceid,
protected int $userid,
protected moodlenet_client $moodlenetclient,
protected client $oauthclient,
protected int $shareformat = self::SHARE_FORMAT_BACKUP,
) {
if (!in_array($shareformat, self::get_allowed_share_formats(), true)) {
throw new moodle_exception('moodlenet:invalidshareformat');
}
}
/**
* Return the list of supported share formats.
*
* @return array Array of supported share format values.
*/
protected static function get_allowed_share_formats(): array {
return [
self::SHARE_FORMAT_BACKUP,
];
}
/**
* Share a 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']
*/
abstract public function share_resource(): array;
/**
* Prepare the data for sharing, in the format specified.
*
* @return stored_file
*/
protected function prepare_share_contents(): stored_file {
return match ($this->shareformat) {
self::SHARE_FORMAT_BACKUP => $this->packager->get_package(),
default => throw new \coding_exception("Unknown share format: {$this->shareformat}'"),
};
}
}
@@ -0,0 +1,252 @@
<?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/>.
/**
* MoodleNet share progress table.
*
* @package core
* @copyright 2023 David Woloszyn <david.woloszyn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\moodlenet;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/tablelib.php');
use html_writer;
use moodle_url;
use stdClass;
use table_sql;
/**
* MoodleNet share progress table.
*
* @package core
* @copyright 2023 David Woloszyn <david.woloszyn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class share_progress_table extends table_sql {
/** @var int The user id records will be displayed for. */
protected $userid;
/**
* Set up the table.
*
* @param string $uniqueid Unique id of table.
* @param moodle_url $url The base URL.
* @param int $userid The user id.
*/
public function __construct($uniqueid, $url, $userid) {
parent::__construct($uniqueid);
$this->userid = $userid;
$this->define_table_columns();
$this->define_baseurl($url);
$this->define_table_configs();
}
/**
* Define table configs.
*/
protected function define_table_configs() {
$this->collapsible(false);
$this->sortable(false);
$this->pageable(true);
$this->set_default_per_page(25);
}
/**
* Set up the columns and headers.
*/
protected function define_table_columns() {
// Define headers and columns.
$cols = [
'name' => get_string('name'),
'type' => get_string('moodlenet:columntype'),
'timecreated' => get_string('moodlenet:columnsenddate'),
'status' => get_string('moodlenet:columnsendstatus'),
];
$this->define_columns(array_keys($cols));
$this->define_headers(array_values($cols));
$this->column_class('status', 'text-center');
}
/**
* Name column.
*
* @param stdClass $row Row data.
* @return string
*/
protected function col_name(stdClass $row): string {
global $OUTPUT;
$class = '';
// Track deletion of resources on Moodle.
$deleted = false;
// Courses.
if ($row->type == share_recorder::TYPE_COURSE) {
if ($row->fullname) {
$name = $row->fullname;
} else {
$name = get_string('moodlenet:deletedcourse');
$deleted = true;
}
// Activities.
} else if ($row->type == share_recorder::TYPE_ACTIVITY) {
if ($cm = get_coursemodule_from_id('', $row->cmid)) {
$name = $cm->name;
} else {
$name = get_string('moodlenet:deletedactivity');
$deleted = true;
}
}
if ($deleted) {
$class = 'font-italic';
}
// Add a link to the resource if it was recorded.
if (!empty($row->resourceurl)) {
// Apply bold to resource links that aren't deleted.
if (!$deleted) {
$class = 'font-weight-bold';
}
$icon = $OUTPUT->pix_icon('i/externallink', get_string('opensinnewwindow'), 'moodle', ['class' => 'ml-1']);
$text = $name . $icon;
$attributes = [
'target' => '_blank',
'rel' => 'noopener noreferrer',
];
$name = html_writer::link($row->resourceurl, $text, $attributes);
}
return html_writer::span($name, $class);
}
/**
* Type column.
*
* @param stdClass $row Row data.
* @return string
*/
protected function col_type(stdClass $row): string {
// Courses.
if ($row->type == share_recorder::TYPE_COURSE) {
$type = get_string('course');
// Activities.
} else if ($row->type == share_recorder::TYPE_ACTIVITY) {
if ($row->modname) {
$type = get_string('modulename', $row->modname);
} else {
// Alternatively, default to 'activity'.
$type = get_string('activity');
}
}
return $type;
}
/**
* Time created column (Send date).
*
* @param stdClass $row Row data.
* @return string
*/
protected function col_timecreated(stdClass $row): string {
$format = get_string('strftimedatefullshort', 'core_langconfig');
return userdate($row->timecreated, $format);
}
/**
* Status column (Send status).
*
* @param stdClass $row Row data.
* @return string
*/
protected function col_status(stdClass $row): string {
// Display a badge indicating the status of the share.
if ($row->status == share_recorder::STATUS_IN_PROGRESS) {
$status = html_writer::span(get_string('inprogress'), 'badge bg-warning text-dark');
} else if ($row->status == share_recorder::STATUS_SENT) {
$status = html_writer::span(get_string('sent'), 'badge bg-success text-white');
} else if ($row->status == share_recorder::STATUS_ERROR) {
$status = html_writer::span(get_string('error'), 'badge bg-danger text-white');
}
return $status;
}
/**
* Builds the SQL query.
*
* @param bool $count When true, return the count SQL.
* @return array containing sql to use and an array of params.
*/
protected function get_sql_and_params($count = false) {
if ($count) {
$select = "COUNT(1)";
} else {
$select = "msp.id, msp.type, msp.courseid, msp.cmid, msp.timecreated, " .
"msp.resourceurl, msp.status, c.fullname, md.name AS modname";
}
$sql = "SELECT $select
FROM {moodlenet_share_progress} msp
LEFT JOIN {course} c ON c.id = msp.courseid
LEFT JOIN {course_modules} cm ON cm.course = msp.courseid
AND cm.id = msp.cmid
LEFT JOIN {modules} md ON md.id = cm.module
WHERE msp.userid = :userid";
$params = ['userid' => $this->userid];
if (!$count) {
$sql .= " ORDER BY msp.status DESC, msp.timecreated DESC";
}
return [$sql, $params];
}
/**
* Query the DB.
*
* @param int $pagesize size of page for paginated displayed table.
* @param bool $useinitialsbar do you want to use the initials bar.
*/
public function query_db($pagesize, $useinitialsbar = true) {
global $DB;
list($countsql, $countparams) = $this->get_sql_and_params(true);
list($sql, $params) = $this->get_sql_and_params();
$total = $DB->count_records_sql($countsql, $countparams);
$this->pagesize($pagesize, $total);
$this->rawdata = $DB->get_records_sql($sql, $params, $this->get_page_start(), $this->get_page_size());
// Set initial bars.
if ($useinitialsbar) {
$this->initialbars($total > $pagesize);
}
}
/**
* Notification to display when there are no results.
*/
public function print_nothing_to_display() {
global $OUTPUT;
echo $OUTPUT->notification(get_string('moodlenet:nosharedresources'), 'info');
}
}
+132
View File
@@ -0,0 +1,132 @@
<?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 moodle_exception;
use stdClass;
/**
* Record the sharing of content to MoodleNet.
*
* @package core
* @copyright 2023 David Woloszyn <david.woloszyn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class share_recorder {
/**
* @var int The content being shared is a course.
*/
public const TYPE_COURSE = 1;
/**
* @var int The content being shared is an activity.
*/
public const TYPE_ACTIVITY = 2;
/**
* @var int The status of the share is 'sent'.
*/
public const STATUS_SENT = 1;
/**
* @var int The status of the share is 'in progress'.
*/
public const STATUS_IN_PROGRESS = 2;
/**
* @var int The status of the share is 'error'.
*/
public const STATUS_ERROR = 3;
/**
* Get all allowed share types.
*
* @return array
*/
protected static function get_allowed_share_types(): array {
return [
self::TYPE_ACTIVITY,
self::TYPE_COURSE
];
}
/**
* Get all allowed share statuses.
* Note that the particular status values aid in sorting.
*
* @return array
*/
protected static function get_allowed_share_statuses(): array {
return [
self::STATUS_SENT,
self::STATUS_IN_PROGRESS,
self::STATUS_ERROR,
];
}
/**
* Create a new share progress record in the DB.
*
* @param int $sharetype The type of share (e.g. TYPE_COURSE).
* @param int $userid The ID of the user performing the share.
* @param int $courseid The associated course id.
* @param int|null $cmid The associated course module id (when sharing activity).
* @return int Returns the inserted record id.
*/
public static function insert_share_progress(int $sharetype, int $userid, int $courseid, ?int $cmid = null): int {
global $DB, $USER;
if (!in_array($sharetype, self::get_allowed_share_types())) {
throw new moodle_exception('moodlenet:invalidsharetype');
}
$data = new stdClass();
$data->type = $sharetype;
$data->courseid = $courseid;
$data->cmid = $cmid;
$data->userid = $userid;
$data->timecreated = time();
$data->status = self::STATUS_IN_PROGRESS;
return $DB->insert_record('moodlenet_share_progress', $data);
}
/**
* Update the share progress record in the DB.
*
* @param int $shareid The id of the share progress row being updated.
* @param int $status The status of the share progress (e.g. STATUS_SENT).
* @param string|null $resourceurl The resource url returned from MoodleNet.
*/
public static function update_share_progress(int $shareid, int $status, ?string $resourceurl = null): void {
global $DB;
if (!in_array($status, self::get_allowed_share_statuses())) {
throw new moodle_exception('moodlenet:invalidsharestatus');
}
$data = new stdClass();
$data->id = $shareid;
$data->resourceurl = $resourceurl;
$data->status = $status;
$DB->update_record('moodlenet_share_progress', $data);
}
}
+123
View File
@@ -0,0 +1,123 @@
<?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 core\oauth2\issuer;
/**
* Class containing static utilities (such as various checks) required by the MoodleNet API.
*
* @package core
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class utilities {
/**
* Check whether the specified issuer is configured as a MoodleNet instance that can be shared to.
*
* @param issuer $issuer The OAuth 2 issuer being validated.
* @return bool true if the issuer is enabled and available to share to.
*/
public static function is_valid_instance(issuer $issuer): bool {
global $CFG;
$issuerid = $issuer->get('id');
$allowedissuer = get_config('moodlenet', 'oauthservice');
return ($CFG->enablesharingtomoodlenet && $issuerid == $allowedissuer && $issuer->get('enabled') &&
$issuer->get('servicetype') == 'moodlenet');
}
/**
* Check whether a user has the capabilities required to share activities or courses to MoodleNet.
*
* @param \core\context\course $coursecontext Course context where the activity or course would be shared from.
* @param int $userid The user ID being checked.
* @param string $type The type of resource being checked (either 'activity' or 'course').
* @return boolean
* @throws \coding_exception If an invalid resource type is provided.
*/
public static function can_user_share(\core\context\course $coursecontext, int $userid, string $type = 'activity'): bool {
if ($type === 'course') {
return (has_capability('moodle/moodlenet:sharecourse', $coursecontext, $userid) &&
has_capability('moodle/backup:backupcourse', $coursecontext, $userid));
} else if ($type === 'activity') {
return (has_capability('moodle/moodlenet:shareactivity', $coursecontext, $userid) &&
has_capability('moodle/backup:backupactivity', $coursecontext, $userid));
}
throw new \coding_exception('Invalid resource type');
}
/**
* Get the support url.
*
* @return string
*/
public static function get_support_url(): string {
global $CFG;
$supporturl = '';
if ($CFG->supportavailability && $CFG->supportavailability !== CONTACT_SUPPORT_DISABLED) {
if (!empty($CFG->supportpage)) {
$supporturl = $CFG->supportpage;
} else {
$supporturl = $CFG->wwwroot . '/user/contactsitesupport.php';
}
}
return $supporturl;
}
/**
* Check the user has a valid capability in any course they are enrolled in.
*
* @param int $userid The user id to query
* @return string Returns 'yes' if capability found, 'no' if not
*/
public static function does_user_have_capability_in_any_course(int $userid): string {
// We are checking this way because we are not always in the course context and need
// a way to retrieve the user's courses to see if any of them have the correct capability.
$capabilities = [
'moodle/moodlenet:sharecourse',
'moodle/moodlenet:shareactivity'
];
// We are using 'no' instead of false to avoid confusing a cache key
// that was not found (returns false) with a user who does not have the capablity.
$isallowed = 'no';
$cache = \cache::make('core', 'moodlenet_usercanshare');
$cachedvalue = $cache->get($userid);
if ($cachedvalue === false) {
foreach ($capabilities as $capability) {
// Find at least one course that contains a capability match.
$course = get_user_capability_course($capability, $userid, true, '', 'id', 1);
if (!empty($course)) {
// Set the cache to 'yes' and break out of the loop.
$isallowed = 'yes';
break;
}
}
$cache->set($userid, $isallowed);
} else {
$isallowed = $cachedvalue;
}
return $isallowed;
}
}