AutorÃa | Ultima modificación | Ver Log |
<?php declare(strict_types=1);
namespace EduSharingApiClient;
use Exception;
use JsonException;
/**
* Class EduSharingNodeHelper
*
* @author Torsten Simon <simon@edu-sharing.net>
* @author Marian Ziegler <ziegler@edu-sharing.net>
*/
class EduSharingNodeHelper extends EduSharingHelperAbstract
{
private EduSharingNodeHelperConfig $config;
public function __construct(EduSharingHelperBase $base, EduSharingNodeHelperConfig $config) {
parent::__construct($base);
$this->config = $config;
}
/**
* creates a usage for a given node
* The given usage can later be used to fetch this node REGARDLESS of the actual user
* The usage gives permanent access to this node and acts similar to a license
* In order to be able to create an usage for a node, the current user (provided via the ticket)
* MUST have CC_PUBLISH permissions on the given node id
* @param string $ticket
* A ticket with the user session who is creating this usage
* @param string $containerId
* A unique page / course id this usage refers to inside your system (e.g. a database id of the page you include the usage)
* @param string $resourceId
* The individual resource id on the current page or course this object refers to
* (you may enumerate or use unique UUID's)
* @param string $nodeId
* The edu-sharing node id the usage shall be created for
* @param string|null $nodeVersion
* Optional: The fixed version this usage should refer to
* If you leave it empty, the usage will always refer to the latest version of the node
* @return Usage
* An usage element you can use with @getNodeByUsage
* Keep all data of this object stored inside your system!
* @throws JsonException
* @throws Exception
*/
public function createUsage(string $ticket, string $containerId, string $resourceId, string $nodeId, string $nodeVersion = null): Usage {
$headers = $this->getSignatureHeaders($ticket);
$headers[] = $this->getRESTAuthenticationHeader($ticket);
$curl = $this->base->handleCurlRequest($this->base->baseUrl . '/rest/usage/v1/usages/repository/-home-', [
CURLOPT_FAILONERROR => false,
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => json_encode([
'appId' => $this->base->appId,
'courseId' => $containerId,
'resourceId' => $resourceId,
'nodeId' => $nodeId,
'nodeVersion' => $nodeVersion,
], 512, JSON_THROW_ON_ERROR),
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HTTPHEADER => $headers
]);
$data = json_decode($curl->content, true, 512, JSON_THROW_ON_ERROR);
if ((int)$curl->info['http_code'] === 403 || (!empty($data['error']) && $data['message'] === 'NO_CCPUBLISH_PERMISSION')) {
throw new MissingRightsException("User missing publish rights.");
}
if (empty($data['parentNodeId']) || empty($data['nodeId'])) {
error_log('Creating usage failed for node: ' . $nodeId . '. Returned content: ' . $curl->content);
throw new Exception('creating usage failed: ' . $nodeId);
}
if ($curl->error === 0 && $curl->info['http_code'] ?? 0 === 200 && empty($data['error'])) {
return new Usage($data['parentNodeId'], $nodeVersion, $containerId, $resourceId, $data['nodeId']);
}
throw new Exception('creating usage failed ' . $curl->info['http_code'] . ': ' . $data['error'] . ' ' . $data['message']);
}
/**
* @DEPRECATED
* Function getUsageIdByParameters
*
* Returns the id of an usage object for a given node, container & resource id of that usage
* 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
* @param string $ticket
* A ticket with the user session who is creating this usage
* @param string $containerId
* A unique page / course id this usage refers to inside your system (e.g. a database id of the page you include the usage)
* @param string $resourceId
* The individual resource id on the current page or course this object refers to
* (you may enumerate or use unique UUID's)
* @return string|null
* The id of the usage, or NULL if no usage with the given data was found
* @throws Exception
*/
public function getUsageIdByParameters(string $ticket, string $nodeId, string $containerId, string $resourceId): ?string {
$headers = $this->getSignatureHeaders($ticket);
$headers[] = $this->getRESTAuthenticationHeader($ticket);
$curl = $this->base->handleCurlRequest($this->base->baseUrl . '/rest/usage/v1/usages/node/' . rawurlencode($nodeId), [
CURLOPT_FAILONERROR => false,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HTTPHEADER => $headers
]);
$data = json_decode($curl->content, true, 512, JSON_THROW_ON_ERROR);
if ($curl->error === 0 && $curl->info['http_code'] ?? 0 === 200 && isset($data['usages'])) {
foreach ($data['usages'] as $usage) {
if ((string)$usage['appId'] === $this->base->appId && (string)$usage['courseId'] === $containerId && (string)$usage['resourceId'] === $resourceId) {
return isset($usage['nodeId']) ? (string)$usage['nodeId'] : null;
}
}
return null;
}
throw new Exception('fetching usage list for course failed '
. ($curl->info['http_code'] ?? 'unknown') . ': ' . ($data['error'] ?? 'unknown') . ' ' . ($data['message'] ?? 'unknown'));
}
/**
* Function getNodeByUsage
*
* Loads the edu-sharing node referred by a given usage
* @param Usage $usage
* The usage, as previously returned by @createUsage
* @param string $displayMode
* The displayMode
* This will ONLY change the content representation inside the "detailsSnippet" return value
* @param array|null $renderingParams
* @param string|null $userId
* The userId can be included for tracking and statistics purposes
* @return array
* Returns an object containing a "detailsSnippet" representation
* as well as the full node as provided by the REST API
* Please refer to the edu-sharing REST documentation for more details
* @throws JsonException
* @throws NodeDeletedException
* @throws UsageDeletedException
* @throws Exception
*/
public function getNodeByUsage(Usage $usage, string $displayMode = DisplayMode::INLINE, ?array $renderingParams = null, ?string $userId = null): array {
$url = $this->base->baseUrl . '/rest/rendering/v1/details/-home-/' . rawurlencode($usage->nodeId);
$url .= '?displayMode=' . rawurlencode($displayMode);
if ($usage->nodeVersion) {
$url .= '&version=' . rawurlencode($usage->nodeVersion);
}
$headers = $this->getUsageSignatureHeaders($usage, $userId);
$curl = $this->base->handleCurlRequest($url, [
CURLOPT_FAILONERROR => false,
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => json_encode($renderingParams),
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HTTPHEADER => $headers
]);
$data = json_decode($curl->content, true, 512, JSON_THROW_ON_ERROR);
$this->handleURLMapping($data, $usage);
if ($curl->error === 0 && (int)($curl->info['http_code'] ?? 0) === 200) {
return $data;
}
if ((int)($curl->info['http_code'] ?? 0) === 403) {
throw new UsageDeletedException('the given usage is deleted and the requested node is not public');
} else if ((int)($curl->info['http_code'] ?? 0) === 404) {
throw new NodeDeletedException('the given node is already deleted ' . $curl->info['http_code'] . ': ' . $data['error'] . ' ' . $data['message']);
} else {
throw new Exception('fetching node by usage failed ' . $curl->info['http_code'] . ': ' . $data['error'] . ' ' . $data['message']);
}
}
/**
* Function deleteUsage
*
* Deletes the given usage
* We trust that you've validated if the current user in your context is allowed to do so
* There is no restriction in deleting usages even from foreign users, as long as they were generated by your app
* Thus, this endpoint does not require any user ticket
* @param string $nodeId
* The edu-sharing node id this usage belongs to
* @param string $usageId
* The usage id
* @throws UsageDeletedException
* @throws Exception
*/
public function deleteUsage(string $nodeId, string $usageId): void {
$headers = $this->getSignatureHeaders($nodeId . $usageId);
$curl = $this->base->handleCurlRequest($this->base->baseUrl . '/rest/usage/v1/usages/node/' . rawurlencode($nodeId) . '/' . rawurlencode($usageId), [
CURLOPT_FAILONERROR => false,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HTTPHEADER => $headers
]);
if ($curl->error === 0 && (int)($curl->info['http_code'] ?? 0) === 200) {
return;
}
if ((int)($curl->info['http_code'] ?? 0) === 404) {
throw new UsageDeletedException('the given usage is already deleted or does not exist');
} else {
throw new Exception('deleting usage failed with curl error ' . $curl->error);
}
}
/**
* Function handleURLMapping
*
* @param $data
* @param Usage $usage
*/
private function handleURLMapping(&$data, Usage $usage): void {
if (!$this->config->urlHandling->enabled) {
return;
}
if (isset($data['node'])) {
$params = '&usageId=' . urlencode($usage->usageId) . '&nodeId=' . urlencode($usage->nodeId) . '&resourceId=' . urlencode($usage->resourceId) . '&containerId=' . urlencode($usage->containerId);
if ($usage->nodeVersion) {
$params .= '&nodeVersion=' . urlencode($usage->nodeVersion);
}
$endpointBase = $this->config->urlHandling->endpointURL . (str_contains($this->config->urlHandling->endpointURL, '?') ? '&' : '?');
$contentUrl = $endpointBase . 'mode=content' . $params;
$data['url'] = [
'content' => $contentUrl,
'download' => $endpointBase . 'mode=download' . $params
];
$data['detailsSnippet'] = str_replace('{{{LMS_INLINE_HELPER_SCRIPT}}}', $contentUrl, $data['detailsSnippet']);
$data['detailsSnippet'] = str_replace('{{{TICKET}}}', '', $data['detailsSnippet']);
}
}
/**
* Function getRedirectUrl
*
* @param string $mode
* @param Usage $usage
* @param array $additionalParams
* Additional query params that shall be passed to the repository url (as key=>value structure)
* @param string|null $userId
* The user id. Note: Due to the current behaviour, this userId will currently NOT obeyed for the tracking results
* of this method, the statistics/tracking when going into the full view will always be anonymous
* @return string
* @throws JsonException
* @throws NodeDeletedException
* @throws UsageDeletedException
* @throws Exception
*/
public function getRedirectUrl(string $mode, Usage $usage, array $additionalParams = [], ?string $userId = null): string {
$headers = $this->getUsageSignatureHeaders($usage);
// DisplayMode::PRERENDER is used in order to differentiate for tracking and statistics
$node = $this->getNodeByUsage($usage, DisplayMode::PRERENDER, null, $userId);
$params = '';
foreach ($headers as $header) {
if (!str_starts_with($header, 'X-')) {
continue;
}
$header = explode(': ', $header);
$params .= '&' . $header[0] . '=' . urlencode($header[1]);
}
foreach($additionalParams as $key => $value) {
foreach ($headers as $header) {
if($header[0] === $key) {
continue(2);
}
}
$params .= '&' . $key . '=' . urlencode($value);
}
if ($mode === 'content') {
$url = $node['node']['content']['url'] ?? '';
$params .= '&closeOnBack=true';
} else if ($mode === 'download') {
$url = $node['node']['downloadUrl'] ?? '';
} else {
throw new Exception('Unknown parameter for mode: ' . $mode);
}
return $url . (str_contains($url, '?') ? '' : '?') . $params;
}
/**
* Function getUsageSignatureHeaders
*
* @param Usage $usage
* @param string|null $userId
* @return array
*/
private function getUsageSignatureHeaders(Usage $usage, ?string $userId = null): array {
$headers = $this->getSignatureHeaders($usage->usageId);
$headers[] = 'X-Edu-Usage-Node-Id: ' . $usage->nodeId;
$headers[] = 'X-Edu-Usage-Course-Id: ' . $usage->containerId;
$headers[] = 'X-Edu-Usage-Resource-Id: ' . $usage->resourceId;
if ($userId !== null) {
$headers[] = 'X-Edu-User-Id: ' . $userId;
}
return $headers;
}
/**
* Function getPreview
*
* @param Usage $usage
* @return CurlResult
*/
public function getPreview(Usage $usage): CurlResult {
$url = $this->base->baseUrl . '/preview?nodeId=' . rawurlencode($usage->nodeId) . '&maxWidth=400&maxHeight=400&crop=true';
if ($usage->nodeVersion) {
$url .= '&version=' . rawurlencode($usage->nodeVersion);
}
$headers = $this->getUsageSignatureHeaders($usage);
return $this->base->handleCurlRequest($url, [
CURLOPT_FAILONERROR => false,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HTTPHEADER => $headers
]);
}
}