Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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