Merge branch 'master' into content-tags

pull/17/head
Frode Petterson 2016-03-18 13:52:53 +01:00
commit 2ac631458f
7 changed files with 716 additions and 156 deletions

View File

@ -0,0 +1,313 @@
<?php
/**
* File info?
*/
/**
* The default file storage class for H5P. Will carry out the requested file
* operations using PHP's standard file operation functions.
*
* Some implementations of H5P that doesn't use the standard file system will
* want to create their own implementation of the \H5P\FileStorage interface.
*
* @package H5P
* @copyright 2016 Joubel AS
* @license MIT
*/
class H5PDefaultStorage implements \H5PFileStorage {
private $path;
/**
* The great Constructor!
*
* @param string $path
* The base location of H5P files
*/
function __construct($path) {
// Set H5P storage path
$this->path = $path;
}
/**
* Store the library folder.
*
* @param array $library
* Library properties
*/
public function saveLibrary($library) {
$dest = $this->path . '/libraries/' . \H5PCore::libraryToString($library, TRUE);
// Make sure destination dir doesn't exist
\H5PCore::deleteFileTree($dest);
// Move library folder
self::copyFileTree($library['uploadDirectory'], $dest);
}
/**
* Store the content folder.
*
* @param string $source
* Path on file system to content directory.
* @param int $id
* What makes this content unique.
*/
public function saveContent($source, $id) {
$dest = "{$this->path}/content/{$id}";
// Remove any old content
\H5PCore::deleteFileTree($dest);
self::copyFileTree($source, $dest);
}
/**
* Remove content folder.
*
* @param int $id
* Content identifier
*/
public function deleteContent($id) {
\H5PCore::deleteFileTree("{$this->path}/content/{$id}");
}
/**
* Creates a stored copy of the content folder.
*
* @param string $id
* Identifier of content to clone.
* @param int $newId
* The cloned content's identifier
*/
public function cloneContent($id, $newId) {
$path = $this->path . '/content/';
self::copyFileTree($path . $id, $path . $newId);
}
/**
* Get path to a new unique tmp folder.
*
* @return string
* Path
*/
public function getTmpPath() {
$temp = "{$this->path}/temp";
self::dirReady($temp);
return "{$temp}/" . uniqid('h5p-');
}
/**
* Fetch content folder and save in target directory.
*
* @param int $id
* Content identifier
* @param string $target
* Where the content folder will be saved
*/
public function exportContent($id, $target) {
self::copyFileTree("{$this->path}/content/{$id}", $target);
}
/**
* Fetch library folder and save in target directory.
*
* @param array $library
* Library properties
* @param string $target
* Where the library folder will be saved
*/
public function exportLibrary($library, $target) {
$folder = \H5PCore::libraryToString($library, TRUE);
self::copyFileTree("{$this->path}/libraries/{$folder}", "{$target}/{$folder}");
}
/**
* Save export in file system
*
* @param string $source
* Path on file system to temporary export file.
* @param string $filename
* Name of export file.
*/
public function saveExport($source, $filename) {
$this->deleteExport($filename);
self::dirReady("{$this->path}/exports");
copy($source, "{$this->path}/exports/{$filename}");
}
/**
* Removes given export file
*
* @param string $filename
*/
public function deleteExport($filename) {
$target = "{$this->path}/exports/{$filename}";
if (file_exists($target)) {
unlink($target);
}
}
/**
* Will concatenate all JavaScrips and Stylesheets into two files in order
* to improve page performance.
*
* @param array $files
* A set of all the assets required for content to display
* @param string $key
* Hashed key for cached asset
*/
public function cacheAssets(&$files, $key) {
foreach ($files as $type => $assets) {
if (empty($assets)) {
continue; // Skip no assets
}
$content = '';
foreach ($assets as $asset) {
// Get content from asset file
$assetContent = file_get_contents($this->path . $asset->path);
$cssRelPath = preg_replace('/[^\/]+$/', '', $asset->path);
// Get file content and concatenate
if ($type === 'scripts') {
$content .= $assetContent . ";\n";
}
else {
// Rewrite relative URLs used inside stylesheets
$content .= preg_replace_callback(
'/url\([\'"]?([^"\')]+)[\'"]?\)/i',
function ($matches) use ($cssRelPath) {
if (preg_match("/^(data:|([a-z0-9]+:)?\/)/i", $matches[1]) === 1) {
return $matches[0]; // Not relative, skip
}
return 'url("../' . $cssRelPath . $matches[1] . '")';
},
$assetContent) . "\n";
}
}
self::dirReady("{$this->path}/cachedassets");
$ext = ($type === 'scripts' ? 'js' : 'css');
$outputfile = "/cachedassets/{$key}.{$ext}";
file_put_contents($this->path . $outputfile, $content);
$files[$type] = array((object) array(
'path' => $outputfile,
'version' => ''
));
}
}
/**
* Will check if there are cache assets available for content.
*
* @param string $key
* Hashed key for cached asset
* @return array
*/
public function getCachedAssets($key) {
$files = array();
$js = "/cachedassets/{$key}.js";
if (file_exists($this->path . $js)) {
$files['scripts'] = array((object) array(
'path' => $js,
'version' => ''
));
}
$css = "/cachedassets/{$key}.css";
if (file_exists($this->path . $css)) {
$files['styles'] = array((object) array(
'path' => $css,
'version' => ''
));
}
return empty($files) ? NULL : $files;
}
/**
* Remove the aggregated cache files.
*
* @param array $keys
* The hash keys of removed files
*/
public function deleteCachedAssets($keys) {
foreach ($keys as $hash) {
foreach (array('js', 'css') as $ext) {
$path = "{$this->path}/cachedassets/{$hash}.{$ext}";
if (file_exists($path)) {
unlink($path);
}
}
}
}
/**
* Recursive function for copying directories.
*
* @param string $source
* From path
* @param string $destination
* To path
* @return boolean
* Indicates if the directory existed.
*/
private static function copyFileTree($source, $destination) {
if (!self::dirReady($destination)) {
throw new \Exception('unabletocopy');
}
$dir = opendir($source);
if ($dir === FALSE) {
trigger_error('Unable to open directory ' . $source, E_USER_WARNING);
throw new \Exception('unabletocopy');
}
while (false !== ($file = readdir($dir))) {
if (($file != '.') && ($file != '..') && $file != '.git' && $file != '.gitignore') {
if (is_dir("{$source}/{$file}")) {
self::copyFileTree("{$source}/{$file}", "{$destination}/{$file}");
}
else {
copy("{$source}/{$file}", "{$destination}/{$file}");
}
}
}
closedir($dir);
}
/**
* Recursive function that makes sure the specified directory exists and
* is writable.
*
* TODO: Will be made private when the editor file handling is done by this
* class!
*
* @param string $path
* @return bool
*/
public static function dirReady($path) {
if (!file_exists($path)) {
$parent = preg_replace("/\/[^\/]+\/?$/", '', $path);
if (!self::dirReady($parent)) {
return FALSE;
}
mkdir($path, 0777, true);
}
if (!is_dir($path)) {
trigger_error('Path is not a directory ' . $path, E_USER_WARNING);
return FALSE;
}
if (!is_writable($path)) {
trigger_error('Unable to write to ' . $path . ' check directory permissions ', E_USER_WARNING);
return FALSE;
}
return TRUE;
}
}

