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