Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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