diff --git a/composer.json b/composer.json index fac6c53..9b1cc88 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ "h5p-development.class.php", "h5p-file-storage.interface.php", "h5p-default-storage.class.php", - "h5p-event-base.class.php" + "h5p-event-base.class.php", + "h5p-metadata.class.php" ] } } diff --git a/fonts/h5p-core-18.eot b/fonts/h5p-core-18.eot deleted file mode 100755 index eba9d6c..0000000 Binary files a/fonts/h5p-core-18.eot and /dev/null differ diff --git a/fonts/h5p-core-18.svg b/fonts/h5p-core-18.svg deleted file mode 100755 index 13da36e..0000000 --- a/fonts/h5p-core-18.svg +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - -{ - "fontFamily": "h5p", - "description": "Font generated by IcoMoon.", - "majorVersion": 1, - "minorVersion": 1, - "version": "Version 1.1", - "fontId": "h5p", - "psName": "h5p", - "subFamily": "Regular", - "fullName": "h5p" -} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fonts/h5p-core-18.ttf b/fonts/h5p-core-18.ttf deleted file mode 100755 index 37f845e..0000000 Binary files a/fonts/h5p-core-18.ttf and /dev/null differ diff --git a/fonts/h5p-core-18.woff b/fonts/h5p-core-18.woff deleted file mode 100755 index 8450f3d..0000000 Binary files a/fonts/h5p-core-18.woff and /dev/null differ diff --git a/fonts/h5p-core-19.eot b/fonts/h5p-core-19.eot new file mode 100644 index 0000000..2348e29 Binary files /dev/null and b/fonts/h5p-core-19.eot differ diff --git a/fonts/h5p-core-19.svg b/fonts/h5p-core-19.svg new file mode 100644 index 0000000..a808aa6 --- /dev/null +++ b/fonts/h5p-core-19.svg @@ -0,0 +1,54 @@ + + + + + + +{ + "fontFamily": "h5p-core-18", + "description": "Font generated by IcoMoon.", + "majorVersion": 1, + "minorVersion": 1, + "version": "Version 1.1", + "fontId": "h5p-core-18", + "psName": "h5p-core-18", + "subFamily": "Regular", + "fullName": "h5p-core-18" +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/h5p-core-19.ttf b/fonts/h5p-core-19.ttf new file mode 100644 index 0000000..729aea4 Binary files /dev/null and b/fonts/h5p-core-19.ttf differ diff --git a/fonts/h5p-core-19.woff b/fonts/h5p-core-19.woff new file mode 100644 index 0000000..be9ead2 Binary files /dev/null and b/fonts/h5p-core-19.woff differ diff --git a/h5p-default-storage.class.php b/h5p-default-storage.class.php index 38af307..26e3e01 100644 --- a/h5p-default-storage.class.php +++ b/h5p-default-storage.class.php @@ -451,6 +451,19 @@ class H5PDefaultStorage implements \H5PFileStorage { return self::dirReady($this->path); } + /** + * Check if the file presave.js exists in the root of the library + * + * @param string $libraryFolder + * @param string $developmentPath + * @return bool + */ + public function hasPresave($libraryFolder, $developmentPath = null) { + $path = is_null($developmentPath) ? 'libraries' . DIRECTORY_SEPARATOR . $libraryFolder : $developmentPath; + $filePath = realpath($this->path . DIRECTORY_SEPARATOR . $path . DIRECTORY_SEPARATOR . 'presave.js'); + return file_exists($filePath); + } + /** * Recursive function for copying directories. * diff --git a/h5p-development.class.php b/h5p-development.class.php index 6c891f9..a60262a 100644 --- a/h5p-development.class.php +++ b/h5p-development.class.php @@ -84,10 +84,19 @@ class H5PDevelopment { // TODO: Validate props? Not really needed, is it? this is a dev site. - // Save/update library. $library['libraryId'] = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']); + + // Convert metadataSettings values to boolean & json_encode it before saving + $library['metadataSettings'] = isset($library['metadataSettings']) ? + H5PMetadata::boolifyAndEncodeSettings($library['metadataSettings']) : + NULL; + + // Save/update library. $this->h5pF->saveLibraryData($library, $library['libraryId'] === FALSE); + // Need to decode it again, since it is served from here. + $library['metadataSettings'] = json_decode($library['metadataSettings']); + $library['path'] = 'development/' . $contents[$i]; $this->libraries[H5PDevelopment::libraryToString($library['machineName'], $library['majorVersion'], $library['minorVersion'])] = $library; } diff --git a/h5p-file-storage.interface.php b/h5p-file-storage.interface.php index 394210b..4dbdbc6 100644 --- a/h5p-file-storage.interface.php +++ b/h5p-file-storage.interface.php @@ -190,4 +190,13 @@ interface H5PFileStorage { * @return bool True if server has the proper write access */ public function hasWriteAccess(); + + /** + * Check if the library has a presave.js in the root folder + * + * @param string $libraryName + * @param string $developmentPath + * @return bool + */ + public function hasPresave($libraryName, $developmentPath = null); } diff --git a/h5p-metadata.class.php b/h5p-metadata.class.php new file mode 100644 index 0000000..3a504e3 --- /dev/null +++ b/h5p-metadata.class.php @@ -0,0 +1,137 @@ + array( + 'type' => 'text', + 'maxLength' => 255 + ), + 'authors' => array( + 'type' => 'json' + ), + 'changes' => array( + 'type' => 'json' + ), + 'source' => array( + 'type' => 'text', + 'maxLength' => 255 + ), + 'license' => array( + 'type' => 'text', + 'maxLength' => 32 + ), + 'licenseVersion' => array( + 'type' => 'text', + 'maxLength' => 10 + ), + 'licenseExtras' => array( + 'type' => 'text', + 'maxLength' => 5000 + ), + 'authorComments' => array( + 'type' => 'text', + 'maxLength' => 5000 + ), + 'yearFrom' => array( + 'type' => 'int' + ), + 'yearTo' => array( + 'type' => 'int' + ) + ); + + /** + * JSON encode metadata + * + * @param object $content + * @return string + */ + public static function toJSON($content) { + // Note: deliberatly creating JSON string "manually" to improve performance + return + '{"title":' . (isset($content->title) ? json_encode($content->title) : 'null') . + ',"authors":' . (isset($content->authors) ? $content->authors : 'null') . + ',"source":' . (isset($content->source) ? '"' . $content->source . '"' : 'null') . + ',"license":' . (isset($content->license) ? '"' . $content->license . '"' : 'null') . + ',"licenseVersion":' . (isset($content->license_version) ? '"' . $content->license_version . '"' : 'null') . + ',"licenseExtras":' . (isset($content->license_extras) ? json_encode($content->license_extras) : 'null') . + ',"yearFrom":' . (isset($content->year_from) ? $content->year_from : 'null') . + ',"yearTo":' . (isset($content->year_to) ? $content->year_to : 'null') . + ',"changes":' . (isset($content->changes) ? $content->changes : 'null') . + ',"authorComments":' . (isset($content->author_comments) ? json_encode($content->author_comments) : 'null') . '}'; + } + + + /** + * Make the metadata into an associative array keyed by the property names + * @param mixed $metadata Array or object containing metadata + * @param bool $include_title + * @param array $types + * @return array + */ + public static function toDBArray($metadata, $include_title = true, &$types = array()) { + $fields = array(); + + if (!is_array($metadata)) { + $metadata = (array) $metadata; + } + + foreach (self::$fields as $key => $config) { + + if ($key === 'title' && !$include_title) { + continue; + } + + if (array_key_exists($key, $metadata)) { + $value = $metadata[$key]; + $db_field_name = strtolower(preg_replace('/(? $config['maxLength']) { + $value = mb_substr($value, 0, $config['maxLength']); + } + $types[] = '%s'; + break; + + case 'int': + $value = ($value !== null) ? intval($value): null; + $types[] = '%d'; + break; + + case 'json': + $value = ($value !== null) ? json_encode($value) : null; + $types[] = '%s'; + break; + } + + $fields[$db_field_name] = $value; + } + } + + return $fields; + } + + /** + * The metadataSettings field in libraryJson uses 1 for true and 0 for false. + * Here we are converting these to booleans, and also doing JSON encoding. + * This is invoked before the library data is beeing inserted/updated to DB. + * + * @param array $metadataSettings + * @return string + */ + public static function boolifyAndEncodeSettings($metadataSettings) { + // Convert metadataSettings values to boolean + if (isset($metadataSettings['disable'])) { + $metadataSettings['disable'] = $metadataSettings['disable'] === 1; + } + if (isset($metadataSettings['disableExtraTitleField'])) { + $metadataSettings['disableExtraTitleField'] = $metadataSettings['disableExtraTitleField'] === 1; + } + + return json_encode($metadataSettings); + } +} diff --git a/h5p.classes.php b/h5p.classes.php index 2efebbd..b0099bc 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -101,6 +101,21 @@ interface H5PFrameworkInterface { */ public function getUploadedH5pPath(); + /** + * Load addon libraries + * + * @return array + */ + public function loadAddons(); + + /** + * Load config for libraries + * + * @param array $libraries + * @return array + */ + public function getLibraryConfig($libraries = NULL); + /** * Get a list of the current installed libraries * @@ -195,6 +210,9 @@ interface H5PFrameworkInterface { * - minorVersion: The library's minorVersion * - patchVersion: The library's patchVersion * - runnable: 1 if the library is a content type, 0 otherwise + * - metadataSettings: Associative array containing: + * - disable: 1 if the library should not support setting metadata (copyright etc) + * - disableExtraTitleField: 1 if the library don't need the extra title field * - fullscreen(optional): 1 if the library supports fullscreen, 0 otherwise * - embedTypes(optional): list of supported embed types * - preloadedJs(optional): list of associative arrays containing: @@ -620,16 +638,34 @@ class H5PValidator { private $h5pOptional = array( 'contentType' => '/^.{1,255}$/', - 'author' => '/^.{1,255}$/', - 'license' => '/^(cc-by|cc-by-sa|cc-by-nd|cc-by-nc|cc-by-nc-sa|cc-by-nc-nd|pd|cr|MIT|GPL1|GPL2|GPL3|MPL|MPL2)$/', 'dynamicDependencies' => array( 'machineName' => '/^[\w0-9\-\.]{1,255}$/i', 'majorVersion' => '/^[0-9]{1,5}$/', 'minorVersion' => '/^[0-9]{1,5}$/', ), + // deprecated + 'author' => '/^.{1,255}$/', + 'authors' => array( + 'name' => '/^.{1,255}$/', + 'role' => '/^\w+$/', + ), + 'source' => '/^(http[s]?:\/\/.+)$/', + 'license' => '/^(CC BY|CC BY-SA|CC BY-ND|CC BY-NC|CC BY-NC-SA|CC BY-NC-ND|CC0 1\.0|GNU GPL|PD|ODC PDDL|CC PDM|U|C)$/', + 'licenseVersion' => '/^(1\.0|2\.0|2\.5|3\.0|4\.0)$/', + 'licenseExtras' => '/^.{1,5000}$/', + 'yearsFrom' => '/^([0-9]{1,4})$/', + 'yearsTo' => '/^([0-9]{1,4})$/', + 'changes' => array( + 'date' => '/^[0-9]{2}-[0-9]{2}-[0-9]{2} [0-9]{1,2}:[0-9]{2}:[0-9]{2}$/', + 'author' => '/^.{1,255}$/', + 'log' => '/^.{1,5000}$/' + ), + 'authorComments' => '/^.{1,5000}$/', 'w' => '/^[0-9]{1,4}$/', 'h' => '/^[0-9]{1,4}$/', + // deprecated 'metaKeywords' => '/^.{1,}$/', + // deprecated 'metaDescription' => '/^.{1,}$/', ); @@ -647,6 +683,10 @@ class H5PValidator { 'author' => '/^.{1,255}$/', 'license' => '/^(cc-by|cc-by-sa|cc-by-nd|cc-by-nc|cc-by-nc-sa|cc-by-nc-nd|pd|cr|MIT|GPL1|GPL2|GPL3|MPL|MPL2)$/', 'description' => '/^.{1,}$/', + 'metadataSettings' => array( + 'disable' => '/^(0|1)$/', + 'disableExtraTitleField' => '/^(0|1)$/' + ), 'dynamicDependencies' => array( 'machineName' => '/^[\w0-9\-\.]{1,255}$/i', 'majorVersion' => '/^[0-9]{1,5}$/', @@ -1405,7 +1445,11 @@ class H5PStorage { // Indicate that the dependencies of this library should be saved. $library['saveDependencies'] = TRUE; - // Save library meta data + // Convert metadataSettings values to boolean & json_encode it before saving + $library['metadataSettings'] = isset($library['metadataSettings']) ? + H5PMetadata::boolifyAndEncodeSettings($library['metadataSettings']) : + NULL; + $this->h5pF->saveLibraryData($library, $new); // Save library folder @@ -1554,6 +1598,16 @@ Class H5PExport { $this->h5pC = $H5PCore; } + /** + * Reverts the replace pattern used by the text editor + * + * @param string $value + * @return string + */ + private static function revertH5PEditorTextEscape($value) { + return str_replace('<', '<', str_replace('>', '>', str_replace(''', "'", str_replace('"', '"', $value)))); + } + /** * Return path to h5p package. * @@ -1584,14 +1638,29 @@ Class H5PExport { // Make embedType into an array $embedTypes = explode(', ', $content['embedType']); - // Build h5p.json + // Build h5p.json, the en-/de-coding will ensure proper escaping $h5pJson = array ( - 'title' => $content['title'], + 'title' => self::revertH5PEditorTextEscape($content['title']), 'language' => (isset($content['language']) && strlen(trim($content['language'])) !== 0) ? $content['language'] : 'und', 'mainLibrary' => $content['library']['name'], - 'embedTypes' => $embedTypes, + 'embedTypes' => $embedTypes ); + foreach(array('authors', 'source', 'license', 'licenseVersion', 'licenseExtras' ,'yearFrom', 'yearTo', 'changes', 'authorComments') as $field) { + if (isset($content['metadata'][$field]) && $content['metadata'][$field] !== '') { + if (($field !== 'authors' && $field !== 'changes') || (count($content['metadata'][$field]) > 0)) { + $h5pJson[$field] = json_decode(json_encode($content['metadata'][$field], TRUE)); + } + } + } + + // Remove all values that are not set + foreach ($h5pJson as $key => $value) { + if (!isset($value)) { + unset($h5pJson[$key]); + } + } + // Add dependencies to h5p foreach ($content['dependencies'] as $dependency) { $library = $dependency['library']; @@ -1609,7 +1678,7 @@ Class H5PExport { $library['minorVersion'] ); - if ($isDevLibrary !== NULL) { + if ($isDevLibrary !== NULL && isset($library['path'])) { $exportFolder = "/" . $library['path']; } } @@ -1770,10 +1839,10 @@ abstract class H5PHubEndpoints { * Functions and storage shared by the other H5P classes */ class H5PCore { - + public static $coreApi = array( 'majorVersion' => 1, - 'minorVersion' => 16 + 'minorVersion' => 19 ); public static $styles = array( 'styles/h5p.css', @@ -1886,6 +1955,10 @@ class H5PCore { $content = $this->h5pF->loadContent($id); if ($content !== NULL) { + // Validate main content's metadata + $validator = new H5PContentValidator($this->h5pF, $this); + $content['metadata'] = $validator->validateMetadata($content['metadata']); + $content['library'] = array( 'id' => $content['libraryId'], 'name' => $content['libraryName'], @@ -1927,6 +2000,10 @@ class H5PCore { return $content['filtered']; } + if (!(isset($content['library']) && isset($content['params']))) { + return NULL; + } + // Validate and filter against main library semantics. $validator = new H5PContentValidator($this->h5pF, $this); $params = (object) array( @@ -1938,6 +2015,25 @@ class H5PCore { } $validator->validateLibrary($params, (object) array('options' => array($params->library))); + // Handle addons: + $addons = $this->h5pF->loadAddons(); + foreach ($addons as $addon) { + $add_to = json_decode($addon['addTo']); + + if (isset($add_to->content->types)) { + foreach($add_to->content->types as $type) { + + if (isset($type->text->regex) && + $this->textAddonMatches($params->params, $type->text->regex)) { + $validator->addon($addon); + + // An addon shall only be added once + break; + } + } + } + } + $params = json_encode($params->params); // Update content dependencies. @@ -1970,6 +2066,75 @@ class H5PCore { return $params; } + /** + * Retrieve a value from a nested mixed array structure. + * + * @param Array $params Array to be looked in. + * @param String $path Supposed path to the value. + * @param String [$delimiter='.'] Property delimiter within the path. + * @return Object|NULL The object found or NULL. + */ + private function retrieveValue ($params, $path, $delimiter='.') { + $path = explode($delimiter, $path); + + // Property not found + if (!isset($params[$path[0]])) { + return NULL; + } + + $first = $params[$path[0]]; + + // End of path, done + if (sizeof($path) === 1) { + return $first; + } + + // We cannot go deeper + if (!is_array($first)) { + return NULL; + } + + // Regular Array + if (isset($first[0])) { + foreach($first as $number => $object) { + $found = $this->retrieveValue($object, implode($delimiter, array_slice($path, 1))); + if (isset($found)) { + return $found; + } + } + return NULL; + } + + // Associative Array + return $this->retrieveValue($first, implode('.', array_slice($path, 1))); + } + + /** + * Determine if params contain any match. + * + * @param {object} params - Parameters. + * @param {string} [pattern] - Regular expression to identify pattern. + * @param {boolean} [found] - Used for recursion. + * @return {boolean} True, if params matches pattern. + */ + private function textAddonMatches($params, $pattern, $found = false) { + $type = gettype($params); + if ($type === 'string') { + if (preg_match($pattern, $params) === 1) { + return true; + } + } + elseif ($type === 'array' || $type === 'object') { + foreach ($params as $value) { + $found = $this->textAddonMatches($value, $pattern, $found); + if ($found === true) { + return true; + } + } + } + return false; + } + /** * Generate content slug * @@ -2848,7 +3013,7 @@ class H5PCore { */ private static function printJson($data, $status_code = NULL) { header('Cache-Control: no-cache'); - header('Content-type: application/json; charset=utf-8'); + header('Content-Type: application/json; charset=utf-8'); print json_encode($data); } @@ -3156,7 +3321,10 @@ class H5PCore { 'licensePD' => $this->h5pF->t('Public Domain'), 'licenseCC010' => $this->h5pF->t('CC0 1.0 Universal (CC0 1.0) Public Domain Dedication'), 'licensePDM' => $this->h5pF->t('Public Domain Mark'), - 'licenseC' => $this->h5pF->t('Copyright') + 'licenseC' => $this->h5pF->t('Copyright'), + 'contentType' => $this->h5pF->t('Content Type'), + 'licenseExtras' => $this->h5pF->t('License Extras'), + 'changes' => $this->h5pF->t('Changelog'), ); } } @@ -3204,6 +3372,19 @@ class H5PContentValidator { $this->dependencies = array(); } + /** + * Add Addon library. + */ + public function addon($library) { + $depKey = 'preloaded-' . $library['machineName']; + $this->dependencies[$depKey] = array( + 'library' => $library, + 'type' => 'preloaded' + ); + $this->nextWeight = $this->h5pC->findLibraryDependencies($this->dependencies, $library, $this->nextWeight); + $this->dependencies[$depKey]['weight'] = $this->nextWeight++; + } + /** * Get the flat dependency tree. * @@ -3213,6 +3394,24 @@ class H5PContentValidator { return $this->dependencies; } + /** + * Validate metadata + * + * @param array $metadata + * @return array Validated & filtered + */ + public function validateMetadata($metadata) { + $semantics = $this->getMetadataSemantics(); + + $group = (object)$metadata; + $this->validateGroup($group, (object) array( + 'type' => 'group', + 'fields' => $semantics, + ), FALSE); + + return (array)$group; + } + /** * Validate given text value against text semantics. * @param $text @@ -3401,8 +3600,17 @@ class H5PContentValidator { // We have a strict set of options to choose from. $strict = TRUE; $options = array(); + foreach ($semantics->options as $option) { - $options[$option->value] = TRUE; + // Support optgroup - just flatten options into one + if (isset($option->type) && $option->type === 'optgroup') { + foreach ($option->options as $suboption) { + $options[$suboption->value] = TRUE; + } + } + elseif (isset($option->value)) { + $options[$option->value] = TRUE; + } } } @@ -3667,12 +3875,24 @@ class H5PContentValidator { $value = NULL; return; } - if (!in_array($value->library, $semantics->options)) { + + // Check for array of objects or array of strings + if (is_object($semantics->options[0])) { + $getLibraryNames = function ($item) { + return $item->name; + }; + $libraryNames = array_map($getLibraryNames, $semantics->options); + } + else { + $libraryNames = $semantics->options; + } + + if (!in_array($value->library, $libraryNames)) { $message = NULL; // Create an understandable error message: $machineNameArray = explode(' ', $value->library); $machineName = $machineNameArray[0]; - foreach ($semantics->options as $semanticsLibrary) { + foreach ($libraryNames as $semanticsLibrary) { $semanticsMachineNameArray = explode(' ', $semanticsLibrary); $semanticsMachineName = $semanticsMachineNameArray[0]; if ($machineName === $semanticsMachineName) { @@ -3708,14 +3928,22 @@ class H5PContentValidator { $library = $this->libraries[$value->library]; } + // Validate parameters $this->validateGroup($value->params, (object) array( 'type' => 'group', 'fields' => $library['semantics'], ), FALSE); - $validKeys = array('library', 'params', 'subContentId'); + + // Validate subcontent's metadata + if (isset($value->metadata)) { + $value->metadata = $this->validateMetadata($value->metadata); + } + + $validKeys = array('library', 'params', 'subContentId', 'metadata'); if (isset($semantics->extraAttributes)) { $validKeys = array_merge($validKeys, $semantics->extraAttributes); } + $this->filterParams($value, $validKeys); if (isset($value->subContentId) && ! preg_match('/^\{?[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\}?$/', $value->subContentId)) { unset($value->subContentId); @@ -4082,6 +4310,240 @@ class H5PContentValidator { return $uri; } + public function getMetadataSemantics() { + static $semantics; + + $cc_versions = array( + (object) array( + 'value' => '4.0', + 'label' => $this->h5pF->t('4.0 International') + ), + (object) array( + 'value' => '3.0', + 'label' => $this->h5pF->t('3.0 Unported') + ), + (object) array( + 'value' => '2.5', + 'label' => $this->h5pF->t('2.5 Generic') + ), + (object) array( + 'value' => '2.0', + 'label' => $this->h5pF->t('2.0 Generic') + ), + (object) array( + 'value' => '1.0', + 'label' => $this->h5pF->t('1.0 Generic') + ) + ); + + $semantics = array( + (object) array( + 'name' => 'title', + 'type' => 'text', + 'label' => $this->h5pF->t('Title'), + 'placeholder' => 'La Gioconda' + ), + (object) array( + 'name' => 'license', + 'type' => 'select', + 'label' => $this->h5pF->t('License'), + 'default' => 'U', + 'options' => array( + (object) array( + 'value' => 'U', + 'label' => $this->h5pF->t('Undisclosed') + ), + (object) array( + 'type' => 'optgroup', + 'label' => $this->h5pF->t('Creative Commons'), + 'options' => [ + (object) array( + 'value' => 'CC BY', + 'label' => $this->h5pF->t('Attribution (CC BY)'), + 'versions' => $cc_versions + ), + (object) array( + 'value' => 'CC BY-SA', + 'label' => $this->h5pF->t('Attribution-ShareAlike (CC BY-SA)'), + 'versions' => $cc_versions + ), + (object) array( + 'value' => 'CC BY-ND', + 'label' => $this->h5pF->t('Attribution-NoDerivs (CC BY-ND)'), + 'versions' => $cc_versions + ), + (object) array( + 'value' => 'CC BY-NC', + 'label' => $this->h5pF->t('Attribution-NonCommercial (CC BY-NC)'), + 'versions' => $cc_versions + ), + (object) array( + 'value' => 'CC BY-NC-SA', + 'label' => $this->h5pF->t('Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)'), + 'versions' => $cc_versions + ), + (object) array( + 'value' => 'CC BY-NC-ND', + 'label' => $this->h5pF->t('Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)'), + 'versions' => $cc_versions + ), + (object) array( + 'value' => 'CC0 1.0', + 'label' => $this->h5pF->t('Public Domain Dedication (CC0)') + ), + (object) array( + 'value' => 'CC PDM', + 'label' => $this->h5pF->t('Public Domain Mark (PDM)') + ), + ] + ), + (object) array( + 'value' => 'GNU GPL', + 'label' => $this->h5pF->t('General Public License v3') + ), + (object) array( + 'value' => 'PD', + 'label' => $this->h5pF->t('Public Domain') + ), + (object) array( + 'value' => 'ODC PDDL', + 'label' => $this->h5pF->t('Public Domain Dedication and Licence') + ), + (object) array( + 'value' => 'C', + 'label' => $this->h5pF->t('Copyright') + ) + ) + ), + (object) array( + 'name' => 'licenseVersion', + 'type' => 'select', + 'label' => $this->h5pF->t('License Version'), + 'options' => $cc_versions, + 'optional' => TRUE + ), + (object) array( + 'name' => 'yearFrom', + 'type' => 'number', + 'label' => $this->h5pF->t('Years (from)'), + 'placeholder' => '1991', + 'min' => '-9999', + 'max' => '9999', + 'optional' => TRUE + ), + (object) array( + 'name' => 'yearTo', + 'type' => 'number', + 'label' => $this->h5pF->t('Years (to)'), + 'placeholder' => '1992', + 'min' => '-9999', + 'max' => '9999', + 'optional' => TRUE + ), + (object) array( + 'name' => 'source', + 'type' => 'text', + 'label' => $this->h5pF->t('Source'), + 'placeholder' => 'https://', + 'optional' => TRUE + ), + (object) array( + 'name' => 'authors', + 'type' => 'list', + 'field' => (object) array ( + 'name' => 'author', + 'type' => 'group', + 'fields'=> array( + (object) array( + 'label' => $this->h5pF->t("Author's name"), + 'name' => 'name', + 'optional' => TRUE, + 'type' => 'text' + ), + (object) array( + 'name' => 'role', + 'type' => 'select', + 'label' => $this->h5pF->t("Author's role"), + 'default' => 'Author', + 'options' => array( + (object) array( + 'value' => 'Author', + 'label' => $this->h5pF->t('Author') + ), + (object) array( + 'value' => 'Editor', + 'label' => $this->h5pF->t('Editor') + ), + (object) array( + 'value' => 'Licensee', + 'label' => $this->h5pF->t('Licensee') + ), + (object) array( + 'value' => 'Originator', + 'label' => $this->h5pF->t('Originator') + ) + ) + ) + ) + ) + ), + (object) array( + 'name' => 'licenseExtras', + 'type' => 'text', + 'widget' => 'textarea', + 'label' => $this->h5pF->t('License Extras'), + 'optional' => TRUE, + 'description' => $this->h5pF->t('Any additional information about the license') + ), + (object) array( + 'name' => 'changes', + 'type' => 'list', + 'field' => (object) array( + 'name' => 'change', + 'type' => 'group', + 'label' => $this->h5pF->t('Changelog'), + 'fields' => array( + (object) array( + 'name' => 'date', + 'type' => 'text', + 'label' => $this->h5pF->t('Date'), + 'optional' => TRUE + ), + (object) array( + 'name' => 'author', + 'type' => 'text', + 'label' => $this->h5pF->t('Changed by'), + 'optional' => TRUE + ), + (object) array( + 'name' => 'log', + 'type' => 'text', + 'widget' => 'textarea', + 'label' => $this->h5pF->t('Description of change'), + 'placeholder' => $this->h5pF->t('Photo cropped, text changed, etc.'), + 'optional' => TRUE + ) + ) + ) + ), + (object) array ( + 'name' => 'authorComments', + 'type' => 'text', + 'widget' => 'textarea', + 'label' => $this->h5pF->t('Author comments'), + 'description' => $this->h5pF->t('Comments for the editor of the content (This text will not be published as a part of copyright info)'), + 'optional' => TRUE + ), + (object) array( + 'name' => 'contentType', + 'type' => 'text', + 'widget' => 'none' + ), + ); + + return $semantics; + } + public function getCopyrightSemantics() { static $semantics; diff --git a/js/h5p-confirmation-dialog.js b/js/h5p-confirmation-dialog.js index a6dd998..fa623c6 100644 --- a/js/h5p-confirmation-dialog.js +++ b/js/h5p-confirmation-dialog.js @@ -351,7 +351,7 @@ H5P.ConfirmationDialog = (function (EventDispatcher) { * * @param {number|null} minHeight */ - this.setViewPortMinimumHeight = function(minHeight) { + this.setViewPortMinimumHeight = function (minHeight) { var container = document.querySelector('.h5p-container') || document.body; container.style.minHeight = (typeof minHeight === 'number') ? (minHeight + 'px') : minHeight; }; diff --git a/js/h5p-content-type.js b/js/h5p-content-type.js index 8be8fcd..47c4d21 100644 --- a/js/h5p-content-type.js +++ b/js/h5p-content-type.js @@ -11,7 +11,7 @@ * @class * @augments H5P.EventDispatcher */ -H5P.ContentType = function (isRootLibrary, library) { +H5P.ContentType = function (isRootLibrary) { function ContentType() {} diff --git a/js/h5p-content-upgrade-process.js b/js/h5p-content-upgrade-process.js index e54dbb7..e683902 100644 --- a/js/h5p-content-upgrade-process.js +++ b/js/h5p-content-upgrade-process.js @@ -25,19 +25,27 @@ H5P.ContentUpgradeProcess = (function (Version) { } self.loadLibrary = loadLibrary; - self.upgrade(name, oldVersion, newVersion, params, function (err, result) { + self.upgrade(name, oldVersion, newVersion, params.params, params.metadata, function (err, upgradedParams, upgradedMetadata) { if (err) { return done(err); } - done(null, JSON.stringify(params)); + done(null, JSON.stringify({params: upgradedParams, metadata: upgradedMetadata})); }); } /** + * Run content upgrade. * + * @public + * @param {string} name + * @param {Version} oldVersion + * @param {Version} newVersion + * @param {Object} params + * @param {Object} metadata + * @param {Function} done */ - ContentUpgradeProcess.prototype.upgrade = function (name, oldVersion, newVersion, params, done) { + ContentUpgradeProcess.prototype.upgrade = function (name, oldVersion, newVersion, params, metadata, done) { var self = this; // Load library details and upgrade routines @@ -47,7 +55,7 @@ H5P.ContentUpgradeProcess = (function (Version) { } // Run upgrade routines on params - self.processParams(library, oldVersion, newVersion, params, function (err, params) { + self.processParams(library, oldVersion, newVersion, params, metadata, function (err, params, metadata) { if (err) { return done(err); } @@ -61,7 +69,7 @@ H5P.ContentUpgradeProcess = (function (Version) { next(err); }); }, function (err) { - done(err, params); + done(err, params, metadata); }); }); }); @@ -77,7 +85,7 @@ H5P.ContentUpgradeProcess = (function (Version) { * @param {Object} params * @param {Function} next */ - ContentUpgradeProcess.prototype.processParams = function (library, oldVersion, newVersion, params, next) { + ContentUpgradeProcess.prototype.processParams = function (library, oldVersion, newVersion, params, metadata, next) { if (H5PUpgrades[library.name] === undefined) { if (library.upgradesScript) { // Upgrades script should be loaded so the upgrades should be here. @@ -110,16 +118,19 @@ H5P.ContentUpgradeProcess = (function (Version) { var unnecessaryWrapper = (upgrade.contentUpgrade !== undefined ? upgrade.contentUpgrade : upgrade); try { - unnecessaryWrapper(params, function (err, upgradedParams) { + unnecessaryWrapper(params, function (err, upgradedParams, upgradedExtras) { params = upgradedParams; + if (upgradedExtras && upgradedExtras.metadata) { // Optional + metadata = upgradedExtras.metadata; + } nextMinor(err); - }); + }, {metadata: metadata}); } catch (err) { - if (console && console.log) { - console.log("Error", err.stack); - console.log("Error", err.name); - console.log("Error", err.message); + if (console && console.error) { + console.error("Error", err.stack); + console.error("Error", err.name); + console.error("Error", err.message); } next(err); } @@ -127,7 +138,7 @@ H5P.ContentUpgradeProcess = (function (Version) { }, nextMajor); } }, function (err) { - next(err, params); + next(err, params, metadata); }); }; @@ -155,7 +166,7 @@ H5P.ContentUpgradeProcess = (function (Version) { // Look for available upgrades var usedLib = params.library.split(' ', 2); for (var i = 0; i < field.options.length; i++) { - var availableLib = field.options[i].split(' ', 2); + var availableLib = (typeof field.options[i] === 'string') ? field.options[i].split(' ', 2) : field.options[i].name.split(' ', 2); if (availableLib[0] === usedLib[0]) { if (availableLib[1] === usedLib[1]) { return done(); // Same version @@ -169,10 +180,13 @@ H5P.ContentUpgradeProcess = (function (Version) { } // A newer version is available, upgrade params - return self.upgrade(availableLib[0], usedVer, availableVer, params.params, function (err, upgraded) { + return self.upgrade(availableLib[0], usedVer, availableVer, params.params, params.metadata, function (err, upgradedParams, upgradedMetadata) { if (!err) { params.library = availableLib[0] + ' ' + availableVer.major + '.' + availableVer.minor; - params.params = upgraded; + params.params = upgradedParams; + if (upgradedMetadata) { + params.metadata = upgradedMetadata; + } } done(err, params); }); diff --git a/js/h5p-content-upgrade-worker.js b/js/h5p-content-upgrade-worker.js index 26ad038..3507a35 100644 --- a/js/h5p-content-upgrade-worker.js +++ b/js/h5p-content-upgrade-worker.js @@ -1,3 +1,4 @@ +/* global importScripts */ var H5P = H5P || {}; importScripts('h5p-version.js', 'h5p-content-upgrade-process.js'); diff --git a/js/h5p-content-upgrade.js b/js/h5p-content-upgrade.js index 9c1e4ce..bb5244a 100644 --- a/js/h5p-content-upgrade.js +++ b/js/h5p-content-upgrade.js @@ -1,4 +1,4 @@ -/*jshint -W083 */ +/* global H5PAdminIntegration H5PUtils */ (function ($, Version) { var info, $container, librariesCache = {}, scriptsCache = {}; diff --git a/js/h5p-data-view.js b/js/h5p-data-view.js index 89080e0..2f708f8 100644 --- a/js/h5p-data-view.js +++ b/js/h5p-data-view.js @@ -1,3 +1,4 @@ +/* global H5PUtils */ var H5PDataView = (function ($) { /** @@ -198,7 +199,6 @@ var H5PDataView = (function ($) { * @param number col ID of column */ H5PDataView.prototype.createFacets = function (input, col) { - var self = this; var facets = ''; if (input instanceof Array) { diff --git a/js/h5p-event-dispatcher.js b/js/h5p-event-dispatcher.js index a6707b4..2027cf7 100644 --- a/js/h5p-event-dispatcher.js +++ b/js/h5p-event-dispatcher.js @@ -10,7 +10,7 @@ var H5P = window.H5P = window.H5P || {}; * @param {boolean} [extras.bubbles] * @param {boolean} [extras.external] */ -H5P.Event = function(type, data, extras) { +H5P.Event = function (type, data, extras) { this.type = type; this.data = data; var bubbles = false; @@ -34,7 +34,7 @@ H5P.Event = function(type, data, extras) { /** * Prevent this event from bubbling up to parent */ - this.preventBubbling = function() { + this.preventBubbling = function () { bubbles = false; }; @@ -44,7 +44,7 @@ H5P.Event = function(type, data, extras) { * @returns {boolean} * true if bubbling false otherwise */ - this.getBubbles = function() { + this.getBubbles = function () { return bubbles; }; @@ -54,7 +54,7 @@ H5P.Event = function(type, data, extras) { * @returns {boolean} * true if external and not already scheduled, otherwise false */ - this.scheduleForExternal = function() { + this.scheduleForExternal = function () { if (external && !scheduledForExternal) { scheduledForExternal = true; return true; diff --git a/js/h5p-library-details.js b/js/h5p-library-details.js index 77d366b..b5ee012 100644 --- a/js/h5p-library-details.js +++ b/js/h5p-library-details.js @@ -1,4 +1,5 @@ -var H5PLibraryDetails= H5PLibraryDetails || {}; +/* global H5PAdminIntegration H5PUtils */ +var H5PLibraryDetails = H5PLibraryDetails || {}; (function ($) { @@ -68,7 +69,7 @@ var H5PLibraryDetails= H5PLibraryDetails || {}; */ H5PLibraryDetails.createContentTable = function () { // Remove it if it exists: - if(H5PLibraryDetails.$contentTable) { + if (H5PLibraryDetails.$contentTable) { H5PLibraryDetails.$contentTable.remove(); } @@ -77,10 +78,10 @@ var H5PLibraryDetails= H5PLibraryDetails || {}; var i = (H5PLibraryDetails.currentPage*H5PLibraryDetails.PAGER_SIZE); var lastIndex = (i+H5PLibraryDetails.PAGER_SIZE); - if(lastIndex > H5PLibraryDetails.currentContent.length) { + if (lastIndex > H5PLibraryDetails.currentContent.length) { lastIndex = H5PLibraryDetails.currentContent.length; } - for(; i' + content.title + ''])); } @@ -97,7 +98,7 @@ var H5PLibraryDetails= H5PLibraryDetails || {}; H5PLibraryDetails.$next = $(''); H5PLibraryDetails.$previous.on('click', function () { - if(H5PLibraryDetails.$previous.hasClass('disabled')) { + if (H5PLibraryDetails.$previous.hasClass('disabled')) { return; } @@ -107,7 +108,7 @@ var H5PLibraryDetails= H5PLibraryDetails || {}; }); H5PLibraryDetails.$next.on('click', function () { - if(H5PLibraryDetails.$next.hasClass('disabled')) { + if (H5PLibraryDetails.$next.hasClass('disabled')) { return; } @@ -127,7 +128,7 @@ var H5PLibraryDetails= H5PLibraryDetails || {}; H5PLibraryDetails.$pagerInfo.hide(); // User has updated the pageNumber - var pageNumerUpdated = function() { + var pageNumerUpdated = function () { var newPageNum = $gotoInput.val()-1; var intRegex = /^\d+$/; @@ -135,7 +136,7 @@ var H5PLibraryDetails= H5PLibraryDetails || {}; H5PLibraryDetails.$pagerInfo.css({display: 'inline-block'}); // Check if input value is valid, and that it has actually changed - if(!(intRegex.test(newPageNum) && newPageNum >= 0 && newPageNum < H5PLibraryDetails.getNumPages() && newPageNum != H5PLibraryDetails.currentPage)) { + if (!(intRegex.test(newPageNum) && newPageNum >= 0 && newPageNum < H5PLibraryDetails.getNumPages() && newPageNum != H5PLibraryDetails.currentPage)) { return; } @@ -185,7 +186,7 @@ var H5PLibraryDetails= H5PLibraryDetails || {}; H5PLibraryDetails.updatePager = function () { H5PLibraryDetails.$pagerInfo.css({display: 'inline-block'}); - if(H5PLibraryDetails.getNumPages() > 0) { + if (H5PLibraryDetails.getNumPages() > 0) { var message = H5PUtils.translateReplace(H5PLibraryDetails.library.translations.pageXOfY, { '$x': (H5PLibraryDetails.currentPage+1), '$y': H5PLibraryDetails.getNumPages() @@ -211,7 +212,7 @@ var H5PLibraryDetails= H5PLibraryDetails || {}; var searchString = $('.h5p-content-search > input').val(); // If search string same as previous, just do nothing - if(H5PLibraryDetails.currentFilter === searchString) { + if (H5PLibraryDetails.currentFilter === searchString) { return; } @@ -219,7 +220,7 @@ var H5PLibraryDetails= H5PLibraryDetails || {}; // If empty search, use the complete list H5PLibraryDetails.currentContent = H5PLibraryDetails.library.content; } - else if(H5PLibraryDetails.filterCache[searchString]) { + else if (H5PLibraryDetails.filterCache[searchString]) { // If search is cached, no need to filter H5PLibraryDetails.currentContent = H5PLibraryDetails.filterCache[searchString]; } @@ -227,10 +228,10 @@ var H5PLibraryDetails= H5PLibraryDetails || {}; var listToFilter = H5PLibraryDetails.library.content; // Check if we can filter the already filtered results (for performance) - if(searchString.length > 1 && H5PLibraryDetails.currentFilter === searchString.substr(0, H5PLibraryDetails.currentFilter.length)) { + if (searchString.length > 1 && H5PLibraryDetails.currentFilter === searchString.substr(0, H5PLibraryDetails.currentFilter.length)) { listToFilter = H5PLibraryDetails.currentContent; } - H5PLibraryDetails.currentContent = $.grep(listToFilter, function(content) { + H5PLibraryDetails.currentContent = $.grep(listToFilter, function (content) { return content.title && content.title.match(new RegExp(searchString, 'i')); }); } @@ -256,7 +257,7 @@ var H5PLibraryDetails= H5PLibraryDetails || {}; $('input', H5PLibraryDetails.$search).on('change keypress paste input', function () { // Here we start the filtering // We wait at least 500 ms after last input to perform search - if(inputTimer) { + if (inputTimer) { clearTimeout(inputTimer); } diff --git a/js/h5p-library-list.js b/js/h5p-library-list.js index 1ee1bc8..344b736 100644 --- a/js/h5p-library-list.js +++ b/js/h5p-library-list.js @@ -1,4 +1,4 @@ -/*jshint multistr: true */ +/* global H5PAdminIntegration H5PUtils */ var H5PLibraryList = H5PLibraryList || {}; (function ($) { @@ -25,7 +25,7 @@ var H5PLibraryList = H5PLibraryList || {}; */ H5PLibraryList.createLibraryList = function (libraries) { var t = H5PAdminIntegration.l10n; - if(libraries.listData === undefined || libraries.listData.length === 0) { + if (libraries.listData === undefined || libraries.listData.length === 0) { return $('
' + t.NA + '
'); } diff --git a/js/h5p-resizer.js b/js/h5p-resizer.js index 772cafd..4ed65e0 100644 --- a/js/h5p-resizer.js +++ b/js/h5p-resizer.js @@ -21,7 +21,7 @@ iframe.style.width = '100%'; // Tell iframe that it needs to resize when our window resizes - var resize = function (event) { + var resize = function () { if (iframe.contentWindow) { // Limit resize calls to avoid flickering respond('resize'); @@ -64,7 +64,7 @@ * @param {Object} data Payload * @param {Function} respond Send a response to the iframe */ - actionHandlers.resize = function (iframe, data, respond) { + actionHandlers.resize = function (iframe, data) { // Resize iframe so all content is visible. Use scrollHeight to make sure we get everything iframe.style.height = data.scrollHeight + 'px'; }; diff --git a/js/h5p-utils.js b/js/h5p-utils.js index f47d1b6..b5aa333 100644 --- a/js/h5p-utils.js +++ b/js/h5p-utils.js @@ -1,3 +1,4 @@ +/* global H5PAdminIntegration*/ var H5PUtils = H5PUtils || {}; (function ($) { @@ -9,7 +10,7 @@ var H5PUtils = H5PUtils || {}; H5PUtils.createTable = function (headers) { var $table = $('
'); - if(headers) { + if (headers) { var $thead = $(''); var $tr = $(''); @@ -44,7 +45,7 @@ var H5PUtils = H5PUtils || {}; }; } - $('', value).appendTo($tr); + $('', value).appendTo($tr); }); return $tr; diff --git a/js/h5p-x-api-event.js b/js/h5p-x-api-event.js index c1d6c66..e012ac1 100644 --- a/js/h5p-x-api-event.js +++ b/js/h5p-x-api-event.js @@ -133,9 +133,10 @@ H5P.XAPIEvent.prototype.setObject = function (instance) { } } else { - if (H5PIntegration && H5PIntegration.contents && H5PIntegration.contents['cid-' + instance.contentId].title) { + var content = H5P.getContentForInstance(instance.contentId); + if (content && content.metadata && content.metadata.title) { this.data.statement.object.definition.name = { - "en-US": H5P.createTitle(H5PIntegration.contents['cid-' + instance.contentId].title) + "en-US": H5P.createTitle(content.metadata.title) }; } } @@ -150,7 +151,6 @@ H5P.XAPIEvent.prototype.setObject = function (instance) { */ H5P.XAPIEvent.prototype.setContext = function (instance) { if (instance.parent && (instance.parent.contentId || instance.parent.subContentId)) { - var parentId = instance.parent.subContentId === undefined ? instance.parent.contentId : instance.parent.subContentId; this.data.statement.context = { "contextActivities": { "parent": [ @@ -217,7 +217,7 @@ H5P.XAPIEvent.prototype.setActor = function () { * @returns {number} * The max score, or null if not defined */ -H5P.XAPIEvent.prototype.getMaxScore = function() { +H5P.XAPIEvent.prototype.getMaxScore = function () { return this.getVerifiedStatementValue(['result', 'score', 'max']); }; @@ -227,7 +227,7 @@ H5P.XAPIEvent.prototype.getMaxScore = function() { * @returns {number} * The score, or null if not defined */ -H5P.XAPIEvent.prototype.getScore = function() { +H5P.XAPIEvent.prototype.getScore = function () { return this.getVerifiedStatementValue(['result', 'score', 'raw']); }; @@ -256,7 +256,7 @@ H5P.XAPIEvent.prototype.getContentXAPIId = function (instance) { H5P.XAPIEvent.prototype.isFromChild = function () { var parentId = this.getVerifiedStatementValue(['context', 'contextActivities', 'parent', 0, 'id']); return !parentId || parentId.indexOf('subContentId') === -1; -} +}; /** * Figure out if a property exists in the statement and return it @@ -267,7 +267,7 @@ H5P.XAPIEvent.prototype.isFromChild = function () { * @returns {*} * The value of the property if it is set, null otherwise. */ -H5P.XAPIEvent.prototype.getVerifiedStatementValue = function(keys) { +H5P.XAPIEvent.prototype.getVerifiedStatementValue = function (keys) { var val = this.data.statement; for (var i = 0; i < keys.length; i++) { if (val[keys[i]] === undefined) { diff --git a/js/h5p-x-api.js b/js/h5p-x-api.js index 8a27eb9..66971cd 100644 --- a/js/h5p-x-api.js +++ b/js/h5p-x-api.js @@ -92,7 +92,7 @@ H5P.EventDispatcher.prototype.triggerXAPIScored = function (score, maxScore, ver this.trigger(event); }; -H5P.EventDispatcher.prototype.setActivityStarted = function() { +H5P.EventDispatcher.prototype.setActivityStarted = function () { if (this.activityStartTime === undefined) { // Don't trigger xAPI events in the editor if (this.contentId !== undefined && diff --git a/js/h5p.js b/js/h5p.js index ef56834..7f63c1f 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -89,7 +89,7 @@ H5P.init = function (target) { } // H5Ps added in normal DIV. - var $containers = H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () { + H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () { var $element = H5P.jQuery(this).addClass('h5p-initialized'); var $container = H5P.jQuery('
').appendTo($element); var contentId = $element.data('content-id'); @@ -99,7 +99,8 @@ H5P.init = function (target) { } var library = { library: contentData.library, - params: JSON.parse(contentData.jsonContent) + params: JSON.parse(contentData.jsonContent), + metadata: contentData.metadata }; H5P.getUserData(contentId, 'state', function (err, previousState) { @@ -163,7 +164,7 @@ H5P.init = function (target) { if (displayOptions.frame) { // Special handling of copyrights if (displayOptions.copyright) { - var copyrights = H5P.getCopyrights(instance, library.params, contentId); + var copyrights = H5P.getCopyrights(instance, library.params, contentId, library.metadata); if (!copyrights) { displayOptions.copyright = false; } @@ -300,7 +301,7 @@ H5P.init = function (target) { }); // When resize has been prepared tell parent window to resize - H5P.communicator.on('resizePrepared', function (data) { + H5P.communicator.on('resizePrepared', function () { H5P.communicator.send('resize', { scrollHeight: document.body.scrollHeight }); @@ -493,7 +494,7 @@ H5P.fullScreen = function ($element, instance, exitCallback, body, forceSemiFull } var $container = $element; - var $classes, $iframe; + var $classes, $iframe, $body; if (body === undefined) { $body = H5P.$body; } @@ -795,6 +796,10 @@ H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) { extras.previousState = library.userDatas.state; } + if (library.metadata) { + extras.metadata = library.metadata; + } + // Makes all H5P libraries extend H5P.ContentType: var standalone = extras.standalone || false; // This order makes it possible for an H5P library to override H5P.ContentType functions! @@ -973,9 +978,11 @@ H5P.Dialog = function (name, title, content, $element) { * Parameters of the content instance. * @param {number} contentId * Identifies the H5P content + * @param {Object} metadata + * Metadata of the content instance. * @returns {string} Copyright information. */ -H5P.getCopyrights = function (instance, parameters, contentId) { +H5P.getCopyrights = function (instance, parameters, contentId, metadata) { var copyrights; if (instance.getCopyrights !== undefined) { @@ -994,6 +1001,11 @@ H5P.getCopyrights = function (instance, parameters, contentId) { H5P.findCopyrights(copyrights, parameters, contentId); } + var metadataCopyrights = H5P.buildMetadataCopyrights(metadata, instance.libraryInfo.machineName); + if (metadataCopyrights !== undefined) { + copyrights.addMediaInFront(metadataCopyrights); + } + if (copyrights !== undefined) { // Convert to string copyrights = copyrights.toString(); @@ -1010,8 +1022,19 @@ H5P.getCopyrights = function (instance, parameters, contentId) { * To search for file objects in. * @param {number} contentId * Used to insert thumbnails for images. + * @param {Object} extras - Extras. + * @param {object} extras.metadata - Metadata + * @param {object} extras.machineName - Library name of some kind. + * Metadata of the content instance. */ -H5P.findCopyrights = function (info, parameters, contentId) { +H5P.findCopyrights = function (info, parameters, contentId, extras) { + // If extras are + if (extras) { + extras.params = parameters; + buildFromMetadata(extras, extras.machineName, contentId); + } + + var lastContentTypeName; // Cycle through parameters for (var field in parameters) { if (!parameters.hasOwnProperty(field)) { @@ -1021,6 +1044,8 @@ H5P.findCopyrights = function (info, parameters, contentId) { /** * @deprecated This hack should be removed after 2017-11-01 * The code that was using this was removed by HFP-574 + * This note was seen on 2018-04-04, and consultation with + * higher authorities lead to keeping the code for now ;-) */ if (field === 'overrideSettings') { console.warn("The semantics field 'overrideSettings' is DEPRECATED and should not be used."); @@ -1030,12 +1055,21 @@ H5P.findCopyrights = function (info, parameters, contentId) { var value = parameters[field]; + if (value && value.library && typeof value.library === 'string') { + lastContentTypeName = value.library.split(' ')[0]; + } + else if (value && value.library && typeof value.library === 'object') { + lastContentTypeName = (value.library.library && typeof value.library.library === 'string') ? value.library.library.split(' ')[0] : lastContentTypeName; + } + if (value instanceof Array) { // Cycle through array H5P.findCopyrights(info, value, contentId); } else if (value instanceof Object) { - // Check if object is a file with copyrights + buildFromMetadata(value, lastContentTypeName, contentId); + + // Check if object is a file with copyrights (old core) if (value.copyright === undefined || value.copyright.license === undefined || value.path === undefined || @@ -1054,6 +1088,43 @@ H5P.findCopyrights = function (info, parameters, contentId) { } } } + + function buildFromMetadata(data, name, contentId) { + if (data.metadata) { + const metadataCopyrights = H5P.buildMetadataCopyrights(data.metadata, name); + if (metadataCopyrights !== undefined) { + if (data.params && data.params.contentName === 'Image' && data.params.file) { + const path = data.params.file.path; + const width = data.params.file.width; + const height = data.params.file.height; + metadataCopyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(path, contentId), width, height)); + } + info.addMedia(metadataCopyrights); + } + } + } +}; + +H5P.buildMetadataCopyrights = function (metadata) { + if (metadata && metadata.license !== undefined && metadata.license !== 'U') { + var dataset = { + contentType: metadata.contentType, + title: metadata.title, + author: (metadata.authors && metadata.authors.length > 0) ? metadata.authors.map(function (author) { + return (author.role) ? author.name + ' (' + author.role + ')' : author.name; + }).join(', ') : undefined, + source: metadata.source, + year: (metadata.yearFrom) ? (metadata.yearFrom + ((metadata.yearTo) ? '-' + metadata.yearTo: '')) : undefined, + license: metadata.license, + version: metadata.licenseVersion, + licenseExtras: metadata.licenseExtras, + changes: (metadata.changes && metadata.changes.length > 0) ? metadata.changes.map(function (change) { + return change.log + (change.author ? ', ' + change.author : '') + (change.date ? ', ' + change.date : ''); + }).join(' / ') : undefined + }; + + return new H5P.MediaCopyright(dataset); + } }; /** @@ -1110,10 +1181,10 @@ H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size) { updateEmbed(); // Select text and expand textareas - $dialog.find('.h5p-embed-code-container').each(function(index, value) { - H5P.jQuery(this).css('height', this.scrollHeight + 'px').focus(function() { - H5P.jQuery(this).select(); - }); + $dialog.find('.h5p-embed-code-container').each(function () { + H5P.jQuery(this).css('height', this.scrollHeight + 'px').focus(function () { + H5P.jQuery(this).select(); + }); }); $dialog.find('.h5p-embed-code-container').eq(0).select(); positionInner(); @@ -1130,7 +1201,7 @@ H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size) { $expander.addClass('h5p-open').text(H5P.t('hideAdvanced')); $content.show(); } - $dialog.find('.h5p-embed-code-container').each(function(index, value) { + $dialog.find('.h5p-embed-code-container').each(function () { H5P.jQuery(this).css('height', this.scrollHeight + 'px'); }); positionInner(); @@ -1175,6 +1246,17 @@ H5P.ContentCopyrights = function () { } }; + /** + * Add sub content in front. + * + * @param {H5P.MediaCopyright} newMedia + */ + this.addMediaInFront = function (newMedia) { + if (newMedia !== undefined) { + media.unshift(newMedia); + } + }; + /** * Add sub content. * @@ -1294,7 +1376,7 @@ H5P.MediaCopyright = function (copyright, labels, order, extraFields) { link = copyrightLicense.link.replace(':version', copyrightLicense.linkVersions ? copyrightLicense.linkVersions[version] : version); } else if (versionInfo && copyrightLicense.hasOwnProperty('link')) { - link = versionInfo.link + link = versionInfo.link; } if (link) { value = '' + value + ''; @@ -1331,16 +1413,19 @@ H5P.MediaCopyright = function (copyright, labels, order, extraFields) { if (order === undefined) { // Set default order - order = ['title', 'author', 'year', 'source', 'license']; + order = ['contentType', 'title', 'license', 'author', 'year', 'source', 'licenseExtras', 'changes']; } for (var i = 0; i < order.length; i++) { var fieldName = order[i]; - if (copyright[fieldName] !== undefined) { + if (copyright[fieldName] !== undefined && copyright[fieldName] !== '') { var humanValue = copyright[fieldName]; if (fieldName === 'license') { humanValue = humanizeLicense(copyright.license, copyright.version); } + if (fieldName === 'source') { + humanValue = (humanValue) ? '' + humanValue + '' : undefined; + } list.add(new H5P.Field(getLabel(fieldName), humanValue)); } } @@ -1529,7 +1614,8 @@ H5P.Coords = function (x, y, w, h) { this.y = x.y; this.w = x.w; this.h = x.h; - } else { + } + else { if (x !== undefined) { this.x = x; } @@ -1561,8 +1647,8 @@ H5P.libraryFromString = function (library) { if (res !== null) { return { 'machineName': res[1], - 'majorVersion': res[2], - 'minorVersion': res[3] + 'majorVersion': parseInt(res[2]), + 'minorVersion': parseInt(res[3]) }; } else { @@ -1785,7 +1871,7 @@ H5P.on = function (instance, eventType, handler) { * @returns {string} UUID */ H5P.createUUID = function () { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(char) { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (char) { var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8); return newChar.toString(16); }); @@ -1944,8 +2030,11 @@ H5P.createTitle = function (rawTitle, maxLength) { * @returns {string|null} Returns the string that should be set as crossorigin policy for elements or null if * no policy is set. */ - H5P.getCrossOrigin = function () { - return H5PIntegration.crossorigin ? H5PIntegration.crossorigin : null; + H5P.getCrossOrigin = function (url) { + var crossorigin = H5PIntegration.crossorigin; + var urlRegex = H5PIntegration.crossoriginRegex; + + return crossorigin && urlRegex && url.match(urlRegex) ? crossorigin : null; }; /** @@ -2010,7 +2099,7 @@ H5P.createTitle = function (rawTitle, maxLength) { } preloadedData[options.subContentId][dataId] = data; - contentUserDataAjax(contentId, dataId, options.subContentId, function (error, data) { + contentUserDataAjax(contentId, dataId, options.subContentId, function (error) { if (options.errorCallback && error) { options.errorCallback(error); } @@ -2041,9 +2130,220 @@ H5P.createTitle = function (rawTitle, maxLength) { contentUserDataAjax(contentId, dataId, subContentId, undefined, null); }; + /** + * Function for getting content for a certain ID + * + * @param {number} contentId + * @return {Object} + */ + H5P.getContentForInstance = function (contentId) { + var key = 'cid-' + contentId; + var exists = H5PIntegration && H5PIntegration.contents && + H5PIntegration.contents[key]; + + return exists ? H5PIntegration.contents[key] : undefined; + }; + + /** + * Prepares the content parameters for storing in the clipboard. + * + * @class + * @param {Object} parameters The parameters for the content to store + * @param {string} [genericProperty] If only part of the parameters are generic, which part + * @param {string} [specificKey] If the parameters are specific, what content type does it fit + * @returns {Object} Ready for the clipboard + */ + H5P.ClipboardItem = function (parameters, genericProperty, specificKey) { + var self = this; + + /** + * Set relative dimensions when params contains a file with a width and a height. + * Very useful to be compatible with wysiwyg editors. + * + * @private + */ + var setDimensionsFromFile = function () { + if (!self.generic) { + return; + } + var params = self.specific[self.generic]; + if (!params.params.file || !params.params.file.width || !params.params.file.height) { + return; + } + + self.width = 20; // % + self.height = (params.params.file.height / params.params.file.width) * self.width; + }; + + if (!genericProperty) { + genericProperty = 'action'; + parameters = { + action: parameters + }; + } + + self.specific = parameters; + + if (genericProperty && parameters[genericProperty]) { + self.generic = genericProperty; + } + if (specificKey) { + self.from = specificKey; + } + + if (window.H5PEditor && H5PEditor.contentId) { + self.contentId = H5PEditor.contentId; + } + + if (!self.specific.width && !self.specific.height) { + setDimensionsFromFile(); + } + }; + + /** + * Store item in the H5P Clipboard. + * + * @param {H5P.ClipboardItem|*} clipboardItem + */ + H5P.clipboardify = function (clipboardItem) { + if (!(clipboardItem instanceof H5P.ClipboardItem)) { + clipboardItem = new H5P.ClipboardItem(clipboardItem); + } + H5P.setClipboard(clipboardItem); + }; + + /** + * This is a cache for pasted data to prevent parsing multiple times. + * @type {Object} + */ + var parsedClipboard = null; + + /** + * Retrieve parsed clipboard data. + * + * @return {Object} + */ + H5P.getClipboard = function () { + if (!parsedClipboard) { + parsedClipboard = parseClipboard(); + } + + return parsedClipboard; + }; + + /** + * Set item in the H5P Clipboard. + * + * @param {H5P.ClipboardItem|object} clipboardItem - Data to be set. + */ + H5P.setClipboard = function (clipboardItem) { + localStorage.setItem('h5pClipboard', JSON.stringify(clipboardItem)); + + // Clear cache + parsedClipboard = null; + + // Trigger an event so all 'Paste' buttons may be enabled. + H5P.externalDispatcher.trigger('datainclipboard', {reset: false}); + }; + + /** + * Get config for a library + * + * @param string machineName + * @return Object + */ + H5P.getLibraryConfig = function (machineName) { + var hasConfig = H5PIntegration.libraryConfig && H5PIntegration.libraryConfig[machineName]; + return hasConfig ? H5PIntegration.libraryConfig[machineName] : {}; + }; + + /** + * Get item from the H5P Clipboard. + * + * @private + * @param {boolean} [skipUpdateFileUrls] + * @return {Object} + */ + var parseClipboard = function () { + var clipboardData = localStorage.getItem('h5pClipboard'); + if (!clipboardData) { + return; + } + + // Try to parse clipboard dat + try { + clipboardData = JSON.parse(clipboardData); + } + catch (err) { + console.error('Unable to parse JSON from clipboard.', err); + return; + } + + // Update file URLs + updateFileUrls(clipboardData.specific, function (path) { + var isTmpFile = (path.substr(-4, 4) === '#tmp'); + if (!isTmpFile && clipboardData.contentId) { + // Comes from existing content + + if (H5PEditor.contentId) { + // .. to existing content + return '../' + clipboardData.contentId + '/' + path; + } + else { + // .. to new content + return (H5PEditor.contentRelUrl ? H5PEditor.contentRelUrl : '../content/') + clipboardData.contentId + '/' + path; + } + } + return path; // Will automatically be looked for in tmp folder + }); + + + if (clipboardData.generic) { + // Use reference instead of key + clipboardData.generic = clipboardData.specific[clipboardData.generic]; + + // Avoid multiple content with same ID + delete clipboardData.generic.subContentId; + } + + return clipboardData; + }; + + /** + * Update file URLs. Useful when copying content. + * + * @private + * @param {object} params Reference + * @param {function} handler Modifies the path to work when pasted + */ + var updateFileUrls = function (params, handler) { + for (var prop in params) { + if (params.hasOwnProperty(prop) && params[prop] instanceof Object) { + var obj = params[prop]; + if (obj.path !== undefined && obj.mime !== undefined) { + obj.path = handler(obj.path); + } + else { + updateFileUrls(obj, handler); + } + } + } + }; + // Init H5P when page is fully loadded $(document).ready(function () { + window.addEventListener('storage', function (event) { + // Pick up clipboard changes from other tabs + if (event.key === 'h5pClipboard') { + // Clear cache + parsedClipboard = null; + + // Trigger an event so all 'Paste' buttons may be enabled. + H5P.externalDispatcher.trigger('datainclipboard', {reset: event.newValue === null}); + } + }); + var ccVersions = { 'default': '4.0', '4.0': H5P.t('licenseCC40'), @@ -2062,34 +2362,38 @@ H5P.createTitle = function (rawTitle, maxLength) { 'U': H5P.t('licenseU'), 'CC BY': { label: H5P.t('licenseCCBY'), - link: 'http://creativecommons.org/licenses/by/:version/legalcode', + link: 'http://creativecommons.org/licenses/by/:version', versions: ccVersions }, 'CC BY-SA': { label: H5P.t('licenseCCBYSA'), - link: 'http://creativecommons.org/licenses/by-sa/:version/legalcode', + link: 'http://creativecommons.org/licenses/by-sa/:version', versions: ccVersions }, 'CC BY-ND': { label: H5P.t('licenseCCBYND'), - link: 'http://creativecommons.org/licenses/by-nd/:version/legalcode', + link: 'http://creativecommons.org/licenses/by-nd/:version', versions: ccVersions }, 'CC BY-NC': { label: H5P.t('licenseCCBYNC'), - link: 'http://creativecommons.org/licenses/by-nc/:version/legalcode', + link: 'http://creativecommons.org/licenses/by-nc/:version', versions: ccVersions }, 'CC BY-NC-SA': { label: H5P.t('licenseCCBYNCSA'), - link: 'http://creativecommons.org/licenses/by-nc-sa/:version/legalcode', + link: 'http://creativecommons.org/licenses/by-nc-sa/:version', versions: ccVersions }, 'CC BY-NC-ND': { label: H5P.t('licenseCCBYNCND'), - link: 'http://creativecommons.org/licenses/by-nc-nd/:version/legalcode', + link: 'http://creativecommons.org/licenses/by-nc-nd/:version', versions: ccVersions }, + 'CC0 1.0': { + label: H5P.t('licenseCC010'), + link: 'https://creativecommons.org/publicdomain/zero/1.0/' + }, 'GNU GPL': { label: H5P.t('licenseGPL'), link: 'http://www.gnu.org/licenses/gpl-:version-standalone.html', @@ -2119,7 +2423,10 @@ H5P.createTitle = function (rawTitle, maxLength) { } }, 'ODC PDDL': 'Public Domain Dedication and Licence', - 'CC PDM': H5P.t('licensePDM'), + 'CC PDM': { + label: H5P.t('licensePDM'), + link: 'https://creativecommons.org/publicdomain/mark/1.0/' + }, 'C': H5P.t('licenseC'), }; diff --git a/styles/h5p.css b/styles/h5p.css index f4324a7..e3ece5c 100644 --- a/styles/h5p.css +++ b/styles/h5p.css @@ -3,11 +3,11 @@ /* Custom H5P font to use for icons. */ @font-face { font-family: 'h5p'; - src: url('../fonts/h5p-core-18.eot?cb8kvi'); - src: url('../fonts/h5p-core-18.eot?cb8kvi#iefix') format('embedded-opentype'), - url('../fonts/h5p-core-18.ttf?cb8kvi') format('truetype'), - url('../fonts/h5p-core-18.woff?cb8kvi') format('woff'), - url('../fonts/h5p-core-18.svg?cb8kvi#h5p') format('svg'); + src: url('../fonts/h5p-core-19.eot?cb8kvi'); + src: url('../fonts/h5p-core-19.eot?cb8kvi#iefix') format('embedded-opentype'), + url('../fonts/h5p-core-19.ttf?cb8kvi') format('truetype'), + url('../fonts/h5p-core-19.woff?cb8kvi') format('woff'), + url('../fonts/h5p-core-19.svg?cb8kvi#h5p') format('svg'); font-weight: normal; font-style: normal; }