h5p-php-library/h5p.classes.php

431 lines
14 KiB
PHP
Raw Normal View History

<?php
interface h5pFramework {
2012-11-22 14:00:06 +01:00
public function setErrorMessage($message);
public function setInfoMessage($message);
public function t($message, $replacements);
public function getUploadedH5pDir();
public function getTempPath();
public function getUploadedH5pPath();
2012-11-28 23:29:22 +01:00
public function isStoredLibrary($machineName, $minimumVersion);
}
2012-11-26 22:48:51 +01:00
class h5pValidator {
2012-11-22 14:00:06 +01:00
public $h5pF;
2012-11-24 01:09:25 +01:00
// Schemas used to validate the h5p files
private $h5pRequired = array(
2012-11-28 14:06:55 +01:00
'title' => '/^.{1,255}$/',
'mainVersion' => '/^[0-9]{1,5}$/',
'language' => '/^[a-z]{1,5}$/',
'machineName' => '/^[a-z0-9\-]{1,255}$/',
2012-11-24 01:09:25 +01:00
'preloadedDependencies' => array(
2012-11-28 14:06:55 +01:00
'machineName' => '/^[a-z0-9\-]{1,255}$/',
'minimumVersion' => '/^[0-9]{1,5}$/',
2012-11-24 01:09:25 +01:00
),
2012-11-28 14:06:55 +01:00
'init' => '/^[$a-z_][0-9a-z_\.$]{1,254}$/i',
2012-11-24 01:09:25 +01:00
'embedTypes' => array('iframe', 'div'),
);
private $h5pOptional = array(
2012-11-28 14:06:55 +01:00
'contentType' => '/^.{1,255}$/',
'utilization' => '/^.{1,}$/',
'subVersion' => '/^[0-9]{1,5}$/',
'author' => '/^.{1,255}$/',
'lisence' => '/^(iframe|div)$/',
2012-11-24 01:09:25 +01:00
'dynamicDependencies' => array(
2012-11-28 14:06:55 +01:00
'machineName' => '/^[a-z0-9\-]{1,255}$/',
'minimumVersion' => '/^[0-9]{1,5}$/',
2012-11-24 01:09:25 +01:00
),
2012-11-28 14:06:55 +01:00
'preloadedJs' => '/^(\\[a-z_\-\s0-9\.]+)+\.(?i)(js)$/',
'preloadedCss' => '/^(\\[a-z_\-\s0-9\.]+)+\.(?i)(js)$/',
'w' => '/^[0-9]{1,4}$/',
'h' => '/^[0-9]{1,4}$/',
'metaKeywords' => '/^.{1,}$/',
'metaDescription' => '/^.{1,}$/k',
2012-11-24 01:09:25 +01:00
);
2012-11-22 14:00:06 +01:00
2012-11-29 08:51:36 +01:00
// These are the same as above, except the preloadedDependencies are optional. Created in the constructor.
private $h5pLibraryRequired;
private $h5pLibraryOptional;
/**
* Constructor for the h5pValidator
*
* @param object $h5pFramework
* The frameworks implementation of the h5pFramework interface
*/
public function __construct($h5pFramework) {
2012-11-22 14:00:06 +01:00
$this->h5pF = $h5pFramework;
2012-11-29 08:51:36 +01:00
$this->h5pLibraryRequired = $this->arrayCopy($this->h5pLibraryRequired);
$requiredDependencies = $this->h5pLibraryRequired['requiredDependencies'];
unset($this->h5pLibraryRequired['requiredDependencies']);
$this->h5pLibraryOptional = $this->arrayCopy($this->h5pLibraryRequired);
$this->h5pLibraryOptional['requiredDependencies'] = $requiredDependencies;
2012-11-22 14:00:06 +01:00
}
/**
* Validates a .h5p file
*
* @return boolean
* TRUE if the .h5p file is valid
*/
2012-11-22 14:00:06 +01:00
public function validatePackage() {
// Requires PEAR
require_once 'Archive/Tar.php';
// Create a temporary dir to extract package in.
$tmp_dir = $this->h5pFramework->getUploadedH5pDir();
$tmp_path = $this->h5pFramework->getUploadedH5pPath();
2012-11-22 14:00:06 +01:00
2012-11-28 15:21:34 +01:00
$valid = TRUE;
2012-11-22 14:00:06 +01:00
// Extract and then remove the package file.
$tar = new Archive_Tar($tmp_path, 'bz2');
if (!$tar->extract($tmp_dir)) {
2012-11-28 15:21:34 +01:00
$this->h5pF->setErrorMessage($this->t('The file you uploaded is not a valid HTML5 Pack.'));
2012-11-22 14:00:06 +01:00
$this->rRmdir($tmp_dir);
return;
}
unlink($tmp_path);
// Process content and libraries
$libraries = array();
$files = scandir($tmp_dir);
$mainH5pData;
$mainH5pExists = $imageExists = $contentExists = FALSE;
foreach ($files as $file) {
if (in_array($file, array('.', '..'))) {
continue;
}
$file_path = $tmp_dir . DIRECTORY_SEPARATOR . $file;
if (strtolower($file) == 'h5p.json') {
$mainH5pData = $this->getJsonData($file_path);
if ($mainH5pData === FALSE) {
2012-11-29 08:51:36 +01:00
$valid = FALSE;
$this->h5pF->setErrorMessage($this->t('Could not find or parse the main h5p.json file'));
}
else {
$validH5p = $this->isValidH5pData($mainH5pData, $file, $this->h5pRequired, $this->h5pOptional);
2012-11-29 08:51:36 +01:00
if ($validH5p) {
$mainH5pExists = TRUE;
}
else {
$valid = FALSE;
$this->h5pF->setErrorMessage($this->t('Could not find or parse the main h5p.json file'));
}
}
}
2012-11-22 14:00:06 +01:00
elseif (strtolower($file) == 'h5p.jpg') {
$imageExists = TRUE;
}
elseif ($file == 'content') {
2012-11-29 08:51:36 +01:00
$jsonData = $this->getJsonData($file_path . DIRECTORY_SEPARATOR . 'content.json');
if ($jsonData === FALSE) {
$this->h5pF->setErrorMessage($this->t('Could not find or parse the content.json file'));
2012-11-28 15:21:34 +01:00
$valid = FALSE;
continue;
}
2012-11-29 08:51:36 +01:00
else {
$contentExists = TRUE;
2012-11-29 08:51:36 +01:00
// In the future we might let the librarys provide validation functions for content.json
2012-11-28 15:21:34 +01:00
}
}
elseif (strpos($file, '.') !== FALSE) {
// Illegal file fond. This is ignored.
continue;
}
2012-11-22 14:00:06 +01:00
else {
2012-11-28 15:21:34 +01:00
if (preg_match('/[^a-z0-9\-]/', $file) === 0) {
$this->h5pF->setErrorMessage($this->t('Invalid library name: %name', array('%name' => $file)));
$valid = FALSE;
2012-11-22 14:00:06 +01:00
continue;
}
2012-11-29 08:51:36 +01:00
$h5pData = $this->getJsonData($file_path . DIRECTORY_SEPARATOR . 'h5p.json');
if ($h5pData === FALSE) {
$this->h5pF->setErrorMessage($this->t('Could not find h5p.json file with valid json format for library %name', array('%name' => $file)));
2012-11-28 15:21:34 +01:00
$valid = FALSE;
2012-11-22 14:00:06 +01:00
continue;
}
2012-11-29 08:51:36 +01:00
$validLibrary = $this->isValidH5pData($h5pData, $library_name, $this->h5pLibraryRequired, $this->h5pLibraryOptional);
2012-11-26 22:48:51 +01:00
if (isset($h5pData->preloadedJs)) {
$validLibrary = $this->isExcistingFiles($h5pData->preloadedJs, $tmp_dir, $file) && $validLibrary;
2012-11-22 14:00:06 +01:00
}
2012-11-26 22:48:51 +01:00
if (isset($h5pData->preloadedCss)) {
$validLibrary = $this->isExcistingFiles($h5pData->preloadedCss, $tmp_dir, $file) && $validLibrary;
2012-11-22 14:00:06 +01:00
}
if ($validLibrary) {
2012-11-28 23:29:22 +01:00
$libraries[$file][$h5pData['mainVersion']] = $h5pData;
}
$valid = $validLibrary && $valid;
2012-11-22 14:00:06 +01:00
}
}
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) {
$libraries[] = $mainH5pData;
2012-11-28 23:29:22 +01:00
$missingLibraries = $this->getMissingLibraries($libraries);
foreach ($missingLibraries as $missing) {
if ($this->h5pF->isStoredLibrary($missing['machineName'], $missing['minimumVersion'])) {
unset($missingLibraries[$missing['machineName']]);
}
}
$valid = empty($missingLibraries) && $valid;
}
if (!$valid) {
$this->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 mainVersion second
* @return array
* A list of libraries that are missing keyed with machineName and holds objects with
* machineName and minimumVersion properties
*/
2012-11-28 23:29:22 +01:00
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));
}
}
2012-11-28 23:29:22 +01:00
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 and minimumVersion properties
* @param array $libraries
* A multidimensional array of libraries keyed with machineName first and mainVersion second
* @return
* A list of libraries that are missing keyed with machineName and holds objects with
* machineName and minimumVersion properties
*/
2012-11-28 23:29:22 +01:00
private function getMissingDependencies($dependencies, $libraries) {
$missing = array();
foreach ($library['preloadedDependencies'] as $dependency) {
if (isset($libraries[$dependency['machineName']])) {
if ($libraries[$dependency['machineName']]['minimumVersion'] < $dependency['minimumVersion']) {
$missing[$dependency['machineName']] = $dependency;
}
}
else {
$missing[$dependency['machineName']] = $dependency;
}
}
return $missing;
2012-11-26 22:48:51 +01:00
}
/**
* 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
*/
2012-11-26 22:48:51 +01:00
private function isExcistingFiles($files, $tmp_dir, $library) {
foreach ($files as $file_path) {
$path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $file_path);
if (!file_exists($tmp_dir . DIRECTORY_SEPARATOR . $path)) {
2012-11-28 15:21:34 +01:00
$this->h5pF->setErrorMessage('The JS file %file is missing from library: %name', array('%file' => $file_path, '%name' => $library));
2012-11-26 22:48:51 +01:00
return FALSE;
2012-11-22 14:00:06 +01:00
}
}
2012-11-26 22:48:51 +01:00
return TRUE;
}
/**
* Validates h5p.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
*/
2012-11-29 08:51:36 +01:00
private function isValidH5pData($h5pData, $library_name, $required, $optional) {
$valid = $this->isValidRequiredH5pData($h5pData, $required, $library_name);
$valid = $this->isValidOptionalH5pData($h5pData, $optional, $library_name) && $valid;
2012-11-28 15:21:34 +01:00
return $valid;
2012-11-28 13:56:42 +01:00
}
/**
* 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
*/
2012-11-28 15:21:34 +01:00
private function isValidOptionalH5pData($h5pData, $requirements, $library_name) {
$valid = TRUE;
2012-11-28 13:56:42 +01:00
foreach ($h5pData as $key => $value) {
if (isset($requirements[$key])) {
2012-11-28 15:21:34 +01:00
$valid = isValidRequirement($h5pData, $requirement, $library_name, $property_name) && $valid;
2012-11-28 13:56:42 +01:00
}
// Else: ignore, a package can have parameters that this library doesn't care about, but that library
// specific implementations does care about...
}
2012-11-28 15:21:34 +01:00
return $valid;
2012-11-28 13:56:42 +01:00
}
/**
*
*
* @param <type> $h5pData
* @param <type> $requirement
* @param <type> $library_name
* @param <type> $property_name
* @return boolean
*/
2012-11-28 15:21:34 +01:00
private function isValidRequirement($h5pData, $requirement, $library_name, $property_name) {
$valid = TRUE;
2012-11-28 13:56:42 +01:00
if (is_string($requirement)) {
// The requirement is a regexp, match it against the data
if (is_string($h5pData)) {
if (preg_match($requirement, $h5pData) === 0) {
2012-11-28 15:21:34 +01:00
$this->h5pF->setErrorMessage($this->t("Ivalid data provided for %property in %library", array('%property' => $property_name, '%library' => $library_name)));
$valid = FALSE;
2012-11-28 13:56:42 +01:00
}
}
else {
2012-11-28 15:21:34 +01:00
$this->h5pF->setErrorMessage($this->t("Ivalid data provided for %property in %library", array('%property' => $property_name, '%library' => $library_name)));
$valid = FALSE;
2012-11-28 13:56:42 +01:00
}
}
elseif (is_array($requirement)) {
// We have sub requirements
if (is_array($h5pData)) {
2012-11-28 15:21:34 +01:00
$valid = $this->isValidRequiredH5pData($h5pData, $requirement, $library_name) && $valid;
2012-11-28 13:56:42 +01:00
}
else {
2012-11-28 15:21:34 +01:00
$this->h5pF->setErrorMessage($this->t("Ivalid data provided for %property in %library", array('%property' => $property_name, '%library' => $library_name)));
$valid = FALSE;
2012-11-28 13:56:42 +01:00
}
}
else {
2012-11-28 15:21:34 +01:00
$this->h5pF->setErrorMessage($this->t("Can't read the property %property in %library", array('%property' => $property_name, '%library' => $library_name)));
$valid = FALSE;
2012-11-28 13:56:42 +01:00
}
2012-11-28 15:21:34 +01:00
return $valid;
2012-11-28 13:56:42 +01:00
}
2012-11-28 15:21:34 +01:00
private function isValidRequiredH5pData($h5pData, $requirements, $library_name) {
$valid = TRUE;
2012-11-28 13:56:42 +01:00
foreach ($requirements as $required => $requirement) {
if (is_int($required)) {
// We have an array of allowed options
2012-11-28 15:21:34 +01:00
return isValidH5pDataOptions($h5pData, $requirements, $library_name);
}
2012-11-28 13:56:42 +01:00
if (isset($h5pData[$required])) {
2012-11-28 15:21:34 +01:00
$valid = validateRequirement($h5pData[$required], $requirement, $library_name, $required) && $valid;
2012-11-28 13:56:42 +01:00
}
else {
2012-11-28 15:21:34 +01:00
$this->h5pF->setErrorMessage($this->t('The required property %property is missing from %library', array('%property' => $required, '%library' => $library_name)));
$valid = FALSE;
2012-11-28 13:56:42 +01:00
}
}
2012-11-28 15:21:34 +01:00
return $valid;
2012-11-22 14:00:06 +01:00
}
2012-11-28 15:21:34 +01:00
private function isValidH5pDataOptions($selected, $allowed, $library_name) {
$valid = TRUE;
foreach ($selected as $value) {
if (!in_array($value, $allowed)) {
2012-11-28 15:21:34 +01:00
$this->h5pF->setErrorMessage($this->t('Illegal option %option in %library', array('%option' => $value, '%library' => $library_name)));
$valid = FALSE;
}
}
2012-11-28 15:21:34 +01:00
return $valid;
}
2012-11-29 08:51:36 +01:00
private function getJsonData($file_path) {
$json = file_get_contents($file_path);
if (!$json) {
return FALSE;
}
$jsonData = json_decode($json);
if (!$jsonData) {
return FALSE;
}
return $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;
}
2012-11-22 14:00:06 +01:00
/**
* Recursive function for removing directories.
*
* @param string $dir Directory.
* @return boolean Indicates if the directory existed.
*/
2012-11-28 23:29:22 +01:00
public static function delTree($dir) {
$files = array_diff(scandir($dir), array('.','..'));
foreach ($files as $file) {
(is_dir("$dir/$file")) ? delTree("$dir/$file") : unlink("$dir/$file");
}
return rmdir($dir);
}
}
?>