See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
1 <?php 2 3 /** 4 * File info? 5 */ 6 7 /** 8 * The default file storage class for H5P. Will carry out the requested file 9 * operations using PHP's standard file operation functions. 10 * 11 * Some implementations of H5P that doesn't use the standard file system will 12 * want to create their own implementation of the \H5P\FileStorage interface. 13 * 14 * @package H5P 15 * @copyright 2016 Joubel AS 16 * @license MIT 17 */ 18 class H5PDefaultStorage implements \H5PFileStorage { 19 private $path, $alteditorpath; 20 21 /** 22 * The great Constructor! 23 * 24 * @param string $path 25 * The base location of H5P files 26 * @param string $alteditorpath 27 * Optional. Use a different editor path 28 */ 29 function __construct($path, $alteditorpath = NULL) { 30 // Set H5P storage path 31 $this->path = $path; 32 $this->alteditorpath = $alteditorpath; 33 } 34 35 /** 36 * Store the library folder. 37 * 38 * @param array $library 39 * Library properties 40 */ 41 public function saveLibrary($library) { 42 $dest = $this->path . '/libraries/' . \H5PCore::libraryToString($library, TRUE); 43 44 // Make sure destination dir doesn't exist 45 \H5PCore::deleteFileTree($dest); 46 47 // Move library folder 48 self::copyFileTree($library['uploadDirectory'], $dest); 49 } 50 51 /** 52 * Store the content folder. 53 * 54 * @param string $source 55 * Path on file system to content directory. 56 * @param array $content 57 * Content properties 58 */ 59 public function saveContent($source, $content) { 60 $dest = "{$this->path}/content/{$content['id']}"; 61 62 // Remove any old content 63 \H5PCore::deleteFileTree($dest); 64 65 self::copyFileTree($source, $dest); 66 } 67 68 /** 69 * Remove content folder. 70 * 71 * @param array $content 72 * Content properties 73 */ 74 public function deleteContent($content) { 75 \H5PCore::deleteFileTree("{$this->path}/content/{$content['id']}"); 76 } 77 78 /** 79 * Creates a stored copy of the content folder. 80 * 81 * @param string $id 82 * Identifier of content to clone. 83 * @param int $newId 84 * The cloned content's identifier 85 */ 86 public function cloneContent($id, $newId) { 87 $path = $this->path . '/content/'; 88 if (file_exists($path . $id)) { 89 self::copyFileTree($path . $id, $path . $newId); 90 } 91 } 92 93 /** 94 * Get path to a new unique tmp folder. 95 * 96 * @return string 97 * Path 98 */ 99 public function getTmpPath() { 100 $temp = "{$this->path}/temp"; 101 self::dirReady($temp); 102 return "{$temp}/" . uniqid('h5p-'); 103 } 104 105 /** 106 * Fetch content folder and save in target directory. 107 * 108 * @param int $id 109 * Content identifier 110 * @param string $target 111 * Where the content folder will be saved 112 */ 113 public function exportContent($id, $target) { 114 $source = "{$this->path}/content/{$id}"; 115 if (file_exists($source)) { 116 // Copy content folder if it exists 117 self::copyFileTree($source, $target); 118 } 119 else { 120 // No contnet folder, create emty dir for content.json 121 self::dirReady($target); 122 } 123 } 124 125 /** 126 * Fetch library folder and save in target directory. 127 * 128 * @param array $library 129 * Library properties 130 * @param string $target 131 * Where the library folder will be saved 132 * @param string $developmentPath 133 * Folder that library resides in 134 */ 135 public function exportLibrary($library, $target, $developmentPath=NULL) { 136 $folder = \H5PCore::libraryToString($library, TRUE); 137 $srcPath = ($developmentPath === NULL ? "/libraries/{$folder}" : $developmentPath); 138 self::copyFileTree("{$this->path}{$srcPath}", "{$target}/{$folder}"); 139 } 140 141 /** 142 * Save export in file system 143 * 144 * @param string $source 145 * Path on file system to temporary export file. 146 * @param string $filename 147 * Name of export file. 148 * @throws Exception Unable to save the file 149 */ 150 public function saveExport($source, $filename) { 151 $this->deleteExport($filename); 152 153 if (!self::dirReady("{$this->path}/exports")) { 154 throw new Exception("Unable to create directory for H5P export file."); 155 } 156 157 if (!copy($source, "{$this->path}/exports/{$filename}")) { 158 throw new Exception("Unable to save H5P export file."); 159 } 160 } 161 162 /** 163 * Removes given export file 164 * 165 * @param string $filename 166 */ 167 public function deleteExport($filename) { 168 $target = "{$this->path}/exports/{$filename}"; 169 if (file_exists($target)) { 170 unlink($target); 171 } 172 } 173 174 /** 175 * Check if the given export file exists 176 * 177 * @param string $filename 178 * @return boolean 179 */ 180 public function hasExport($filename) { 181 $target = "{$this->path}/exports/{$filename}"; 182 return file_exists($target); 183 } 184 185 /** 186 * Will concatenate all JavaScrips and Stylesheets into two files in order 187 * to improve page performance. 188 * 189 * @param array $files 190 * A set of all the assets required for content to display 191 * @param string $key 192 * Hashed key for cached asset 193 */ 194 public function cacheAssets(&$files, $key) { 195 foreach ($files as $type => $assets) { 196 if (empty($assets)) { 197 continue; // Skip no assets 198 } 199 200 $content = ''; 201 foreach ($assets as $asset) { 202 // Get content from asset file 203 $assetContent = file_get_contents($this->path . $asset->path); 204 $cssRelPath = preg_replace('/[^\/]+$/', '', $asset->path); 205 206 // Get file content and concatenate 207 if ($type === 'scripts') { 208 $content .= $assetContent . ";\n"; 209 } 210 else { 211 // Rewrite relative URLs used inside stylesheets 212 $content .= preg_replace_callback( 213 '/url\([\'"]?([^"\')]+)[\'"]?\)/i', 214 function ($matches) use ($cssRelPath) { 215 if (preg_match("/^(data:|([a-z0-9]+:)?\/)/i", $matches[1]) === 1) { 216 return $matches[0]; // Not relative, skip 217 } 218 return 'url("../' . $cssRelPath . $matches[1] . '")'; 219 }, 220 $assetContent) . "\n"; 221 } 222 } 223 224 self::dirReady("{$this->path}/cachedassets"); 225 $ext = ($type === 'scripts' ? 'js' : 'css'); 226 $outputfile = "/cachedassets/{$key}.{$ext}"; 227 file_put_contents($this->path . $outputfile, $content); 228 $files[$type] = array((object) array( 229 'path' => $outputfile, 230 'version' => '' 231 )); 232 } 233 } 234 235 /** 236 * Will check if there are cache assets available for content. 237 * 238 * @param string $key 239 * Hashed key for cached asset 240 * @return array 241 */ 242 public function getCachedAssets($key) { 243 $files = array(); 244 245 $js = "/cachedassets/{$key}.js"; 246 if (file_exists($this->path . $js)) { 247 $files['scripts'] = array((object) array( 248 'path' => $js, 249 'version' => '' 250 )); 251 } 252 253 $css = "/cachedassets/{$key}.css"; 254 if (file_exists($this->path . $css)) { 255 $files['styles'] = array((object) array( 256 'path' => $css, 257 'version' => '' 258 )); 259 } 260 261 return empty($files) ? NULL : $files; 262 } 263 264 /** 265 * Remove the aggregated cache files. 266 * 267 * @param array $keys 268 * The hash keys of removed files 269 */ 270 public function deleteCachedAssets($keys) { 271 foreach ($keys as $hash) { 272 foreach (array('js', 'css') as $ext) { 273 $path = "{$this->path}/cachedassets/{$hash}.{$ext}"; 274 if (file_exists($path)) { 275 unlink($path); 276 } 277 } 278 } 279 } 280 281 /** 282 * Read file content of given file and then return it. 283 * 284 * @param string $file_path 285 * @return string 286 */ 287 public function getContent($file_path) { 288 return file_get_contents($file_path); 289 } 290 291 /** 292 * Save files uploaded through the editor. 293 * The files must be marked as temporary until the content form is saved. 294 * 295 * @param \H5peditorFile $file 296 * @param int $contentid 297 */ 298 public function saveFile($file, $contentId) { 299 // Prepare directory 300 if (empty($contentId)) { 301 // Should be in editor tmp folder 302 $path = $this->getEditorPath(); 303 } 304 else { 305 // Should be in content folder 306 $path = $this->path . '/content/' . $contentId; 307 } 308 $path .= '/' . $file->getType() . 's'; 309 self::dirReady($path); 310 311 // Add filename to path 312 $path .= '/' . $file->getName(); 313 314 copy($_FILES['file']['tmp_name'], $path); 315 316 return $file; 317 } 318 319 /** 320 * Copy a file from another content or editor tmp dir. 321 * Used when copy pasting content in H5P Editor. 322 * 323 * @param string $file path + name 324 * @param string|int $fromid Content ID or 'editor' string 325 * @param int $toid Target Content ID 326 */ 327 public function cloneContentFile($file, $fromId, $toId) { 328 // Determine source path 329 if ($fromId === 'editor') { 330 $sourcepath = $this->getEditorPath(); 331 } 332 else { 333 $sourcepath = "{$this->path}/content/{$fromId}"; 334 } 335 $sourcepath .= '/' . $file; 336 337 // Determine target path 338 $filename = basename($file); 339 $filedir = str_replace($filename, '', $file); 340 $targetpath = "{$this->path}/content/{$toId}/{$filedir}"; 341 342 // Make sure it's ready 343 self::dirReady($targetpath); 344 345 $targetpath .= $filename; 346 347 // Check to see if source exist and if target doesn't 348 if (!file_exists($sourcepath) || file_exists($targetpath)) { 349 return; // Nothing to copy from or target already exists 350 } 351 352 copy($sourcepath, $targetpath); 353 } 354 355 /** 356 * Copy a content from one directory to another. Defaults to cloning 357 * content from the current temporary upload folder to the editor path. 358 * 359 * @param string $source path to source directory 360 * @param string $contentId Id of contentarray 361 */ 362 public function moveContentDirectory($source, $contentId = NULL) { 363 if ($source === NULL) { 364 return NULL; 365 } 366 367 // TODO: Remove $contentId and never copy temporary files into content folder. JI-366 368 if ($contentId === NULL || $contentId == 0) { 369 $target = $this->getEditorPath(); 370 } 371 else { 372 // Use content folder 373 $target = "{$this->path}/content/{$contentId}"; 374 } 375 376 $contentSource = $source . '/' . 'content'; 377 $contentFiles = array_diff(scandir($contentSource), array('.','..', 'content.json')); 378 foreach ($contentFiles as $file) { 379 if (is_dir("{$contentSource}/{$file}")) { 380 self::copyFileTree("{$contentSource}/{$file}", "{$target}/{$file}"); 381 } 382 else { 383 copy("{$contentSource}/{$file}", "{$target}/{$file}"); 384 } 385 } 386 387 // TODO: Return list of all files so that they can be marked as temporary. JI-366 388 } 389 390 /** 391 * Checks to see if content has the given file. 392 * Used when saving content. 393 * 394 * @param string $file path + name 395 * @param int $contentId 396 * @return string File ID or NULL if not found 397 */ 398 public function getContentFile($file, $contentId) { 399 $path = "{$this->path}/content/{$contentId}/{$file}"; 400 return file_exists($path) ? $path : NULL; 401 } 402 403 /** 404 * Checks to see if content has the given file. 405 * Used when saving content. 406 * 407 * @param string $file path + name 408 * @param int $contentid 409 * @return string|int File ID or NULL if not found 410 */ 411 public function removeContentFile($file, $contentId) { 412 $path = "{$this->path}/content/{$contentId}/{$file}"; 413 if (file_exists($path)) { 414 unlink($path); 415 416 // Clean up any empty parent directories to avoid cluttering the file system 417 $parts = explode('/', $path); 418 while (array_pop($parts) !== NULL) { 419 $dir = implode('/', $parts); 420 if (is_dir($dir) && count(scandir($dir)) === 2) { // empty contains '.' and '..' 421 rmdir($dir); // Remove empty parent 422 } 423 else { 424 return; // Not empty 425 } 426 } 427 } 428 } 429 430 /** 431 * Check if server setup has write permission to 432 * the required folders 433 * 434 * @return bool True if site can write to the H5P files folder 435 */ 436 public function hasWriteAccess() { 437 return self::dirReady($this->path); 438 } 439 440 /** 441 * Check if the file presave.js exists in the root of the library 442 * 443 * @param string $libraryFolder 444 * @param string $developmentPath 445 * @return bool 446 */ 447 public function hasPresave($libraryFolder, $developmentPath = null) { 448 $path = is_null($developmentPath) ? 'libraries' . '/' . $libraryFolder : $developmentPath; 449 $filePath = realpath($this->path . '/' . $path . '/' . 'presave.js'); 450 return file_exists($filePath); 451 } 452 453 /** 454 * Check if upgrades script exist for library. 455 * 456 * @param string $machineName 457 * @param int $majorVersion 458 * @param int $minorVersion 459 * @return string Relative path 460 */ 461 public function getUpgradeScript($machineName, $majorVersion, $minorVersion) { 462 $upgrades = "/libraries/{$machineName}-{$majorVersion}.{$minorVersion}/upgrades.js"; 463 if (file_exists($this->path . $upgrades)) { 464 return $upgrades; 465 } 466 else { 467 return NULL; 468 } 469 } 470 471 /** 472 * Store the given stream into the given file. 473 * 474 * @param string $path 475 * @param string $file 476 * @param resource $stream 477 * @return bool 478 */ 479 public function saveFileFromZip($path, $file, $stream) { 480 $filePath = $path . '/' . $file; 481 482 // Make sure the directory exists first 483 $matches = array(); 484 preg_match('/(.+)\/[^\/]*$/', $filePath, $matches); 485 self::dirReady($matches[1]); 486 487 // Store in local storage folder 488 return file_put_contents($filePath, $stream); 489 } 490 491 /** 492 * Recursive function for copying directories. 493 * 494 * @param string $source 495 * From path 496 * @param string $destination 497 * To path 498 * @return boolean 499 * Indicates if the directory existed. 500 * 501 * @throws Exception Unable to copy the file 502 */ 503 private static function copyFileTree($source, $destination) { 504 if (!self::dirReady($destination)) { 505 throw new \Exception('unabletocopy'); 506 } 507 508 $ignoredFiles = self::getIgnoredFiles("{$source}/.h5pignore"); 509 510 $dir = opendir($source); 511 if ($dir === FALSE) { 512 trigger_error('Unable to open directory ' . $source, E_USER_WARNING); 513 throw new \Exception('unabletocopy'); 514 } 515 516 while (false !== ($file = readdir($dir))) { 517 if (($file != '.') && ($file != '..') && $file != '.git' && $file != '.gitignore' && !in_array($file, $ignoredFiles)) { 518 if (is_dir("{$source}/{$file}")) { 519 self::copyFileTree("{$source}/{$file}", "{$destination}/{$file}"); 520 } 521 else { 522 copy("{$source}/{$file}", "{$destination}/{$file}"); 523 } 524 } 525 } 526 closedir($dir); 527 } 528 529 /** 530 * Retrieve array of file names from file. 531 * 532 * @param string $file 533 * @return array Array with files that should be ignored 534 */ 535 private static function getIgnoredFiles($file) { 536 if (file_exists($file) === FALSE) { 537 return array(); 538 } 539 540 $contents = file_get_contents($file); 541 if ($contents === FALSE) { 542 return array(); 543 } 544 545 return preg_split('/\s+/', $contents); 546 } 547 548 /** 549 * Recursive function that makes sure the specified directory exists and 550 * is writable. 551 * 552 * @param string $path 553 * @return bool 554 */ 555 private static function dirReady($path) { 556 if (!file_exists($path)) { 557 $parent = preg_replace("/\/[^\/]+\/?$/", '', $path); 558 if (!self::dirReady($parent)) { 559 return FALSE; 560 } 561 562 mkdir($path, 0777, true); 563 } 564 565 if (!is_dir($path)) { 566 trigger_error('Path is not a directory ' . $path, E_USER_WARNING); 567 return FALSE; 568 } 569 570 if (!is_writable($path)) { 571 trigger_error('Unable to write to ' . $path . ' – check directory permissions –', E_USER_WARNING); 572 return FALSE; 573 } 574 575 return TRUE; 576 } 577 578 /** 579 * Easy helper function for retrieving the editor path 580 * 581 * @return string Path to editor files 582 */ 583 private function getEditorPath() { 584 return ($this->alteditorpath !== NULL ? $this->alteditorpath : "{$this->path}/editor"); 585 } 586 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body