diff --git a/fonts/h5p-core-24.eot b/fonts/h5p-core-24.eot deleted file mode 100644 index e22a6ea..0000000 Binary files a/fonts/h5p-core-24.eot and /dev/null differ diff --git a/fonts/h5p-core-24.svg b/fonts/h5p-core-24.svg deleted file mode 100644 index 06c7ab5..0000000 --- a/fonts/h5p-core-24.svg +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - -{ - "fontFamily": "h5p-core", - "description": "Font generated by IcoMoon.", - "majorVersion": 1, - "minorVersion": 1, - "version": "Version 1.1", - "fontId": "h5p-core", - "psName": "h5p-core", - "subFamily": "Regular", - "fullName": "h5p-core" -} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fonts/h5p-core-24.ttf b/fonts/h5p-core-24.ttf deleted file mode 100644 index 496dadb..0000000 Binary files a/fonts/h5p-core-24.ttf and /dev/null differ diff --git a/fonts/h5p-core-24.woff b/fonts/h5p-core-24.woff deleted file mode 100644 index c8d6370..0000000 Binary files a/fonts/h5p-core-24.woff and /dev/null differ diff --git a/fonts/h5p-core-28.eot b/fonts/h5p-core-28.eot new file mode 100644 index 0000000..4b2bf45 Binary files /dev/null and b/fonts/h5p-core-28.eot differ diff --git a/fonts/h5p-core-28.svg b/fonts/h5p-core-28.svg new file mode 100644 index 0000000..7de6180 --- /dev/null +++ b/fonts/h5p-core-28.svg @@ -0,0 +1,114 @@ + + + + + + +{ + "fontFamily": "h5p-core-27", + "description": "Font generated by IcoMoon.", + "copyright": "H5P", + "majorVersion": 1, + "minorVersion": 1, + "version": "Version 1.1", + "fontId": "h5p-core-27", + "psName": "h5p-core-27", + "subFamily": "Regular", + "fullName": "h5p-core-27" +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/h5p-core-28.ttf b/fonts/h5p-core-28.ttf new file mode 100644 index 0000000..520db3a Binary files /dev/null and b/fonts/h5p-core-28.ttf differ diff --git a/fonts/h5p-core-28.woff b/fonts/h5p-core-28.woff new file mode 100644 index 0000000..6ba9109 Binary files /dev/null and b/fonts/h5p-core-28.woff differ diff --git a/fonts/h5p-hub-publish.eot b/fonts/h5p-hub-publish.eot new file mode 100644 index 0000000..6afdd28 Binary files /dev/null and b/fonts/h5p-hub-publish.eot differ diff --git a/fonts/h5p-hub-publish.svg b/fonts/h5p-hub-publish.svg new file mode 100644 index 0000000..e32badd --- /dev/null +++ b/fonts/h5p-hub-publish.svg @@ -0,0 +1,38 @@ + + + + + + +{ + "fontFamily": "h5p-hub", + "description": "Font generated by IcoMoon.", + "majorVersion": 1, + "minorVersion": 3, + "version": "Version 1.3", + "fontId": "h5p-hub", + "psName": "h5p-hub", + "subFamily": "Regular", + "fullName": "h5p-hub" +} + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/h5p-hub-publish.ttf b/fonts/h5p-hub-publish.ttf new file mode 100644 index 0000000..d1d40dd Binary files /dev/null and b/fonts/h5p-hub-publish.ttf differ diff --git a/fonts/h5p-hub-publish.woff b/fonts/h5p-hub-publish.woff new file mode 100644 index 0000000..c053534 Binary files /dev/null and b/fonts/h5p-hub-publish.woff differ diff --git a/h5p-metadata.class.php b/h5p-metadata.class.php index 572cfd0..5b045aa 100644 --- a/h5p-metadata.class.php +++ b/h5p-metadata.class.php @@ -9,6 +9,10 @@ abstract class H5PMetadata { 'type' => 'text', 'maxLength' => 255 ), + 'a11yTitle' => array( + 'type' => 'text', + 'maxLength' => 255, + ), 'authors' => array( 'type' => 'json' ), @@ -57,6 +61,7 @@ abstract class H5PMetadata { // Note: deliberatly creating JSON string "manually" to improve performance return '{"title":' . (isset($content->title) ? json_encode($content->title) : 'null') . + ',"a11yTitle":' . (isset($content->a11y_title) ? $content->a11y_title : 'null') . ',"authors":' . (isset($content->authors) ? $content->authors : 'null') . ',"source":' . (isset($content->source) ? '"' . $content->source . '"' : 'null') . ',"license":' . (isset($content->license) ? '"' . $content->license . '"' : 'null') . diff --git a/h5p.classes.php b/h5p.classes.php index c1c648f..6fde273 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -19,13 +19,18 @@ interface H5PFrameworkInterface { /** * Fetches a file from a remote server using HTTP GET * - * @param string $url Where you want to get or send data. - * @param array $data Data to post to the URL. - * @param bool $blocking Set to 'FALSE' to instantly time out (fire and forget). - * @param string $stream Path to where the file should be saved. - * @return string The content (response body). NULL if something went wrong + * @param string $url Where you want to get or send data. + * @param array $data Data to post to the URL. + * @param bool $blocking Set to 'FALSE' to instantly time out (fire and forget). + * @param string $stream Path to where the file should be saved. + * @param bool $fullData Return additional response data such as headers and potentially other data + * @param array $headers Headers to send + * @param array $files Files to send + * @param string $method + * + * @return string|array The content (response body), or an array with data. NULL if something went wrong */ - public function fetchExternalData($url, $data = NULL, $blocking = TRUE, $stream = NULL); + public function fetchExternalData($url, $data = NULL, $blocking = TRUE, $stream = NULL, $fullData = FALSE, $headers = array(), $files = array(), $method = 'POST'); /** * Set the tutorial URL for a library. All versions of the library is set @@ -623,6 +628,44 @@ interface H5PFrameworkInterface { * @return boolean */ public function libraryHasUpgrade($library); + + /** + * Replace content hub metadata cache + * + * @param JsonSerializable $metadata Metadata as received from content hub + * @param string $lang Language in ISO 639-1 + * + * @return mixed + */ + public function replaceContentHubMetadataCache($metadata, $lang); + + /** + * Get content hub metadata cache from db + * + * @param string $lang Language code in ISO 639-1 + * + * @return JsonSerializable Json string + */ + public function getContentHubMetadataCache($lang = 'en'); + + /** + * Get time of last content hub metadata check + * + * @param string $lang Language code iin ISO 639-1 format + * + * @return string|null Time in RFC7231 format + */ + public function getContentHubMetadataChecked($lang = 'en'); + + /** + * Set time of last content hub metadata check + * + * @param int|null $time Time in RFC7231 format + * @param string $lang Language code iin ISO 639-1 format + * + * @return bool True if successful + */ + public function setContentHubMetadataChecked($time, $lang = 'en'); } /** @@ -1623,7 +1666,7 @@ class H5PStorage { } // Go through the libraries again to save dependencies. - $library_ids = []; + $library_ids = array(); foreach ($this->h5pC->librariesJsonData as &$library) { if (!$library['saveDependencies']) { continue; @@ -1986,11 +2029,29 @@ abstract class H5PSaveContentStorages { const DATABASE = 1; const LOCALSTORAGE = 2; const DATABASE_LOCALSTORAGE = 3; + +abstract class H5PContentHubSyncStatus { + const NOT_SYNCED = 0; + const SYNCED = 1; + const WAITING = 2; + const FAILED = 3; +} + +abstract class H5PContentStatus { + const STATUS_UNPUBLISHED = 0; + const STATUS_DOWNLOADED = 1; + const STATUS_WAITING = 2; + const STATUS_FAILED_DOWNLOAD = 3; + const STATUS_FAILED_VALIDATION = 4; + const STATUS_SUSPENDED = 5; } abstract class H5PHubEndpoints { const CONTENT_TYPES = 'api.h5p.org/v1/content-types/'; const SITES = 'api.h5p.org/v1/sites'; + const METADATA = 'hub-api.h5p.org/v1/metadata'; + const CONTENT = 'hub-api.h5p.org/v1/contents'; + const REGISTER = 'hub-api.h5p.org/v1/accounts'; public static function createURL($endpoint) { $protocol = (extension_loaded('openssl') ? 'https' : 'http'); @@ -2028,7 +2089,7 @@ class H5PCore { 'js/h5p-utils.js', ); - public static $defaultContentWhitelist = 'json png jpg jpeg gif bmp tif tiff svg eot ttf woff woff2 otf webm mp4 ogg mp3 m4a wav txt pdf rtf doc docx xls xlsx ppt pptx odt ods odp xml csv diff patch swf md textile vtt webvtt'; + public static $defaultContentWhitelist = 'json png jpg jpeg gif bmp tif tiff svg eot ttf woff woff2 otf webm mp4 ogg mp3 m4a wav txt pdf rtf doc docx xls xlsx ppt pptx odt ods odp xml csv diff patch swf md textile vtt webvtt gltf glb'; public static $defaultLibraryWhitelistExtras = 'js css'; public $librariesJsonData, $contentJsonData, $mainJsonData, $h5pF, $fs, $h5pD, $disableFileCheck; @@ -2834,10 +2895,11 @@ class H5PCore { * implementation is responsible for invoking this, eg using cron * * @param bool $fetchingDisabled + * @param bool $onlyRegister Only register site with H5P.org * * @return bool|object Returns endpoint data if found, otherwise FALSE */ - public function fetchLibrariesMetadata($fetchingDisabled = FALSE) { + public function fetchLibrariesMetadata($fetchingDisabled = FALSE, $onlyRegister = false) { // Gather data $uuid = $this->h5pF->getOption('site_uuid', ''); $platform = $this->h5pF->getPlatformInfo(); @@ -2875,12 +2937,17 @@ class H5PCore { $this->h5pF->setInfoMessage( $this->h5pF->t('Your site was successfully registered with the H5P Hub.') ); + $uuid = $json->uuid; // TODO: Uncomment when key is once again available in H5P Settings // $this->h5pF->setInfoMessage( // $this->h5pF->t('You have been provided a unique key that identifies you with the Hub when receiving new updates. The key is available for viewing in the "H5P Settings" page.') // ); } + if ($onlyRegister) { + return $uuid; + } + if ($this->h5pF->getOption('send_usage_statistics', TRUE)) { $siteData = array_merge( $registrationData, @@ -3304,6 +3371,72 @@ class H5PCore { return $data; } + /** + * Update content hub metadata cache + */ + public function updateContentHubMetadataCache($lang = 'en') { + $url = H5PHubEndpoints::createURL(H5PHubEndpoints::METADATA); + $lastModified = $this->h5pF->getContentHubMetadataChecked($lang); + + $headers = array(); + if (!empty($lastModified)) { + $headers['If-Modified-Since'] = $lastModified; + } + $data = $this->h5pF->fetchExternalData("{$url}?lang={$lang}", NULL, TRUE, NULL, TRUE, $headers, NULL, 'GET'); + $lastChecked = new DateTime('now', new DateTimeZone('GMT')); + + if ($data['status'] !== 200 && $data['status'] !== 304) { + // If this was not a success, set the error message and return + $this->h5pF->setErrorMessage( + $this->h5pF->t('No metadata was received from the H5P Hub. Please try again later.') + ); + return null; + } + + // Update timestamp + $this->h5pF->setContentHubMetadataChecked($lastChecked->getTimestamp(), $lang); + + // Not modified + if ($data['status'] === 304) { + return null; + } + $this->h5pF->replaceContentHubMetadataCache($data['data'], $lang); + // TODO: If 200 should we have checked if it decodes? Or 'success'? Not sure if necessary though + return $data['data']; + } + + /** + * Get updated content hub metadata cache + * + * @param string $lang Language as ISO 639-1 code + * + * @return JsonSerializable|string + */ + public function getUpdatedContentHubMetadataCache($lang = 'en') { + $lastUpdate = $this->h5pF->getContentHubMetadataChecked($lang); + if (!$lastUpdate) { + return $this->updateContentHubMetadataCache($lang); + } + + $lastUpdate = new DateTime($lastUpdate); + $expirationTime = $lastUpdate->getTimestamp() + (60 * 60 * 24); // Check once per day + if (time() > $expirationTime) { + $update = $this->updateContentHubMetadataCache($lang); + if (!empty($update)) { + return $update; + } + } + + $storedCache = $this->h5pF->getContentHubMetadataCache($lang); + if (!$storedCache) { + // We don't have the value stored for some reason, reset last update and re-fetch + $this->h5pF->setContentHubMetadataChecked(null, $lang); + return $this->updateContentHubMetadataCache($lang); + } + + return $storedCache; + } + /** * Check if the current server setup is valid and set error messages * @@ -3512,8 +3645,473 @@ class H5PCore { 'offlineDialogRetryMessage' => $this->h5pF->t('Retrying in :num....'), 'offlineDialogRetryButtonLabel' => $this->h5pF->t('Retry now'), 'offlineSuccessfulSubmit' => $this->h5pF->t('Successfully submitted results.'), + 'mainTitle' => $this->h5pF->t('Sharing :title'), + 'editInfoTitle' => $this->h5pF->t('Edit info for :title'), + 'cancel' => $this->h5pF->t('Cancel'), + 'back' => $this->h5pF->t('Back'), + 'next' => $this->h5pF->t('Next'), + 'reviewInfo' => $this->h5pF->t('Review info'), + 'share' => $this->h5pF->t('Share'), + 'saveChanges' => $this->h5pF->t('Save changes'), + 'registerOnHub' => $this->h5pF->t('Register on the H5P Hub'), + 'updateRegistrationOnHub' => $this->h5pF->t('Save account settings'), + 'requiredInfo' => $this->h5pF->t('Required Info'), + 'optionalInfo' => $this->h5pF->t('Optional Info'), + 'reviewAndShare' => $this->h5pF->t('Review & Share'), + 'reviewAndSave' => $this->h5pF->t('Review & Save'), + 'shared' => $this->h5pF->t('Shared'), + 'currentStep' => $this->h5pF->t('Step :step of :total'), + 'sharingNote' => $this->h5pF->t('All content details can be edited after sharing'), + 'licenseDescription' => $this->h5pF->t('Select a license for your content'), + 'licenseVersion' => $this->h5pF->t('License Version'), + 'licenseVersionDescription' => $this->h5pF->t('Select a license version'), + 'disciplineLabel' => $this->h5pF->t('Disciplines'), + 'disciplineDescription' => $this->h5pF->t('You can select multiple disciplines'), + 'disciplineLimitReachedMessage' => $this->h5pF->t('You can select up to :numDisciplines disciplines'), + 'discipline' => array( + 'searchPlaceholder' => $this->h5pF->t('Type to search for disciplines'), + 'in' => $this->h5pF->t('in'), + 'dropdownButton' => $this->h5pF->t('Dropdown button'), + ), + 'removeChip' => $this->h5pF->t('Remove :chip from the list'), + 'keywordsPlaceholder' => $this->h5pF->t('Add keywords'), + 'keywords' => $this->h5pF->t('Keywords'), + 'keywordsDescription' => $this->h5pF->t('You can add multiple keywords separated by commas. Press "Enter" or "Add" to confirm keywords'), + 'altText' => $this->h5pF->t('Alt text'), + 'reviewMessage' => $this->h5pF->t('Please review the info below before you share'), + 'subContentWarning' => $this->h5pF->t('Sub-content (images, questions etc.) will be shared under :license unless otherwise specified in the authoring tool'), + 'disciplines' => $this->h5pF->t('Disciplines'), + 'shortDescription' => $this->h5pF->t('Short description'), + 'longDescription' => $this->h5pF->t('Long description'), + 'icon' => $this->h5pF->t('Icon'), + 'screenshots' => $this->h5pF->t('Screenshots'), + 'helpChoosingLicense' => $this->h5pF->t('Help me choose a license'), + 'shareFailed' => $this->h5pF->t('Share failed.'), + 'editingFailed' => $this->h5pF->t('Editing failed.'), + 'shareTryAgain' => $this->h5pF->t('Something went wrong, please try to share again.'), + 'pleaseWait' => $this->h5pF->t('Please wait...'), + 'language' => $this->h5pF->t('Language'), + 'level' => $this->h5pF->t('Level'), + 'shortDescriptionPlaceholder' => $this->h5pF->t('Short description of your content'), + 'longDescriptionPlaceholder' => $this->h5pF->t('Long description of your content'), + 'description' => $this->h5pF->t('Description'), + 'iconDescription' => $this->h5pF->t('640x480px. If not selected content will use category icon'), + 'screenshotsDescription' => $this->h5pF->t('Add up to five screenshots of your content'), + 'submitted' => $this->h5pF->t('Submitted!'), + 'isNowSubmitted' => $this->h5pF->t('Is now submitted to H5P Hub'), + 'changeHasBeenSubmitted' => $this->h5pF->t('A change has been submited for'), + 'contentAvailable' => $this->h5pF->t('Your content will normally be available in the Hub within one business day.'), + 'contentUpdateSoon' => $this->h5pF->t('Your content will update soon'), + 'contentLicenseTitle' => $this->h5pF->t('Content License Info'), + 'licenseDialogDescription' => $this->h5pF->t('Click on a specific license to get info about proper usage'), + 'publisherFieldTitle' => $this->h5pF->t('Publisher'), + 'publisherFieldDescription' => $this->h5pF->t('This will display as the "Publisher name" on shared content'), + 'emailAddress' => $this->h5pF->t('Email Address'), + 'publisherDescription' => $this->h5pF->t('Publisher description'), + 'publisherDescriptionText' => $this->h5pF->t('This will be displayed under "Publisher info" on shared content'), + 'contactPerson' => $this->h5pF->t('Contact Person'), + 'phone' => $this->h5pF->t('Phone'), + 'address' => $this->h5pF->t('Address'), + 'city' => $this->h5pF->t('City'), + 'zip' => $this->h5pF->t('Zip'), + 'country' => $this->h5pF->t('Country'), + 'logoUploadText' => $this->h5pF->t('Organization logo or avatar'), + 'acceptTerms' => $this->h5pF->t('I accept the terms of use'), + 'successfullyRegistred' => $this->h5pF->t('You have successfully registered an account on the H5P Hub'), + 'successfullyRegistredDescription' => $this->h5pF->t('You account details can be changed'), + 'successfullyUpdated' => $this->h5pF->t('Your H5P Hub account settings have successfully been changed'), + 'accountDetailsLinkText' => $this->h5pF->t('here'), + 'registrationTitle' => $this->h5pF->t('H5P Hub Registration'), + 'registrationFailed' => $this->h5pF->t('An error occurred'), + 'registrationFailedDescription' => $this->h5pF->t('We were not able to create an account at this point. Something went wrong. Try again later.'), + 'maxLength' => $this->h5pF->t(':length is the maximum number of characters'), + 'keywordExists' => $this->h5pF->t('Keyword already exists!'), + 'licenseDetails' => $this->h5pF->t('License details'), + 'remove' => $this->h5pF->t('Remove'), + 'removeImage' => $this->h5pF->t('Remove image'), + 'cancelPublishConfirmationDialogTitle' => $this->h5pF->t('Cancel sharing'), + 'cancelPublishConfirmationDialogDescription' => $this->h5pF->t('Are you sure you want to cancel the sharing process?'), + 'cancelPublishConfirmationDialogCancelButtonText' => $this->h5pF->t('No'), + 'cancelPublishConfirmationDialogConfirmButtonText' => $this->h5pF->t('Yes'), + 'add' => $this->h5pF->t('Add'), + 'age' => $this->h5pF->t('Typical age'), + 'ageDescription' => $this->h5pF->t('The target audience of this content. Possible input formats separated by commas: "1,34-45,-50,59-".'), + 'invalidAge' => $this->h5pF->t('Invalid input format for Typical age. Possible input formats separated by commas: "1, 34-45, -50, -59-".'), + 'contactPersonDescription' => $this->h5pF->t('H5P will reach out to the contact person in case there are any issues with the content shared by the publisher. The contact person\'s name or other information will not be published or shared with third parties'), + 'emailAddressDescription' => $this->h5pF->t('The email address will be used by H5P to reach out to the publisher in case of any issues with the content or in case the publisher needs to recover their account. It will not be published or shared with any third parties'), + 'copyrightWarning' => $this->h5pF->t('Copyrighted material cannot be shared in the H5P Content Hub. If the content is licensed with a OER friendly license like Creative Commons, please choose the appropriate license. If not this content cannot be shared.'), + 'keywordsExits' => $this->h5pF->t('Keywords already exists!'), + 'someKeywordsExits' => $this->h5pF->t('Some of these keywords already exist'), + ); } + + /** + * Publish content on the H5P Hub. + * + * @param bigint $id + * @return stdClass + */ + public function hubRetrieveContent($id) { + $headers = array( + 'Authorization' => $this->hubGetAuthorizationHeader(), + 'Accept' => 'application/json', + ); + + $response = $this->h5pF->fetchExternalData( + H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT . "/{$id}"), + NULL, TRUE, NULL, TRUE, $headers + ); + + if (empty($response['data'])) { + throw new Exception($this->h5pF->t('Unable to authorize with the H5P Hub. Please check your Hub registration and connection.')); + } + + if (isset($response['status']) && $response['status'] !== 200) { + if ($response['status'] === 404) { + $this->h5pF->setErrorMessage($this->h5pF->t('Content is not shared on the H5P OER Hub.')); + return NULL; + } + throw new Exception($this->h5pF->t('Connecting to the content hub failed, please try again later.')); + } + + $hub_content = json_decode($response['data'])->data; + $hub_content->id = "$hub_content->id"; + return $hub_content; + } + + /** + * Publish content on the H5P Hub. + * + * @param array $data Data from content publishing process + * @param array $files Files to upload with the content publish + * @param bigint $content_hub_id For updating existing content + * @return stdClass + */ + public function hubPublishContent($data, $files, $content_hub_id = NULL) { + $headers = array( + 'Authorization' => $this->hubGetAuthorizationHeader(), + 'Accept' => 'application/json', + ); + + $data['published'] = '1'; + $endpoint = H5PHubEndpoints::CONTENT; + if ($content_hub_id !== NULL) { + $endpoint .= "/{$content_hub_id}"; + $data['_method'] = 'PUT'; + } + + $response = $this->h5pF->fetchExternalData( + H5PHubEndpoints::createURL($endpoint), + $data, TRUE, NULL, TRUE, $headers, $files + ); + + if (empty($response['data']) || $response['status'] === 403) { + throw new Exception($this->h5pF->t('Unable to authorize with the H5P Hub. Please check your Hub registration and connection.')); + } + + if (isset($response['status']) && $response['status'] !== 200) { + throw new Exception($this->h5pF->t('Connecting to the content hub failed, please try again later.')); + } + + $result = json_decode($response['data']); + if (isset($result->success) && $result->success === TRUE) { + return $result; + } + elseif (!empty($result->errors)) { + // Relay any error messages + $e = new Exception($this->h5pF->t('Validation failed.')); + $e->errors = $result->errors; + throw $e; + } + } + + /** + * Creates the authorization header needed to access the private parts of + * the H5P Hub. + * + * @return string + */ + public function hubGetAuthorizationHeader() { + $site_uuid = $this->h5pF->getOption('site_uuid', ''); + $hub_secret = $this->h5pF->getOption('hub_secret', ''); + if (empty($site_uuid)) { + $this->h5pF->setErrorMessage($this->h5pF->t('Missing Site UUID. Please check your Hub registration.')); + } + elseif (empty($hub_secret)) { + $this->h5pF->setErrorMessage($this->h5pF->t('Missing Hub Secret. Please check your Hub registration.')); + } + return 'Basic ' . base64_encode("$site_uuid:$hub_secret"); + } + + /** + * Unpublish content from content hub + * + * @param integer $hubId Content hub id + * + * @return bool True if successful + */ + public function hubUnpublishContent($hubId) { + $headers = array( + 'Authorization' => $this->hubGetAuthorizationHeader(), + 'Accept' => 'application/json', + ); + + $url = H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT); + $response = $this->h5pF->fetchExternalData("{$url}/{$hubId}", array( + 'published' => '0', + ), true, null, true, $headers, array(), 'PUT'); + + // Remove shared status if successful + if (!empty($response) && $response['status'] === 200) { + $msg = $this->h5pF->t('Content successfully unpublished'); + $this->h5pF->setInfoMessage($msg); + + return true; + } + $msg = $this->h5pF->t('Content unpublish failed'); + $this->h5pF->setErrorMessage($msg); + + return false; + } + + /** + * Sync content with content hub + * + * @param integer $hubId Content hub id + * @param string $exportPath Export path where .h5p for content can be found + * + * @return bool + */ + public function hubSyncContent($hubId, $exportPath) { + $headers = array( + 'Authorization' => $this->hubGetAuthorizationHeader(), + 'Accept' => 'application/json', + ); + + $url = H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT); + $response = $this->h5pF->fetchExternalData("{$url}/{$hubId}", array( + 'download_url' => $exportPath, + 'resync' => '1', + ), true, null, true, $headers, array(), 'PUT'); + + if (!empty($response) && $response['status'] === 200) { + $msg = $this->h5pF->t('Content sync queued'); + $this->h5pF->setInfoMessage($msg); + return true; + } + + $msg = $this->h5pF->t('Content sync failed'); + $this->h5pF->setErrorMessage($msg); + return false; + } + + /** + * Fetch account info for our site from the content hub + * + * @return array|bool|string False if account is not setup, otherwise data + */ + public function hubAccountInfo() { + $siteUuid = $this->h5pF->getOption('site_uuid', null); + $secret = $this->h5pF->getOption('hub_secret', null); + if (empty($siteUuid) && !empty($secret)) { + $this->h5pF->setErrorMessage($this->h5pF->t('H5P Hub secret is set without a site uuid. This may be fixed by restoring the site uuid or removing the hub secret and registering a new account with the content hub.')); + throw new Exception('Hub secret not set'); + } + + if (empty($siteUuid) || empty($secret)) { + return false; + } + + $headers = array( + 'Authorization' => $this->hubGetAuthorizationHeader(), + 'Accept' => 'application/json', + ); + + $url = H5PHubEndpoints::createURL(H5PHubEndpoints::REGISTER); + $accountInfo = $this->h5pF->fetchExternalData("{$url}/{$siteUuid}", + null, true, null, true, $headers, array(), 'GET'); + + if ($accountInfo['status'] === 401) { + // Unauthenticated, invalid hub secret and site uuid combination + $this->h5pF->setErrorMessage($this->h5pF->t('Hub account authentication info is invalid. This may be fixed by an admin by restoring the hub secret or register a new account with the content hub.')); + return false; + } + + if ($accountInfo['status'] !== 200) { + return false; + } + + return json_decode($accountInfo['data'])->data; + } + + /** + * Register account + * + * @param array $formData Form data. Should include: name, email, description, + * contact_person, phone, address, city, zip, country, remove_logo + * @param object $logo Input image + * + * @return array + */ + public function hubRegisterAccount($formData, $logo) { + + $uuid = $this->h5pF->getOption('site_uuid', ''); + if (empty($uuid)) { + // Attempt to fetch a new site uuid + $uuid = $this->fetchLibrariesMetadata(false, true); + if (!$uuid) { + return [ + 'message' => $this->h5pF->t('Site is missing a unique site uuid and was unable to set a new one. The H5P Content Hub is disabled until this problem can be resolved. Please make sure the H5P Hub is enabled in the H5P settings and try again later.'), + 'status_code' => 403, + 'error_code' => 'MISSING_SITE_UUID', + 'success' => FALSE, + ]; + } + } + + $formData['site_uuid'] = $uuid; + + $headers = []; + $endpoint = H5PHubEndpoints::REGISTER; + // Update if already registered + $hasRegistered = $this->h5pF->getOption('hub_secret'); + if ($hasRegistered) { + $endpoint .= "/{$uuid}"; + $formData['_method'] = 'PUT'; + $headers = [ + 'Authorization' => $this->hubGetAuthorizationHeader(), + ]; + } + + $url = H5PHubEndpoints::createURL($endpoint); + $registration = $this->h5pF->fetchExternalData( + $url, + $formData, + NULL, + NULL, + TRUE, + $headers, + isset($logo) ? ['logo' => $logo] : [] + ); + + try { + $results = json_decode($registration['data']); + } catch (Exception $e) { + return [ + 'message' => 'Could not parse json response.', + 'status_code' => 424, + 'error_code' => 'COULD_NOT_PARSE_RESPONSE', + 'success' => FALSE, + ]; + } + + if (isset($results->errors->site_uuid)) { + return [ + 'message' => 'Site UUID is not unique. This must be fixed by an admin by restoring the hub secret or remove the site uuid and register as a new account with the content hub.', + 'status_code' => 403, + 'error_code' => 'SITE_UUID_NOT_UNIQUE', + 'success' => FALSE, + ]; + } + + if (isset($results->errors->logo)) { + return [ + 'message' => $results->errors->logo[0], + 'status_code' => 400, + 'success' => FALSE, + ]; + } + + if ( + !isset($results->success) + || $results->success === FALSE + || !$hasRegistered && !isset($results->account->secret) + || $registration['status'] !== 200 + ) { + return [ + 'message' => 'Registration failed.', + 'status_code' => 422, + 'error_code' => 'REGISTRATION_FAILED', + 'success' => FALSE, + ]; + } + + if (!$hasRegistered) { + $this->h5pF->setOption('hub_secret', $results->account->secret); + } + + return [ + 'message' => $this->h5pF->t('Account successfully registered.'), + 'status_code' => 200, + 'success' => TRUE, + ]; + } + + /** + * Get status of content from content hub + * + * @param string $hubContentId + * @param int $syncStatus + * + * @return false|int Returns a new H5PContentStatus if successful, else false + */ + public function getHubContentStatus($hubContentId, $syncStatus) { + $headers = array( + 'Authorization' => $this->hubGetAuthorizationHeader(), + 'Accept' => 'application/json', + ); + + $url = H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT); + $response = $this->h5pF->fetchExternalData("{$url}/{$hubContentId}/status", + null, true, null, true, $headers); + + if (isset($response['status']) && $response['status'] === 403) { + $msg = $this->h5pF->t('The request for content status was unauthorized. This could be because the content belongs to a different account, or your account is not setup properly.'); + $this->h5pF->setErrorMessage($msg); + return false; + } + if (empty($response) || $response['status'] !== 200) { + $msg = $this->h5pF->t('Could not get content hub sync status for content.'); + $this->h5pF->setErrorMessage($msg); + return false; + } + + $data = json_decode($response['data']); + + if (isset($data->messages)) { + // TODO: Is this the right place/way to display them? + + if (!empty($data->messages->info)) { + foreach ($data->messages->info as $info) { + $this->h5pF->setInfoMessage($info); + } + } + if (!empty($data->messages->error)) { + foreach ($data->messages->error as $error) { + $this->h5pF->setErrorMessage($error->message, $error->code); + } + } + } + + $contentStatus = intval($data->status); + // Content status updated + if ($contentStatus !== H5PContentStatus::STATUS_WAITING) { + $newState = H5PContentHubSyncStatus::SYNCED; + if ($contentStatus !== H5PContentStatus::STATUS_DOWNLOADED) { + $newState = H5PContentHubSyncStatus::FAILED; + } + else if (intval($syncStatus) !== $contentStatus) { + // Content status successfully transitioned to synced/downloaded + $successMsg = $this->h5pF->t('Content was successfully shared on the content hub.'); + $this->h5pF->setInfoMessage($successMsg); + } + + return $newState; + } + + return false; + } } /** @@ -4260,7 +4858,7 @@ class H5PContentValidator { return '<'; } - if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9\-]+)([^>]*)>?|()$%', $string, $matches)) { + if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9\-]+)\s*([^>]*)>?|()$%', $string, $matches)) { // Seriously malformed. return ''; } @@ -4322,7 +4920,13 @@ class H5PContentValidator { // Attribute name, href for instance. if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) { $attrName = strtolower($match[1]); - $skip = ($attrName == 'style' || substr($attrName, 0, 2) == 'on'); + $skip = ( + $attrName == 'style' || + substr($attrName, 0, 2) == 'on' || + substr($attrName, 0, 1) == '-' || + // Ignore long attributes to avoid unnecessary processing overhead. + strlen($attrName) > 96 + ); $working = $mode = 1; $attr = preg_replace('/^[-a-zA-Z]+/', '', $attr); } @@ -4526,6 +5130,12 @@ class H5PContentValidator { 'label' => $this->h5pF->t('Title'), 'placeholder' => 'La Gioconda' ), + (object) array( + 'name' => 'a11yTitle', + 'type' => 'text', + 'label' => $this->h5pF->t('Assistive Technologies label'), + 'optional' => TRUE, + ), (object) array( 'name' => 'license', 'type' => 'select', diff --git a/js/h5p-hub-registration.js b/js/h5p-hub-registration.js new file mode 100644 index 0000000..9a15f10 --- /dev/null +++ b/js/h5p-hub-registration.js @@ -0,0 +1,37 @@ +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=57)}([function(e,t,n){"use strict";e.exports=n(19)},function(e,t,n){e.exports=n(28)()},function(e,t,n){"use strict";var r=n(8),i=Object.prototype.toString;function o(e){return"[object Array]"===i.call(e)}function a(e){return void 0===e}function l(e){return null!==e&&"object"==typeof e}function u(e){if("[object Object]"!==i.call(e))return!1;var t=Object.getPrototypeOf(e);return null===t||t===Object.prototype}function c(e){return"[object Function]"===i.call(e)}function s(e,t){if(null!=e)if("object"!=typeof e&&(e=[e]),o(e))for(var n=0,r=e.length;n1)for(var n=1;n=200&&e<300}};u.headers={common:{Accept:"application/json, text/plain, */*"}},r.forEach(["delete","get","head"],(function(e){u.headers[e]={}})),r.forEach(["post","put","patch"],(function(e){u.headers[e]=r.merge(o)})),e.exports=u}).call(this,n(6))},function(e,t,n){"use strict";var r=n(2),i=n(38),o=n(40),a=n(9),l=n(41),u=n(44),c=n(45),s=n(13);e.exports=function(e){return new Promise((function(t,n){var f=e.data,d=e.headers;r.isFormData(f)&&delete d["Content-Type"];var p=new XMLHttpRequest;if(e.auth){var h=e.auth.username||"",m=e.auth.password?unescape(encodeURIComponent(e.auth.password)):"";d.Authorization="Basic "+btoa(h+":"+m)}var v=l(e.baseURL,e.url);if(p.open(e.method.toUpperCase(),a(v,e.params,e.paramsSerializer),!0),p.timeout=e.timeout,p.onreadystatechange=function(){if(p&&4===p.readyState&&(0!==p.status||p.responseURL&&0===p.responseURL.indexOf("file:"))){var r="getAllResponseHeaders"in p?u(p.getAllResponseHeaders()):null,o={data:e.responseType&&"text"!==e.responseType?p.response:p.responseText,status:p.status,statusText:p.statusText,headers:r,config:e,request:p};i(t,n,o),p=null}},p.onabort=function(){p&&(n(s("Request aborted",e,"ECONNABORTED",p)),p=null)},p.onerror=function(){n(s("Network Error",e,null,p)),p=null},p.ontimeout=function(){var t="timeout of "+e.timeout+"ms exceeded";e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),n(s(t,e,"ECONNABORTED",p)),p=null},r.isStandardBrowserEnv()){var g=(e.withCredentials||c(v))&&e.xsrfCookieName?o.read(e.xsrfCookieName):void 0;g&&(d[e.xsrfHeaderName]=g)}if("setRequestHeader"in p&&r.forEach(d,(function(e,t){void 0===f&&"content-type"===t.toLowerCase()?delete d[t]:p.setRequestHeader(t,e)})),r.isUndefined(e.withCredentials)||(p.withCredentials=!!e.withCredentials),e.responseType)try{p.responseType=e.responseType}catch(t){if("json"!==e.responseType)throw t}"function"==typeof e.onDownloadProgress&&p.addEventListener("progress",e.onDownloadProgress),"function"==typeof e.onUploadProgress&&p.upload&&p.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then((function(e){p&&(p.abort(),n(e),p=null)})),f||(f=null),p.send(f)}))}},function(e,t,n){"use strict";var r=n(39);e.exports=function(e,t,n,i,o){var a=new Error(e);return r(a,t,n,i,o)}},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){t=t||{};var n={},i=["url","method","data"],o=["headers","auth","proxy","params"],a=["baseURL","transformRequest","transformResponse","paramsSerializer","timeout","timeoutMessage","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","decompress","maxContentLength","maxBodyLength","maxRedirects","transport","httpAgent","httpsAgent","cancelToken","socketPath","responseEncoding"],l=["validateStatus"];function u(e,t){return r.isPlainObject(e)&&r.isPlainObject(t)?r.merge(e,t):r.isPlainObject(t)?r.merge({},t):r.isArray(t)?t.slice():t}function c(i){r.isUndefined(t[i])?r.isUndefined(e[i])||(n[i]=u(void 0,e[i])):n[i]=u(e[i],t[i])}r.forEach(i,(function(e){r.isUndefined(t[e])||(n[e]=u(void 0,t[e]))})),r.forEach(o,c),r.forEach(a,(function(i){r.isUndefined(t[i])?r.isUndefined(e[i])||(n[i]=u(void 0,e[i])):n[i]=u(void 0,t[i])})),r.forEach(l,(function(r){r in t?n[r]=u(e[r],t[r]):r in e&&(n[r]=u(void 0,e[r]))}));var s=i.concat(o).concat(a).concat(l),f=Object.keys(e).concat(Object.keys(t)).filter((function(e){return-1===s.indexOf(e)}));return r.forEach(f,c),n}},function(e,t,n){"use strict";function r(e){this.message=e}r.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},r.prototype.__CANCEL__=!0,e.exports=r},function(e,t,n){"use strict";!function e(){if("undefined"!=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&"function"==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE){0;try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(e){console.error(e)}}}(),e.exports=n(20)},function(e,t,n){(function(t,n){ +/*! + * @overview es6-promise - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE + * @version v4.2.8+1e68dce6 + */var r;r=function(){"use strict";function e(e){return"function"==typeof e}var r=Array.isArray?Array.isArray:function(e){return"[object Array]"===Object.prototype.toString.call(e)},i=0,o=void 0,a=void 0,l=function(e,t){h[i]=e,h[i+1]=t,2===(i+=2)&&(a?a(m):w())},u="undefined"!=typeof window?window:void 0,c=u||{},s=c.MutationObserver||c.WebKitMutationObserver,f="undefined"==typeof self&&void 0!==t&&"[object process]"==={}.toString.call(t),d="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel;function p(){var e=setTimeout;return function(){return e(m,1)}}var h=new Array(1e3);function m(){for(var e=0;eO.length&&O.push(e)}function L(e,t,n){return null==e?0:function e(t,n,r,i){var l=typeof t;"undefined"!==l&&"boolean"!==l||(t=null);var u=!1;if(null===t)u=!0;else switch(l){case"string":case"number":u=!0;break;case"object":switch(t.$$typeof){case o:case a:u=!0}}if(u)return r(i,t,""===n?"."+I(t,0):n),1;if(u=0,n=""===n?".":n+":",Array.isArray(t))for(var c=0;c