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