Skip to content

Commit ba41cfb

Browse files
MDL-83957 core_ltix: Integrate builder API into Deep Linking API
1 parent cdee513 commit ba41cfb

13 files changed

Lines changed: 1143 additions & 29 deletions
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//field
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
namespace core_ltix\local\lticore\facades\service;
18+
19+
/**
20+
* Service facade for deep linking launches.
21+
*
22+
* This facade provides the necessary interface implementation for deep linking
23+
* while handling the specifics of content selection flows.
24+
*
25+
* @package core_ltix
26+
* @copyright 2025 Muhammad Arnaldo <muhammad.arnaldo@moodle.com>
27+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28+
*/
29+
class deep_linking_launch_service_facade implements launch_service_facade_interface {
30+
/**
31+
* Constructor.
32+
*
33+
* @param \stdClass $toolconfig the tool configuration.
34+
* @param \core\context $context the launch context.
35+
* @param int $userid the user ID performing the launch.
36+
* @param string $returnurl the return URL for deep linking.
37+
* @param string $messagetype the message type.
38+
*/
39+
public function __construct(
40+
protected \stdClass $toolconfig,
41+
protected \core\context $context,
42+
protected int $userid,
43+
protected string $returnurl,
44+
protected string $messagetype = 'LtiDeepLinkingRequest',
45+
) {
46+
}
47+
48+
/**
49+
* Get the target link URI for the deep linking request.
50+
*
51+
* @return string the target link URI.
52+
*/
53+
public function get_target_link_uri(): string {
54+
// Call into each of the services, allowing them a chance to change the target_link_uri of the launch.
55+
$targetlinkuri = $this->toolconfig->lti_toolurl;
56+
57+
foreach (\core_ltix\helper::get_services() as $service) {
58+
$targetlinkuri = $service->override_target_link_uri(
59+
toolconfig: $this->toolconfig,
60+
messagetype: $this->messagetype,
61+
targetlinkuri: $targetlinkuri,
62+
context: $this->context,
63+
userid: $this->userid,
64+
);
65+
}
66+
return $targetlinkuri;
67+
}
68+
69+
/**
70+
* Get launch parameters specific to deep linking.
71+
*
72+
* @return array array of launch parameters.
73+
*/
74+
public function get_launch_parameters(): array {
75+
$params = [];
76+
foreach (\core_ltix\helper::get_services() as $service) {
77+
$params = $service->get_launch_params(
78+
toolconfig: $this->toolconfig,
79+
messagetype: $this->messagetype,
80+
targetlinkuri: $this->toolconfig->lti_toolurl,
81+
context: $this->context,
82+
userid: $this->userid,
83+
);
84+
}
85+
return $params;
86+
}
87+
88+
/**
89+
* Parse custom parameter values for deep linking.
90+
*
91+
* @param string $value the parameter value to parse.
92+
* @return string the parsed parameter value.
93+
*/
94+
public function parse_custom_param_value(string $value): string {
95+
$val = $value;
96+
foreach (\core_ltix\helper::get_services() as $service) {
97+
$value = $service->parse_value($val);
98+
if ($val != $value) {
99+
break;
100+
}
101+
}
102+
return $value;
103+
}
104+
}

public/ltix/classes/local/lticore/message/payload/custom/factory/custom_param_parser_factory.php

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace core_ltix\local\lticore\message\payload\custom\factory;
44

