]['library']['libraryId'] * - libraryId stored in $librariesInUse[]['preloaded'] */ public function saveLibraryUsage($contentId, $librariesInUse); /** * Loads a library * * @param string $machineName * @param int $majorVersion * @param int $minorVersion * @return array|FALSE * Array representing the library with dependency descriptions * FALSE if the library doesn't exist */ public function loadLibrary($machineName, $majorVersion, $minorVersion); } class H5PValidator { public $h5pF; public $h5pC; // Schemas used to validate the h5p files private $h5pRequired = array( 'title' => '/^.{1,255}$/', 'language' => '/^[a-z]{1,5}$/', 'preloadedDependencies' => array( 'machineName' => '/^[\w0-9\-\.]{1,255}$/i', 'majorVersion' => '/^[0-9]{1,5}$/', 'minorVersion' => '/^[0-9]{1,5}$/', ), 'mainLibrary' => '/^[$a-z_][0-9a-z_\.$]{1,254}$/i', 'embedTypes' => array('iframe', 'div'), ); private $h5pOptional = array( 'contentType' => '/^.{1,255}$/', 'description' => '/^.{1,}$/', '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)$/', 'dynamicDependencies' => array( 'machineName' => '/^[\w0-9\-\.]{1,255}$/i', 'majorVersion' => '/^[0-9]{1,5}$/', 'minorVersion' => '/^[0-9]{1,5}$/', ), 'externalResources' => array( 'machineName' => '/^[\w0-9\-\.]{1,255}$/i', 'majorVersion' => '/^[0-9]{1,5}$/', 'minorVersion' => '/^[0-9]{1,5}$/', 'url' => '/^http:\/\/[a-z_\-\.0-9]+\.[a-z]{2, 10}$/i', 'type' => '/^(css|js)$/', ), 'w' => '/^[0-9]{1,4}$/', 'h' => '/^[0-9]{1,4}$/', 'metaKeywords' => '/^.{1,}$/', 'metaDescription' => '/^.{1,}$/k', ); // Schemas used to validate the library files private $libraryRequired = array( 'title' => '/^.{1,255}$/', 'majorVersion' => '/^[0-9]{1,5}$/', 'minorVersion' => '/^[0-9]{1,5}$/', 'patchVersion' => '/^[0-9]{1,5}$/', 'machineName' => '/^[\w0-9\-\.]{1,255}$/i', 'runnable' => '/^(0|1)$/', ); private $libraryOptional = array( '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)$/', 'description' => '/^.{1,}$/', 'dynamicDependencies' => array( 'machineName' => '/^[\w0-9\-\.]{1,255}$/i', 'majorVersion' => '/^[0-9]{1,5}$/', 'minorVersion' => '/^[0-9]{1,5}$/', ), 'preloadedDependencies' => array( 'machineName' => '/^[\w0-9\-\.]{1,255}$/i', 'majorVersion' => '/^[0-9]{1,5}$/', 'minorVersion' => '/^[0-9]{1,5}$/', ), 'editorDependencies' => array( 'machineName' => '/^[\w0-9\-\.]{1,255}$/i', 'majorVersion' => '/^[0-9]{1,5}$/', 'minorVersion' => '/^[0-9]{1,5}$/', ), 'externalResources' => array( 'machineName' => '/^[\w0-9\-\.]{1,255}$/i', 'majorVersion' => '/^[0-9]{1,5}$/', 'minorVersion' => '/^[0-9]{1,5}$/', 'url' => '/^http:\/\/[a-z_\-\.0-9]+\.[a-z]{2, 10}$/i', 'type' => '/^(css|js)$/', ), 'preloadedJs' => array( 'path' => '/^((\\\|\/)?[a-z_\-\s0-9]+)+\.js$/i', ), 'preloadedCss' => array( 'path' => '/^((\\\|\/)?[a-z_\-\s0-9]+)+\.css$/i', ), 'w' => '/^[0-9]{1,4}$/', 'h' => '/^[0-9]{1,4}$/', 'embedTypes' => array('iframe', 'div'), ); /** * Constructor for the H5PValidator * * @param object $H5PFramework * The frameworks implementation of the H5PFrameworkInterface */ public function __construct($H5PFramework, $H5PCore) { $this->h5pF = $H5PFramework; $this->h5pC = $H5PCore; } /** * Validates a .h5p file * * @return boolean * TRUE if the .h5p file is valid */ public function isValidPackage() { // Create a temporary dir to extract package in. $tmp_dir = $this->h5pF->getUploadedH5pFolderPath(); $tmp_path = $this->h5pF->getUploadedH5pPath(); $valid = TRUE; // Extract and then remove the package file. $zip = new ZipArchive; if ($zip->open($tmp_path) === true) { $zip->extractTo($tmp_dir); $zip->close(); } else { $this->h5pF->setErrorMessage($this->h5pF->t('The file you uploaded is not a valid HTML5 Pack.')); $this->h5pC->delTree($tmp_dir); return; } unlink($tmp_path); // Process content and libraries $libraries = array(); $files = scandir($tmp_dir); $mainH5pData; $libraryJsonData; $mainH5pExists = $imageExists = $contentExists = FALSE; foreach ($files as $file) { // TODO: Any reason not to just drop anything starting with .? if (in_array(substr($file, 0, 1), array('.', '_'))) { continue; } $file_path = $tmp_dir . DIRECTORY_SEPARATOR . $file; // Check for h5p.json file. if (strtolower($file) == 'h5p.json') { $mainH5pData = $this->getJsonData($file_path); if ($mainH5pData === FALSE) { $valid = FALSE; $this->h5pF->setErrorMessage($this->h5pF->t('Could not find or parse the main h5p.json file')); } else { $validH5p = $this->isValidH5pData($mainH5pData, $file, $this->h5pRequired, $this->h5pOptional); if ($validH5p) { $mainH5pExists = TRUE; } else { $valid = FALSE; $this->h5pF->setErrorMessage($this->h5pF->t('Could not find or parse the main h5p.json file')); } } } // Check for h5p.jpg? elseif (strtolower($file) == 'h5p.jpg') { $imageExists = TRUE; } // Content directory holds content. elseif ($file == 'content') { if (!is_dir($file_path)) { $this->h5pF->setErrorMessage($this->h5pF->t('Invalid content folder')); $valid = FALSE; continue; } $contentJsonData = $this->getJsonData($file_path . DIRECTORY_SEPARATOR . 'content.json'); if ($contentJsonData === FALSE) { $this->h5pF->setErrorMessage($this->h5pF->t('Could not find or parse the content.json file')); $valid = FALSE; continue; } else { $contentExists = TRUE; // In the future we might let the librarys provide validation functions for content.json } } // The rest should be library folders else { if (!is_dir($file_path)) { // Ignore this. Probably a file that shouldn't have been included. continue; } if (preg_match('/^[\w0-9\-\.]{1,255}$/i', $file) === 0) { $this->h5pF->setErrorMessage($this->h5pF->t('Invalid library name: %name', array('%name' => $file))); $valid = FALSE; continue; } $h5pData = $this->getJsonData($file_path . DIRECTORY_SEPARATOR . 'library.json'); if ($h5pData === FALSE) { $this->h5pF->setErrorMessage($this->h5pF->t('Could not find library.json file with valid json format for library %name', array('%name' => $file))); $valid = FALSE; continue; } // validate json if a semantics file is provided $semanticsPath = $file_path . DIRECTORY_SEPARATOR . 'semantics.json'; if (file_exists($semanticsPath)) { $semantics = $this->getJsonData($semanticsPath, TRUE); if ($semantics === FALSE) { $this->h5pF->setErrorMessage($this->h5pF->t('Invalid semantics.json file has been included in the library %name', array('%name' => $file))); $valid = FALSE; continue; } else { $h5pData['semantics'] = $semantics; } } $validLibrary = $this->isValidH5pData($h5pData, $file, $this->libraryRequired, $this->libraryOptional); if (isset($h5pData['preloadedJs'])) { $validLibrary = $this->isExistingFiles($h5pData['preloadedJs'], $tmp_dir, $file) && $validLibrary; } if (isset($h5pData['preloadedCss'])) { $validLibrary = $this->isExistingFiles($h5pData['preloadedCss'], $tmp_dir, $file) && $validLibrary; } if ($validLibrary) { $libraries[$file] = $h5pData; } $valid = $validLibrary && $valid; } } if (!$contentExists) { $this->h5pF->setErrorMessage($this->h5pF->t('A valid content folder is missing')); $valid = FALSE; } if (!$mainH5pExists) { $this->h5pF->setErrorMessage($this->h5pF->t('A valid main h5p.json file is missing')); $valid = FALSE; } if ($valid) { $this->h5pC->librariesJsonData = $libraries; $this->h5pC->mainJsonData = $mainH5pData; $this->h5pC->contentJsonData = $contentJsonData; $libraries['mainH5pData'] = $mainH5pData; $missingLibraries = $this->getMissingLibraries($libraries); foreach ($missingLibraries as $missing) { if ($this->h5pF->getLibraryId($missing['machineName'], $missing['majorVersion'], $missing['minorVersion'])) { unset($missingLibraries[$missing['machineName']]); } } $valid = empty($missingLibraries) && $valid; } if (!$valid) { $this->h5pC->delTree($tmp_dir); } return $valid; } /** * Use the dependency declarations to find any missing libraries * * @param array $libraries * A multidimensional array of libraries keyed with machineName first and majorVersion second * @return array * A list of libraries that are missing keyed with machineName and holds objects with * machineName, majorVersion and minorVersion properties */ private function getMissingLibraries($libraries) { $missing = array(); foreach ($libraries as $library) { if (isset($library['preloadedDependencies'])) { array_merge($missing, $this->getMissingDependencies($library['preloadedDependencies'], $libraries)); } if (isset($library['dynamicDependencies'])) { array_merge($missing, $this->getMissingDependencies($library['dynamicDependencies'], $libraries)); } if (isset($library['editorDependencies'])) { array_merge($missing, $this->getMissingDependencies($library['editorDependencies'], $libraries)); } } return $missing; } /** * Helper function for getMissingLibraries, searches for dependency required libraries in * the provided list of libraries * * @param array $dependencies * A list of objects with machineName, majorVersion and minorVersion properties * @param array $libraries * An array of libraries keyed with machineName * @return * A list of libraries that are missing keyed with machineName and holds objects with * machineName, majorVersion and minorVersion properties */ private function getMissingDependencies($dependencies, $libraries) { $missing = array(); foreach ($dependencies as $dependency) { if (isset($libraries[$dependency['machineName']])) { if (!$this->h5pC->isSameVersion($libraries[$dependency['machineName']], $dependency)) { $missing[$dependency['machineName']] = $dependency; } } else { $missing[$dependency['machineName']] = $dependency; } } return $missing; } /** * Figure out if the provided file paths exists * * Triggers error messages if files doesn't exist * * @param array $files * List of file paths relative to $tmp_dir * @param string $tmp_dir * Path to the directory where the $files are stored. * @param string $library * Name of the library we are processing * @return boolean * TRUE if all the files excists */ private function isExistingFiles($files, $tmp_dir, $library) { foreach ($files as $file) { $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $file['path']); if (!file_exists($tmp_dir . DIRECTORY_SEPARATOR . $library . DIRECTORY_SEPARATOR . $path)) { $this->h5pF->setErrorMessage($this->h5pF->t('The file "%file" is missing from library: "%name"', array('%file' => $path, '%name' => $library))); return FALSE; } } return TRUE; } /** * Validates h5p.json and library.json data * * Error messages are triggered if the data isn't valid * * @param array $h5pData * h5p data * @param string $library_name * Name of the library we are processing * @param array $required * Validation pattern for required properties * @param array $optional * Validation pattern for optional properties * @return boolean * TRUE if the $h5pData is valid */ private function isValidH5pData($h5pData, $library_name, $required, $optional) { $valid = $this->isValidRequiredH5pData($h5pData, $required, $library_name); $valid = $this->isValidOptionalH5pData($h5pData, $optional, $library_name) && $valid; return $valid; } /** * Helper function for isValidH5pData * * Validates the optional part of the h5pData * * Triggers error messages * * @param array $h5pData * h5p data * @param array $requirements * Validation pattern * @param string $library_name * Name of the library we are processing * @return boolean * TRUE if the optional part of the $h5pData is valid */ private function isValidOptionalH5pData($h5pData, $requirements, $library_name) { $valid = TRUE; foreach ($h5pData as $key => $value) { if (isset($requirements[$key])) { $valid = $this->isValidRequirement($value, $requirements[$key], $library_name, $key) && $valid; } // Else: ignore, a package can have parameters that this library doesn't care about, but that library // specific implementations does care about... } return $valid; } /** * * * @param $h5pData * @param $requirement * @param $library_name * @param $property_name * @return boolean */ private function isValidRequirement($h5pData, $requirement, $library_name, $property_name) { $valid = TRUE; if (is_string($requirement)) { // The requirement is a regexp, match it against the data if (is_string($h5pData) || is_int($h5pData)) { if (preg_match($requirement, $h5pData) === 0) { $this->h5pF->setErrorMessage($this->h5pF->t("Invalid data provided for %property in %library", array('%property' => $property_name, '%library' => $library_name))); $valid = FALSE; } } else { $this->h5pF->setErrorMessage($this->h5pF->t("Invalid data provided for %property in %library", array('%property' => $property_name, '%library' => $library_name))); $valid = FALSE; } } elseif (is_array($requirement)) { // We have sub requirements if (is_array($h5pData)) { if (is_array(current($h5pData))) { foreach ($h5pData as $sub_h5pData) { $valid = $this->isValidRequiredH5pData($sub_h5pData, $requirement, $library_name) && $valid; } } else { $valid = $this->isValidRequiredH5pData($h5pData, $requirement, $library_name) && $valid; } } else { $this->h5pF->setErrorMessage($this->h5pF->t("Invalid data provided for %property in %library", array('%property' => $property_name, '%library' => $library_name))); $valid = FALSE; } } else { $this->h5pF->setErrorMessage($this->h5pF->t("Can't read the property %property in %library", array('%property' => $property_name, '%library' => $library_name))); $valid = FALSE; } return $valid; } private function isValidRequiredH5pData($h5pData, $requirements, $library_name) { $valid = TRUE; foreach ($requirements as $required => $requirement) { if (is_int($required)) { // We have an array of allowed options return $this->isValidH5pDataOptions($h5pData, $requirements, $library_name); } if (isset($h5pData[$required])) { $valid = $this->isValidRequirement($h5pData[$required], $requirement, $library_name, $required) && $valid; } else { $this->h5pF->setErrorMessage($this->h5pF->t('The required property %property is missing from %library', array('%property' => $required, '%library' => $library_name))); $valid = FALSE; } } return $valid; } private function isValidH5pDataOptions($selected, $allowed, $library_name) { $valid = TRUE; foreach ($selected as $value) { if (!in_array($value, $allowed)) { $this->h5pF->setErrorMessage($this->h5pF->t('Illegal option %option in %library', array('%option' => $value, '%library' => $library_name))); $valid = FALSE; } } return $valid; } private function getJsonData($file_path, $return_as_string = FALSE) { $json = file_get_contents($file_path); if (!$json) { return FALSE; } $jsonData = json_decode($json, TRUE); if (!$jsonData) { return FALSE; } return $return_as_string ? $json : $jsonData; } private function arrayCopy(array $array) { $result = array(); foreach ($array as $key => $val) { if (is_array($val)) { $result[$key] = arrayCopy($val); } elseif (is_object($val)) { $result[$key] = clone $val; } else { $result[$key] = $val; } } return $result; } } class H5PStorage { public $h5pF; public $h5pC; /** * Constructor for the H5PStorage * * @param object $H5PFramework * The frameworks implementation of the H5PFrameworkInterface */ public function __construct($H5PFramework, $H5PCore) { $this->h5pF = $H5PFramework; $this->h5pC = $H5PCore; } public function savePackage($contentId, $contentMainId = NULL) { foreach ($this->h5pC->librariesJsonData as $key => &$library) { $libraryId = $this->h5pF->getLibraryId($key, $library['majorVersion'], $library['minorVersion']); if (!$libraryId) { $new = TRUE; } elseif ($this->h5pF->isPatchedLibrary($library)) { $new = FALSE; $library['libraryId'] = $libraryId; } else { // We already have the same or a newer version of this library continue; } $this->h5pF->saveLibraryData($library, $new); $current_path = $this->h5pF->getUploadedH5pFolderPath() . DIRECTORY_SEPARATOR . $key; $destination_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'libraries' . DIRECTORY_SEPARATOR . $library['libraryId']; $this->h5pC->delTree($destination_path); rename($current_path, $destination_path); } $current_path = $this->h5pF->getUploadedH5pFolderPath() . DIRECTORY_SEPARATOR . 'content'; $destination_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId; rename($current_path, $destination_path); $contentJson = file_get_contents($destination_path . DIRECTORY_SEPARATOR . 'content.json'); $this->h5pF->saveContentData($contentId, $contentJson, $this->h5pC->mainJsonData, $contentMainId); $librariesInUse = array(); $this->getLibraryUsage($librariesInUse, $this->h5pC->mainJsonData); $this->h5pF->saveLibraryUsage($contentId, $librariesInUse); $this->h5pC->delTree($this->h5pF->getUploadedH5pFolderPath()); } public function deletePackage($contentId) { $this->h5pC->delTree($this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId); $this->h5pF->deleteContentData($contentId); } public function updatePackage($contentId, $contentMainId = NULL) { $this->deletePackage($contentId); $this->savePackage($contentId, $contentMainId); } public function copyPackage($contentId, $copyFromId, $contentMainId = NULL) { $source_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $copyFromId; $destination_path = $this->h5pF->getH5pPath() . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId; $this->h5pC->copyTree($source_path, $destination_path); $this->h5pF->copyContentData($contentId, $copyFromId, $contentMainId); } public function getLibraryUsage(&$librariesInUse, $jsonData, $dynamic = FALSE) { if (isset($jsonData['preloadedDependencies'])) { foreach ($jsonData['preloadedDependencies'] as $preloadedDependency) { $library = $this->h5pF->loadLibrary($preloadedDependency['machineName'], $preloadedDependency['majorVersion'], $preloadedDependency['minorVersion']); $librariesInUse[$preloadedDependency['machineName']] = array( 'library' => $library, 'preloaded' => $dynamic ? 0 : 1, ); $this->getLibraryUsage($librariesInUse, $library, $dynamic); } } if (isset($jsonData['dynamicDependencies'])) { foreach ($jsonData['dynamicDependencies'] as $dynamicDependency) { if (!isset($librariesInUse[$dynamicDependency['machineName']])) { $library = $this->h5pF->loadLibrary($dynamicDependency['machineName'], $dynamicDependency['majorVersion'], $dynamicDependency['minorVersion']); $librariesInUse[$dynamicDependency['machineName']] = array( 'library' => $library, 'preloaded' => 0, ); } $this->getLibraryUsage($librariesInUse, $library, TRUE); } } } } class H5PCore { public $h5pF; public $librariesJsonData; public $contentJsonData; public $mainJsonData; /** * Constructor for the H5PCore * * @param object $H5PFramework * The frameworks implementation of the H5PFrameworkInterface */ public function __construct($H5PFramework) { $this->h5pF = $H5PFramework; } public function isSameVersion($library, $dependency) { if ($library['majorVersion'] != $dependency['majorVersion']) { return FALSE; } if ($library['minorVersion'] != $dependency['minorVersion']) { return FALSE; } return TRUE; } /** * Recursive function for removing directories. * * @param string $dir Directory. * @return boolean Indicates if the directory existed. */ public function delTree($dir) { if (!is_dir($dir)) { return; } $files = array_diff(scandir($dir), array('.','..')); foreach ($files as $file) { (is_dir("$dir/$file")) ? $this->delTree("$dir/$file") : unlink("$dir/$file"); } return rmdir($dir); } public function copyTree($source, $destination) { $dir = opendir($source); @mkdir($destination); while (false !== ($file = readdir($dir))) { if (($file != '.') && ($file != '..')) { if (is_dir($source . '/' . $file)) { $this->copyTree($source . '/' . $file, $destination . '/' . $file); } else { copy($source . '/' . $file,$destination . '/' . $file); } } } closedir($dir); } } ?>