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