1 |
efrain |
1 |
<?php declare(strict_types=1);
|
|
|
2 |
|
|
|
3 |
namespace EduSharingApiClient;
|
|
|
4 |
|
|
|
5 |
use Exception;
|
|
|
6 |
use JsonException;
|
|
|
7 |
|
|
|
8 |
/**
|
|
|
9 |
* Class EduSharingNodeHelper
|
|
|
10 |
*
|
|
|
11 |
* @author Torsten Simon <simon@edu-sharing.net>
|
|
|
12 |
* @author Marian Ziegler <ziegler@edu-sharing.net>
|
|
|
13 |
*/
|
|
|
14 |
class EduSharingNodeHelper extends EduSharingHelperAbstract
|
|
|
15 |
{
|
|
|
16 |
private EduSharingNodeHelperConfig $config;
|
|
|
17 |
|
|
|
18 |
public function __construct(EduSharingHelperBase $base, EduSharingNodeHelperConfig $config) {
|
|
|
19 |
parent::__construct($base);
|
|
|
20 |
$this->config = $config;
|
|
|
21 |
}
|
|
|
22 |
|
|
|
23 |
/**
|
|
|
24 |
* creates a usage for a given node
|
|
|
25 |
* The given usage can later be used to fetch this node REGARDLESS of the actual user
|
|
|
26 |
* The usage gives permanent access to this node and acts similar to a license
|
|
|
27 |
* In order to be able to create an usage for a node, the current user (provided via the ticket)
|
|
|
28 |
* MUST have CC_PUBLISH permissions on the given node id
|
|
|
29 |
* @param string $ticket
|
|
|
30 |
* A ticket with the user session who is creating this usage
|
|
|
31 |
* @param string $containerId
|
|
|
32 |
* A unique page / course id this usage refers to inside your system (e.g. a database id of the page you include the usage)
|
|
|
33 |
* @param string $resourceId
|
|
|
34 |
* The individual resource id on the current page or course this object refers to
|
|
|
35 |
* (you may enumerate or use unique UUID's)
|
|
|
36 |
* @param string $nodeId
|
|
|
37 |
* The edu-sharing node id the usage shall be created for
|
|
|
38 |
* @param string|null $nodeVersion
|
|
|
39 |
* Optional: The fixed version this usage should refer to
|
|
|
40 |
* If you leave it empty, the usage will always refer to the latest version of the node
|
|
|
41 |
* @return Usage
|
|
|
42 |
* An usage element you can use with @getNodeByUsage
|
|
|
43 |
* Keep all data of this object stored inside your system!
|
|
|
44 |
* @throws JsonException
|
|
|
45 |
* @throws Exception
|
|
|
46 |
*/
|
|
|
47 |
public function createUsage(string $ticket, string $containerId, string $resourceId, string $nodeId, string $nodeVersion = null): Usage {
|
|
|
48 |
$headers = $this->getSignatureHeaders($ticket);
|
|
|
49 |
$headers[] = $this->getRESTAuthenticationHeader($ticket);
|
|
|
50 |
$curl = $this->base->handleCurlRequest($this->base->baseUrl . '/rest/usage/v1/usages/repository/-home-', [
|
|
|
51 |
CURLOPT_FAILONERROR => false,
|
|
|
52 |
CURLOPT_POST => 1,
|
|
|
53 |
CURLOPT_POSTFIELDS => json_encode([
|
|
|
54 |
'appId' => $this->base->appId,
|
|
|
55 |
'courseId' => $containerId,
|
|
|
56 |
'resourceId' => $resourceId,
|
|
|
57 |
'nodeId' => $nodeId,
|
|
|
58 |
'nodeVersion' => $nodeVersion,
|
|
|
59 |
], 512, JSON_THROW_ON_ERROR),
|
|
|
60 |
CURLOPT_RETURNTRANSFER => 1,
|
|
|
61 |
CURLOPT_HTTPHEADER => $headers
|
|
|
62 |
]);
|
|
|
63 |
$data = json_decode($curl->content, true, 512, JSON_THROW_ON_ERROR);
|
|
|
64 |
if ((int)$curl->info['http_code'] === 403 || (!empty($data['error']) && $data['message'] === 'NO_CCPUBLISH_PERMISSION')) {
|
|
|
65 |
throw new MissingRightsException("User missing publish rights.");
|
|
|
66 |
}
|
|
|
67 |
if (empty($data['parentNodeId']) || empty($data['nodeId'])) {
|
|
|
68 |
error_log('Creating usage failed for node: ' . $nodeId . '. Returned content: ' . $curl->content);
|
|
|
69 |
throw new Exception('creating usage failed: ' . $nodeId);
|
|
|
70 |
}
|
|
|
71 |
if ($curl->error === 0 && $curl->info['http_code'] ?? 0 === 200 && empty($data['error'])) {
|
|
|
72 |
return new Usage($data['parentNodeId'], $nodeVersion, $containerId, $resourceId, $data['nodeId']);
|
|
|
73 |
}
|
|
|
74 |
throw new Exception('creating usage failed ' . $curl->info['http_code'] . ': ' . $data['error'] . ' ' . $data['message']);
|
|
|
75 |
}
|
|
|
76 |
|
|
|
77 |
/**
|
|
|
78 |
* @DEPRECATED
|
|
|
79 |
* Function getUsageIdByParameters
|
|
|
80 |
*
|
|
|
81 |
* Returns the id of an usage object for a given node, container & resource id of that usage
|
|
|
82 |
* This is only relevant for legacy plugins which do not store the usage id and need to fetch it in order to delete an usage
|
|
|
83 |
* @param string $ticket
|
|
|
84 |
* A ticket with the user session who is creating this usage
|
|
|
85 |
* @param string $containerId
|
|
|
86 |
* A unique page / course id this usage refers to inside your system (e.g. a database id of the page you include the usage)
|
|
|
87 |
* @param string $resourceId
|
|
|
88 |
* The individual resource id on the current page or course this object refers to
|
|
|
89 |
* (you may enumerate or use unique UUID's)
|
|
|
90 |
* @return string|null
|
|
|
91 |
* The id of the usage, or NULL if no usage with the given data was found
|
|
|
92 |
* @throws Exception
|
|
|
93 |
*/
|
|
|
94 |
public function getUsageIdByParameters(string $ticket, string $nodeId, string $containerId, string $resourceId): ?string {
|
|
|
95 |
$headers = $this->getSignatureHeaders($ticket);
|
|
|
96 |
$headers[] = $this->getRESTAuthenticationHeader($ticket);
|
|
|
97 |
$curl = $this->base->handleCurlRequest($this->base->baseUrl . '/rest/usage/v1/usages/node/' . rawurlencode($nodeId), [
|
|
|
98 |
CURLOPT_FAILONERROR => false,
|
|
|
99 |
CURLOPT_RETURNTRANSFER => 1,
|
|
|
100 |
CURLOPT_HTTPHEADER => $headers
|
|
|
101 |
]);
|
|
|
102 |
$data = json_decode($curl->content, true, 512, JSON_THROW_ON_ERROR);
|
|
|
103 |
if ($curl->error === 0 && $curl->info['http_code'] ?? 0 === 200 && isset($data['usages'])) {
|
|
|
104 |
foreach ($data['usages'] as $usage) {
|
|
|
105 |
if ((string)$usage['appId'] === $this->base->appId && (string)$usage['courseId'] === $containerId && (string)$usage['resourceId'] === $resourceId) {
|
|
|
106 |
return isset($usage['nodeId']) ? (string)$usage['nodeId'] : null;
|
|
|
107 |
}
|
|
|
108 |
}
|
|
|
109 |
return null;
|
|
|
110 |
}
|
|
|
111 |
throw new Exception('fetching usage list for course failed '
|
|
|
112 |
. ($curl->info['http_code'] ?? 'unknown') . ': ' . ($data['error'] ?? 'unknown') . ' ' . ($data['message'] ?? 'unknown'));
|
|
|
113 |
}
|
|
|
114 |
|
|
|
115 |
/**
|
|
|
116 |
* Function getNodeByUsage
|
|
|
117 |
*
|
|
|
118 |
* Loads the edu-sharing node referred by a given usage
|
|
|
119 |
* @param Usage $usage
|
|
|
120 |
* The usage, as previously returned by @createUsage
|
|
|
121 |
* @param string $displayMode
|
|
|
122 |
* The displayMode
|
|
|
123 |
* This will ONLY change the content representation inside the "detailsSnippet" return value
|
|
|
124 |
* @param array|null $renderingParams
|
|
|
125 |
* @param string|null $userId
|
|
|
126 |
* The userId can be included for tracking and statistics purposes
|
|
|
127 |
* @return array
|
|
|
128 |
* Returns an object containing a "detailsSnippet" representation
|
|
|
129 |
* as well as the full node as provided by the REST API
|
|
|
130 |
* Please refer to the edu-sharing REST documentation for more details
|
|
|
131 |
* @throws JsonException
|
|
|
132 |
* @throws NodeDeletedException
|
|
|
133 |
* @throws UsageDeletedException
|
|
|
134 |
* @throws Exception
|
|
|
135 |
*/
|
|
|
136 |
public function getNodeByUsage(Usage $usage, string $displayMode = DisplayMode::INLINE, ?array $renderingParams = null, ?string $userId = null): array {
|
|
|
137 |
$url = $this->base->baseUrl . '/rest/rendering/v1/details/-home-/' . rawurlencode($usage->nodeId);
|
|
|
138 |
$url .= '?displayMode=' . rawurlencode($displayMode);
|
|
|
139 |
if ($usage->nodeVersion) {
|
|
|
140 |
$url .= '&version=' . rawurlencode($usage->nodeVersion);
|
|
|
141 |
}
|
|
|
142 |
$headers = $this->getUsageSignatureHeaders($usage, $userId);
|
|
|
143 |
$curl = $this->base->handleCurlRequest($url, [
|
|
|
144 |
CURLOPT_FAILONERROR => false,
|
|
|
145 |
CURLOPT_POST => 1,
|
|
|
146 |
CURLOPT_POSTFIELDS => json_encode($renderingParams),
|
|
|
147 |
CURLOPT_RETURNTRANSFER => 1,
|
|
|
148 |
CURLOPT_HTTPHEADER => $headers
|
|
|
149 |
]);
|
|
|
150 |
$data = json_decode($curl->content, true, 512, JSON_THROW_ON_ERROR);
|
|
|
151 |
$this->handleURLMapping($data, $usage);
|
|
|
152 |
if ($curl->error === 0 && (int)($curl->info['http_code'] ?? 0) === 200) {
|
|
|
153 |
return $data;
|
|
|
154 |
}
|
|
|
155 |
if ((int)($curl->info['http_code'] ?? 0) === 403) {
|
|
|
156 |
throw new UsageDeletedException('the given usage is deleted and the requested node is not public');
|
|
|
157 |
} else if ((int)($curl->info['http_code'] ?? 0) === 404) {
|
|
|
158 |
throw new NodeDeletedException('the given node is already deleted ' . $curl->info['http_code'] . ': ' . $data['error'] . ' ' . $data['message']);
|
|
|
159 |
} else {
|
|
|
160 |
throw new Exception('fetching node by usage failed ' . $curl->info['http_code'] . ': ' . $data['error'] . ' ' . $data['message']);
|
|
|
161 |
}
|
|
|
162 |
}
|
|
|
163 |
|
|
|
164 |
/**
|
|
|
165 |
* Function deleteUsage
|
|
|
166 |
*
|
|
|
167 |
* Deletes the given usage
|
|
|
168 |
* We trust that you've validated if the current user in your context is allowed to do so
|
|
|
169 |
* There is no restriction in deleting usages even from foreign users, as long as they were generated by your app
|
|
|
170 |
* Thus, this endpoint does not require any user ticket
|
|
|
171 |
* @param string $nodeId
|
|
|
172 |
* The edu-sharing node id this usage belongs to
|
|
|
173 |
* @param string $usageId
|
|
|
174 |
* The usage id
|
|
|
175 |
* @throws UsageDeletedException
|
|
|
176 |
* @throws Exception
|
|
|
177 |
*/
|
|
|
178 |
public function deleteUsage(string $nodeId, string $usageId): void {
|
|
|
179 |
$headers = $this->getSignatureHeaders($nodeId . $usageId);
|
|
|
180 |
$curl = $this->base->handleCurlRequest($this->base->baseUrl . '/rest/usage/v1/usages/node/' . rawurlencode($nodeId) . '/' . rawurlencode($usageId), [
|
|
|
181 |
CURLOPT_FAILONERROR => false,
|
|
|
182 |
CURLOPT_CUSTOMREQUEST => 'DELETE',
|
|
|
183 |
CURLOPT_RETURNTRANSFER => 1,
|
|
|
184 |
CURLOPT_HTTPHEADER => $headers
|
|
|
185 |
]);
|
|
|
186 |
if ($curl->error === 0 && (int)($curl->info['http_code'] ?? 0) === 200) {
|
|
|
187 |
return;
|
|
|
188 |
}
|
|
|
189 |
if ((int)($curl->info['http_code'] ?? 0) === 404) {
|
|
|
190 |
throw new UsageDeletedException('the given usage is already deleted or does not exist');
|
|
|
191 |
} else {
|
|
|
192 |
throw new Exception('deleting usage failed with curl error ' . $curl->error);
|
|
|
193 |
}
|
|
|
194 |
}
|
|
|
195 |
|
|
|
196 |
/**
|
|
|
197 |
* Function handleURLMapping
|
|
|
198 |
*
|
|
|
199 |
* @param $data
|
|
|
200 |
* @param Usage $usage
|
|
|
201 |
*/
|
|
|
202 |
private function handleURLMapping(&$data, Usage $usage): void {
|
|
|
203 |
if (!$this->config->urlHandling->enabled) {
|
|
|
204 |
return;
|
|
|
205 |
}
|
|
|
206 |
if (isset($data['node'])) {
|
|
|
207 |
$params = '&usageId=' . urlencode($usage->usageId) . '&nodeId=' . urlencode($usage->nodeId) . '&resourceId=' . urlencode($usage->resourceId) . '&containerId=' . urlencode($usage->containerId);
|
|
|
208 |
if ($usage->nodeVersion) {
|
|
|
209 |
$params .= '&nodeVersion=' . urlencode($usage->nodeVersion);
|
|
|
210 |
}
|
|
|
211 |
$endpointBase = $this->config->urlHandling->endpointURL . (str_contains($this->config->urlHandling->endpointURL, '?') ? '&' : '?');
|
|
|
212 |
$contentUrl = $endpointBase . 'mode=content' . $params;
|
|
|
213 |
$data['url'] = [
|
|
|
214 |
'content' => $contentUrl,
|
|
|
215 |
'download' => $endpointBase . 'mode=download' . $params
|
|
|
216 |
];
|
|
|
217 |
$data['detailsSnippet'] = str_replace('{{{LMS_INLINE_HELPER_SCRIPT}}}', $contentUrl, $data['detailsSnippet']);
|
|
|
218 |
$data['detailsSnippet'] = str_replace('{{{TICKET}}}', '', $data['detailsSnippet']);
|
|
|
219 |
}
|
|
|
220 |
}
|
|
|
221 |
|
|
|
222 |
/**
|
|
|
223 |
* Function getRedirectUrl
|
|
|
224 |
*
|
|
|
225 |
* @param string $mode
|
|
|
226 |
* @param Usage $usage
|
|
|
227 |
* @param array $additionalParams
|
|
|
228 |
* Additional query params that shall be passed to the repository url (as key=>value structure)
|
|
|
229 |
* @param string|null $userId
|
|
|
230 |
* The user id. Note: Due to the current behaviour, this userId will currently NOT obeyed for the tracking results
|
|
|
231 |
* of this method, the statistics/tracking when going into the full view will always be anonymous
|
|
|
232 |
* @return string
|
|
|
233 |
* @throws JsonException
|
|
|
234 |
* @throws NodeDeletedException
|
|
|
235 |
* @throws UsageDeletedException
|
|
|
236 |
* @throws Exception
|
|
|
237 |
*/
|
|
|
238 |
public function getRedirectUrl(string $mode, Usage $usage, array $additionalParams = [], ?string $userId = null): string {
|
|
|
239 |
$headers = $this->getUsageSignatureHeaders($usage);
|
|
|
240 |
// DisplayMode::PRERENDER is used in order to differentiate for tracking and statistics
|
|
|
241 |
$node = $this->getNodeByUsage($usage, DisplayMode::PRERENDER, null, $userId);
|
|
|
242 |
$params = '';
|
|
|
243 |
foreach ($headers as $header) {
|
|
|
244 |
if (!str_starts_with($header, 'X-')) {
|
|
|
245 |
continue;
|
|
|
246 |
}
|
|
|
247 |
$header = explode(': ', $header);
|
|
|
248 |
$params .= '&' . $header[0] . '=' . urlencode($header[1]);
|
|
|
249 |
}
|
|
|
250 |
foreach($additionalParams as $key => $value) {
|
|
|
251 |
foreach ($headers as $header) {
|
|
|
252 |
if($header[0] === $key) {
|
|
|
253 |
continue(2);
|
|
|
254 |
}
|
|
|
255 |
}
|
|
|
256 |
$params .= '&' . $key . '=' . urlencode($value);
|
|
|
257 |
}
|
|
|
258 |
if ($mode === 'content') {
|
|
|
259 |
$url = $node['node']['content']['url'] ?? '';
|
|
|
260 |
$params .= '&closeOnBack=true';
|
|
|
261 |
} else if ($mode === 'download') {
|
|
|
262 |
$url = $node['node']['downloadUrl'] ?? '';
|
|
|
263 |
} else {
|
|
|
264 |
throw new Exception('Unknown parameter for mode: ' . $mode);
|
|
|
265 |
}
|
|
|
266 |
return $url . (str_contains($url, '?') ? '' : '?') . $params;
|
|
|
267 |
}
|
|
|
268 |
|
|
|
269 |
/**
|
|
|
270 |
* Function getUsageSignatureHeaders
|
|
|
271 |
*
|
|
|
272 |
* @param Usage $usage
|
|
|
273 |
* @param string|null $userId
|
|
|
274 |
* @return array
|
|
|
275 |
*/
|
|
|
276 |
private function getUsageSignatureHeaders(Usage $usage, ?string $userId = null): array {
|
|
|
277 |
$headers = $this->getSignatureHeaders($usage->usageId);
|
|
|
278 |
$headers[] = 'X-Edu-Usage-Node-Id: ' . $usage->nodeId;
|
|
|
279 |
$headers[] = 'X-Edu-Usage-Course-Id: ' . $usage->containerId;
|
|
|
280 |
$headers[] = 'X-Edu-Usage-Resource-Id: ' . $usage->resourceId;
|
|
|
281 |
if ($userId !== null) {
|
|
|
282 |
$headers[] = 'X-Edu-User-Id: ' . $userId;
|
|
|
283 |
}
|
|
|
284 |
return $headers;
|
|
|
285 |
}
|
|
|
286 |
|
|
|
287 |
/**
|
|
|
288 |
* Function getPreview
|
|
|
289 |
*
|
|
|
290 |
* @param Usage $usage
|
|
|
291 |
* @return CurlResult
|
|
|
292 |
*/
|
|
|
293 |
public function getPreview(Usage $usage): CurlResult {
|
|
|
294 |
$url = $this->base->baseUrl . '/preview?nodeId=' . rawurlencode($usage->nodeId) . '&maxWidth=400&maxHeight=400&crop=true';
|
|
|
295 |
if ($usage->nodeVersion) {
|
|
|
296 |
$url .= '&version=' . rawurlencode($usage->nodeVersion);
|
|
|
297 |
}
|
|
|
298 |
$headers = $this->getUsageSignatureHeaders($usage);
|
|
|
299 |
return $this->base->handleCurlRequest($url, [
|
|
|
300 |
CURLOPT_FAILONERROR => false,
|
|
|
301 |
CURLOPT_RETURNTRANSFER => 1,
|
|
|
302 |
CURLOPT_HTTPHEADER => $headers
|
|
|
303 |
]);
|
|
|
304 |
}
|
|
|
305 |
}
|