55
use core_ltix\local\lticore\exception\lti_exception;
6+
use core_ltix\local\lticore\facades\service\deep_linking_launch_service_facade;
67
use core_ltix\local\lticore\facades\service\resource_link_launch_service_facade;
78
use core_ltix\local\lticore\message\payload\custom\custom_param_parser;
89
use core_ltix\local\lticore\models\resource_link;
@@ -24,9 +25,11 @@ class custom_param_parser_factory {
2425
* @throws lti_exception if a parser instance cannot be created.
2526
*/
2627
public function get_parser_from_auth_request(\stdClass $toolconfig, lti_token $launchtoken, lti_user $ltiuser): custom_param_parser {
28+
$messagetype = $launchtoken->get_claim(\core_ltix\constants::LTI_JWT_CLAIM_PREFIX . '/claim/message_type');
29+
2730
// TODO: below 'LtiResourceLinkRequest' should be replaced with a const.
28-
if ($launchtoken->get_claim(\core_ltix\constants::LTI_JWT_CLAIM_PREFIX.'/claim/message_type') === 'LtiResourceLinkRequest') {
29-
global $USER;
31+
if ($messagetype === 'LtiResourceLinkRequest') {
32+
global $USER, $DB;
3033
$user = $USER->id == $ltiuser->id ? $USER : \core_user::get_user($ltiuser->id);
3134

3235
$context = \core\context\course::instance(
@@ -46,9 +49,38 @@ public function get_parser_from_auth_request(\stdClass $toolconfig, lti_token $l
4649
context: $context,
4750
user: $user
4851
);
52+
} else if ($messagetype === 'LtiDeepLinkingRequest') {
53+
global $USER, $DB;
54+
$user = $USER->id == $ltiuser->id ? $USER : \core_user::get_user($ltiuser->id);
55+
56+
$context = \core\context\course::instance(
57+
$launchtoken->get_claim(\core_ltix\constants::LTI_JWT_CLAIM_PREFIX . '/claim/context')['id']
58+
);
59+
$deeplinkingsettings = $launchtoken->get_claim('https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings');
60+
61+
if (!$context || !isset($context->id)) {
62+
throw new lti_exception('Missing or invalid context claim in deep linking request');
63+
}
64+
65+
if (!$deeplinkingsettings || !isset($deeplinkingsettings['deep_link_return_url'])) {
66+
throw new lti_exception('Missing or invalid deep linking settings claim');
67+
}
68+
69+
$servicefacade = new deep_linking_launch_service_facade(
70+
toolconfig: $toolconfig,
71+
context: $context,
72+
userid: $ltiuser->id,
73+
returnurl: $deeplinkingsettings['deep_link_return_url']
74+
);
75+
76+
return new custom_param_parser(
77+
\core_ltix\helper::get_capabilities(),
78+
$servicefacade,
79+
context: $context,
80+
user: $user
81+
);
4982
}
50-
throw new lti_exception('Could not create custom_param_parser instance for message type: '.
51-
$launchtoken->get_claim(\core_ltix\constants::LTI_JWT_CLAIM_PREFIX.'/claim/message_type'));
83+
throw new lti_exception('Could not create custom_param_parser instance for message type: ' . $messagetype);
5284
}
5385

5486
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
namespace core_ltix\local\lticore\message\payload;
18+
19+
use core_ltix\constants;
20+
use core_ltix\helper;
21+
use core_ltix\local\lticore\facades\service\deep_linking_launch_service_facade;
22+
use core_ltix\local\lticore\message\payload\custom\custom_param_parser;
23+
24+
/**
25+
* Generates payload data for a 1p1 deep linking launch (ContentItemSelectionRequest).
26+
*
27+
* @package core_ltix
28+
* @copyright 2025 Muhammad Arnaldo <muhammad.arnaldo@moodle.com>
29+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30+
*/
31+
class v1p1_deep_linking_launch_payload_builder {
32+
/**
33+
* Constructor.
34+
*
35+
* @param \stdClass $toolconfig the tool configuration.
36+
* @param \stdClass $user the user data.
37+
* @param deep_linking_launch_service_facade $servicefacade the service facade.
38+
* @param custom_param_parser $customparamparser the custom parameter parser.
39+
* @param int $contextid the launch context ID.
40+
*/
41+
public function __construct(
42+
protected \stdClass $toolconfig,
43+
protected \stdClass $user,
44+
protected deep_linking_launch_service_facade $servicefacade,
45+
protected custom_param_parser $customparamparser,
46+
protected int $contextid,
47+
) {
48+
}
49+
50+
/**
51+
* Get the array of all parameters for this request type.
52+
*
53+
* @return array
54+
*/
55+
public function get_params(): array {
56+
// Create basic unformatted payload data
57+
$unformattedpayloaddata = [
58+
'context' => $this->get_unformatted_context_data(),
59+
'toolplatform' => $this->get_unformatted_tool_platform_data(),
60+
'lis' => $this->get_unformatted_lis_data(),
61+
'user' => $this->get_unformatted_user_data(),
62+
];
63+
$unformattedpayloaddata = array_merge(...array_values($unformattedpayloaddata));
64+
65+
// Add custom param data configured by the tool - do NOT substitute yet.
66+
$toolunformattedpayloaddata = $this->get_unformatted_custom_data($this->toolconfig);
67+
$unformattedpayloaddata = array_merge($toolunformattedpayloaddata, $unformattedpayloaddata);
68+
69+
// Allow services to add claims, again using unformatted payload data.
70+
$serviceunformattedpayloaddata = $this->get_unformatted_service_custom_data($this->servicefacade);
71+
$unformattedpayloaddata = array_merge($unformattedpayloaddata, $serviceunformattedpayloaddata);
72+
73+
// Perform substitution for custom params.
74+
return $this->resolve_substitution($unformattedpayloaddata, $this->customparamparser);
75+
}
76+
77+
/**
78+
* Resolve substitution for custom parameters.
79+
*
80+
* @param array $payloaddata the payload data.
81+
* @param custom_param_parser $customparamparser the custom parameter parser.
82+
* @return array the resolved payload data.
83+
*/
84+
protected function resolve_substitution(
85+
array $payloaddata,
86+
custom_param_parser $customparamparser
87+
): array {
88+
foreach ($payloaddata as $key => $value) {
89+
// Substitution is only performed for custom params.
90+
if (str_starts_with($key, 'custom_')) {
91+
$payloaddata[$key] = $customparamparser->parse($value, $payloaddata);
92+
}
93+
}
94+
95+
return $payloaddata;
96+
}
97+
98+
/**
99+
* Get the unformatted custom data from tool configuration.
100+
*
101+
* @param \stdClass $toolconfig the tool configuration.
102+
* @return array the custom data.
103+
*/
104+
protected function get_unformatted_custom_data(\stdClass $toolconfig): array {
105+
$customdata = [];
106+
107+
if (!empty($toolconfig->lti_customparameters)) {
108+
$toolcustom = helper::split_parameters($toolconfig->lti_customparameters);
109+
foreach ($toolcustom as $key => $val) {
110+
$key2 = helper::map_keyname($key);
111+
$customdata['custom_' . $key2] = $val;
112+
}
113+
}
114+
115+
return $customdata;
116+
}
117+
118+
/**
119+
* Get unformatted context data.
120+
*
121+
* @return array|null the context data, or null if not applicable.
122+
*/
123+
protected function get_unformatted_context_data(): ?array {
124+
$context = \core\context::instance_by_id($this->contextid);
125+
126+
if (($coursecontext = $context->get_course_context(false)) === false) {
127+
return null;
128+
}
129+
130+
$course = get_course($coursecontext->instanceid);
131+
$contexttype = $course->format == 'site' ? 'Group'
132+
: 'CourseSection';
133+
134+
return [
135+
'context_id' => $course->id,
136+
'context_label' => $context->get_context_name(),
137+
'context_title' => $context->get_context_name(),
138+
'context_type' => $contexttype,
139+
];
140+
}
141+
142+
/**
143+
* Get unformatted tool platform data.
144+
*
145+
* @return array the tool platform data.
146+
*/
147+
protected function get_unformatted_tool_platform_data(): array {
148+
global $CFG;
149+
if (!empty($CFG->ltix_institution_name)) {
150+
$name = trim(html_to_text($CFG->ltix_institution_name, 0));
151+
} else if (!empty($CFG->mod_lti_institution_name)) {
152+
// TODO final removal of the mod_lti_institution_name fallback code in Moodle 6.0.
153+
debugging('mod_lti_institution_name is deprecated. Please use ltix_institution_name instead.', DEBUG_DEVELOPER);
154+
$name = trim(html_to_text($CFG->mod_lti_institution_name, 0));
155+
} else {
156+
$name = get_site()->shortname;
157+
}
158+
159+
return [
160+
'tool_consumer_info_product_family_code' => 'moodle',
161+
'tool_consumer_info_version' => strval($CFG->version),
162+
'tool_consumer_instance_guid' => helper::get_organizationid((array)$this->toolconfig),
163+
'tool_consumer_instance_name' => $name,
164+
'tool_consumer_instance_description' => trim(html_to_text(get_site()->fullname, 0)),
165+
];
166+
}
167+
168+
protected function get_unformatted_lis_data(): array {
169+
// Some lis properties only apply when in course-related contexts.
170+
$context = \core\context::instance_by_id($this->contextid);
171+
if (($coursecontext = $context->get_course_context(false)) !== false) {
172+
$course = get_course($coursecontext->instanceid);
173+
$coursesectionsourcedid = $course->idnumber;
174+
}
175+
176+
return [
177+
...(isset($coursesectionsourcedid) ? ['lis_course_section_sourcedid' => $coursesectionsourcedid] : []),
178+
];
179+
}
180+
181+
/**
182+
* Get unformatted user data.
183+
*
184+
* @return array the user data.
185+
*/
186+
protected function get_unformatted_user_data(): array {
187+
$userpayloaddata = [];
188+
// lti1p1 DOES send user data in the initial payload; there is no OIDC auth step.
189+
if ($this->toolconfig->lti_sendname == constants::LTI_SETTING_ALWAYS) {
190+
$userpayloaddata['lis_person_name_given'] = $this->user->firstname;
191+
$userpayloaddata['lis_person_name_family'] = $this->user->lastname;
192+
$userpayloaddata['lis_person_name_full'] = fullname($this->user);
193+
$userpayloaddata['ext_user_username'] = $this->user->username;
194+
$userpayloaddata['lis_person_sourcedid'] = $this->user->idnumber;
195+
}
196+
197+
if ($this->toolconfig->lti_sendemailaddr == constants::LTI_SETTING_ALWAYS) {
198+
$userpayloaddata['lis_person_contact_email_primary'] = $this->user->email;
199+
}
200+
201+
return $userpayloaddata;
202+
}
203+
204+
/**
205+
* Get unformatted service custom data.
206+
*
207+
* @param deep_linking_launch_service_facade $servicefacade the service facade.
208+
* @return array the custom data.
209+
*/
210+
protected function get_unformatted_service_custom_data(deep_linking_launch_service_facade $servicefacade): array {
211+
$servicecustomdata = [];
212+
foreach ($servicefacade->get_launch_parameters() as $param => $val) {
213+
$servicecustomdata['custom_' . $param] = $val;
214+
}
215+
return $servicecustomdata;
216+
}
217+
}

0 commit comments

Comments
 (0)