View File

@ -0,0 +1,120 @@
<?php
/**
* File info?
*/
/**
* Interface needed to handle storage and export of H5P Content.
*/
interface H5PFileStorage {
/**
* Store the library folder.
*
* @param array $library
* Library properties
*/
public function saveLibrary($library);
/**
* Store the content folder.
*
* @param string $source
* Path on file system to content directory.
* @param int $id
* What makes this content unique.
*/
public function saveContent($source, $id);
/**
* Remove content folder.
*
* @param int $id
* Content identifier
*/
public function deleteContent($id);
/**
* Creates a stored copy of the content folder.
*
* @param string $id
* Identifier of content to clone.
* @param int $newId
* The cloned content's identifier
*/
public function cloneContent($id, $newId);
/**
* Get path to a new unique tmp folder.
*
* @return string
* Path
*/
public function getTmpPath();
/**
* Fetch content folder and save in target directory.
*
* @param int $id
* Content identifier
* @param string $target
* Where the content folder will be saved
*/
public function exportContent($id, $target);
/**
* Fetch library folder and save in target directory.
*
* @param array $library
* Library properties
* @param string $target
* Where the library folder will be saved
*/
public function exportLibrary($library, $target);
/**
* Save export in file system
*
* @param string $source
* Path on file system to temporary export file.
* @param string $filename
* Name of export file.
*/
public function saveExport($source, $filename);
/**
* Removes given export file
*
* @param string $filename
*/
public function deleteExport($filename);
/**
* Will concatenate all JavaScrips and Stylesheets into two files in order
* to improve page performance.
*
* @param array $files
* A set of all the assets required for content to display
* @param string $key
* Hashed key for cached asset
*/
public function cacheAssets(&$files, $key);
/**
* Will check if there are cache assets available for content.
*
* @param string $key
* Hashed key for cached asset
* @return array
*/
public function getCachedAssets($key);
/**
* Remove the aggregated cache files.
*
* @param array $keys
* The hash keys of removed files
*/
public function deleteCachedAssets($keys);
}

View File

