diff --git a/h5p-default-storage.class.php b/h5p-default-storage.class.php new file mode 100644 index 0000000..27b250b --- /dev/null +++ b/h5p-default-storage.class.php @@ -0,0 +1,313 @@ +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; + } +} diff --git a/h5p-file-storage.interface.php b/h5p-file-storage.interface.php new file mode 100644 index 0000000..dfaa67a --- /dev/null +++ b/h5p-file-storage.interface.php @@ -0,0 +1,120 @@ +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. + // Check dependencies, make sure Zip is present if (!class_exists('ZipArchive')) { $this->h5pF->setErrorMessage($this->h5pF->t('Your PHP version does not support ZipArchive.')); return FALSE; @@ -675,13 +688,6 @@ class H5PValidator { $tmpDir = $this->h5pF->getUploadedH5pFolderPath(); $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. $zip = new ZipArchive; @@ -704,6 +710,7 @@ class H5PValidator { unlink($tmpPath); // Process content and libraries + $valid = TRUE; $libraries = array(); $files = scandir($tmpDir); $mainH5pData; @@ -1319,10 +1326,13 @@ class H5PStorage { $contentId = $this->h5pC->saveContent($content, $contentMainId); $this->contentId = $contentId; - // Move the content folder - $contents_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content'; - $destination_path = $contents_path . DIRECTORY_SEPARATOR . $contentId; - $this->h5pC->copyFileTree($current_path, $destination_path); + try { + // Save content folder contents + $this->h5pC->fs->saveContent($current_path, $contentId); + } + catch (Exception $e) { + $this->h5pF->setErrorMessage($this->h5pF->t($e->getMessage())); + } // Remove temp content folder H5PCore::deleteFileTree($basePath); @@ -1370,13 +1380,16 @@ class H5PStorage { // Save library meta data $this->h5pF->saveLibraryData($library, $new); - // Make sure destination dir is free - $libraries_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'libraries'; - $destination_path = $libraries_path . DIRECTORY_SEPARATOR . H5PCore::libraryToString($library, TRUE); - H5PCore::deleteFileTree($destination_path); + // Save library folder + $this->h5pC->fs->saveLibrary($library); - // Move library folder - $this->h5pC->copyFileTree($library['uploadDirectory'], $destination_path); + // Remove cachedassets that uses this library + 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']); if ($new) { @@ -1435,26 +1448,10 @@ class H5PStorage { * @param int $contentId * The content id */ - public function deletePackage($contentId) { - H5PCore::deleteFileTree($this->h5pC->path . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId); - $this->h5pF->deleteContentData($contentId); - // TODO: Delete export? - } - - /** - * 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); + public function deletePackage($content) { + $this->h5pC->fs->deleteContent($content['id']); + $this->h5pC->fs->deleteExport(($content['slug'] ? $content['slug'] . '-' : '') . $content['id'] . '.h5p'); + $this->h5pF->deleteContentData($content['id']); } /** @@ -1471,10 +1468,7 @@ class H5PStorage { * The main id of the new content (used in frameworks that support revisioning) */ public function copyPackage($contentId, $copyFromId, $contentMainId = NULL) { - $source_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $copyFromId; - $destination_path = $this->h5pC->path . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . $contentId; - $this->h5pC->copyFileTree($source_path, $destination_path); - + $this->h5pC->fs->cloneContent($contentId, $newContentId); $this->h5pF->copyLibraryUsage($contentId, $copyFromId, $contentMainId); } } @@ -1508,25 +1502,26 @@ Class H5PExport { * @return string */ 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 - if (!H5PCore::dirReady($h5pDir . 'exports')) { - $this->h5pF->setErrorMessage($this->h5pF->t('Unable to write to the exports directory.')); + // Get path to temporary folder, where export will be contained + $tmpPath = $this->h5pC->fs->getTmpPath(); + 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; } - // Create content folder - if ($this->h5pC->copyFileTree($h5pDir . 'content' . DIRECTORY_SEPARATOR . $content['id'], $tempPath . DIRECTORY_SEPARATOR . 'content') === FALSE) { - return FALSE; - } - file_put_contents($tempPath . DIRECTORY_SEPARATOR . 'content' . DIRECTORY_SEPARATOR . 'content.json', $content['params']); + // Update content.json with content from database + file_put_contents("{$tmpPath}/content/content.json", $content['params']); - - // Make embedTypes into an array - $embedTypes = explode(', ', $content['embedType']); // Won't content always be embedded in one way? + // Make embedType into an array + $embedTypes = explode(', ', $content['embedType']); // Build h5p.json $h5pJson = array ( @@ -1540,16 +1535,22 @@ Class H5PExport { foreach ($content['dependencies'] as $dependency) { $library = $dependency['library']; - // Copy library to h5p - $source = $h5pDir . (isset($library['path']) ? $library['path'] : 'libraries' . DIRECTORY_SEPARATOR . H5PCore::libraryToString($library, TRUE)); - $destination = $tempPath . DIRECTORY_SEPARATOR . $library['machineName']; - $this->h5pC->copyFileTree($source, $destination); + try { + // Export required libraries + $this->h5pC->fs->exportLibrary($library, $tmpPath); + } + catch (Exception $e) { + $this->h5pF->setErrorMessage($this->h5pF->t($e->getMessage())); + H5PCore::deleteFileTree($tmpPath); + return FALSE; + } // Do not add editor dependencies to h5p json. if ($dependency['type'] === 'editor') { continue; } + // Add to h5p.json dependencies $h5pJson[$dependency['type'] . 'Dependencies'][] = array( 'machineName' => $library['machineName'], 'majorVersion' => $library['majorVersion'], @@ -1559,15 +1560,18 @@ Class H5PExport { // Save h5p.json $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 $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. $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. foreach ($files as $file) { @@ -1576,9 +1580,19 @@ Class H5PExport { $zip->addFile($file->absolutePath, $file->relativePath); } - // Close zip and remove temp dir + // Close zip and remove tmp dir $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); } /** @@ -1616,11 +1630,7 @@ Class H5PExport { * @param array $content object */ public function deleteExport($content) { - $h5pDir = $this->h5pC->path . DIRECTORY_SEPARATOR; - $zipPath = $h5pDir . 'exports' . DIRECTORY_SEPARATOR . ($content['slug'] ? $content['slug'] . '-' : '') . $content['id'] . '.h5p'; - if (file_exists($zipPath)) { - unlink($zipPath); - } + $this->h5pC->fs->deleteExport(($content['slug'] ? $content['slug'] . '-' : '') . $content['id'] . '.h5p'); } /** @@ -1671,7 +1681,7 @@ class H5PCore { 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 $librariesJsonData, $contentJsonData, $mainJsonData, $h5pF, $path, $development_mode, $h5pD, $disableFileCheck; + public $librariesJsonData, $contentJsonData, $mainJsonData, $h5pF, $fs, $development_mode, $h5pD, $disableFileCheck; const SECONDS_IN_WEEK = 604800; private $exportEnabled; @@ -1697,7 +1707,8 @@ class H5PCore { * * @param object $H5PFramework * 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 boolean $export enabled? * @param int $development_mode mode. @@ -1705,12 +1716,14 @@ class H5PCore { public function __construct($H5PFramework, $path, $url, $language = 'en', $export = FALSE, $development_mode = H5PDevelopment::MODE_NONE) { $this->h5pF = $H5PFramework; - $this->h5pF = $H5PFramework; - $this->path = $path; + $this->fs = ($path instanceof \H5PFileStorage ? $path : new \H5PDefaultStorage($path)); + $this->url = $url; $this->exportEnabled = $export; $this->development_mode = $development_mode; + $this->aggregateAssets = FALSE; // Off by default.. for now + if ($development_mode & H5PDevelopment::MODE_LIBRARY) { $this->h5pD = new H5PDevelopment($this->h5pF, $path . '/', $language); } @@ -1812,10 +1825,7 @@ class H5PCore { $content['slug'] = $this->generateContentSlug($content); // Remove old export file - $oldExport = $this->path . '/exports/' . $content['id'] . '.h5p'; - if (file_exists($oldExport)) { - unlink($oldExport); - } + $this->fs->deleteExport($content['id'] . '.h5p'); } if ($this->exportEnabled) { @@ -1946,10 +1956,28 @@ class H5PCore { * @return array files. */ public function getDependenciesFiles($dependencies, $prefix = '') { + // Build files list for assets $files = array( 'scripts' => 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) { if (isset($dependency['path']) === FALSE) { $dependency['path'] = 'libraries/' . H5PCore::libraryToString($dependency, TRUE); @@ -1960,9 +1988,34 @@ class H5PCore { $this->getDependencyAssets($dependency, 'preloadedJs', $files['scripts'], $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; } + 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. * @@ -2115,39 +2168,6 @@ class H5PCore { 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} * @@ -2395,8 +2415,15 @@ class H5PCore { $type = $this->h5pF->getOption('site_type', 'local'); // Determine remote/visitor origin +<<<<<<< HEAD if ($type === 'network' || ($type === 'local' && !preg_match('/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/i', $_SERVER['REMOTE_ADDR']))) { +======= + $localhostPattern = '/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/i'; + + // localhost + if ($type !== 'internet' && !preg_match($localhostPattern, $_SERVER['REMOTE_ADDR'])) { +>>>>>>> 14605dc7900f71801720972608b3a3d431ffedce if (filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) { // Internet $this->h5pF->setOption('site_type', 'internet'); @@ -2416,12 +2443,22 @@ class H5PCore { * A distinct array of installed libraries */ public function getLibrariesInstalled() { +<<<<<<< HEAD $librariesInstalled = array(); $libs = $this->h5pF->loadLibraries(); foreach($libs as $library) { foreach($library as $libVersion) { $librariesInstalled[$libVersion->name.' '.$libVersion->major_version.'.'.$libVersion->minor_version] = $libVersion->patch_version; +======= + $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; +>>>>>>> 14605dc7900f71801720972608b3a3d431ffedce } } @@ -2429,6 +2466,7 @@ class H5PCore { } /** +<<<<<<< HEAD * Easy way to combine smiliar data sets. * * @param array $inputs Multiple arrays with data @@ -2445,6 +2483,8 @@ class H5PCore { } /** +======= +>>>>>>> 14605dc7900f71801720972608b3a3d431ffedce * Fetch a list of libraries' metadata from h5p.org. * Save URL tutorial to database. Each platform implementation * is responsible for invoking this, eg using cron @@ -2618,33 +2658,88 @@ class H5PCore { } /** - * Recursive function that makes sure the specified directory exists and - * is writable. + * Makes it easier to print response when AJAX request succeeds. * - * @param string $path - * @return bool + * @param mixed $data + * @since 1.6.0 */ - public static function dirReady($path) { - if (!file_exists($path)) { - $parent = preg_replace("/\/[^\/]+\/?$/", '', $path); - if (!H5PCore::dirReady($parent)) { - return FALSE; - } + public static function ajaxSuccess($data = NULL) { + $response = array( + 'success' => TRUE + ); + 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)) { - trigger_error('Path is not a directory ' . $path, E_USER_WARNING); - return FALSE; - } + // Timefactor + $time_factor = self::getTimeFactor(); - if (!is_writable($path)) { - trigger_error('Unable to write to ' . $path . ' – check directory permissions –', E_USER_WARNING); - return FALSE; - } + // Create and return token + return substr(hash('md5', $action . $time_factor . $_SESSION['h5p_token']), -16, 13); + } - 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 } } diff --git a/js/h5p-library-list.js b/js/h5p-library-list.js index dcb0cc8..1ee1bc8 100644 --- a/js/h5p-library-list.js +++ b/js/h5p-library-list.js @@ -50,11 +50,11 @@ var H5PLibraryList = H5PLibraryList || {}; text: library.numLibraryDependencies, class: 'h5p-admin-center' }, - '
\ - \ - \ - \ -
' + '
' + + '' + + (library.detailsUrl ? '' : '') + + (library.deleteUrl ? '' : '') + + '
' ]); H5PLibraryList.addRestricted($('.h5p-admin-restricted', $libraryRow), library.restrictedUrl, library.restricted); @@ -78,7 +78,12 @@ var H5PLibraryList = H5PLibraryList || {}; }); 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. $deleteButton.attr('disabled', true); } diff --git a/js/h5p.js b/js/h5p.js index d3e8b2c..df0d9e6 100644 --- a/js/h5p.js +++ b/js/h5p.js @@ -139,7 +139,7 @@ H5P.init = function (target) { // Check if we should add and display a fullscreen button for this H5P. if (contentData.fullScreen == 1 && H5P.canHasFullScreen) { - H5P.jQuery('
').prependTo($container).children().click(function () { + H5P.jQuery('
').prependTo($container).children().click(function () { H5P.fullScreen($container, instance); }); } @@ -148,7 +148,7 @@ H5P.init = function (target) { var $actions = H5P.jQuery(''); if (!(contentData.disable & H5P.DISABLE_DOWNLOAD)) { // Add export button - H5P.jQuery('
  • ' + H5P.t('download') + '
  • ').appendTo($actions).click(function () { + H5P.jQuery('
  • ' + H5P.t('download') + '
  • ').appendTo($actions).click(function () { window.location.href = contentData.exportUrl; }); } @@ -157,7 +157,7 @@ H5P.init = function (target) { if (copyright) { // Add copyright dialog button - H5P.jQuery('
  • ' + H5P.t('copyrights') + '
  • ').appendTo($actions).click(function () { + H5P.jQuery('
  • ' + H5P.t('copyrights') + '
  • ').appendTo($actions).click(function () { // Open dialog with copyright information var dialog = new H5P.Dialog('copyrights', H5P.t('copyrightInformation'), copyright, $container); dialog.open(); @@ -166,7 +166,7 @@ H5P.init = function (target) { } if (!(contentData.disable & H5P.DISABLE_EMBED)) { // Add embed button - H5P.jQuery('
  • ' + H5P.t('embed') + '
  • ').appendTo($actions).click(function () { + H5P.jQuery('
  • ' + H5P.t('embed') + '
  • ').appendTo($actions).click(function () { H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, { width: $element.width(), height: $element.height() @@ -545,7 +545,7 @@ H5P.fullScreen = function ($element, instance, exitCallback, body) { } before('h5p-semi-fullscreen'); - var $disable = H5P.jQuery('
    ').appendTo($container.find('.h5p-content-controls')); + var $disable = H5P.jQuery('
    ').appendTo($container.find('.h5p-content-controls')); var keyup, disableSemiFullscreen = H5P.exitFullScreen = function () { if (prevViewportContent) { // Use content from the previous viewport tag @@ -887,7 +887,7 @@ H5P.Dialog = function (name, title, content, $element) {
    \

    ' + title + '

    \
    ' + content + '
    \ -
    \ +
    \
    \
    ') .insertAfter($element) @@ -1489,7 +1489,7 @@ H5P.libraryFromString = function (library) { * The full path to the library. */ H5P.getLibraryPath = function (library) { - return H5PIntegration.url + '/libraries/' + library; + return (H5PIntegration.libraryUrl !== undefined ? H5PIntegration.libraryUrl + '/' : H5PIntegration.url + '/libraries/') + library; }; /** @@ -1616,7 +1616,8 @@ H5P.setFinished = function (contentId, score, maxScore, time) { maxScore: maxScore, opened: toUnix(H5P.opened[contentId]), finished: toUnix(new Date()), - time: time + time: time, + token: H5PIntegration.tokens.result }); } }; @@ -1760,7 +1761,8 @@ H5P.createTitle = function (rawTitle, maxLength) { options.data = { data: (data === null ? 0 : data), preload: (preload ? 1 : 0), - invalidate: (invalidate ? 1 : 0) + invalidate: (invalidate ? 1 : 0), + token: H5PIntegration.tokens.contentUserData }; } else { @@ -1772,7 +1774,7 @@ H5P.createTitle = function (rawTitle, maxLength) { }; options.success = function (response) { if (!response.success) { - done(response.error); + done(response.message); return; } diff --git a/styles/h5p-admin.css b/styles/h5p-admin.css index 0122206..f8b2ab1 100644 --- a/styles/h5p-admin.css +++ b/styles/h5p-admin.css @@ -9,6 +9,7 @@ .h5p-admin-table, .h5p-admin-table > tbody { border: none; + width: 100%; } .h5p-admin-table tr:nth-child(odd),