Merge branch 'MDL-83519-main' of https://github.com/HuongNV13/moodle
This commit is contained in:
@@ -1759,6 +1759,12 @@ $string['privacy:metadata:oauth2_refresh_token:token'] = 'The refresh token for
|
||||
$string['privacy:metadata:oauth2_refresh_token:timecreated'] = 'The time when the token was created';
|
||||
$string['privacy:metadata:oauth2_refresh_token:timemodified'] = 'The time when the token was last updated';
|
||||
$string['privacy:metadata:oauth2_refresh_token:userid'] = 'The ID of the user to whom the token corresponds';
|
||||
$string['privacy:metadata:shortlink'] = 'Shortlink URL details';
|
||||
$string['privacy:metadata:shortlink:shortcode'] = 'The shortcode to use for the shortlink';
|
||||
$string['privacy:metadata:shortlink:userid'] = 'The ID of the user associated with the shortlink';
|
||||
$string['privacy:metadata:shortlink:component'] = 'The component associated with the shortlink';
|
||||
$string['privacy:metadata:shortlink:linktype'] = 'The type of link the shortlink can be identified as';
|
||||
$string['privacy:metadata:shortlink:identifier'] = 'The unique identifier associated with the shortlink';
|
||||
$string['privacy:metadata:task_adhoc'] = 'The status of ad hoc tasks.';
|
||||
$string['privacy:metadata:task_adhoc:component'] = 'The component owning the task.';
|
||||
$string['privacy:metadata:task_adhoc:nextruntime'] = 'The earliest time to run this task.';
|
||||
|
||||
@@ -142,6 +142,15 @@ class provider implements
|
||||
'resourceurl' => 'privacy:metadata:moodlenet_share_progress:resourceurl',
|
||||
], 'privacy:metadata:moodlenet_share_progress');
|
||||
|
||||
// The shortlink table includes data that associates a user with a shortlink URL.
|
||||
$collection->add_database_table('shortlink', [
|
||||
'shortcode' => 'privacy:metadata:shortlink:shortcode',
|
||||
'userid' => 'privacy:metadata:shortlink:userid',
|
||||
'component' => 'privacy:metadata:shortlink:component',
|
||||
'linktype' => 'privacy:metadata:shortlink:linktype',
|
||||
'identifier' => 'privacy:metadata:shortlink:identifier',
|
||||
], 'privacy:metadata:shortlink');
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
@@ -163,6 +172,15 @@ class provider implements
|
||||
$params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Shortlink.
|
||||
$sql = "SELECT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {shortlink} sl ON ctx.instanceid = sl.userid
|
||||
AND ctx.contextlevel = :contextlevel
|
||||
WHERE sl.userid = :userid";
|
||||
$params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
return $contextlist;
|
||||
}
|
||||
|
||||
@@ -172,7 +190,6 @@ class provider implements
|
||||
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
|
||||
*/
|
||||
public static function get_users_in_context(userlist $userlist) {
|
||||
// Except for moodlenet_share_progress, don't add any users.
|
||||
$context = $userlist->get_context();
|
||||
|
||||
// MoodleNet share progress uses the user context.
|
||||
@@ -184,6 +201,16 @@ class provider implements
|
||||
$params = ['userid' => $context->instanceid];
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
|
||||
// Shortlink.
|
||||
if ($context->contextlevel == CONTEXT_USER) {
|
||||
// Get all distinct userids from the table.
|
||||
$sql = "SELECT DISTINCT userid
|
||||
FROM {shortlink}
|
||||
WHERE userid = :userid";
|
||||
$params = ['userid' => $context->instanceid];
|
||||
$userlist->add_from_sql('userid', $sql, $params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,16 +219,20 @@ class provider implements
|
||||
* @param approved_contextlist $contextlist The approved contexts to export information for.
|
||||
*/
|
||||
public static function export_user_data(approved_contextlist $contextlist) {
|
||||
// Except for moodlenet_share_progress, none of the core tables should be exported.
|
||||
// Except for moodlenet_share_progress and shortlink, none of the core tables should be exported.
|
||||
global $DB;
|
||||
|
||||
foreach ($contextlist as $context) {
|
||||
// MoodleNet share progress uses the user context.
|
||||
if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $contextlist->get_user()->id) {
|
||||
// Get the user's MoodleNet share progress data.
|
||||
$sharedata = $DB->get_records('moodlenet_share_progress', ['userid' => $context->instanceid]);
|
||||
$subcontext = get_string('privacy:metadata:moodlenet_share_progress', 'moodle');
|
||||
writer::with_context($context)->export_data([$subcontext], (object) $sharedata);
|
||||
|
||||
// Get the user's shortlink data.
|
||||
$shortlinkdata = $DB->get_records('shortlink', ['userid' => $context->instanceid]);
|
||||
$subcontext = get_string('privacy:metadata:shortlink', 'moodle');
|
||||
writer::with_context($context)->export_data([$subcontext], (object) $shortlinkdata);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,13 +243,18 @@ class provider implements
|
||||
* @param \context $context The specific context to delete data for.
|
||||
*/
|
||||
public static function delete_data_for_all_users_in_context(\context $context) {
|
||||
// Except for moodlenet_share_progress, none of the the data from these tables should be deleted.
|
||||
// Except for moodlenet_share_progress and shortlink, none of the data from these tables should be deleted.
|
||||
global $DB;
|
||||
|
||||
// MoodleNet share progress uses the user context.
|
||||
if ($context->contextlevel == CONTEXT_USER) {
|
||||
$DB->delete_records('moodlenet_share_progress', ['userid' => $context->instanceid]);
|
||||
}
|
||||
|
||||
// Shortlink.
|
||||
if ($context->contextlevel == CONTEXT_USER) {
|
||||
$DB->delete_records('shortlink', ['userid' => $context->instanceid]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,15 +263,17 @@ class provider implements
|
||||
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_user(approved_contextlist $contextlist) {
|
||||
// Except for moodlenet_share_progress, none of the the data from these tables should be deleted.
|
||||
// Except for moodlenet_share_progress and shortlink, none of the data from these tables should be deleted.
|
||||
// Note: Although it may be tempting to delete the adhoc task data, do not do so.
|
||||
// The delete process is run as an adhoc task.
|
||||
global $DB;
|
||||
|
||||
foreach ($contextlist as $context) {
|
||||
// MoodleNet share progress uses the user context.
|
||||
if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $contextlist->get_user()->id) {
|
||||
// MoodleNet share progress uses the user context.
|
||||
$DB->delete_records('moodlenet_share_progress', ['userid' => $context->instanceid]);
|
||||
// Shortlink.
|
||||
$DB->delete_records('shortlink', ['userid' => $context->instanceid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,7 +284,7 @@ class provider implements
|
||||
* @param approved_userlist $userlist The approved context and user information to delete information for.
|
||||
*/
|
||||
public static function delete_data_for_users(approved_userlist $userlist) {
|
||||
// Except for moodlenet_share_progress, none of the the data from these tables should be deleted.
|
||||
// Except for moodlenet_share_progress and shortlink, none of the data from these tables should be deleted.
|
||||
// Note: Although it may be tempting to delete the adhoc task data, do not do so.
|
||||
// The delete process is run as an adhoc task.
|
||||
global $DB;
|
||||
@@ -257,9 +295,11 @@ class provider implements
|
||||
return;
|
||||
}
|
||||
|
||||
// MoodleNet share progress uses the user context.
|
||||
if ($context->contextlevel == CONTEXT_USER) {
|
||||
// MoodleNet share progress uses the user context.
|
||||
$DB->delete_records('moodlenet_share_progress', ['userid' => $context->instanceid]);
|
||||
// Shortlink.
|
||||
$DB->delete_records('shortlink', ['userid' => $context->instanceid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<?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\route;
|
||||
|
||||
use core\exception\coding_exception;
|
||||
use core\router\route;
|
||||
use core\router\util;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Shortlink route handler.
|
||||
*
|
||||
* Shortlinks are shorter URLs that redirect to longer Moodle URLs.
|
||||
* E.g. http://mymoodle.com/s/AbCd => http://mymoodle.com/course/view.php?id=11.
|
||||
*
|
||||
* @package core
|
||||
* @copyright Andrew Lyons <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class shortlink {
|
||||
/**
|
||||
* Handle a user-specific Moodle shortlink.
|
||||
*
|
||||
* @param \Psr\Http\Message\ServerRequestInterface $request
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
* @param string $shortcode
|
||||
* @param \moodle_database $db
|
||||
* @param \core\shortlink $manager
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
#[route(
|
||||
path: '/s/{shortcode}',
|
||||
pathtypes: [
|
||||
new \core\router\schema\parameters\path_parameter(
|
||||
name: 'shortcode',
|
||||
type: \core\param::ALPHANUMEXT,
|
||||
),
|
||||
],
|
||||
requirelogin: new \core\router\require_login(
|
||||
requirelogin: true,
|
||||
autologinguest: false,
|
||||
),
|
||||
)]
|
||||
public function handle_shortlink(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
string $shortcode,
|
||||
\moodle_database $db,
|
||||
\core\shortlink $manager,
|
||||
): ResponseInterface {
|
||||
try {
|
||||
$url = $manager->fetch_url_for_shortcode(true, $shortcode);
|
||||
return util::redirect($response, $url);
|
||||
} catch (coding_exception $e) {
|
||||
// Shortlink not found.
|
||||
return util::throw_page_not_found($request, $response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global shortlinks do not require login, but the point that they redirect to may.
|
||||
*
|
||||
* @param \Psr\Http\Message\ServerRequestInterface $request
|
||||
* @param \Psr\Http\Message\ResponseInterface $response
|
||||
* @param string $shortcode
|
||||
* @param \moodle_database $db
|
||||
* @param \core\shortlink $manager
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
#[route(
|
||||
path: '/p/{shortcode}',
|
||||
pathtypes: [
|
||||
new \core\router\schema\parameters\path_parameter(
|
||||
name: 'shortcode',
|
||||
type: \core\param::ALPHANUMEXT,
|
||||
),
|
||||
],
|
||||
)]
|
||||
public function handle_public_shortlink(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
string $shortcode,
|
||||
\moodle_database $db,
|
||||
\core\shortlink $manager,
|
||||
): ResponseInterface {
|
||||
try {
|
||||
$url = $manager->fetch_url_for_shortcode(false, $shortcode);
|
||||
return util::redirect($response, $url);
|
||||
} catch (coding_exception $e) {
|
||||
// Shortlink not found.
|
||||
return util::throw_page_not_found($request, $response, $e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ class router {
|
||||
}
|
||||
}
|
||||
|
||||
if ($CFG->routerconfigured !== true || $userphp) {
|
||||
if ($CFG->routerconfigured != true || $userphp) {
|
||||
$scriptroot .= '/r.php';
|
||||
}
|
||||
|
||||
@@ -232,6 +232,7 @@ class router {
|
||||
route_loader_interface::ROUTE_GROUP_API => $this->configure_api_route($collection),
|
||||
route_loader_interface::ROUTE_GROUP_PAGE => $this->configure_standard_route($collection),
|
||||
route_loader_interface::ROUTE_GROUP_SHIM => $this->configure_shim_route($collection),
|
||||
route_loader_interface::ROUTE_GROUP_SHORTLINK => array_walk($collection, [$this, 'configure_shortlink_route']),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
@@ -273,6 +274,18 @@ class router {
|
||||
->add(di::get(validation_middleware::class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the Short link Route Middleware.
|
||||
*
|
||||
* @param RouteGroupInterface $group
|
||||
*/
|
||||
protected function configure_shortlink_route($group): void {
|
||||
$group
|
||||
->add(di::get(moodle_authentication_middleware::class))
|
||||
->add(di::get(validation_middleware::class));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configure caching for the routes.
|
||||
*/
|
||||
|
||||
@@ -34,13 +34,13 @@ class require_login {
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var bool Whether login is required */
|
||||
public bool $requirelogin = false,
|
||||
/** @var bool Whether a course login is required */
|
||||
/** @var bool Whether to require login or not */
|
||||
public bool $requirelogin = true,
|
||||
/** @var bool Whether to require course login or not */
|
||||
public bool $requirecourselogin = false,
|
||||
/** @var bool The name of the route attribute that the course object can be found in */
|
||||
/** @var string|null The route attribute name used for the course */
|
||||
protected ?string $courseattributename = null,
|
||||
/** @var bool Whether to automatically log in as guest */
|
||||
/** @var bool Whether to autologin guest users */
|
||||
public bool $autologinguest = true,
|
||||
) {
|
||||
if ($requirelogin && $requirecourselogin) {
|
||||
@@ -49,11 +49,13 @@ class require_login {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the course attribute name.
|
||||
* Get the attribute name used for the course.
|
||||
*
|
||||
* @return string
|
||||
* A null value is returned if the course attribute name is not set.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function get_course_attribute_name(): string {
|
||||
public function get_course_attribute_name(): ?string {
|
||||
return $this->courseattributename;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
namespace core\router;
|
||||
|
||||
use core\route\shortlink;
|
||||
use Slim\App;
|
||||
use Slim\Interfaces\RouteGroupInterface;
|
||||
use Slim\Interfaces\RouteInterface;
|
||||
@@ -35,6 +36,7 @@ class route_loader extends abstract_route_loader implements route_loader_interfa
|
||||
route_loader_interface::ROUTE_GROUP_API => $this->configure_api_routes($app, route_loader_interface::ROUTE_GROUP_API),
|
||||
route_loader_interface::ROUTE_GROUP_PAGE => $this->configure_standard_routes($app),
|
||||
route_loader_interface::ROUTE_GROUP_SHIM => $this->configure_shim_routes($app),
|
||||
route_loader_interface::ROUTE_GROUP_SHORTLINK => $this->configure_shortlink_routes($app),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -92,6 +94,23 @@ class route_loader extends abstract_route_loader implements route_loader_interfa
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure all shortlink routes.
|
||||
*
|
||||
* @param App $app
|
||||
* @return RouteInterface[]
|
||||
*/
|
||||
protected function configure_shortlink_routes(App $app): array {
|
||||
return array_map(
|
||||
function (array $moodleroute) use ($app): RouteInterface {
|
||||
$slimroute = $app->map(...$moodleroute);
|
||||
$this->set_route_name_for_callable($slimroute, $moodleroute['callable']);
|
||||
return $slimroute;
|
||||
},
|
||||
$this->get_all_shortlink_routes(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all API routes.
|
||||
*
|
||||
@@ -174,4 +193,26 @@ class route_loader extends abstract_route_loader implements route_loader_interfa
|
||||
|
||||
return $cachedata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all shortlink routes.
|
||||
*
|
||||
* Note: This method caches results in MUC.
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
protected function get_all_shortlink_routes(): array {
|
||||
$cache = \cache::make('core', 'routes');
|
||||
|
||||
if (!($cachedata = $cache->get('shortlink_routes'))) {
|
||||
$cachedata = $this->get_all_routes_in_class(
|
||||
componentpath: '/',
|
||||
classinfo: new \ReflectionClass(shortlink::class),
|
||||
);
|
||||
|
||||
$cache->set('shortlink_routes', $cachedata);
|
||||
}
|
||||
|
||||
return $cachedata;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,14 +31,17 @@ interface route_loader_interface {
|
||||
/** @var string The route path prefix to use for API calls */
|
||||
public const ROUTE_GROUP_API = '/api/rest/v2';
|
||||
|
||||
/** @var string The route path prefix to use for API calls */
|
||||
/** @var string The route path prefix to use for shims */
|
||||
public const ROUTE_GROUP_SHIM = 'shim';
|
||||
|
||||
/** @var string The route path prefix to use for API calls */
|
||||
/** @var string The route path prefix to use for page calls */
|
||||
public const ROUTE_GROUP_PAGE = '/';
|
||||
|
||||
/** @var string The route path prefix to use for shortlinks */
|
||||
public const ROUTE_GROUP_SHORTLINK = '/s/';
|
||||
|
||||
/**
|
||||
* Configure all routes for the Application.
|
||||
* Configure all routes for the application.
|
||||
*
|
||||
* This method returns a set of RouteGroupInterface instances for each route prefix.
|
||||
*
|
||||
|
||||
@@ -102,14 +102,18 @@ class util {
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param string|null $message
|
||||
* @param \Throwable|null $previous
|
||||
* @return ResponseInterface
|
||||
* @throws \Slim\Exception\HttpNotFoundException
|
||||
*/
|
||||
public static function throw_page_not_found(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
?string $message = null,
|
||||
?\Throwable $previous = null,
|
||||
): ResponseInterface {
|
||||
throw new \Slim\Exception\HttpNotFoundException($request);
|
||||
throw new \Slim\Exception\HttpNotFoundException($request, $message, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,7 +132,6 @@ class util {
|
||||
->withHeader('Location', (string) $url);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the route name for the specified callable.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
<?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;
|
||||
|
||||
use core\exception\coding_exception;
|
||||
|
||||
/**
|
||||
* Shortlink manager for Moodle.
|
||||
*
|
||||
* @package core
|
||||
* @copyright Andrew Lyons <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class shortlink {
|
||||
/** @var int The minimum shortcode length */
|
||||
private const SHORTCODE_MIN_LENGTH = 2;
|
||||
|
||||
/** @var int The maximum shortcode length */
|
||||
private const SHORTCODE_MAX_LENGTH = 8;
|
||||
|
||||
/** @var string The list of possible characters */
|
||||
private const SHORTCODE_CHARS = 'ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz0123456789_-';
|
||||
|
||||
/** @var int The number of shortcodes to generate at once */
|
||||
private const GENERATE_COUNT = 30;
|
||||
|
||||
/**
|
||||
* Constructor for the shortlink manager.
|
||||
*
|
||||
* @param \moodle_database $db
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var \moodle_database The DB handler */
|
||||
private \moodle_database $db,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the URL for the specified shortcode.
|
||||
*
|
||||
* @param bool $isuserspecific
|
||||
* @param string $shortcode
|
||||
* @throws coding_exception
|
||||
* @return url
|
||||
*/
|
||||
public function fetch_url_for_shortcode(
|
||||
bool $isuserspecific,
|
||||
string $shortcode,
|
||||
): url {
|
||||
global $USER;
|
||||
|
||||
$result = $this->db->get_record('shortlink', [
|
||||
'shortcode' => $shortcode,
|
||||
'userid' => $isuserspecific ? $USER->id : 0,
|
||||
]);
|
||||
|
||||
if ($result === false) {
|
||||
throw new coding_exception("Shortlink not found for shortcode {$shortcode}");
|
||||
}
|
||||
|
||||
$component = $result->component;
|
||||
$linktype = $result->linktype;
|
||||
$identifier = $result->identifier;
|
||||
|
||||
$handler = $this->get_shortlink_handler($component);
|
||||
$this->validate_linktype($component, $handler, $linktype);
|
||||
|
||||
$link = $handler->process_shortlink($linktype, $identifier);
|
||||
if ($link === null) {
|
||||
throw new coding_exception("No URL found for shortcode {$shortcode}");
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a shortlink URL for the specified user.
|
||||
*
|
||||
* To create a public shortlink, a userid of 0 can be used.
|
||||
*
|
||||
* @param string $component
|
||||
* @param string $linktype
|
||||
* @param int|string $identifier
|
||||
* @param int $userid
|
||||
* @param int $minlength
|
||||
* @param int $maxlength
|
||||
* @throws coding_exception
|
||||
* @return url
|
||||
*/
|
||||
public function create_shortlink(
|
||||
string $component,
|
||||
string $linktype,
|
||||
int|string $identifier,
|
||||
int $userid,
|
||||
int $minlength = self::SHORTCODE_MIN_LENGTH,
|
||||
int $maxlength = self::SHORTCODE_MAX_LENGTH,
|
||||
): url {
|
||||
if ($userid === 0) {
|
||||
return $this->create_public_shortlink(
|
||||
component: $component,
|
||||
linktype: $linktype,
|
||||
identifier: $identifier,
|
||||
minlength: $minlength,
|
||||
maxlength: $maxlength,
|
||||
);
|
||||
}
|
||||
$shortcode = $this->generate_and_store_shortcode(
|
||||
userids: $userid,
|
||||
component: $component,
|
||||
linktype: $linktype,
|
||||
identifier: $identifier,
|
||||
minlength: $minlength,
|
||||
maxlength: $maxlength,
|
||||
);
|
||||
|
||||
$url = \core\router\util::get_path_for_callable(
|
||||
callable: [\core\route\shortlink::class, 'handle_shortlink'],
|
||||
params: ['shortcode' => $shortcode],
|
||||
);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a public shortlink URL.
|
||||
*
|
||||
* @param string $component
|
||||
* @param string $linktype
|
||||
* @param int|string $identifier
|
||||
* @param int $minlength
|
||||
* @param int $maxlength
|
||||
* @throws coding_exception
|
||||
* @return url
|
||||
*/
|
||||
public function create_public_shortlink(
|
||||
string $component,
|
||||
string $linktype,
|
||||
int|string $identifier,
|
||||
int $minlength = self::SHORTCODE_MIN_LENGTH,
|
||||
int $maxlength = self::SHORTCODE_MAX_LENGTH,
|
||||
): url {
|
||||
$shortcode = $this->generate_and_store_shortcode(
|
||||
userids: 0,
|
||||
component: $component,
|
||||
linktype: $linktype,
|
||||
identifier: $identifier,
|
||||
minlength: $minlength,
|
||||
maxlength: $maxlength,
|
||||
);
|
||||
|
||||
$url = \core\router\util::get_path_for_callable(
|
||||
callable: [\core\route\shortlink::class, 'handle_public_shortlink'],
|
||||
params: ['shortcode' => $shortcode],
|
||||
);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a shortlink for a set of users.
|
||||
*
|
||||
* The shortlink returned will be the same for all specified users.
|
||||
*
|
||||
* @param string $component
|
||||
* @param string $linktype
|
||||
* @param string $identifier
|
||||
* @param array $userids
|
||||
* @param int $minlength
|
||||
* @param int $maxlength
|
||||
* @throws coding_exception
|
||||
* @return url
|
||||
*/
|
||||
public function create_shortlink_for_users(
|
||||
string $component,
|
||||
string $linktype,
|
||||
int|string $identifier,
|
||||
array $userids,
|
||||
int $minlength = self::SHORTCODE_MIN_LENGTH,
|
||||
int $maxlength = self::SHORTCODE_MAX_LENGTH,
|
||||
): url {
|
||||
// Ensure none of the user IDs are 0.
|
||||
if (in_array(0, $userids, true)) {
|
||||
throw new coding_exception('User-specific short links cannot be created for user ID 0.');
|
||||
}
|
||||
|
||||
$shortcode = $this->generate_and_store_shortcode(
|
||||
userids: $userids,
|
||||
component: $component,
|
||||
linktype: $linktype,
|
||||
identifier: $identifier,
|
||||
minlength: $minlength,
|
||||
maxlength: $maxlength,
|
||||
);
|
||||
|
||||
$url = \core\router\util::get_path_for_callable(
|
||||
callable: [\core\route\shortlink::class, 'handle_shortlink'],
|
||||
params: ['shortcode' => $shortcode],
|
||||
);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and store a shortcode.
|
||||
*
|
||||
* @param string $component
|
||||
* @param string $linktype
|
||||
* @param int|string $identifier
|
||||
* @param int|array $userids
|
||||
* @param int $minlength
|
||||
* @param int $maxlength
|
||||
* @return string
|
||||
*/
|
||||
private function generate_and_store_shortcode(
|
||||
string $component,
|
||||
string $linktype,
|
||||
int|string $identifier,
|
||||
int|array $userids,
|
||||
int $minlength,
|
||||
int $maxlength,
|
||||
): string {
|
||||
$handler = $this->get_shortlink_handler($component);
|
||||
$this->validate_linktype($component, $handler, $linktype);
|
||||
|
||||
$transaction = $this->db->start_delegated_transaction();
|
||||
|
||||
$userid = is_int($userids) ? $userids : 0;
|
||||
$userids = is_int($userids) ? [$userids] : $userids;
|
||||
$shortcode = $this->get_unused_shortcode(
|
||||
userid: $userid,
|
||||
minlength: $minlength,
|
||||
maxlength: $maxlength,
|
||||
);
|
||||
$linkdata = [
|
||||
'component' => $component,
|
||||
'linktype' => $linktype,
|
||||
'identifier' => $identifier,
|
||||
'shortcode' => $shortcode,
|
||||
'timecreated' => time(),
|
||||
];
|
||||
// Add in user ids.
|
||||
$linkdata = array_map(
|
||||
fn (int $userid) => (object) array_merge($linkdata, ['userid' => $userid]),
|
||||
$userids,
|
||||
);
|
||||
|
||||
$this->db->insert_records('shortlink', $linkdata);
|
||||
$transaction->allow_commit();
|
||||
|
||||
return $shortcode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an unused shortcode suitable for this user.
|
||||
*
|
||||
* @param int $userid
|
||||
* @param int $minlength
|
||||
* @param int $maxlength
|
||||
* @return string
|
||||
*/
|
||||
private function get_unused_shortcode(
|
||||
int $userid,
|
||||
int $minlength = self::SHORTCODE_MIN_LENGTH,
|
||||
int $maxlength = self::SHORTCODE_MAX_LENGTH,
|
||||
): string {
|
||||
do {
|
||||
$shortcodes = array_map(
|
||||
fn() => $this->generate_shortcode(
|
||||
minlength: $minlength,
|
||||
maxlength: $maxlength,
|
||||
),
|
||||
array_fill(0, self::GENERATE_COUNT, null),
|
||||
);
|
||||
|
||||
[$sql, $params] = $this->db->get_in_or_equal($shortcodes, SQL_PARAMS_NAMED);
|
||||
|
||||
if ($userid === 0) {
|
||||
// Global shortcodes must be entirely unique.
|
||||
$existing = $this->db->get_records_select_menu(
|
||||
table: 'shortlink',
|
||||
select: "shortcode {$sql}",
|
||||
params: $params,
|
||||
fields: 'id, shortcode',
|
||||
);
|
||||
} else {
|
||||
// User-specific shortcodes may be re-used by other users, but not by the same user, and not globally.
|
||||
$params['userid'] = $userid;
|
||||
$existing = $this->db->get_records_select_menu(
|
||||
table: 'shortlink',
|
||||
select: "(userid = 0 OR userid = :userid) AND shortcode $sql",
|
||||
params: $params,
|
||||
fields: 'id, shortcode',
|
||||
);
|
||||
}
|
||||
|
||||
$unused = array_diff($shortcodes, $existing);
|
||||
if (count($unused) === 0) {
|
||||
// If we didn't find any unused shortcodes, increase the max length before trying again.
|
||||
$maxlength++;
|
||||
}
|
||||
} while (count($unused) === 0);
|
||||
|
||||
return array_shift($unused);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a shortcode.
|
||||
*
|
||||
* @param int $minlength
|
||||
* @param int $maxlength
|
||||
* @return string
|
||||
*/
|
||||
private function generate_shortcode(
|
||||
int $minlength = self::SHORTCODE_MIN_LENGTH,
|
||||
int $maxlength = self::SHORTCODE_MAX_LENGTH,
|
||||
): string {
|
||||
if ($minlength < 1 || $maxlength < 1 || $minlength > $maxlength) {
|
||||
throw new coding_exception('Invalid min/max length for shortcode generation');
|
||||
}
|
||||
|
||||
if ($minlength === $maxlength) {
|
||||
$data = array_fill(0, $minlength, null);
|
||||
} else {
|
||||
$data = array_fill(0, rand($minlength, $maxlength), null);
|
||||
}
|
||||
|
||||
return implode('', array_map(
|
||||
fn (): string => self::SHORTCODE_CHARS[rand(0, strlen(self::SHORTCODE_CHARS) - 1)],
|
||||
$data,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shortlink handler for the specified component.
|
||||
*
|
||||
* @param string $component
|
||||
* @throws coding_exception
|
||||
* @return shortlink_handler_interface
|
||||
*/
|
||||
private function get_shortlink_handler(string $component): shortlink_handler_interface {
|
||||
try {
|
||||
$handler = \core\di::get("{$component}\\shortlink_handler");
|
||||
} catch (\DI\NotFoundException $e) {
|
||||
throw new coding_exception("No shortlink handler found for component {$component}");
|
||||
}
|
||||
|
||||
if (!$handler instanceof shortlink_handler_interface) {
|
||||
throw new coding_exception("Shortlink handler for component {$component} must implement shortlink_handler_interface");
|
||||
}
|
||||
|
||||
return $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a link type for the specified handler.
|
||||
*
|
||||
* @param string $component
|
||||
* @param shortlink_handler_interface $handler
|
||||
* @param string $linktype
|
||||
* @throws coding_exception
|
||||
*/
|
||||
private function validate_linktype(
|
||||
string $component,
|
||||
shortlink_handler_interface $handler,
|
||||
string $linktype,
|
||||
): void {
|
||||
if (!in_array($linktype, $handler->get_valid_linktypes(), true)) {
|
||||
throw new coding_exception("Invalid link type {$linktype} for component {$component}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Shortlink handler for the Moodle core subsystem.
|
||||
*
|
||||
* @package core
|
||||
* @copyright Andrew Lyons <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class shortlink_handler implements shortlink_handler_interface {
|
||||
#[\Override]
|
||||
public function get_valid_linktypes(): array {
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function process_shortlink(
|
||||
string $type,
|
||||
string $identifier,
|
||||
): ?\core\url {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Class shortlink_handler_interface.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2025 Andrew Lyons <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
interface shortlink_handler_interface {
|
||||
/**
|
||||
* Get the valid link types for this handler.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_valid_linktypes(): array;
|
||||
|
||||
/**
|
||||
* Handle processing of a shortlink, returning the relevant URL.
|
||||
*
|
||||
* If no valid URL is found, this method should return null.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $identifier
|
||||
* @return null|\core\url
|
||||
*/
|
||||
public function process_shortlink(
|
||||
string $type,
|
||||
string $identifier,
|
||||
): ?\core\url;
|
||||
}
|
||||
@@ -684,7 +684,7 @@ All values that are not arrays should be a string.');
|
||||
): self {
|
||||
global $CFG;
|
||||
|
||||
if ($CFG->routerconfigured !== true) {
|
||||
if ($CFG->routerconfigured != true) {
|
||||
$path = '/r.php/' . ltrim($path, '/');
|
||||
}
|
||||
$url = new self($path, $params, $anchor);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<XMLDB PATH="lib/db" VERSION="20250528" COMMENT="XMLDB file for core Moodle tables"
|
||||
<XMLDB PATH="lib/db" VERSION="20250629" COMMENT="XMLDB file for core Moodle tables"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
|
||||
>
|
||||
@@ -5021,5 +5021,23 @@
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="shortlink" COMMENT="Short codes for user short links">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="shortcode" TYPE="char" LENGTH="12" NOTNULL="true" SEQUENCE="false" COMMENT="The shortcode to use for the short link"/>
|
||||
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="linktype" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="identifier" TYPE="char" LENGTH="1333" NOTNULL="true" SEQUENCE="false" COMMENT="An identifier for this component / type combination"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
<INDEXES>
|
||||
<INDEX NAME="shortcode_userid" UNIQUE="true" FIELDS="userid, shortcode"/>
|
||||
<INDEX NAME="shortcode" UNIQUE="false" FIELDS="shortcode"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
</TABLES>
|
||||
</XMLDB>
|
||||
|
||||
@@ -2037,5 +2037,35 @@ function xmldb_main_upgrade($oldversion) {
|
||||
upgrade_main_savepoint(true, 2025081900.03);
|
||||
}
|
||||
|
||||
if ($oldversion < 2025081900.04) {
|
||||
|
||||
// Define table shortlink to be created.
|
||||
$table = new xmldb_table('shortlink');
|
||||
|
||||
// Adding fields to table shortlink.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('shortcode', XMLDB_TYPE_CHAR, '12', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0);
|
||||
$table->add_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('linktype', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('identifier', XMLDB_TYPE_CHAR, '1333', null, XMLDB_NOTNULL, null, null);
|
||||
|
||||
// Adding keys to table shortlink.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
|
||||
$table->add_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
|
||||
|
||||
// Adding indexes to table shortlink.
|
||||
$table->add_index('shortcode_userid', XMLDB_INDEX_UNIQUE, ['userid', 'shortcode']);
|
||||
$table->add_index('shortcode', XMLDB_INDEX_NOTUNIQUE, ['shortcode']);
|
||||
|
||||
// Conditionally launch create table for shortlink.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2025081900.04);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -40,20 +40,30 @@ final class provider_test extends provider_testcase {
|
||||
*/
|
||||
public function test_get_contexts_for_userid(): void {
|
||||
$this->resetAfterTest();
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Check that there are no contexts used for the user yet.
|
||||
$this->assertEmpty(provider::get_contexts_for_userid($user->id));
|
||||
$this->assertEmpty(provider::get_contexts_for_userid($user1->id));
|
||||
|
||||
// Insert a record.
|
||||
$this->insert_dummy_moodlenet_share_progress_record($user->id);
|
||||
$this->insert_dummy_moodlenet_share_progress_record($user1->id);
|
||||
$this->insert_dummy_shortlink_record($user2->id);
|
||||
|
||||
// Check that we only get back one context.
|
||||
$contextlist = provider::get_contexts_for_userid($user->id);
|
||||
// Check that we only get back one context for user1.
|
||||
$contextlist = provider::get_contexts_for_userid($user1->id);
|
||||
$this->assertCount(1, $contextlist);
|
||||
|
||||
// Check that the context returned is the expected one.
|
||||
$usercontext = \context_user::instance($user->id);
|
||||
$usercontext = \context_user::instance($user1->id);
|
||||
$this->assertEquals($usercontext->id, $contextlist->get_contextids()[0]);
|
||||
|
||||
// Check that we only get back one context for user2.
|
||||
$contextlist = provider::get_contexts_for_userid($user2->id);
|
||||
$this->assertCount(1, $contextlist);
|
||||
|
||||
// Check that the context returned is the expected one.
|
||||
$usercontext = \context_user::instance($user2->id);
|
||||
$this->assertEquals($usercontext->id, $contextlist->get_contextids()[0]);
|
||||
}
|
||||
|
||||
@@ -68,8 +78,10 @@ final class provider_test extends provider_testcase {
|
||||
// Create some users.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
$usercontext1 = \context_user::instance($user1->id);
|
||||
$usercontext2 = \context_user::instance($user2->id);
|
||||
$usercontext3 = \context_user::instance($user3->id);
|
||||
|
||||
// Get userlists and check they are empty for now.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, 'core');
|
||||
@@ -80,9 +92,14 @@ final class provider_test extends provider_testcase {
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(0, $userlist2);
|
||||
|
||||
$userlist3 = new \core_privacy\local\request\userlist($usercontext3, 'core');
|
||||
provider::get_users_in_context($userlist3);
|
||||
$this->assertCount(0, $userlist3);
|
||||
|
||||
// Insert records for both users.
|
||||
$this->insert_dummy_moodlenet_share_progress_record($user1->id);
|
||||
$this->insert_dummy_moodlenet_share_progress_record($user2->id);
|
||||
$this->insert_dummy_shortlink_record($user3->id);
|
||||
|
||||
// Check the userlists contain the correct users.
|
||||
$userlist1 = new \core_privacy\local\request\userlist($usercontext1, 'core');
|
||||
@@ -94,6 +111,11 @@ final class provider_test extends provider_testcase {
|
||||
provider::get_users_in_context($userlist2);
|
||||
$this->assertCount(1, $userlist2);
|
||||
$this->assertEquals($user2->id, $userlist2->get_userids()[0]);
|
||||
|
||||
$userlist3 = new \core_privacy\local\request\userlist($usercontext3, 'core');
|
||||
provider::get_users_in_context($userlist3);
|
||||
$this->assertCount(1, $userlist3);
|
||||
$this->assertEquals($user3->id, $userlist3->get_userids()[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,10 +130,12 @@ final class provider_test extends provider_testcase {
|
||||
// Create some users.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Insert a record for each user.
|
||||
$this->insert_dummy_moodlenet_share_progress_record($user1->id);
|
||||
$this->insert_dummy_moodlenet_share_progress_record($user2->id);
|
||||
$this->insert_dummy_shortlink_record($user3->id);
|
||||
|
||||
$subcontexts = [
|
||||
get_string('privacy:metadata:moodlenet_share_progress', 'moodle')
|
||||
@@ -140,6 +164,32 @@ final class provider_test extends provider_testcase {
|
||||
$this->assertEquals($userdata->timecreated, reset($data)->timecreated);
|
||||
$this->assertEquals($userdata->resourceurl, reset($data)->resourceurl);
|
||||
$this->assertEquals($userdata->status, reset($data)->status);
|
||||
|
||||
$subcontexts = [
|
||||
get_string('privacy:metadata:shortlink', 'moodle')
|
||||
];
|
||||
|
||||
// Check if user3 has any exported data yet.
|
||||
$usercontext3 = \context_user::instance($user3->id);
|
||||
$writer = writer::with_context($usercontext3);
|
||||
$this->assertFalse($writer->has_any_data());
|
||||
|
||||
// Export user3's data and check the count.
|
||||
$approvedlist = new approved_contextlist($user3, 'core', [$usercontext3->id]);
|
||||
provider::export_user_data($approvedlist);
|
||||
$data = (array)$writer->get_data($subcontexts);
|
||||
$this->assertCount(1, $data);
|
||||
|
||||
// Get the inserted data.
|
||||
$userdata = $DB->get_record('shortlink', ['userid' => $user3->id]);
|
||||
|
||||
// Check exported data against the inserted data.
|
||||
$this->assertEquals($userdata->id, reset($data)->id);
|
||||
$this->assertEquals($userdata->shortcode, reset($data)->shortcode);
|
||||
$this->assertEquals($userdata->userid, reset($data)->userid);
|
||||
$this->assertEquals($userdata->component, reset($data)->component);
|
||||
$this->assertEquals($userdata->linktype, reset($data)->linktype);
|
||||
$this->assertEquals($userdata->identifier, reset($data)->identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,10 +208,14 @@ final class provider_test extends provider_testcase {
|
||||
// Insert a record for each user.
|
||||
$this->insert_dummy_moodlenet_share_progress_record($user1->id);
|
||||
$this->insert_dummy_moodlenet_share_progress_record($user2->id);
|
||||
$this->insert_dummy_shortlink_record($user1->id);
|
||||
$this->insert_dummy_shortlink_record($user2->id);
|
||||
|
||||
// Get all users' data.
|
||||
$usersdata = $DB->get_records('moodlenet_share_progress', []);
|
||||
$this->assertCount(2, $usersdata);
|
||||
$usersdata = $DB->get_records('shortlink', []);
|
||||
$this->assertCount(2, $usersdata);
|
||||
|
||||
// Delete everything for a user1 in context.
|
||||
$usercontext1 = \context_user::instance($user1->id);
|
||||
@@ -171,6 +225,9 @@ final class provider_test extends provider_testcase {
|
||||
$usersdata = $DB->get_records('moodlenet_share_progress', []);
|
||||
$this->assertCount(1, $usersdata);
|
||||
$this->assertEquals($user2->id, reset($usersdata)->userid);
|
||||
$usersdata = $DB->get_records('shortlink', []);
|
||||
$this->assertCount(1, $usersdata);
|
||||
$this->assertEquals($user2->id, reset($usersdata)->userid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,10 +246,14 @@ final class provider_test extends provider_testcase {
|
||||
// Insert a record for each user.
|
||||
$this->insert_dummy_moodlenet_share_progress_record($user1->id);
|
||||
$this->insert_dummy_moodlenet_share_progress_record($user2->id);
|
||||
$this->insert_dummy_shortlink_record($user1->id);
|
||||
$this->insert_dummy_shortlink_record($user2->id);
|
||||
|
||||
// Get all users' data.
|
||||
$usersdata = $DB->get_records('moodlenet_share_progress', []);
|
||||
$this->assertCount(2, $usersdata);
|
||||
$usersdata = $DB->get_records('shortlink', []);
|
||||
$this->assertCount(2, $usersdata);
|
||||
|
||||
// Delete everything for user1.
|
||||
$usercontext1 = \context_user::instance($user1->id);
|
||||
@@ -203,6 +264,9 @@ final class provider_test extends provider_testcase {
|
||||
$usersdata = $DB->get_records('moodlenet_share_progress', []);
|
||||
$this->assertCount(1, $usersdata);
|
||||
$this->assertEquals($user2->id, reset($usersdata)->userid);
|
||||
$usersdata = $DB->get_records('shortlink', []);
|
||||
$this->assertCount(1, $usersdata);
|
||||
$this->assertEquals($user2->id, reset($usersdata)->userid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,16 +287,22 @@ final class provider_test extends provider_testcase {
|
||||
// Insert a record for each user.
|
||||
$this->insert_dummy_moodlenet_share_progress_record($user1->id);
|
||||
$this->insert_dummy_moodlenet_share_progress_record($user2->id);
|
||||
$this->insert_dummy_shortlink_record($user1->id);
|
||||
$this->insert_dummy_shortlink_record($user2->id);
|
||||
|
||||
// Check the count on all user's data.
|
||||
$usersdata = $DB->get_records('moodlenet_share_progress', []);
|
||||
$this->assertCount(2, $usersdata);
|
||||
$usersdata = $DB->get_records('shortlink', []);
|
||||
$this->assertCount(2, $usersdata);
|
||||
|
||||
// Attempt to delete data for user1 using user2's context (should have no effect).
|
||||
$approvedlist = new approved_userlist($usercontext2, 'core', [$user1->id]);
|
||||
provider::delete_data_for_users($approvedlist);
|
||||
$usersdata = $DB->get_records('moodlenet_share_progress', []);
|
||||
$this->assertCount(2, $usersdata);
|
||||
$usersdata = $DB->get_records('shortlink', []);
|
||||
$this->assertCount(2, $usersdata);
|
||||
|
||||
// Delete data for user1 using its correct context.
|
||||
$approvedlist = new approved_userlist($usercontext1, 'core', [$user1->id]);
|
||||
@@ -242,6 +312,9 @@ final class provider_test extends provider_testcase {
|
||||
$usersdata = $DB->get_records('moodlenet_share_progress', []);
|
||||
$this->assertCount(1, $usersdata);
|
||||
$this->assertEquals($user2->id, reset($usersdata)->userid);
|
||||
$usersdata = $DB->get_records('shortlink', []);
|
||||
$this->assertCount(1, $usersdata);
|
||||
$this->assertEquals($user2->id, reset($usersdata)->userid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,4 +328,29 @@ final class provider_test extends provider_testcase {
|
||||
$cmid = 456;
|
||||
share_recorder::insert_share_progress($sharetype, $userid, $courseid, $cmid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to insert a shortlink record for use in the tests.
|
||||
*
|
||||
* @param int $userid The ID of the user to link the record to.
|
||||
*/
|
||||
protected function insert_dummy_shortlink_record(
|
||||
int $userid,
|
||||
): void {
|
||||
// Mock the handler.
|
||||
$handler = $this->createMock(\core\shortlink_handler_interface::class);
|
||||
$handler->method('get_valid_linktypes')
|
||||
->willReturn(['view']);
|
||||
$handler->method('process_shortlink')
|
||||
->with(
|
||||
$this->equalTo('view'),
|
||||
$this->equalTo(123),
|
||||
)
|
||||
->willReturn(new \core\url('https://example.com'));
|
||||
\core\di::set("mod_example\\shortlink_handler", $handler);
|
||||
|
||||
// Create a shortlink for the user.
|
||||
$manager = \core\di::get(\core\shortlink::class);
|
||||
$manager->create_shortlink('mod_example', 'view', 123, $userid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Tests for the short link manager.
|
||||
*
|
||||
* @package core
|
||||
* @category test
|
||||
* @copyright Andrew Lyons <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core\shortlink
|
||||
*/
|
||||
final class shortlink_test extends \advanced_testcase {
|
||||
public function test_create_public_shortlink(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Mock the handler.
|
||||
$this->mock_handler('mod_example', 'submit', '123');
|
||||
|
||||
// Create a public short link.
|
||||
$manager = \core\di::get(\core\shortlink::class);
|
||||
$link = $manager->create_public_shortlink('mod_example', 'submit', 123);
|
||||
|
||||
$this->assertNotEmpty($link);
|
||||
$this->assertInstanceOf(\core\url::class, $link);
|
||||
|
||||
// Extract the shortcode.
|
||||
$parts = explode('/', (string) $link);
|
||||
$shortcode = end($parts);
|
||||
|
||||
// Fetch the URL.
|
||||
$url = $manager->fetch_url_for_shortcode(false, $shortcode);
|
||||
$this->assertEquals('https://example.com', (string) $url);
|
||||
}
|
||||
|
||||
public function test_create_user_shortlink(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Mock the handler.
|
||||
$this->mock_handler('mod_example', 'submit', '123');
|
||||
|
||||
// Create a public short link.
|
||||
$manager = \core\di::get(\core\shortlink::class);
|
||||
$link = $manager->create_shortlink('mod_example', 'submit', 123, $user->id);
|
||||
|
||||
$this->assertNotEmpty($link);
|
||||
$this->assertInstanceOf(\core\url::class, $link);
|
||||
|
||||
// Extract the shortcode.
|
||||
$parts = explode('/', (string) $link);
|
||||
$shortcode = end($parts);
|
||||
|
||||
// Fetching the URL as a user link should return the URL.
|
||||
$this->setUser($user);
|
||||
$url = $manager->fetch_url_for_shortcode(true, $shortcode);
|
||||
$this->assertEquals('https://example.com', (string) $url);
|
||||
|
||||
// Fetching the URL as someone else should not return the URL.
|
||||
$otheruser = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($otheruser);
|
||||
$this->expectException(\core\exception\coding_exception::class);
|
||||
$manager->fetch_url_for_shortcode(true, $shortcode);
|
||||
}
|
||||
|
||||
public function test_create_users_shortlink(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$otheruser = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Mock the handler.
|
||||
$this->mock_handler('mod_example', 'submit', '123');
|
||||
|
||||
// Create a public short link.
|
||||
$manager = \core\di::get(\core\shortlink::class);
|
||||
$link = $manager->create_shortlink_for_users('mod_example', 'submit', 123, [$user->id, $otheruser->id]);
|
||||
|
||||
$this->assertNotEmpty($link);
|
||||
$this->assertInstanceOf(\core\url::class, $link);
|
||||
|
||||
// Extract the shortcode.
|
||||
$parts = explode('/', (string) $link);
|
||||
$shortcode = end($parts);
|
||||
|
||||
// Fetching the URL as a user link should return the URL.
|
||||
$this->setUser($user);
|
||||
$url = $manager->fetch_url_for_shortcode(true, $shortcode);
|
||||
$this->assertEquals('https://example.com', (string) $url);
|
||||
|
||||
$this->setUser($otheruser);
|
||||
$url = $manager->fetch_url_for_shortcode(true, $shortcode);
|
||||
$this->assertEquals('https://example.com', (string) $url);
|
||||
|
||||
$yetanotheruser = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($yetanotheruser);
|
||||
$this->expectException(\core\exception\coding_exception::class);
|
||||
$manager->fetch_url_for_shortcode(true, $shortcode);
|
||||
}
|
||||
|
||||
public function test_handler_deleted_after_creation(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$otheruser = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Mock the handler.
|
||||
$this->mock_handler('mod_example', 'submit', '123');
|
||||
|
||||
// Create a public short link.
|
||||
$manager = \core\di::get(\core\shortlink::class);
|
||||
$link = $manager->create_shortlink_for_users('mod_example', 'submit', 123, [$user->id, $otheruser->id]);
|
||||
|
||||
$this->assertNotEmpty($link);
|
||||
$this->assertInstanceOf(\core\url::class, $link);
|
||||
|
||||
// Extract the shortcode.
|
||||
$parts = explode('/', (string) $link);
|
||||
$shortcode = end($parts);
|
||||
|
||||
// Delete the handler.
|
||||
\core\di::set(\mod_example\shortlink_handler::class, null);
|
||||
|
||||
// Fetching the URL as someone else should not return the URL.
|
||||
$this->setUser($user);
|
||||
$this->expectException(\core\exception\coding_exception::class);
|
||||
$manager->fetch_url_for_shortcode(true, $shortcode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a link type deleted after creation is not accessible.
|
||||
*/
|
||||
public function test_linktype_deleted_after_creation(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$otheruser = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Mock the handler.
|
||||
$this->mock_handler('mod_example', 'submit', '123');
|
||||
|
||||
// Create a public short link.
|
||||
$manager = \core\di::get(\core\shortlink::class);
|
||||
$link = $manager->create_shortlink_for_users('mod_example', 'submit', 123, [$user->id, $otheruser->id]);
|
||||
|
||||
$this->assertNotEmpty($link);
|
||||
$this->assertInstanceOf(\core\url::class, $link);
|
||||
|
||||
// Extract the shortcode.
|
||||
$parts = explode('/', (string) $link);
|
||||
$shortcode = end($parts);
|
||||
|
||||
// Simulate the link type being deleted by changing it in the DB.
|
||||
$DB->set_field('shortlink', 'linktype', 'invalid');
|
||||
|
||||
$this->setUser($user);
|
||||
$this->expectException(\core\exception\coding_exception::class);
|
||||
$manager->fetch_url_for_shortcode(true, $shortcode);
|
||||
}
|
||||
|
||||
public function test_identifier_changed_after_creation(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$otheruser = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Mock the handler but have it return null for this URL
|
||||
$this->mock_handler('mod_example', 'submit', '123', null);
|
||||
|
||||
// Create a public short link.
|
||||
$manager = \core\di::get(\core\shortlink::class);
|
||||
$link = $manager->create_shortlink_for_users('mod_example', 'submit', 123, [$user->id, $otheruser->id]);
|
||||
|
||||
$this->assertNotEmpty($link);
|
||||
$this->assertInstanceOf(\core\url::class, $link);
|
||||
|
||||
// Extract the shortcode.
|
||||
$parts = explode('/', (string) $link);
|
||||
$shortcode = end($parts);
|
||||
|
||||
$this->setUser($user);
|
||||
$this->expectException(\core\exception\coding_exception::class);
|
||||
$manager->fetch_url_for_shortcode(true, $shortcode);
|
||||
}
|
||||
|
||||
public function test_unknown_handler(): void {
|
||||
$manager = \core\di::get(\core\shortlink::class);
|
||||
|
||||
$this->expectException(\core\exception\coding_exception::class);
|
||||
$this->expectExceptionMessageMatches('/No shortlink handler found for component mod_example$/');
|
||||
|
||||
$manager->create_shortlink('mod_example', 'invalid', 123, 0);
|
||||
}
|
||||
|
||||
public function test_invalid_handler(): void {
|
||||
\core\di::set(\mod_example\shortlink_handler::class, new class {});
|
||||
$manager = \core\di::get(\core\shortlink::class);
|
||||
|
||||
$this->expectException(\core\exception\coding_exception::class);
|
||||
$this->expectExceptionMessageMatches('/Shortlink handler for component mod_example must implement shortlink_handler_interface/');
|
||||
|
||||
$manager->create_shortlink('mod_example', 'invalid', 123, 0);
|
||||
}
|
||||
|
||||
public function test_invalid_linktype(): void {
|
||||
// Mock the handler.
|
||||
$handler = $this->createMock(\core\shortlink_handler_interface::class);
|
||||
$handler->method('get_valid_linktypes')
|
||||
->willReturn([]);
|
||||
|
||||
\core\di::set(\mod_example\shortlink_handler::class, $handler);
|
||||
|
||||
$manager = \core\di::get(\core\shortlink::class);
|
||||
|
||||
$this->expectException(\core\exception\coding_exception::class);
|
||||
$this->expectExceptionMessageMatches('/Invalid link type submit for component mod_example/');
|
||||
|
||||
$manager->create_shortlink('mod_example', 'submit', 123, 0);
|
||||
}
|
||||
|
||||
public function test_creation_for_users_including_public(): void {
|
||||
$this->resetAfterTest();
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Create a public short link.
|
||||
$manager = \core\di::get(\core\shortlink::class);
|
||||
$this->expectException(\core\exception\coding_exception::class);
|
||||
$manager->create_shortlink_for_users('mod_example', 'submit', 123, [$user->id, 0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider valid_min_max_length_provider
|
||||
*/
|
||||
public function test_creation_min_max_length(
|
||||
int $minlength,
|
||||
int $maxlength,
|
||||
int $actualminlengthvalue,
|
||||
): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Mock the handler.
|
||||
$this->mock_handler('mod_example', 'submit', '123');
|
||||
|
||||
$manager = \core\di::get(\core\shortlink::class);
|
||||
|
||||
$link = $manager->create_shortlink('mod_example', 'submit', 123, 0, 1, 1);
|
||||
$parts = explode('/', (string) $link);
|
||||
$shortcode = end($parts);
|
||||
|
||||
$this->assertLessThanOrEqual($actualminlengthvalue, strlen($shortcode));
|
||||
}
|
||||
|
||||
public static function valid_min_max_length_provider(): \Iterator {
|
||||
// One char.
|
||||
yield [1, 1, 1];
|
||||
yield [1, 2, 1];
|
||||
yield [2, 2, 2];
|
||||
yield [4, 4, 4];
|
||||
yield [4, 40, 4];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider invalid_min_max_length_provider
|
||||
*/
|
||||
public function test_creation_invalid_min_max_length(
|
||||
int $minlength,
|
||||
int $maxlength,
|
||||
): void {
|
||||
$this->resetAfterTest();
|
||||
$this->mock_handler('mod_example', 'submit', '123');
|
||||
|
||||
$manager = \core\di::get(\core\shortlink::class);
|
||||
$this->expectException(\core\exception\coding_exception::class);
|
||||
$manager->create_shortlink('mod_example', 'submit', 123, 0, $minlength, $maxlength);
|
||||
}
|
||||
|
||||
public static function invalid_min_max_length_provider(): \Iterator {
|
||||
// Empty.
|
||||
yield [0, 0];
|
||||
// Min greater than max.
|
||||
yield [1, 0];
|
||||
// Min greater than max.
|
||||
yield [2, 1];
|
||||
}
|
||||
|
||||
private function mock_handler(
|
||||
string $component,
|
||||
string $linktype,
|
||||
string $identifier,
|
||||
?string $response = 'https://example.com',
|
||||
): void {
|
||||
// Mock the handler.
|
||||
$handler = $this->createMock(\core\shortlink_handler_interface::class);
|
||||
$handler->method('get_valid_linktypes')
|
||||
->willReturn(['submit']);
|
||||
$handler->method('process_shortlink')
|
||||
->with(
|
||||
$this->equalTo($linktype),
|
||||
$this->equalTo($identifier),
|
||||
)
|
||||
->willReturn($response ? new \core\url($response) : null);
|
||||
\core\di::set("{$component}\\shortlink_handler", $handler);
|
||||
}
|
||||
|
||||
public function test_creation_run_out_of_characters(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Mock the handler.
|
||||
$this->mock_handler('mod_example', 'submit', '123');
|
||||
|
||||
$manager = \core\di::get(\core\shortlink::class);
|
||||
|
||||
// We know that there are at least 25 + 25 + 10 + 2 possible character combinations for the default URL.
|
||||
// Generate 100 short links and check that they are all unique.
|
||||
$shortcodes = [];
|
||||
for ($i = 0; $i < 100; $i++) {
|
||||
$link = $manager->create_public_shortlink('mod_example', 'submit', 123, 1, 1);
|
||||
$parts = explode('/', (string) $link);
|
||||
$shortcode = end($parts);
|
||||
$this->assertGreaterThanOrEqual(1, strlen($shortcode));
|
||||
|
||||
$shortcodes[$shortcode] = $shortcode;
|
||||
}
|
||||
|
||||
$this->assertCount(100, $shortcodes);
|
||||
}
|
||||
}
|
||||
@@ -351,12 +351,21 @@ class notification_helper {
|
||||
];
|
||||
$url = new \moodle_url('/mod/assign/view.php', $urlparams);
|
||||
|
||||
// Shortlink for SMS.
|
||||
$shortlink = \core_sms\manager::create_shortlink_for_users(
|
||||
component: 'mod_assign',
|
||||
linktype: 'view',
|
||||
identifier: $assignmentobj->get_course_module()->id,
|
||||
userids: [$userid],
|
||||
);
|
||||
|
||||
$stringparams = [
|
||||
'firstname' => $user->firstname,
|
||||
'assignmentname' => $assignmentobj->get_instance()->name,
|
||||
'coursename' => $assignmentobj->get_course()->fullname,
|
||||
'duedate' => userdate($duedate),
|
||||
'url' => $url,
|
||||
'shortlink' => $shortlink,
|
||||
];
|
||||
|
||||
$messagedata = [
|
||||
@@ -439,6 +448,14 @@ class notification_helper {
|
||||
];
|
||||
$url = new \moodle_url('/mod/assign/view.php', $urlparams);
|
||||
|
||||
// Shortlink for SMS.
|
||||
$shortlink = \core_sms\manager::create_shortlink_for_users(
|
||||
component: 'mod_assign',
|
||||
linktype: 'view',
|
||||
identifier: $assignmentobj->get_course_module()->id,
|
||||
userids: [$userid],
|
||||
);
|
||||
|
||||
// Prepare the cut-off date html string.
|
||||
$snippet = '';
|
||||
if (!empty($cutoffdate)) {
|
||||
@@ -452,6 +469,7 @@ class notification_helper {
|
||||
'duedate' => userdate($duedate),
|
||||
'url' => $url,
|
||||
'cutoffsnippet' => $snippet,
|
||||
'shortlink' => $shortlink,
|
||||
];
|
||||
|
||||
$messagedata = [
|
||||
@@ -530,11 +548,22 @@ class notification_helper {
|
||||
'id' => $assignmentobj->get_course_module()->id,
|
||||
'action' => 'view',
|
||||
];
|
||||
$url = new \moodle_url('/mod/assign/view.php', $urlparams);
|
||||
|
||||
// Shortlink for SMS.
|
||||
$shortlink = \core_sms\manager::create_shortlink_for_users(
|
||||
component: 'mod_assign',
|
||||
linktype: 'view',
|
||||
identifier: $assignmentobj->get_course_module()->id,
|
||||
userids: [$userid],
|
||||
);
|
||||
|
||||
$assignmentsfordigest[$assignment->id] = [
|
||||
'assignmentname' => $assignmentobj->get_instance()->name,
|
||||
'coursename' => $assignmentobj->get_course()->fullname,
|
||||
'duetime' => userdate($duedate, get_string('strftimetime12', 'langconfig')),
|
||||
'url' => new \moodle_url('/mod/assign/view.php', $urlparams),
|
||||
'url' => $url,
|
||||
'shortlink' => $shortlink,
|
||||
];
|
||||
}
|
||||
$assignments->close();
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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 mod_assign;
|
||||
|
||||
use core\shortlink_handler_interface;
|
||||
|
||||
/**
|
||||
* Shortlink handler for mod_assign.
|
||||
*
|
||||
* @package mod_assign
|
||||
* @copyright 2025 Andrew Lyons <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class shortlink_handler implements shortlink_handler_interface {
|
||||
#[\Override]
|
||||
public function get_valid_linktypes(): array {
|
||||
return [
|
||||
'view',
|
||||
];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function process_shortlink(
|
||||
string $type,
|
||||
string $identifier,
|
||||
): ?\core\url {
|
||||
return match ($type) {
|
||||
'view' => new \core\url('/mod/assign/view.php', [
|
||||
'id' => $identifier,
|
||||
]),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -79,12 +79,12 @@ $string['assignmentduesoonhtml'] = '<p>Hi {$a->firstname},</p>
|
||||
<p>The assignment <strong>{$a->assignmentname}</strong> in course {$a->coursename} is due soon.</p>
|
||||
<p><strong>Due: {$a->duedate}</strong></p>
|
||||
<p><a href="{$a->url}">Go to activity</a></p>';
|
||||
$string['assignmentduesoonsms'] = 'Your assignment {$a->assignmentname} is due on {$a->duedate}: {$a->url}';
|
||||
$string['assignmentduesoonsms'] = 'Your assignment {$a->assignmentname} is due on {$a->duedate}: {$a->shortlink}';
|
||||
$string['assignmentoverduehtml'] = '<p>Hi {$a->firstname},</p>
|
||||
<p><strong>{$a->assignmentname}</strong> in course {$a->coursename} was due on <strong>{$a->duedate}</strong>.</p>
|
||||
<p>You might still be able to submit your assignment{$a->cutoffsnippet}, but your submission will be marked as late.</p>
|
||||
<p><a href="{$a->url}">Go to activity</a></p>';
|
||||
$string['assignmentoverduesms'] = 'Your assignment {$a->assignmentname} is overdue on {$a->duedate}: {$a->url}';
|
||||
$string['assignmentoverduesms'] = 'Your assignment {$a->assignmentname} is overdue on {$a->duedate}: {$a->shortlink}';
|
||||
$string['assignmentoverduehtmlcutoffsnippet'] = ' <strong>by {$a->cutoffdate}</strong>';
|
||||
$string['assignmentduesoonsubject'] = 'Due on {$a->duedate}: {$a->assignmentname}';
|
||||
$string['assignmentoverduesubject'] = 'Overdue: {$a->assignmentname}';
|
||||
|
||||
@@ -139,6 +139,19 @@ abstract class gateway {
|
||||
* @return string The truncated message.
|
||||
*/
|
||||
public function truncate_message(string $message): string {
|
||||
if (strlen($message) > static::MESSAGE_LENGTH_LIMIT) {
|
||||
$message = self::remove_urls_from_message($message);
|
||||
}
|
||||
return \core_text::substr($message, 0, static::MESSAGE_LENGTH_LIMIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove URLs from the message.
|
||||
*
|
||||
* @param string $message The message to check.
|
||||
* @return string The updated message.
|
||||
*/
|
||||
public static function remove_urls_from_message(string $message): string {
|
||||
return trim(preg_replace('/https?:\/\/\S+/', '', $message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace core_sms;
|
||||
|
||||
use Generator;
|
||||
use stdClass;
|
||||
use core\url;
|
||||
|
||||
/**
|
||||
* SMS manager.
|
||||
@@ -440,4 +441,60 @@ class manager {
|
||||
|
||||
return $phonenumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a shortlink for the specified user.
|
||||
*
|
||||
* To create a global shortlink, a userid of 0 can be used.
|
||||
*
|
||||
* @param string $component
|
||||
* @param string $linktype
|
||||
* @param int|string $identifier
|
||||
* @param int $userid
|
||||
* @throws \coding_exception
|
||||
* @return url
|
||||
*/
|
||||
public static function create_shortlink(
|
||||
string $component,
|
||||
string $linktype,
|
||||
null|int|string $identifier,
|
||||
int $userid,
|
||||
): url {
|
||||
return \core\di::get(\core\shortlink::class)->create_shortlink(
|
||||
component: $component,
|
||||
linktype: $linktype,
|
||||
identifier: $identifier,
|
||||
userid: $userid,
|
||||
minlength: 4,
|
||||
maxlength: 4,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a shortlink for a set of users.
|
||||
*
|
||||
* The shortlink returned will be the same for all specified users.
|
||||
*
|
||||
* @param string $component
|
||||
* @param string $linktype
|
||||
* @param string $identifier
|
||||
* @param array $userids
|
||||
* @throws \coding_exception
|
||||
* @return url
|
||||
*/
|
||||
public static function create_shortlink_for_users(
|
||||
string $component,
|
||||
string $linktype,
|
||||
string $identifier,
|
||||
array $userids,
|
||||
): url {
|
||||
return \core\di::get(\core\shortlink::class)->create_shortlink_for_users(
|
||||
component: $component,
|
||||
linktype: $linktype,
|
||||
identifier: $identifier,
|
||||
userids: $userids,
|
||||
minlength: 4,
|
||||
maxlength: 4,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,4 +79,54 @@ final class gateway_test extends \advanced_testcase {
|
||||
$this->expectException(\coding_exception::class);
|
||||
$othergw->update_message_status($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test truncation of messages.
|
||||
*
|
||||
* @dataProvider get_truncation_strings
|
||||
* @param string $original
|
||||
* @param string $truncated
|
||||
*/
|
||||
public function test_truncate_message(
|
||||
string $content,
|
||||
string $expected,
|
||||
): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$manager = \core\di::get(\core_sms\manager::class);
|
||||
$config = new \stdClass();
|
||||
$config->api_key = 'test_api_key';
|
||||
|
||||
$gw = $manager->create_gateway_instance(
|
||||
classname: \smsgateway_dummy\gateway::class,
|
||||
name: 'dummy',
|
||||
enabled: true,
|
||||
config: $config,
|
||||
);
|
||||
|
||||
$truncated = $gw->truncate_message($content);
|
||||
$this->assertSame($expected, $truncated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_truncate_message.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_truncation_strings(): array {
|
||||
return [
|
||||
'Over limit with URL' => [
|
||||
'content' => 'Moodle is a flexible, open-source learning platform designed to help educators deliver online courses and manage student progress effectively. Visit https://moodle.org',
|
||||
'expected' => 'Moodle is a flexible, open-source learning platform designed to help educators deliver online courses and manage student progress effectively. Visit',
|
||||
],
|
||||
'Over limit' => [
|
||||
'content' => 'Moodle is a widely used open-source learning platform that empowers educators to build customizable online courses, track student performance, and foster collaborative digital learning.',
|
||||
'expected' => 'Moodle is a widely used open-source learning platform that empowers educators to build customizable online courses, track student performance, and foster collab',
|
||||
],
|
||||
'Under limit' => [
|
||||
'content' => 'Moodle is a widely used open-source learning platform that empowers educators.',
|
||||
'expected' => 'Moodle is a widely used open-source learning platform that empowers educators.',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2025081900.03; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2025081900.04; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '5.1dev (Build: 20250819)'; // Human-friendly version name
|
||||
|
||||
Reference in New Issue
Block a user