@ -537,6 +537,29 @@ interface H5PFrameworkInterface {
* @return boolean * @return boolean
*/ */
public function isContentSlugAvailable($slug); public function isContentSlugAvailable($slug);
/**
* Stores hash keys for cached assets, aggregated JavaScripts and
* stylesheets, and connects it to libraries so that we know which cache file
* to delete when a library is updated.
*
* @param string $key
* Hash key for the given libraries
* @param array $libraries
* List of dependencies(libraries) used to create the key
*/
public function saveCachedAssets($key, $libraries);
/**
* Locate hash keys for given library and delete them.
* Used when cache file are deleted.
*
* @param int $library_id
* Library identifier
* @return array
* List of hash keys removed
*/
public function deleteCachedAssets($library_id);
} }
/** /**
@ -641,17 +664,7 @@ class H5PValidator {
* TRUE if the .h5p file is valid * TRUE if the .h5p file is valid
*/ */
public function isValidPackage($skipContent = FALSE, $upgradeOnly = FALSE) { public function isValidPackage($skipContent = FALSE, $upgradeOnly = FALSE) {
// Check that directories are writable // Check dependencies, make sure Zip is present
if (!H5PCore::dirReady($this->h5pC->path . DIRECTORY_SEPARATOR . 'content')) {
$this->h5pF->setErrorMessage($this->h5pF->t('Unable to write to the content directory.'));
return FALSE;
}
if (!H5PCore::dirReady($this->h5pC->path . DIRECTORY_SEPARATOR . 'libraries')) {
$this->h5pF->setErrorMessage($this->h5pF->t('Unable to write to the libraries directory.'));
return FALSE;
}
// Make sure Zip is present.
if (!class_exists('ZipArchive')) { if (!class_exists('ZipArchive')) {
$this->h5pF->setErrorMessage($this->h5pF->t('Your PHP version does not support ZipArchive.')); $this->h5pF->setErrorMessage($this->h5pF->t('Your PHP version does not support ZipArchive.'));
return FALSE; return FALSE;
@ -661,13 +674,6 @@ class H5PValidator {
$tmpDir = $this->h5pF->getUploadedH5pFolderPath(); $tmpDir = $this->h5pF->getUploadedH5pFolderPath();
$tmpPath = $this->h5pF->getUploadedH5pPath(); $tmpPath = $this->h5pF->getUploadedH5pPath();
if (!H5PCore::dirReady($tmpDir)) {
$this->h5pF->setErrorMessage($this->h5pF->t('Unable to write to the temporary directory.'));
return FALSE;
}
$valid = TRUE;
// Extract and then remove the package file. // Extract and then remove the package file.
$zip = new ZipArchive; $zip = new ZipArchive;
@ -690,6 +696,7 @@ class H5PValidator {
unlink($tmpPath); unlink($tmpPath);
// Process content and libraries // Process content and libraries
$valid = TRUE;
$libraries = array(); $libraries = array();
$files = scandir($tmpDir); $files = scandir($tmpDir);
$mainH5pData; $mainH5pData;
@ -1305,10 +1312,13 @@ class H5PStorage {
$contentId = $this->h5pC->saveContent($content, $contentMainId); $contentId = $this->h5pC->saveContent($content, $contentMainId);
$this->contentId = $contentId; $this->contentId = $contentId;
// Move the content folder try {
$contents_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content'; // Save content folder contents
$destination_path = $contents_path . DIRECTORY_SEPARATOR . $contentId; $this->h5pC->fs->saveContent($current_path, $contentId);
$this->h5pC->copyFileTree($current_path, $destination_path); }
catch (Exception $e) {
$this->h5pF->setErrorMessage($this->h5pF->t($e->getMessage()));
}
// Remove temp content folder // Remove temp content folder
H5PCore::deleteFileTree($basePath); H5PCore::deleteFileTree($basePath);
@ -1356,13 +1366,16 @@ class H5PStorage {
// Save library meta data // Save library meta data
$this->h5pF->saveLibraryData($library, $new); $this->h5pF->saveLibraryData($library, $new);
// Make sure destination dir is free // Save library folder
$libraries_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'libraries'; $this->h5pC->fs->saveLibrary($library);
$destination_path = $libraries_path . DIRECTORY_SEPARATOR . H5PCore::libraryToString($library, TRUE);
H5PCore::deleteFileTree($destination_path);
// Move library folder // Remove cachedassets that uses this library
$this->h5pC->copyFileTree($library['uploadDirectory'], $destination_path); if ($this->h5pC->aggregateAssets && isset($library['libraryId'])) {
$removedKeys = $this->h5pF->deleteCachedAssets($library['libraryId']);
$this->h5pC->fs->deleteCachedAssets($removedKeys);
}
// Remove tmp folder
H5PCore::deleteFileTree($library['uploadDirectory']); H5PCore::deleteFileTree($library['uploadDirectory']);
if ($new) { if ($new) {
@ -1421,26 +1434,10 @@ class H5PStorage {
* @param int $contentId * @param int $contentId
* The content id * The content id
*/ */
public function deletePackage($contentId) { public function deletePackage($content) {
H5PCore::deleteFileTree($this->h5pC->path . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId); $this->h5pC->fs->deleteContent($content['id']);
$this->h5pF->deleteContentData($contentId); $this->h5pC->fs->deleteExport(($content['slug'] ? $content['slug'] . '-' : '') . $content['id'] . '.h5p');
// TODO: Delete export? $this->h5pF->deleteContentData($content['id']);
}
/**
* Update an H5P package
*
* @param int $contentId
* The content id
* @param int $contentMainId
* The content main id (used by frameworks supporting revisioning)
* @return boolean
* TRUE if one or more libraries were updated
* FALSE otherwise
*/
public function updatePackage($contentId, $contentMainId = NULL, $options = array()) {
$this->deletePackage($contentId);
return $this->savePackage($contentId, $contentMainId, FALSE, $options);
} }
/** /**
@ -1457,10 +1454,7 @@ class H5PStorage {
* The main id of the new content (used in frameworks that support revisioning) * The main id of the new content (used in frameworks that support revisioning)
*/ */
public function copyPackage($contentId, $copyFromId, $contentMainId = NULL) { public function copyPackage($contentId, $copyFromId, $contentMainId = NULL) {
$source_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $copyFromId; $this->h5pC->fs->cloneContent($contentId, $newContentId);
$destination_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId;
$this->h5pC->copyFileTree($source_path, $destination_path);
$this->h5pF->copyLibraryUsage($contentId, $copyFromId, $contentMainId); $this->h5pF->copyLibraryUsage($contentId, $copyFromId, $contentMainId);
} }
} }
@ -1494,25 +1488,26 @@ Class H5PExport {
* @return string * @return string
*/ */
public function createExportFile($content) { public function createExportFile($content) {
$h5pDir = $this->h5pC->path . DIRECTORY_SEPARATOR;
$tempPath = $h5pDir . 'temp' . DIRECTORY_SEPARATOR . $content['id'];
$zipPath = $h5pDir . 'exports' . DIRECTORY_SEPARATOR . $content['slug'] . '-' . $content['id'] . '.h5p';
// Make sure the exports dir is ready // Get path to temporary folder, where export will be contained
if (!H5PCore::dirReady($h5pDir . 'exports')) { $tmpPath = $this->h5pC->fs->getTmpPath();
$this->h5pF->setErrorMessage($this->h5pF->t('Unable to write to the exports directory.')); mkdir($tmpPath, 0777, true);
try {
// Create content folder and populate with files
$this->h5pC->fs->exportContent($content['id'], "{$tmpPath}/content");
}
catch (Exception $e) {
$this->h5pF->setErrorMessage($this->h5pF->t($e->getMessage()));
H5PCore::deleteFileTree($tmpPath);
return FALSE; return FALSE;
} }
// Create content folder // Update content.json with content from database
if ($this->h5pC->copyFileTree($h5pDir . 'content' . DIRECTORY_SEPARATOR . $content['id'], $tempPath . DIRECTORY_SEPARATOR . 'content') === FALSE) { file_put_contents("{$tmpPath}/content/content.json", $content['params']);
return FALSE;
}
file_put_contents($tempPath . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . 'content.json', $content['params']);
// Make embedType into an array
// Make embedTypes into an array $embedTypes = explode(', ', $content['embedType']);
$embedTypes = explode(', ', $content['embedType']); // Won't content always be embedded in one way?
// Build h5p.json // Build h5p.json
$h5pJson = array ( $h5pJson = array (
@ -1526,16 +1521,22 @@ Class H5PExport {
foreach ($content['dependencies'] as $dependency) { foreach ($content['dependencies'] as $dependency) {
$library = $dependency['library']; $library = $dependency['library'];
// Copy library to h5p try {
$source = $h5pDir . (isset($library['path']) ? $library['path'] : 'libraries' . DIRECTORY_SEPARATOR . H5PCore::libraryToString($library, TRUE)); // Export required libraries
$destination = $tempPath . DIRECTORY_SEPARATOR . $library['machineName']; $this->h5pC->fs->exportLibrary($library, $tmpPath);
$this->h5pC->copyFileTree($source, $destination); }
catch (Exception $e) {
$this->h5pF->setErrorMessage($this->h5pF->t($e->getMessage()));
H5PCore::deleteFileTree($tmpPath);
return FALSE;
}
// Do not add editor dependencies to h5p json. // Do not add editor dependencies to h5p json.
if ($dependency['type'] === 'editor') { if ($dependency['type'] === 'editor') {
continue; continue;
} }
// Add to h5p.json dependencies
$h5pJson[$dependency['type'] . 'Dependencies'][] = array( $h5pJson[$dependency['type'] . 'Dependencies'][] = array(
'machineName' => $library['machineName'], 'machineName' => $library['machineName'],
'majorVersion' => $library['majorVersion'], 'majorVersion' => $library['majorVersion'],
@ -1545,15 +1546,18 @@ Class H5PExport {
// Save h5p.json // Save h5p.json
$results = print_r(json_encode($h5pJson), true); $results = print_r(json_encode($h5pJson), true);
file_put_contents($tempPath . DIRECTORY_SEPARATOR . 'h5p.json', $results); file_put_contents("{$tmpPath}/h5p.json", $results);
// Get a complete file list from our tmp dir // Get a complete file list from our tmp dir
$files = array(); $files = array();
self::populateFileList($tempPath, $files); self::populateFileList($tmpPath, $files);
// Get path to temporary export target file
$tmpFile = $this->h5pC->fs->getTmpPath();
// Create new zip instance. // Create new zip instance.
$zip = new ZipArchive(); $zip = new ZipArchive();
$zip->open($zipPath, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE); $zip->open($tmpFile, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE);
// Add all the files from the tmp dir. // Add all the files from the tmp dir.
foreach ($files as $file) { foreach ($files as $file) {
@ -1562,9 +1566,19 @@ Class H5PExport {
$zip->addFile($file->absolutePath, $file->relativePath); $zip->addFile($file->absolutePath, $file->relativePath);
} }
// Close zip and remove temp dir // Close zip and remove tmp dir
$zip->close(); $zip->close();
H5PCore::deleteFileTree($tempPath); H5PCore::deleteFileTree($tmpPath);
try {
// Save export
$this->h5pC->fs->saveExport($tmpFile, $content['slug'] . '-' . $content['id'] . '.h5p');
}
catch (Exception $e) {
$this->h5pF->setErrorMessage($this->h5pF->t($e->getMessage()));
}
unlink($tmpFile);
} }
/** /**
@ -1602,11 +1616,7 @@ Class H5PExport {
* @param array $content object * @param array $content object
*/ */
public function deleteExport($content) { public function deleteExport($content) {
$h5pDir = $this->h5pC->path . DIRECTORY_SEPARATOR; $this->h5pC->fs->deleteExport(($content['slug'] ? $content['slug'] . '-' : '') . $content['id'] . '.h5p');
$zipPath = $h5pDir . 'exports' . DIRECTORY_SEPARATOR . ($content['slug'] ? $content['slug'] . '-' : '') . $content['id'] . '.h5p';
if (file_exists($zipPath)) {
unlink($zipPath);
}
} }
/** /**
@ -1654,10 +1664,10 @@ class H5PCore {
'js/h5p-utils.js', 'js/h5p-utils.js',
); );
public static $defaultContentWhitelist = 'json png jpg jpeg gif bmp tif tiff svg eot ttf woff otf webm mp4 ogg mp3 txt pdf rtf doc docx xls xlsx ppt pptx odt ods odp xml csv diff patch swf md textile'; public static $defaultContentWhitelist = 'json png jpg jpeg gif bmp tif tiff svg eot ttf woff woff2 otf webm mp4 ogg mp3 txt pdf rtf doc docx xls xlsx ppt pptx odt ods odp xml csv diff patch swf md textile';
public static $defaultLibraryWhitelistExtras = 'js css'; public static $defaultLibraryWhitelistExtras = 'js css';
public $librariesJsonData, $contentJsonData, $mainJsonData, $h5pF, $path, $development_mode, $h5pD, $disableFileCheck; public $librariesJsonData, $contentJsonData, $mainJsonData, $h5pF, $fs, $development_mode, $h5pD, $disableFileCheck;
const SECONDS_IN_WEEK = 604800; const SECONDS_IN_WEEK = 604800;
private $exportEnabled; private $exportEnabled;
@ -1683,7 +1693,8 @@ class H5PCore {
* *
* @param object $H5PFramework * @param object $H5PFramework
* The frameworks implementation of the H5PFrameworkInterface * The frameworks implementation of the H5PFrameworkInterface
* @param string $path H5P file storage directory. * @param string|\H5PFileStorage $path H5P file storage directory or class.
* @param string $url To file storage directory.
* @param string $language code. Defaults to english. * @param string $language code. Defaults to english.
* @param boolean $export enabled? * @param boolean $export enabled?
* @param int $development_mode mode. * @param int $development_mode mode.
@ -1691,12 +1702,14 @@ class H5PCore {
public function __construct($H5PFramework, $path, $url, $language = 'en', $export = FALSE, $development_mode = H5PDevelopment::MODE_NONE) { public function __construct($H5PFramework, $path, $url, $language = 'en', $export = FALSE, $development_mode = H5PDevelopment::MODE_NONE) {
$this->h5pF = $H5PFramework; $this->h5pF = $H5PFramework;
$this->h5pF = $H5PFramework; $this->fs = ($path instanceof \H5PFileStorage ? $path : new \H5PDefaultStorage($path));
$this->path = $path;
$this->url = $url; $this->url = $url;
$this->exportEnabled = $export; $this->exportEnabled = $export;
$this->development_mode = $development_mode; $this->development_mode = $development_mode;
$this->aggregateAssets = FALSE; // Off by default.. for now
if ($development_mode & H5PDevelopment::MODE_LIBRARY) { if ($development_mode & H5PDevelopment::MODE_LIBRARY) {
$this->h5pD = new H5PDevelopment($this->h5pF, $path . '/', $language); $this->h5pD = new H5PDevelopment($this->h5pF, $path . '/', $language);
} }
@ -1795,10 +1808,7 @@ class H5PCore {
$content['slug'] = $this->generateContentSlug($content); $content['slug'] = $this->generateContentSlug($content);
// Remove old export file // Remove old export file
$oldExport = $this->path . '/exports/' . $content['id'] . '.h5p'; $this->fs->deleteExport($content['id'] . '.h5p');
if (file_exists($oldExport)) {
unlink($oldExport);
}
} }
if ($this->exportEnabled) { if ($this->exportEnabled) {
@ -1929,10 +1939,28 @@ class H5PCore {
* @return array files. * @return array files.
*/ */
public function getDependenciesFiles($dependencies, $prefix = '') { public function getDependenciesFiles($dependencies, $prefix = '') {
// Build files list for assets
$files = array( $files = array(
'scripts' => array(), 'scripts' => array(),
'styles' => array() 'styles' => array()
); );
// Avoid caching empty files
if (empty($dependencies)) {
return $files;
}
if ($this->aggregateAssets) {
// Get aggregated files for assets
$key = self::getDependenciesHash($dependencies);
$files = $this->fs->getCachedAssets($key);
if ($files) {
return $files; // Using cached assets
}
}
// Using content dependencies
foreach ($dependencies as $dependency) { foreach ($dependencies as $dependency) {
if (isset($dependency['path']) === FALSE) { if (isset($dependency['path']) === FALSE) {
$dependency['path'] = 'libraries/' . H5PCore::libraryToString($dependency, TRUE); $dependency['path'] = 'libraries/' . H5PCore::libraryToString($dependency, TRUE);
@ -1943,9 +1971,34 @@ class H5PCore {
$this->getDependencyAssets($dependency, 'preloadedJs', $files['scripts'], $prefix); $this->getDependencyAssets($dependency, 'preloadedJs', $files['scripts'], $prefix);
$this->getDependencyAssets($dependency, 'preloadedCss', $files['styles'], $prefix); $this->getDependencyAssets($dependency, 'preloadedCss', $files['styles'], $prefix);
} }
if ($this->aggregateAssets) {
// Aggregate and store assets
$this->fs->cacheAssets($files, $key);
// Keep track of which libraries have been cached in case they are updated
$this->h5pF->saveCachedAssets($key, $dependencies);
}
return $files; return $files;
} }
private static function getDependenciesHash(&$dependencies) {
// Build hash of dependencies
$toHash = array();
// Use unique identifier for each library version
foreach ($dependencies as $dep) {
$toHash[] = "{$dep['machineName']}-{$dep['majorVersion']}.{$dep['minorVersion']}.{$dep['patchVersion']}";
}
// Sort in case the same dependencies comes in a different order
sort($toHash);
// Calculate hash sum
return hash('sha1', implode('', $toHash));
}
/** /**
* Load library semantics. * Load library semantics.
* *
@ -2098,39 +2151,6 @@ class H5PCore {
return rmdir($dir); return rmdir($dir);
} }
/**
* Recursive function for copying directories.
*
* @param string $source
* Path to the directory we'll be copying
* @return boolean
* Indicates if the directory existed.
*/
public function copyFileTree($source, $destination) {
if (!H5PCore::dirReady($destination)) {
$this->h5pF->setErrorMessage($this->h5pF->t('Unable to copy file tree.'));
return FALSE;
}
$dir = opendir($source);
if ($dir === FALSE) {
$this->h5pF->setErrorMessage($this->h5pF->t('Unable to copy file tree.'));
return FALSE;
}
while (false !== ($file = readdir($dir))) {
if (($file != '.') && ($file != '..') && $file != '.git' && $file != '.gitignore') {
if (is_dir($source . DIRECTORY_SEPARATOR . $file)) {
$this->copyFileTree($source . DIRECTORY_SEPARATOR . $file, $destination . DIRECTORY_SEPARATOR . $file);
}
else {
copy($source . DIRECTORY_SEPARATOR . $file,$destination . DIRECTORY_SEPARATOR . $file);
}
}
}
closedir($dir);
}
/** /**
* Writes library data as string on the form {machineName} {majorVersion}.{minorVersion} * Writes library data as string on the form {machineName} {majorVersion}.{minorVersion}
* *
@ -2370,6 +2390,50 @@ class H5PCore {
return $html; return $html;
} }
/**
* Detects if the site was accessed from localhost,
* through a local network or from the internet.
*/
public function detectSiteType() {
$type = $this->h5pF->getOption('site_type', 'local');
// Determine remote/visitor origin
$localhostPattern = '/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/i';
// localhost
if ($type !== 'internet' && !preg_match($localhostPattern, $_SERVER['REMOTE_ADDR'])) {
if (filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) {
// Internet
$this->h5pF->setOption('site_type', 'internet');
}
elseif ($type === 'local') {
// Local network
$this->h5pF->setOption('site_type', 'network');
}
}
}
/**
* Get a list of installed libraries, different minor versions will
* return separate entries.
*
* @return array
* A distinct array of installed libraries
*/
public function getLibrariesInstalled() {
$librariesInstalled = [];
$libs = $this->h5pF->loadLibraries();
foreach($libs as $libName => $library) {
foreach($library as $libVersion) {
$librariesInstalled[] = $libName.' '.$libVersion->major_version.'.'.$libVersion->minor_version.'.'.$libVersion->patch_version;
}
}
return $librariesInstalled;
}
/** /**
* Fetch a list of libraries' metadata from h5p.org. * Fetch a list of libraries' metadata from h5p.org.
* Save URL tutorial to database. Each platform implementation * Save URL tutorial to database. Each platform implementation
@ -2507,33 +2571,88 @@ class H5PCore {
} }
/** /**
* Recursive function that makes sure the specified directory exists and * Makes it easier to print response when AJAX request succeeds.
* is writable.
* *
* @param string $path * @param mixed $data
* @return bool * @since 1.6.0
*/ */
public static function dirReady($path) { public static function ajaxSuccess($data = NULL) {
if (!file_exists($path)) { $response = array(
$parent = preg_replace("/\/[^\/]+\/?$/", '', $path); 'success' => TRUE
if (!H5PCore::dirReady($parent)) { );
return FALSE; if ($data !== NULL) {
} $response['data'] = $data;
}
self::printJson($response);
}
mkdir($path, 0777, true); /**
* Makes it easier to print response when AJAX request fails.
* Will exit after printing error.
*
* @param string $message
* @since 1.6.0
*/
public static function ajaxError($message = NULL) {
$response = array(
'success' => FALSE
);
if ($message !== NULL) {
$response['message'] = $message;
}
self::printJson($response);
}
/**
* Print JSON headers with UTF-8 charset and json encode response data.
* Makes it easier to respond using JSON.
*
* @param mixed $data
*/
private static function printJson($data) {
header('Cache-Control: no-cache');
header('Content-type: application/json; charset=utf-8');
print json_encode($data);
}
/**
* Get a new H5P security token for the given action.
*
* @param string $action
* @return string token
*/
public static function createToken($action) {
if (!isset($_SESSION['h5p_token'])) {
// Create an unique key which is used to create action tokens for this session.
$_SESSION['h5p_token'] = uniqid();
} }
if (!is_dir($path)) { // Timefactor
trigger_error('Path is not a directory ' . $path, E_USER_WARNING); $time_factor = self::getTimeFactor();
return FALSE;
}
if (!is_writable($path)) { // Create and return token
trigger_error('Unable to write to ' . $path . ' check directory permissions ', E_USER_WARNING); return substr(hash('md5', $action . $time_factor . $_SESSION['h5p_token']), -16, 13);
return FALSE; }
}
return TRUE; /**
* Create a time based number which is unique for each 12 hour.
* @return int
*/
private static function getTimeFactor() {
return ceil(time() / (86400 / 2));
}
/**
* Verify if the given token is valid for the given action.
*
* @param string $action
* @param string $token
* @return boolean valid token
*/
public static function validToken($action, $token) {
$time_factor = self::getTimeFactor();
return $token === substr(hash('md5', $action . $time_factor . $_SESSION['h5p_token']), -16, 13) || // Under 12 hours
$token === substr(hash('md5', $action . ($time_factor - 1) . $_SESSION['h5p_token']), -16, 13); // Between 12-24 hours
} }
} }

View File

@ -16,4 +16,4 @@
$frame.change(toggle); $frame.change(toggle);
toggle(); toggle();
}); });
})(jQuery); })(H5P.jQuery);

View File

@ -50,11 +50,11 @@ var H5PLibraryList = H5PLibraryList || {};
text: library.numLibraryDependencies, text: library.numLibraryDependencies,
class: 'h5p-admin-center' class: 'h5p-admin-center'
}, },
'<div class="h5p-admin-buttons-wrapper">\ '<div class="h5p-admin-buttons-wrapper">' +
<button class="h5p-admin-upgrade-library"></button>\ '<button class="h5p-admin-upgrade-library"></button>' +
<button class="h5p-admin-view-library" title="' + t.viewLibrary + '"></button>\ (library.detailsUrl ? '<button class="h5p-admin-view-library" title="' + t.viewLibrary + '"></button>' : '') +
<button class="h5p-admin-delete-library"></button>\ (library.deleteUrl ? '<button class="h5p-admin-delete-library"></button>' : '') +
</div>' '</div>'
]); ]);
H5PLibraryList.addRestricted($('.h5p-admin-restricted', $libraryRow), library.restrictedUrl, library.restricted); H5PLibraryList.addRestricted($('.h5p-admin-restricted', $libraryRow), library.restrictedUrl, library.restricted);
@ -78,7 +78,12 @@ var H5PLibraryList = H5PLibraryList || {};
}); });
var $deleteButton = $('.h5p-admin-delete-library', $libraryRow); var $deleteButton = $('.h5p-admin-delete-library', $libraryRow);
if (libraries.notCached !== undefined || hasContent || (library.numContentDependencies !== '' && library.numContentDependencies !== 0) || (library.numLibraryDependencies !== '' && library.numLibraryDependencies !== 0)) { if (libraries.notCached !== undefined ||
hasContent ||
(library.numContentDependencies !== '' &&
library.numContentDependencies !== 0) ||
(library.numLibraryDependencies !== '' &&
library.numLibraryDependencies !== 0)) {
// Disabled delete if content. // Disabled delete if content.
$deleteButton.attr('disabled', true); $deleteButton.attr('disabled', true);
} }

