Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
    $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
}