View File

@ -375,7 +375,8 @@ H5P.getHeadTags = function (contentId) {
return tags; return tags;
}; };
return createStyleTags(H5PIntegration.core.styles) + return '<base target="_parent">' +
createStyleTags(H5PIntegration.core.styles) +
createStyleTags(H5PIntegration.contents['cid-' + contentId].styles) + createStyleTags(H5PIntegration.contents['cid-' + contentId].styles) +
createScriptTags(H5PIntegration.core.scripts) + createScriptTags(H5PIntegration.core.scripts) +
createScriptTags(H5PIntegration.contents['cid-' + contentId].scripts) + createScriptTags(H5PIntegration.contents['cid-' + contentId].scripts) +
@ -1488,7 +1489,7 @@ H5P.libraryFromString = function (library) {
* The full path to the library. * The full path to the library.
*/ */
H5P.getLibraryPath = function (library) { H5P.getLibraryPath = function (library) {
return H5PIntegration.url + '/libraries/' + library; return (H5PIntegration.libraryUrl !== undefined ? H5PIntegration.libraryUrl + '/' : H5PIntegration.url + '/libraries/') + library;
}; };
/** /**
@ -1609,14 +1610,14 @@ H5P.setFinished = function (contentId, score, maxScore, time) {
}; };
// Post the results // Post the results
// TODO: Should we use a variable with the complete path? H5P.jQuery.post(H5PIntegration.ajax.setFinished, {
H5P.jQuery.post(H5PIntegration.ajaxPath + 'setFinished', {
contentId: contentId, contentId: contentId,
score: score, score: score,
maxScore: maxScore, maxScore: maxScore,
opened: toUnix(H5P.opened[contentId]), opened: toUnix(H5P.opened[contentId]),
finished: toUnix(new Date()), finished: toUnix(new Date()),
time: time time: time,
token: H5PIntegration.tokens.result
}); });
} }
}; };
@ -1760,7 +1761,8 @@ H5P.createTitle = function (rawTitle, maxLength) {
options.data = { options.data = {
data: (data === null ? 0 : data), data: (data === null ? 0 : data),
preload: (preload ? 1 : 0), preload: (preload ? 1 : 0),
invalidate: (invalidate ? 1 : 0) invalidate: (invalidate ? 1 : 0),
token: H5PIntegration.tokens.contentUserData
}; };
} }
else { else {
@ -1772,7 +1774,7 @@ H5P.createTitle = function (rawTitle, maxLength) {
}; };
options.success = function (response) { options.success = function (response) {
if (!response.success) { if (!response.success) {
done(response.error); done(response.message);
return; return;
} }

View File

@ -9,6 +9,7 @@
.h5p-admin-table, .h5p-admin-table,
.h5p-admin-table > tbody { .h5p-admin-table > tbody {
border: none; border: none;
width: 100%;
} }
.h5p-admin-table tr:nth-child(odd), .h5p-admin-table tr:nth-child(odd),