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
use ZipArchive;
6
 
7
/**
8
 * Interface defining functions the h5p library needs the framework to implement
9
 */
10
interface H5PFrameworkInterface {
11
 
12
  /**
13
   * Returns info for the current platform
14
   *
15
   * @return array
16
   *   An associative array containing:
17
   *   - name: The name of the platform, for instance "Wordpress"
18
   *   - version: The version of the platform, for instance "4.0"
19
   *   - h5pVersion: The version of the H5P plugin/module
20
   */
21
  public function getPlatformInfo();
22
 
23
 
24
  /**
25
   * Fetches a file from a remote server using HTTP GET
26
   *
27
   * @param  string  $url  Where you want to get or send data.
28
   * @param  array  $data  Data to post to the URL.
29
   * @param  bool  $blocking  Set to 'FALSE' to instantly time out (fire and forget).
30
   * @param  string  $stream  Path to where the file should be saved.
31
   * @param  bool  $fullData  Return additional response data such as headers and potentially other data
32
   * @param  array  $headers  Headers to send
33
   * @param  array  $files Files to send
34
   * @param  string  $method
35
   *
36
   * @return string|array The content (response body), or an array with data. NULL if something went wrong
37
   */
38
  public function fetchExternalData($url, $data = NULL, $blocking = TRUE, $stream = NULL, $fullData = FALSE, $headers = array(), $files = array(), $method = 'POST');
39
 
40
  /**
41
   * Set the tutorial URL for a library. All versions of the library is set
42
   *
43
   * @param string $machineName
44
   * @param string $tutorialUrl
45
   */
46
  public function setLibraryTutorialUrl($machineName, $tutorialUrl);
47
 
48
  /**
49
   * Show the user an error message
50
   *
51
   * @param string $message The error message
52
   * @param string $code An optional code
53
   */
54
  public function setErrorMessage($message, $code = NULL);
55
 
56
  /**
57
   * Show the user an information message
58
   *
59
   * @param string $message
60
   *  The error message
61
   */
62
  public function setInfoMessage($message);
63
 
64
  /**
65
   * Return messages
66
   *
67
   * @param string $type 'info' or 'error'
68
   * @return string[]
69
   */
70
  public function getMessages($type);
71
 
72
  /**
73
   * Translation function
74
   *
75
   * @param string $message
76
   *  The english string to be translated.
77
   * @param array $replacements
78
   *   An associative array of replacements to make after translation. Incidences
79
   *   of any key in this array are replaced with the corresponding value. Based
80
   *   on the first character of the key, the value is escaped and/or themed:
81
   *    - !variable: inserted as is
82
   *    - @variable: escape plain text to HTML
83
   *    - %variable: escape text and theme as a placeholder for user-submitted
84
   *      content
85
   * @return string Translated string
86
   * Translated string
87
   */
88
  public function t($message, $replacements = array());
89
 
90
  /**
91
   * Get URL to file in the specific library
92
   * @param string $libraryFolderName
93
   * @param string $fileName
94
   * @return string URL to file
95
   */
96
  public function getLibraryFileUrl($libraryFolderName, $fileName);
97
 
98
  /**
99
   * Get the Path to the last uploaded h5p
100
   *
101
   * @return string
102
   *   Path to the folder where the last uploaded h5p for this session is located.
103
   */
104
  public function getUploadedH5pFolderPath();
105
 
106
  /**
107
   * Get the path to the last uploaded h5p file
108
   *
109
   * @return string
110
   *   Path to the last uploaded h5p
111
   */
112
  public function getUploadedH5pPath();
113
 
114
  /**
115
   * Load addon libraries
116
   *
117
   * @return array
118
   */
119
  public function loadAddons();
120
 
121
  /**
122
   * Load config for libraries
123
   *
124
   * @param array $libraries
125
   * @return array
126
   */
127
  public function getLibraryConfig($libraries = NULL);
128
 
129
  /**
130
   * Get a list of the current installed libraries
131
   *
132
   * @return array
133
   *   Associative array containing one entry per machine name.
134
   *   For each machineName there is a list of libraries(with different versions)
135
   */
136
  public function loadLibraries();
137
 
138
  /**
139
   * Returns the URL to the library admin page
140
   *
141
   * @return string
142
   *   URL to admin page
143
   */
144
  public function getAdminUrl();
145
 
146
  /**
147
   * Get id to an existing library.
148
   * If version number is not specified, the newest version will be returned.
149
   *
150
   * @param string $machineName
151
   *   The librarys machine name
152
   * @param int $majorVersion
153
   *   Optional major version number for library
154
   * @param int $minorVersion
155
   *   Optional minor version number for library
156
   * @return int
157
   *   The id of the specified library or FALSE
158
   */
159
  public function getLibraryId($machineName, $majorVersion = NULL, $minorVersion = NULL);
160
 
161
  /**
162
   * Get file extension whitelist
163
   *
164
   * The default extension list is part of h5p, but admins should be allowed to modify it
165
   *
166
   * @param boolean $isLibrary
167
   *   TRUE if this is the whitelist for a library. FALSE if it is the whitelist
168
   *   for the content folder we are getting
169
   * @param string $defaultContentWhitelist
170
   *   A string of file extensions separated by whitespace
171
   * @param string $defaultLibraryWhitelist
172
   *   A string of file extensions separated by whitespace
173
   */
174
  public function getWhitelist($isLibrary, $defaultContentWhitelist, $defaultLibraryWhitelist);
175
 
176
  /**
177
   * Is the library a patched version of an existing library?
178
   *
179
   * @param object $library
180
   *   An associative array containing:
181
   *   - machineName: The library machineName
182
   *   - majorVersion: The librarys majorVersion
183
   *   - minorVersion: The librarys minorVersion
184
   *   - patchVersion: The librarys patchVersion
185
   * @return boolean
186
   *   TRUE if the library is a patched version of an existing library
187
   *   FALSE otherwise
188
   */
189
  public function isPatchedLibrary($library);
190
 
191
  /**
192
   * Is H5P in development mode?
193
   *
194
   * @return boolean
195
   *  TRUE if H5P development mode is active
196
   *  FALSE otherwise
197
   */
198
  public function isInDevMode();
199
 
200
  /**
201
   * Is the current user allowed to update libraries?
202
   *
203
   * @return boolean
204
   *  TRUE if the user is allowed to update libraries
205
   *  FALSE if the user is not allowed to update libraries
206
   */
207
  public function mayUpdateLibraries();
208
 
209
  /**
210
   * Store data about a library
211
   *
212
   * Also fills in the libraryId in the libraryData object if the object is new
213
   *
214
   * @param object $libraryData
215
   *   Associative array containing:
216
   *   - libraryId: The id of the library if it is an existing library.
217
   *   - title: The library's name
218
   *   - machineName: The library machineName
219
   *   - majorVersion: The library's majorVersion
220
   *   - minorVersion: The library's minorVersion
221
   *   - patchVersion: The library's patchVersion
222
   *   - runnable: 1 if the library is a content type, 0 otherwise
223
   *   - metadataSettings: Associative array containing:
224
   *      - disable: 1 if the library should not support setting metadata (copyright etc)
225
   *      - disableExtraTitleField: 1 if the library don't need the extra title field
226
   *   - fullscreen(optional): 1 if the library supports fullscreen, 0 otherwise
227
   *   - embedTypes(optional): list of supported embed types
228
   *   - preloadedJs(optional): list of associative arrays containing:
229
   *     - path: path to a js file relative to the library root folder
230
   *   - preloadedCss(optional): list of associative arrays containing:
231
   *     - path: path to css file relative to the library root folder
232
   *   - dropLibraryCss(optional): list of associative arrays containing:
233
   *     - machineName: machine name for the librarys that are to drop their css
234
   *   - semantics(optional): Json describing the content structure for the library
235
   *   - language(optional): associative array containing:
236
   *     - languageCode: Translation in json format
237
   * @param bool $new
238
   * @return
239
   */
240
  public function saveLibraryData(&$libraryData, $new = TRUE);
241
 
242
  /**
243
   * Insert new content.
244
   *
245
   * @param array $content
246
   *   An associative array containing:
247
   *   - id: The content id
248
   *   - params: The content in json format
249
   *   - library: An associative array containing:
250
   *     - libraryId: The id of the main library for this content
251
   * @param int $contentMainId
252
   *   Main id for the content if this is a system that supports versions
253
   */
254
  public function insertContent($content, $contentMainId = NULL);
255
 
256
  /**
257
   * Update old content.
258
   *
259
   * @param array $content
260
   *   An associative array containing:
261
   *   - id: The content id
262
   *   - params: The content in json format
263
   *   - library: An associative array containing:
264
   *     - libraryId: The id of the main library for this content
265
   * @param int $contentMainId
266
   *   Main id for the content if this is a system that supports versions
267
   */
268
  public function updateContent($content, $contentMainId = NULL);
269
 
270
  /**
271
   * Resets marked user data for the given content.
272
   *
273
   * @param int $contentId
274
   */
275
  public function resetContentUserData($contentId);
276
 
277
  /**
278
   * Save what libraries a library is depending on
279
   *
280
   * @param int $libraryId
281
   *   Library Id for the library we're saving dependencies for
282
   * @param array $dependencies
283
   *   List of dependencies as associative arrays containing:
284
   *   - machineName: The library machineName
285
   *   - majorVersion: The library's majorVersion
286
   *   - minorVersion: The library's minorVersion
287
   * @param string $dependency_type
288
   *   What type of dependency this is, the following values are allowed:
289
   *   - editor
290
   *   - preloaded
291
   *   - dynamic
292
   */
293
  public function saveLibraryDependencies($libraryId, $dependencies, $dependency_type);
294
 
295
  /**
296
   * Give an H5P the same library dependencies as a given H5P
297
   *
298
   * @param int $contentId
299
   *   Id identifying the content
300
   * @param int $copyFromId
301
   *   Id identifying the content to be copied
302
   * @param int $contentMainId
303
   *   Main id for the content, typically used in frameworks
304
   *   That supports versions. (In this case the content id will typically be
305
   *   the version id, and the contentMainId will be the frameworks content id
306
   */
307
  public function copyLibraryUsage($contentId, $copyFromId, $contentMainId = NULL);
308
 
309
  /**
310
   * Deletes content data
311
   *
312
   * @param int $contentId
313
   *   Id identifying the content
314
   */
315
  public function deleteContentData($contentId);
316
 
317
  /**
318
   * Delete what libraries a content item is using
319
   *
320
   * @param int $contentId
321
   *   Content Id of the content we'll be deleting library usage for
322
   */
323
  public function deleteLibraryUsage($contentId);
324
 
325
  /**
326
   * Saves what libraries the content uses
327
   *
328
   * @param int $contentId
329
   *   Id identifying the content
330
   * @param array $librariesInUse
331
   *   List of libraries the content uses. Libraries consist of associative arrays with:
332
   *   - library: Associative array containing:
333
   *     - dropLibraryCss(optional): comma separated list of machineNames
334
   *     - machineName: Machine name for the library
335
   *     - libraryId: Id of the library
336
   *   - type: The dependency type. Allowed values:
337
   *     - editor
338
   *     - dynamic
339
   *     - preloaded
340
   */
341
  public function saveLibraryUsage($contentId, $librariesInUse);
342
 
343
  /**
344
   * Get number of content/nodes using a library, and the number of
345
   * dependencies to other libraries
346
   *
347
   * @param int $libraryId
348
   *   Library identifier
349
   * @param boolean $skipContent
350
   *   Flag to indicate if content usage should be skipped
351
   * @return array
352
   *   Associative array containing:
353
   *   - content: Number of content using the library
354
   *   - libraries: Number of libraries depending on the library
355
   */
356
  public function getLibraryUsage($libraryId, $skipContent = FALSE);
357
 
358
  /**
359
   * Loads a library
360
   *
361
   * @param string $machineName
362
   *   The library's machine name
363
   * @param int $majorVersion
364
   *   The library's major version
365
   * @param int $minorVersion
366
   *   The library's minor version
367
   * @return array|FALSE
368
   *   FALSE if the library does not exist.
369
   *   Otherwise an associative array containing:
370
   *   - libraryId: The id of the library if it is an existing library.
371
   *   - title: The library's name
372
   *   - machineName: The library machineName
373
   *   - majorVersion: The library's majorVersion
374
   *   - minorVersion: The library's minorVersion
375
   *   - patchVersion: The library's patchVersion
376
   *   - runnable: 1 if the library is a content type, 0 otherwise
377
   *   - fullscreen(optional): 1 if the library supports fullscreen, 0 otherwise
378
   *   - embedTypes(optional): list of supported embed types
379
   *   - preloadedJs(optional): comma separated string with js file paths
380
   *   - preloadedCss(optional): comma separated sting with css file paths
381
   *   - dropLibraryCss(optional): list of associative arrays containing:
382
   *     - machineName: machine name for the librarys that are to drop their css
383
   *   - semantics(optional): Json describing the content structure for the library
384
   *   - preloadedDependencies(optional): list of associative arrays containing:
385
   *     - machineName: Machine name for a library this library is depending on
386
   *     - majorVersion: Major version for a library this library is depending on
387
   *     - minorVersion: Minor for a library this library is depending on
388
   *   - dynamicDependencies(optional): list of associative arrays containing:
389
   *     - machineName: Machine name for a library this library is depending on
390
   *     - majorVersion: Major version for a library this library is depending on
391
   *     - minorVersion: Minor for a library this library is depending on
392
   *   - editorDependencies(optional): list of associative arrays containing:
393
   *     - machineName: Machine name for a library this library is depending on
394
   *     - majorVersion: Major version for a library this library is depending on
395
   *     - minorVersion: Minor for a library this library is depending on
396
   */
397
  public function loadLibrary($machineName, $majorVersion, $minorVersion);
398
 
399
  /**
400
   * Loads library semantics.
401
   *
402
   * @param string $machineName
403
   *   Machine name for the library
404
   * @param int $majorVersion
405
   *   The library's major version
406
   * @param int $minorVersion
407
   *   The library's minor version
408
   * @return string
409
   *   The library's semantics as json
410
   */
411
  public function loadLibrarySemantics($machineName, $majorVersion, $minorVersion);
412
 
413
  /**
414
   * Makes it possible to alter the semantics, adding custom fields, etc.
415
   *
416
   * @param array $semantics
417
   *   Associative array representing the semantics
418
   * @param string $machineName
419
   *   The library's machine name
420
   * @param int $majorVersion
421
   *   The library's major version
422
   * @param int $minorVersion
423
   *   The library's minor version
424
   */
425
  public function alterLibrarySemantics(&$semantics, $machineName, $majorVersion, $minorVersion);
426
 
427
  /**
428
   * Delete all dependencies belonging to given library
429
   *
430
   * @param int $libraryId
431
   *   Library identifier
432
   */
433
  public function deleteLibraryDependencies($libraryId);
434
 
435
  /**
436
   * Start an atomic operation against the dependency storage
437
   */
438
  public function lockDependencyStorage();
439
 
440
  /**
441
   * Stops an atomic operation against the dependency storage
442
   */
443
  public function unlockDependencyStorage();
444
 
445
 
446
  /**
447
   * Delete a library from database and file system
448
   *
449
   * @param stdClass $library
450
   *   Library object with id, name, major version and minor version.
451
   */
452
  public function deleteLibrary($library);
453
 
454
  /**
455
   * Load content.
456
   *
457
   * @param int $id
458
   *   Content identifier
459
   * @return array
460
   *   Associative array containing:
461
   *   - contentId: Identifier for the content
462
   *   - params: json content as string
463
   *   - embedType: csv of embed types
464
   *   - title: The contents title
465
   *   - language: Language code for the content
466
   *   - libraryId: Id for the main library
467
   *   - libraryName: The library machine name
468
   *   - libraryMajorVersion: The library's majorVersion
469
   *   - libraryMinorVersion: The library's minorVersion
470
   *   - libraryEmbedTypes: CSV of the main library's embed types
471
   *   - libraryFullscreen: 1 if fullscreen is supported. 0 otherwise.
472
   */
473
  public function loadContent($id);
474
 
475
  /**
476
   * Load dependencies for the given content of the given type.
477
   *
478
   * @param int $id
479
   *   Content identifier
480
   * @param int $type
481
   *   Dependency types. Allowed values:
482
   *   - editor
483
   *   - preloaded
484
   *   - dynamic
485
   * @return array
486
   *   List of associative arrays containing:
487
   *   - libraryId: The id of the library if it is an existing library.
488
   *   - machineName: The library machineName
489
   *   - majorVersion: The library's majorVersion
490
   *   - minorVersion: The library's minorVersion
491
   *   - patchVersion: The library's patchVersion
492
   *   - preloadedJs(optional): comma separated string with js file paths
493
   *   - preloadedCss(optional): comma separated sting with css file paths
494
   *   - dropCss(optional): csv of machine names
495
   */
496
  public function loadContentDependencies($id, $type = NULL);
497
 
498
  /**
499
   * Get stored setting.
500
   *
501
   * @param string $name
502
   *   Identifier for the setting
503
   * @param string $default
504
   *   Optional default value if settings is not set
505
   * @return mixed
506
   *   Whatever has been stored as the setting
507
   */
508
  public function getOption($name, $default = NULL);
509
 
510
  /**
511
   * Stores the given setting.
512
   * For example when did we last check h5p.org for updates to our libraries.
513
   *
514
   * @param string $name
515
   *   Identifier for the setting
516
   * @param mixed $value Data
517
   *   Whatever we want to store as the setting
518
   */
519
  public function setOption($name, $value);
520
 
521
  /**
522
   * This will update selected fields on the given content.
523
   *
524
   * @param int $id Content identifier
525
   * @param array $fields Content fields, e.g. filtered or slug.
526
   */
527
  public function updateContentFields($id, $fields);
528
 
529
  /**
530
   * Will clear filtered params for all the content that uses the specified
531
   * libraries. This means that the content dependencies will have to be rebuilt,
532
   * and the parameters re-filtered.
533
   *
534
   * @param array $library_ids
535
   */
536
  public function clearFilteredParameters($library_ids);
537
 
538
  /**
539
   * Get number of contents that has to get their content dependencies rebuilt
540
   * and parameters re-filtered.
541
   *
542
   * @return int
543
   */
544
  public function getNumNotFiltered();
545
 
546
  /**
547
   * Get number of contents using library as main library.
548
   *
549
   * @param int $libraryId
550
   * @param array $skip
551
   * @return int
552
   */
553
  public function getNumContent($libraryId, $skip = NULL);
554
 
555
  /**
556
   * Determines if content slug is used.
557
   *
558
   * @param string $slug
559
   * @return boolean
560
   */
561
  public function isContentSlugAvailable($slug);
562
 
563
  /**
564
   * Generates statistics from the event log per library
565
   *
566
   * @param string $type Type of event to generate stats for
567
   * @return array Number values indexed by library name and version
568
   */
569
  public function getLibraryStats($type);
570
 
571
  /**
572
   * Aggregate the current number of H5P authors
573
   * @return int
574
   */
575
  public function getNumAuthors();
576
 
577
  /**
578
   * Stores hash keys for cached assets, aggregated JavaScripts and
579
   * stylesheets, and connects it to libraries so that we know which cache file
580
   * to delete when a library is updated.
581
   *
582
   * @param string $key
583
   *  Hash key for the given libraries
584
   * @param array $libraries
585
   *  List of dependencies(libraries) used to create the key
586
   */
587
  public function saveCachedAssets($key, $libraries);
588
 
589
  /**
590
   * Locate hash keys for given library and delete them.
591
   * Used when cache file are deleted.
592
   *
593
   * @param int $library_id
594
   *  Library identifier
595
   * @return array
596
   *  List of hash keys removed
597
   */
598
  public function deleteCachedAssets($library_id);
599
 
600
  /**
601
   * Get the amount of content items associated to a library
602
   * return int
603
   */
604
  public function getLibraryContentCount();
605
 
606
  /**
607
   * Will trigger after the export file is created.
608
   */
609
  public function afterExportCreated($content, $filename);
610
 
611
  /**
612
   * Check if user has permissions to an action
613
   *
614
   * @method hasPermission
615
   * @param  [H5PPermission] $permission Permission type, ref H5PPermission
616
   * @param  [int]           $id         Id need by platform to determine permission
617
   * @return boolean
618
   */
619
  public function hasPermission($permission, $id = NULL);
620
 
621
  /**
622
   * Replaces existing content type cache with the one passed in
623
   *
624
   * @param object $contentTypeCache Json with an array called 'libraries'
625
   *  containing the new content type cache that should replace the old one.
626
   */
627
  public function replaceContentTypeCache($contentTypeCache);
628
 
629
  /**
630
   * Checks if the given library has a higher version.
631
   *
632
   * @param array $library
633
   * @return boolean
634
   */
635
  public function libraryHasUpgrade($library);
636
 
637
  /**
638
   * Replace content hub metadata cache
639
   *
640
   * @param JsonSerializable $metadata Metadata as received from content hub
641
   * @param string $lang Language in ISO 639-1
642
   *
643
   * @return mixed
644
   */
645
  public function replaceContentHubMetadataCache($metadata, $lang);
646
 
647
  /**
648
   * Get content hub metadata cache from db
649
   *
650
   * @param  string  $lang Language code in ISO 639-1
651
   *
652
   * @return JsonSerializable Json string
653
   */
654
  public function getContentHubMetadataCache($lang = 'en');
655
 
656
  /**
657
   * Get time of last content hub metadata check
658
   *
659
   * @param  string  $lang Language code iin ISO 639-1 format
660
   *
661
   * @return string|null Time in RFC7231 format
662
   */
663
  public function getContentHubMetadataChecked($lang = 'en');
664
 
665
  /**
666
   * Set time of last content hub metadata check
667
   *
668
   * @param  int|null  $time Time in RFC7231 format
669
   * @param  string  $lang Language code iin ISO 639-1 format
670
   *
671
   * @return bool True if successful
672
   */
673
  public function setContentHubMetadataChecked($time, $lang = 'en');
674
}
675
 
676
/**
677
 * This class is used for validating H5P files
678
 */
679
class H5PValidator {
680
  public $h5pF;
681
  public $h5pC;
682
  public $h5pCV;
683
 
684
  // Schemas used to validate the h5p files
685
  private $h5pRequired = array(
686
    'title' => '/^.{1,255}$/',
687
    'language' => '/^[-a-zA-Z]{1,10}$/',
688
    'preloadedDependencies' => array(
689
      'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
690
      'majorVersion' => '/^[0-9]{1,5}$/',
691
      'minorVersion' => '/^[0-9]{1,5}$/',
692
    ),
693
    'mainLibrary' => '/^[$a-z_][0-9a-z_\.$]{1,254}$/i',
694
    'embedTypes' => array('iframe', 'div'),
695
  );
696
 
697
  private $h5pOptional = array(
698
    'contentType' => '/^.{1,255}$/',
699
    'dynamicDependencies' => array(
700
      'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
701
      'majorVersion' => '/^[0-9]{1,5}$/',
702
      'minorVersion' => '/^[0-9]{1,5}$/',
703
    ),
704
    // deprecated
705
    'author' => '/^.{1,255}$/',
706
    'authors' => array(
707
      'name' => '/^.{1,255}$/',
708
      'role' => '/^\w+$/',
709
    ),
710
    'source' => '/^(http[s]?:\/\/.+)$/',
711
    'license' => '/^(CC BY|CC BY-SA|CC BY-ND|CC BY-NC|CC BY-NC-SA|CC BY-NC-ND|CC0 1\.0|GNU GPL|PD|ODC PDDL|CC PDM|U|C)$/',
712
    'licenseVersion' => '/^(1\.0|2\.0|2\.5|3\.0|4\.0)$/',
713
    'licenseExtras' => '/^.{1,5000}$/s',
714
    'yearsFrom' => '/^([0-9]{1,4})$/',
715
    'yearsTo' => '/^([0-9]{1,4})$/',
716
    'changes' => array(
717
      'date' => '/^[0-9]{2}-[0-9]{2}-[0-9]{2} [0-9]{1,2}:[0-9]{2}:[0-9]{2}$/',
718
      'author' => '/^.{1,255}$/',
719
      'log' => '/^.{1,5000}$/s'
720
    ),
721
    'authorComments' => '/^.{1,5000}$/s',
722
    'w' => '/^[0-9]{1,4}$/',
723
    'h' => '/^[0-9]{1,4}$/',
724
    // deprecated
725
    'metaKeywords' => '/^.{1,}$/',
726
    // deprecated
727
    'metaDescription' => '/^.{1,}$/',
728
  );
729
 
730
  // Schemas used to validate the library files
731
  private $libraryRequired = array(
732
    'title' => '/^.{1,255}$/',
733
    'majorVersion' => '/^[0-9]{1,5}$/',
734
    'minorVersion' => '/^[0-9]{1,5}$/',
735
    'patchVersion' => '/^[0-9]{1,5}$/',
736
    'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
737
    'runnable' => '/^(0|1)$/',
738
  );
739
 
740
  private $libraryOptional  = array(
741
    'author' => '/^.{1,255}$/',
742
    'license' => '/^(cc-by|cc-by-sa|cc-by-nd|cc-by-nc|cc-by-nc-sa|cc-by-nc-nd|pd|cr|MIT|GPL1|GPL2|GPL3|MPL|MPL2)$/',
743
    'description' => '/^.{1,}$/',
744
    'metadataSettings' => array(
745
      'disable' => '/^(0|1)$/',
746
      'disableExtraTitleField' => '/^(0|1)$/'
747
    ),
748
    'dynamicDependencies' => array(
749
      'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
750
      'majorVersion' => '/^[0-9]{1,5}$/',
751
      'minorVersion' => '/^[0-9]{1,5}$/',
752
    ),
753
    'preloadedDependencies' => array(
754
      'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
755
      'majorVersion' => '/^[0-9]{1,5}$/',
756
      'minorVersion' => '/^[0-9]{1,5}$/',
757
    ),
758
    'editorDependencies' => array(
759
      'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
760
      'majorVersion' => '/^[0-9]{1,5}$/',
761
      'minorVersion' => '/^[0-9]{1,5}$/',
762
    ),
763
    'preloadedJs' => array(
764
      'path' => '/^((\\\|\/)?[a-z_\-\s0-9\.]+)+\.js$/i',
765
    ),
766
    'preloadedCss' => array(
767
      'path' => '/^((\\\|\/)?[a-z_\-\s0-9\.]+)+\.css$/i',
768
    ),
769
    'dropLibraryCss' => array(
770
      'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
771
    ),
772
    'w' => '/^[0-9]{1,4}$/',
773
    'h' => '/^[0-9]{1,4}$/',
774
    'embedTypes' => array('iframe', 'div'),
775
    'fullscreen' => '/^(0|1)$/',
776
    'coreApi' => array(
777
      'majorVersion' => '/^[0-9]{1,5}$/',
778
      'minorVersion' => '/^[0-9]{1,5}$/',
779
    ),
780
  );
781
 
782
  /**
783
   * Constructor for the H5PValidator
784
   *
785
   * @param H5PFrameworkInterface $H5PFramework
786
   *  The frameworks implementation of the H5PFrameworkInterface
787
   * @param H5PCore $H5PCore
788
   */
789
  public function __construct($H5PFramework, $H5PCore) {
790
    $this->h5pF = $H5PFramework;
791
    $this->h5pC = $H5PCore;
792
    $this->h5pCV = new H5PContentValidator($this->h5pF, $this->h5pC);
793
  }
794
 
795
  /**
796
   * Validates a .h5p file
797
   *
798
   * @param bool $skipContent
799
   * @param bool $upgradeOnly
800
   * @return bool TRUE if the .h5p file is valid
801
   * TRUE if the .h5p file is valid
802
   */
803
  public function isValidPackage($skipContent = FALSE, $upgradeOnly = FALSE) {
804
    // Create a temporary dir to extract package in.
805
    $tmpDir = $this->h5pF->getUploadedH5pFolderPath();
806
    $tmpPath = $this->h5pF->getUploadedH5pPath();
807
 
808
    // Check dependencies, make sure Zip is present
809
    if (!class_exists('ZipArchive')) {
810
      $this->h5pF->setErrorMessage($this->h5pF->t('Your PHP version does not support ZipArchive.'), 'zip-archive-unsupported');
811
      unlink($tmpPath);
812
      return FALSE;
813
    }
814
    if (!extension_loaded('mbstring')) {
815
      $this->h5pF->setErrorMessage($this->h5pF->t('The mbstring PHP extension is not loaded. H5P need this to function properly'), 'mbstring-unsupported');
816
      unlink($tmpPath);
817
      return FALSE;
818
    }
819
 
820
    // Only allow files with the .h5p extension:
821
    if (strtolower(substr($tmpPath, -3)) !== 'h5p') {
822
      $this->h5pF->setErrorMessage($this->h5pF->t('The file you uploaded is not a valid HTML5 Package (It does not have the .h5p file extension)'), 'missing-h5p-extension');
823
      unlink($tmpPath);
824
      return FALSE;
825
    }
826
 
827
    // Extract and then remove the package file.
828
    $zip = new ZipArchive;
829
 
830
    // Open the package
831
    if ($zip->open($tmpPath) !== TRUE) {
832
      $this->h5pF->setErrorMessage($this->h5pF->t('The file you uploaded is not a valid HTML5 Package (We are unable to unzip it)'), 'unable-to-unzip');
833
      unlink($tmpPath);
834
      return FALSE;
835
    }
836
 
837
    if ($this->h5pC->disableFileCheck !== TRUE) {
838
      list($contentWhitelist, $contentRegExp) = $this->getWhitelistRegExp(FALSE);
839
      list($libraryWhitelist, $libraryRegExp) = $this->getWhitelistRegExp(TRUE);
840
    }
841
    $canInstall = $this->h5pC->mayUpdateLibraries();
842
 
843
    $valid = TRUE;
844
    $libraries = array();
845
 
846
    $totalSize = 0;
847
    $mainH5pExists = FALSE;
848
    $contentExists = FALSE;
849
 
850
    // Check for valid file types, JSON files + file sizes before continuing to unpack.
851
    for ($i = 0; $i < $zip->numFiles; $i++) {
852
      $fileStat = $zip->statIndex($i);
853
 
854
      if (!empty($this->h5pC->maxFileSize) && $fileStat['size'] > $this->h5pC->maxFileSize) {
855
        // Error file is too large
856
        $this->h5pF->setErrorMessage($this->h5pF->t('One of the files inside the package exceeds the maximum file size allowed. (%file %used > %max)', array('%file' => $fileStat['name'], '%used' => ($fileStat['size'] / 1048576) . ' MB', '%max' => ($this->h5pC->maxFileSize / 1048576) . ' MB')), 'file-size-too-large');
857
        $valid = FALSE;
858
      }
859
      $totalSize += $fileStat['size'];
860
 
861
      $fileName = mb_strtolower($fileStat['name']);
862
      if (preg_match('/(^[\._]|\/[\._]|\\\[\._])/', $fileName) !== 0) {
863
        continue; // Skip any file or folder starting with a . or _
864
      }
865
      elseif ($fileName === 'h5p.json') {
866
        $mainH5pExists = TRUE;
867
      }
868
      elseif ($fileName === 'content/content.json') {
869
        $contentExists = TRUE;
870
      }
871
      elseif (substr($fileName, 0, 8) === 'content/') {
872
        // This is a content file, check that the file type is allowed
873
        if ($skipContent === FALSE && $this->h5pC->disableFileCheck !== TRUE && !preg_match($contentRegExp, $fileName)) {
874
          $this->h5pF->setErrorMessage($this->h5pF->t('File "%filename" not allowed. Only files with the following extensions are allowed: %files-allowed.', array('%filename' => $fileStat['name'], '%files-allowed' => $contentWhitelist)), 'not-in-whitelist');
875
          $valid = FALSE;
876
        }
877
      }
878
      elseif ($canInstall && strpos($fileName, '/') !== FALSE) {
879
        // This is a library file, check that the file type is allowed
880
        if ($this->h5pC->disableFileCheck !== TRUE && !preg_match($libraryRegExp, $fileName)) {
881
          $this->h5pF->setErrorMessage($this->h5pF->t('File "%filename" not allowed. Only files with the following extensions are allowed: %files-allowed.', array('%filename' => $fileStat['name'], '%files-allowed' => $libraryWhitelist)), 'not-in-whitelist');
882
          $valid = FALSE;
883
        }
884
 
885
        // Further library validation happens after the files are extracted
886
      }
887
    }
888
 
889
    if (!empty($this->h5pC->maxTotalSize) && $totalSize > $this->h5pC->maxTotalSize) {
890
      // Error total size of the zip is too large
891
      $this->h5pF->setErrorMessage($this->h5pF->t('The total size of the unpacked files exceeds the maximum size allowed. (%used > %max)', array('%used' => ($totalSize / 1048576) . ' MB', '%max' => ($this->h5pC->maxTotalSize / 1048576) . ' MB')), 'total-size-too-large');
892
      $valid = FALSE;
893
    }
894
 
895
    if ($skipContent === FALSE) {
896
      // Not skipping content, require two valid JSON files from the package
897
      if (!$contentExists) {
898
        $this->h5pF->setErrorMessage($this->h5pF->t('A valid content folder is missing'), 'invalid-content-folder');
899
        $valid = FALSE;
900
      }
901
      else {
902
        $contentJsonData = $this->getJson($tmpPath, $zip, 'content/content.json'); // TODO: Is this case-senstivie?
903
        if ($contentJsonData === NULL) {
904
          return FALSE; // Breaking error when reading from the archive.
905
        }
906
        elseif ($contentJsonData === FALSE) {
907
          $valid = FALSE; // Validation error when parsing JSON
908
        }
909
      }
910
 
911
      if (!$mainH5pExists) {
912
        $this->h5pF->setErrorMessage($this->h5pF->t('A valid main h5p.json file is missing'), 'invalid-h5p-json-file');
913
        $valid = FALSE;
914
      }
915
      else {
916
        $mainH5pData = $this->getJson($tmpPath, $zip, 'h5p.json', TRUE);
917
        if ($mainH5pData === NULL) {
918
          return FALSE; // Breaking error when reading from the archive.
919
        }
920
        elseif ($mainH5pData === FALSE) {
921
          $valid = FALSE; // Validation error when parsing JSON
922
        }
923
        elseif (!$this->isValidH5pData($mainH5pData, 'h5p.json', $this->h5pRequired, $this->h5pOptional)) {
924
          $this->h5pF->setErrorMessage($this->h5pF->t('The main h5p.json file is not valid'), 'invalid-h5p-json-file'); // Is this message a bit redundant?
925
          $valid = FALSE;
926
        }
927
      }
928
    }
929
 
930
    if (!$valid) {
931
      // If something has failed during the initial checks of the package
932
      // we will not unpack it or continue validation.
933
      $zip->close();
934
      unlink($tmpPath);
935
      return FALSE;
936
    }
937
 
938
    // Extract the files from the package
939
    for ($i = 0; $i < $zip->numFiles; $i++) {
940
      $fileName = $zip->statIndex($i)['name'];
941
 
942
      if (preg_match('/(^[\._]|\/[\._]|\\\[\._])/', $fileName) !== 0) {
943
        continue; // Skip any file or folder starting with a . or _
944
      }
945
 
946
      $isContentFile = (substr($fileName, 0, 8) === 'content/');
947
      $isFolder = (strpos($fileName, '/') !== FALSE);
948
 
949
      if ($skipContent !== FALSE && $isContentFile) {
950
        continue; // Skipping any content files
951
      }
952
 
953
      if (!($isContentFile || ($canInstall && $isFolder))) {
954
        continue; // Not something we want to unpack
955
      }
956
 
957
      // Get file stream
958
      $fileStream = $zip->getStream($fileName);
959
      if (!$fileStream) {
960
        // This is a breaking error, there's no need to continue. (the rest of the files will fail as well)
961
        $this->h5pF->setErrorMessage($this->h5pF->t('Unable to read file from the package: %fileName', array('%fileName' => $fileName)), 'unable-to-read-package-file');
962
        $zip->close();
963
        unlink($path);
964
        H5PCore::deleteFileTree($tmpDir);
965
        return FALSE;
966
      }
967
 
968
      // Use file interface to allow overrides
969
      $this->h5pC->fs->saveFileFromZip($tmpDir, $fileName, $fileStream);
970
 
971
      // Clean up
972
      if (is_resource($fileStream)) {
973
        fclose($fileStream);
974
      }
975
    }
976
 
977
    // We're done with the zip file, clean up the stuff
978
    $zip->close();
979
    unlink($tmpPath);
980
 
981
    if ($canInstall) {
982
      // Process and validate libraries using the unpacked library folders
983
      $files = scandir($tmpDir);
984
      foreach ($files as $file) {
985
        $filePath = $tmpDir . '/' . $file;
986
 
987
        if ($file === '.' || $file === '..' || $file === 'content' || !is_dir($filePath)) {
988
          continue; // Skip
989
        }
990
 
991
        $libraryH5PData = $this->getLibraryData($file, $filePath, $tmpDir);
992
        if ($libraryH5PData === FALSE) {
993
          $valid = FALSE;
994
          continue; // Failed, but continue validating the rest of the libraries
995
        }
996
 
997
        // Library's directory name must be:
998
        // - <machineName>
999
        //     - or -
1000
        // - <machineName>-<majorVersion>.<minorVersion>
1001
        // where machineName, majorVersion and minorVersion is read from library.json
1002
        if ($libraryH5PData['machineName'] !== $file && H5PCore::libraryToFolderName($libraryH5PData) !== $file) {
1003
          $this->h5pF->setErrorMessage($this->h5pF->t('Library directory name must match machineName or machineName-majorVersion.minorVersion (from library.json). (Directory: %directoryName , machineName: %machineName, majorVersion: %majorVersion, minorVersion: %minorVersion)', array(
1004
              '%directoryName' => $file,
1005
              '%machineName' => $libraryH5PData['machineName'],
1006
              '%majorVersion' => $libraryH5PData['majorVersion'],
1007
              '%minorVersion' => $libraryH5PData['minorVersion'])), 'library-directory-name-mismatch');
1008
          $valid = FALSE;
1009
          continue; // Failed, but continue validating the rest of the libraries
1010
        }
1011
 
1012
        $libraryH5PData['uploadDirectory'] = $filePath;
1013
        $libraries[H5PCore::libraryToString($libraryH5PData)] = $libraryH5PData;
1014
      }
1015
    }
1016
 
1017
    if ($valid) {
1018
      if ($upgradeOnly) {
1019
        // When upgrading, we only add the already installed libraries, and
1020
        // the new dependent libraries
1021
        $upgrades = array();
1022
        foreach ($libraries as $libString => &$library) {
1023
          // Is this library already installed?
1024
          if ($this->h5pF->getLibraryId($library['machineName']) !== FALSE) {
1025
            $upgrades[$libString] = $library;
1026
          }
1027
        }
1028
        while ($missingLibraries = $this->getMissingLibraries($upgrades)) {
1029
          foreach ($missingLibraries as $libString => $missing) {
1030
            $library = $libraries[$libString];
1031
            if ($library) {
1032
              $upgrades[$libString] = $library;
1033
            }
1034
          }
1035
        }
1036
 
1037
        $libraries = $upgrades;
1038
      }
1039
 
1040
      $this->h5pC->librariesJsonData = $libraries;
1041
 
1042
      if ($skipContent === FALSE) {
1043
        $this->h5pC->mainJsonData = $mainH5pData;
1044
        $this->h5pC->contentJsonData = $contentJsonData;
1045
        $libraries['mainH5pData'] = $mainH5pData; // Check for the dependencies in h5p.json as well as in the libraries
1046
      }
1047
 
1048
      $missingLibraries = $this->getMissingLibraries($libraries);
1049
      foreach ($missingLibraries as $libString => $missing) {
1050
        if ($this->h5pC->getLibraryId($missing, $libString)) {
1051
          unset($missingLibraries[$libString]);
1052
        }
1053
      }
1054
 
1055
      if (!empty($missingLibraries)) {
1056
        // We still have missing libraries, check if our main library has an upgrade (BUT only if we has content)
1057
        $mainDependency = NULL;
1058
        if (!$skipContent && !empty($mainH5pData)) {
1059
          foreach ($mainH5pData['preloadedDependencies'] as $dep) {
1060
            if ($dep['machineName'] === $mainH5pData['mainLibrary']) {
1061
              $mainDependency = $dep;
1062
            }
1063
          }
1064
        }
1065
 
1066
        if ($skipContent || !$mainDependency || !$this->h5pF->libraryHasUpgrade(array(
1067
              'machineName' => $mainDependency['machineName'],
1068
              'majorVersion' => $mainDependency['majorVersion'],
1069
              'minorVersion' => $mainDependency['minorVersion']
1070
            ))) {
1071
          foreach ($missingLibraries as $libString => $library) {
1072
            if (!empty($mainDependency) && $library['machineName'] === $mainDependency['machineName']) {
1073
              $this->h5pF->setErrorMessage($this->h5pF->t('Missing main library @library', array('@library' => $libString )), 'missing-main-library');
1074
            }
1075
            else {
1076
              $this->h5pF->setErrorMessage($this->h5pF->t('Missing required library @library', array('@library' => $libString)), 'missing-required-library');
1077
            }
1078
            $valid = FALSE;
1079
          }
1080
          if (!$this->h5pC->mayUpdateLibraries()) {
1081
            $this->h5pF->setInfoMessage($this->h5pF->t("Note that the libraries may exist in the file you uploaded, but you're not allowed to upload new libraries. Contact the site administrator about this."));
1082
            $valid = FALSE;
1083
          }
1084
        }
1085
      }
1086
    }
1087
    if (!$valid) {
1088
      H5PCore::deleteFileTree($tmpDir);
1089
    }
1090
    return $valid;
1091
  }
1092
 
1093
  /**
1094
   * Help read JSON from the archive
1095
   *
1096
   * @param string $path
1097
   * @param ZipArchive $zip
1098
   * @param string $file
1099
   * @return mixed JSON content if valid, FALSE for invalid, NULL for breaking error.
1100
   */
1101
  private function getJson($path, $zip, $file, $assoc = FALSE) {
1102
    // Get stream
1103
    $stream = $zip->getStream($file);
1104
    if (!$stream) {
1105
      // Breaking error, no need to continue validating.
1106
      $this->h5pF->setErrorMessage($this->h5pF->t('Unable to read file from the package: %fileName', array('%fileName' => $file)), 'unable-to-read-package-file');
1107
      $zip->close();
1108
      unlink($path);
1109
      return NULL;
1110
    }
1111
 
1112
    // Read data
1113
    $contents = '';
1114
    while (!feof($stream)) {
1115
      $contents .= fread($stream, 2);
1116
    }
1117
 
1118
    // Decode the data
1119
    $json = json_decode($contents, $assoc);
1120
    if ($json === NULL) {
1121
      // JSON cannot be decoded or the recursion limit has been reached.
1122
      $this->h5pF->setErrorMessage($this->h5pF->t('Unable to parse JSON from the package: %fileName', array('%fileName' => $file)), 'unable-to-parse-package');
1123
      return FALSE;
1124
    }
1125
 
1126
    // All OK
1127
    return $json;
1128
  }
1129
 
1130
  /**
1131
   * Help retrieve file type regexp whitelist from plugin.
1132
   *
1133
   * @param bool $isLibrary Separate list with more allowed file types
1134
   * @return string RegExp
1135
   */
1136
  private function getWhitelistRegExp($isLibrary) {
1137
    $whitelist = $this->h5pF->getWhitelist($isLibrary, H5PCore::$defaultContentWhitelist, H5PCore::$defaultLibraryWhitelistExtras);
1138
    return array($whitelist, '/\.(' . preg_replace('/ +/i', '|', preg_quote($whitelist)) . ')$/i');
1139
  }
1140
 
1141
  /**
1142
   * Validates a H5P library
1143
   *
1144
   * @param string $file
1145
   *  Name of the library folder
1146
   * @param string $filePath
1147
   *  Path to the library folder
1148
   * @param string $tmpDir
1149
   *  Path to the temporary upload directory
1150
   * @return boolean|array
1151
   *  H5P data from library.json and semantics if the library is valid
1152
   *  FALSE if the library isn't valid
1153
   */
1154
  public function getLibraryData($file, $filePath, $tmpDir) {
1155
    if (preg_match('/^[\w0-9\-\.]{1,255}$/i', $file) === 0) {
1156
      $this->h5pF->setErrorMessage($this->h5pF->t('Invalid library name: %name', array('%name' => $file)), 'invalid-library-name');
1157
      return FALSE;
1158
    }
1159
    $h5pData = $this->getJsonData($filePath . '/' . 'library.json');
1160
    if ($h5pData === FALSE) {
1161
      $this->h5pF->setErrorMessage($this->h5pF->t('Could not find library.json file with valid json format for library %name', array('%name' => $file)), 'invalid-library-json-file');
1162
      return FALSE;
1163
    }
1164
 
1165
    // validate json if a semantics file is provided
1166
    $semanticsPath = $filePath . '/' . 'semantics.json';
1167
    if (file_exists($semanticsPath)) {
1168
      $semantics = $this->getJsonData($semanticsPath, TRUE);
1169
      if ($semantics === FALSE) {
1170
        $this->h5pF->setErrorMessage($this->h5pF->t('Invalid semantics.json file has been included in the library %name', array('%name' => $file)), 'invalid-semantics-json-file');
1171
        return FALSE;
1172
      }
1173
      else {
1174
        $h5pData['semantics'] = $semantics;
1175
      }
1176
    }
1177
 
1178
    // validate language folder if it exists
1179
    $languagePath = $filePath . '/' . 'language';
1180
    if (is_dir($languagePath)) {
1181
      $languageFiles = scandir($languagePath);
1182
      foreach ($languageFiles as $languageFile) {
1183
        if (in_array($languageFile, array('.', '..'))) {
1184
          continue;
1185
        }
1186
        if (preg_match('/^(-?[a-z]+){1,7}\.json$/i', $languageFile) === 0) {
1187
          $this->h5pF->setErrorMessage($this->h5pF->t('Invalid language file %file in library %library', array('%file' => $languageFile, '%library' => $file)), 'invalid-language-file');
1188
          return FALSE;
1189
        }
1190
        $languageJson = $this->getJsonData($languagePath . '/' . $languageFile, TRUE);
1191
        if ($languageJson === FALSE) {
1192
          $this->h5pF->setErrorMessage($this->h5pF->t('Invalid language file %languageFile has been included in the library %name', array('%languageFile' => $languageFile, '%name' => $file)), 'invalid-language-file');
1193
          return FALSE;
1194
        }
1195
        $parts = explode('.', $languageFile); // $parts[0] is the language code
1196
        $h5pData['language'][$parts[0]] = $languageJson;
1197
      }
1198
    }
1199
 
1200
    // Check for icon:
1201
    $h5pData['hasIcon'] = file_exists($filePath . '/' . 'icon.svg');
1202
 
1203
    $validLibrary = $this->isValidH5pData($h5pData, $file, $this->libraryRequired, $this->libraryOptional);
1204
 
1205
    //$validLibrary = $this->h5pCV->validateContentFiles($filePath, TRUE) && $validLibrary;
1206
 
1207
    if (isset($h5pData['preloadedJs'])) {
1208
      $validLibrary = $this->isExistingFiles($h5pData['preloadedJs'], $tmpDir, $file) && $validLibrary;
1209
    }
1210
    if (isset($h5pData['preloadedCss'])) {
1211
      $validLibrary = $this->isExistingFiles($h5pData['preloadedCss'], $tmpDir, $file) && $validLibrary;
1212
    }
1213
    if ($validLibrary) {
1214
      return $h5pData;
1215
    }
1216
    else {
1217
      return FALSE;
1218
    }
1219
  }
1220
 
1221
  /**
1222
   * Use the dependency declarations to find any missing libraries
1223
   *
1224
   * @param array $libraries
1225
   *  A multidimensional array of libraries keyed with machineName first and majorVersion second
1226
   * @return array
1227
   *  A list of libraries that are missing keyed with machineName and holds objects with
1228
   *  machineName, majorVersion and minorVersion properties
1229
   */
1230
  private function getMissingLibraries($libraries) {
1231
    $missing = array();
1232
    foreach ($libraries as $library) {
1233
      if (isset($library['preloadedDependencies'])) {
1234
        $missing = array_merge($missing, $this->getMissingDependencies($library['preloadedDependencies'], $libraries));
1235
      }
1236
      if (isset($library['dynamicDependencies'])) {
1237
        $missing = array_merge($missing, $this->getMissingDependencies($library['dynamicDependencies'], $libraries));
1238
      }
1239
      if (isset($library['editorDependencies'])) {
1240
        $missing = array_merge($missing, $this->getMissingDependencies($library['editorDependencies'], $libraries));
1241
      }
1242
    }
1243
    return $missing;
1244
  }
1245
 
1246
  /**
1247
   * Helper function for getMissingLibraries, searches for dependency required libraries in
1248
   * the provided list of libraries
1249
   *
1250
   * @param array $dependencies
1251
   *  A list of objects with machineName, majorVersion and minorVersion properties
1252
   * @param array $libraries
1253
   *  An array of libraries keyed with machineName
1254
   * @return
1255
   *  A list of libraries that are missing keyed with machineName and holds objects with
1256
   *  machineName, majorVersion and minorVersion properties
1257
   */
1258
  private function getMissingDependencies($dependencies, $libraries) {
1259
    $missing = array();
1260
    foreach ($dependencies as $dependency) {
1261
      $libString = H5PCore::libraryToString($dependency);
1262
      if (!isset($libraries[$libString])) {
1263
        $missing[$libString] = $dependency;
1264
      }
1265
    }
1266
    return $missing;
1267
  }
1268
 
1269
  /**
1270
   * Figure out if the provided file paths exists
1271
   *
1272
   * Triggers error messages if files doesn't exist
1273
   *
1274
   * @param array $files
1275
   *  List of file paths relative to $tmpDir
1276
   * @param string $tmpDir
1277
   *  Path to the directory where the $files are stored.
1278
   * @param string $library
1279
   *  Name of the library we are processing
1280
   * @return boolean
1281
   *  TRUE if all the files excists
1282
   */
1283
  private function isExistingFiles($files, $tmpDir, $library) {
1284
    foreach ($files as $file) {
1285
      $path = str_replace(array('/', '\\'), '/', $file['path']);
1286
      if (!file_exists($tmpDir . '/' . $library . '/' . $path)) {
1287
        $this->h5pF->setErrorMessage($this->h5pF->t('The file "%file" is missing from library: "%name"', array('%file' => $path, '%name' => $library)), 'library-missing-file');
1288
        return FALSE;
1289
      }
1290
    }
1291
    return TRUE;
1292
  }
1293
 
1294
  /**
1295
   * Validates h5p.json and library.json data
1296
   *
1297
   * Error messages are triggered if the data isn't valid
1298
   *
1299
   * @param array $h5pData
1300
   *  h5p data
1301
   * @param string $library_name
1302
   *  Name of the library we are processing
1303
   * @param array $required
1304
   *  Validation pattern for required properties
1305
   * @param array $optional
1306
   *  Validation pattern for optional properties
1307
   * @return boolean
1308
   *  TRUE if the $h5pData is valid
1309
   */
1310
  private function isValidH5pData($h5pData, $library_name, $required, $optional) {
1311
    $valid = $this->isValidRequiredH5pData($h5pData, $required, $library_name);
1312
    $valid = $this->isValidOptionalH5pData($h5pData, $optional, $library_name) && $valid;
1313
 
1314
    // Check the library's required API version of Core.
1315
    // If no requirement is set this implicitly means 1.0.
1316
    if (isset($h5pData['coreApi']) && !empty($h5pData['coreApi'])) {
1317
      if (($h5pData['coreApi']['majorVersion'] > H5PCore::$coreApi['majorVersion']) ||
1318
          ( ($h5pData['coreApi']['majorVersion'] == H5PCore::$coreApi['majorVersion']) &&
1319
            ($h5pData['coreApi']['minorVersion'] > H5PCore::$coreApi['minorVersion']) )) {
1320
 
1321
        $this->h5pF->setErrorMessage(
1322
            $this->h5pF->t('The system was unable to install the <em>%component</em> component from the package, it requires a newer version of the H5P plugin. This site is currently running version %current, whereas the required version is %required or higher. You should consider upgrading and then try again.',
1323
                array(
1324
                  '%component' => (isset($h5pData['title']) ? $h5pData['title'] : $library_name),
1325
                  '%current' => H5PCore::$coreApi['majorVersion'] . '.' . H5PCore::$coreApi['minorVersion'],
1326
                  '%required' => $h5pData['coreApi']['majorVersion'] . '.' . $h5pData['coreApi']['minorVersion']
1327
                )
1328
            ),
1329
            'api-version-unsupported'
1330
        );
1331
 
1332
        $valid = false;
1333
      }
1334
    }
1335
 
1336
    return $valid;
1337
  }
1338
 
1339
  /**
1340
   * Helper function for isValidH5pData
1341
   *
1342
   * Validates the optional part of the h5pData
1343
   *
1344
   * Triggers error messages
1345
   *
1346
   * @param array $h5pData
1347
   *  h5p data
1348
   * @param array $requirements
1349
   *  Validation pattern
1350
   * @param string $library_name
1351
   *  Name of the library we are processing
1352
   * @return boolean
1353
   *  TRUE if the optional part of the $h5pData is valid
1354
   */
1355
  private function isValidOptionalH5pData($h5pData, $requirements, $library_name) {
1356
    $valid = TRUE;
1357
 
1358
    foreach ($h5pData as $key => $value) {
1359
      if (isset($requirements[$key])) {
1360
        $valid = $this->isValidRequirement($value, $requirements[$key], $library_name, $key) && $valid;
1361
      }
1362
      // Else: ignore, a package can have parameters that this library doesn't care about, but that library
1363
      // specific implementations does care about...
1364
    }
1365
 
1366
    return $valid;
1367
  }
1368
 
1369
  /**
1370
   * Validate a requirement given as regexp or an array of requirements
1371
   *
1372
   * @param mixed $h5pData
1373
   *  The data to be validated
1374
   * @param mixed $requirement
1375
   *  The requirement the data is to be validated against, regexp or array of requirements
1376
   * @param string $library_name
1377
   *  Name of the library we are validating(used in error messages)
1378
   * @param string $property_name
1379
   *  Name of the property we are validating(used in error messages)
1380
   * @return boolean
1381
   *  TRUE if valid, FALSE if invalid
1382
   */
1383
  private function isValidRequirement($h5pData, $requirement, $library_name, $property_name) {
1384
    $valid = TRUE;
1385
 
1386
    if (is_string($requirement)) {
1387
      if ($requirement == 'boolean') {
1388
        if (!is_bool($h5pData)) {
1389
         $this->h5pF->setErrorMessage($this->h5pF->t("Invalid data provided for %property in %library. Boolean expected.", array('%property' => $property_name, '%library' => $library_name)));
1390
         $valid = FALSE;
1391
        }
1392
      }
1393
      else {
1394
        // The requirement is a regexp, match it against the data
1395
        if (is_string($h5pData) || is_int($h5pData)) {
1396
          if (preg_match($requirement, $h5pData) === 0) {
1397
             $this->h5pF->setErrorMessage($this->h5pF->t("Invalid data provided for %property in %library", array('%property' => $property_name, '%library' => $library_name)));
1398
             $valid = FALSE;
1399
          }
1400
        }
1401
        else {
1402
          $this->h5pF->setErrorMessage($this->h5pF->t("Invalid data provided for %property in %library", array('%property' => $property_name, '%library' => $library_name)));
1403
          $valid = FALSE;
1404
        }
1405
      }
1406
    }
1407
    elseif (is_array($requirement)) {
1408
      // We have sub requirements
1409
      if (is_array($h5pData)) {
1410
        if (is_array(current($h5pData))) {
1411
          foreach ($h5pData as $sub_h5pData) {
1412
            $valid = $this->isValidRequiredH5pData($sub_h5pData, $requirement, $library_name) && $valid;
1413
          }
1414
        }
1415
        else {
1416
          $valid = $this->isValidRequiredH5pData($h5pData, $requirement, $library_name) && $valid;
1417
        }
1418
      }
1419
      else {
1420
        $this->h5pF->setErrorMessage($this->h5pF->t("Invalid data provided for %property in %library", array('%property' => $property_name, '%library' => $library_name)));
1421
        $valid = FALSE;
1422
      }
1423
    }
1424
    else {
1425
      $this->h5pF->setErrorMessage($this->h5pF->t("Can't read the property %property in %library", array('%property' => $property_name, '%library' => $library_name)));
1426
      $valid = FALSE;
1427
    }
1428
    return $valid;
1429
  }
1430
 
1431
  /**
1432
   * Validates the required h5p data in libraray.json and h5p.json
1433
   *
1434
   * @param mixed $h5pData
1435
   *  Data to be validated
1436
   * @param array $requirements
1437
   *  Array with regexp to validate the data against
1438
   * @param string $library_name
1439
   *  Name of the library we are validating (used in error messages)
1440
   * @return boolean
1441
   *  TRUE if all the required data exists and is valid, FALSE otherwise
1442
   */
1443
  private function isValidRequiredH5pData($h5pData, $requirements, $library_name) {
1444
    $valid = TRUE;
1445
    foreach ($requirements as $required => $requirement) {
1446
      if (is_int($required)) {
1447
        // We have an array of allowed options
1448
        return $this->isValidH5pDataOptions($h5pData, $requirements, $library_name);
1449
      }
1450
      if (isset($h5pData[$required])) {
1451
        $valid = $this->isValidRequirement($h5pData[$required], $requirement, $library_name, $required) && $valid;
1452
      }
1453
      else {
1454
        $this->h5pF->setErrorMessage($this->h5pF->t('The required property %property is missing from %library', array('%property' => $required, '%library' => $library_name)), 'missing-required-property');
1455
        $valid = FALSE;
1456
      }
1457
    }
1458
    return $valid;
1459
  }
1460
 
1461
  /**
1462
   * Validates h5p data against a set of allowed values(options)
1463
   *
1464
   * @param array $selected
1465
   *  The option(s) that has been specified
1466
   * @param array $allowed
1467
   *  The allowed options
1468
   * @param string $library_name
1469
   *  Name of the library we are validating (used in error messages)
1470
   * @return boolean
1471
   *  TRUE if the specified data is valid, FALSE otherwise
1472
   */
1473
  private function isValidH5pDataOptions($selected, $allowed, $library_name) {
1474
    $valid = TRUE;
1475
    foreach ($selected as $value) {
1476
      if (!in_array($value, $allowed)) {
1477
        $this->h5pF->setErrorMessage($this->h5pF->t('Illegal option %option in %library', array('%option' => $value, '%library' => $library_name)), 'illegal-option-in-library');
1478
        $valid = FALSE;
1479
      }
1480
    }
1481
    return $valid;
1482
  }
1483
 
1484
  /**
1485
   * Fetch json data from file
1486
   *
1487
   * @param string $filePath
1488
   *  Path to the file holding the json string
1489
   * @param boolean $return_as_string
1490
   *  If true the json data will be decoded in order to validate it, but will be
1491
   *  returned as string
1492
   * @return mixed
1493
   *  FALSE if the file can't be read or the contents can't be decoded
1494
   *  string if the $return as string parameter is set
1495
   *  array otherwise
1496
   */
1497
  private function getJsonData($filePath, $return_as_string = FALSE) {
1498
    $json = file_get_contents($filePath);
1499
    if ($json === FALSE) {
1500
      return FALSE; // Cannot read from file.
1501
    }
1502
    $jsonData = json_decode($json, TRUE);
1503
    if ($jsonData === NULL) {
1504
      return FALSE; // JSON cannot be decoded or the recursion limit has been reached.
1505
    }
1506
    return $return_as_string ? $json : $jsonData;
1507
  }
1508
 
1509
  /**
1510
   * Helper function that copies an array
1511
   *
1512
   * @param array $array
1513
   *  The array to be copied
1514
   * @return array
1515
   *  Copy of $array. All objects are cloned
1516
   */
1517
  private function arrayCopy(array $array) {
1518
    $result = array();
1519
    foreach ($array as $key => $val) {
1520
      if (is_array($val)) {
1521
        $result[$key] = self::arrayCopy($val);
1522
      }
1523
      elseif (is_object($val)) {
1524
        $result[$key] = clone $val;
1525
      }
1526
      else {
1527
        $result[$key] = $val;
1528
      }
1529
    }
1530
    return $result;
1531
  }
1532
}
1533
 
1534
/**
1535
 * This class is used for saving H5P files
1536
 */
1537
class H5PStorage {
1538
 
1539
  public $h5pF;
1540
  public $h5pC;
1541
 
1542
  public $contentId = NULL; // Quick fix so WP can get ID of new content.
1543
 
1544
  /**
1545
   * Constructor for the H5PStorage
1546
   *
1547
   * @param H5PFrameworkInterface|object $H5PFramework
1548
   *  The frameworks implementation of the H5PFrameworkInterface
1549
   * @param H5PCore $H5PCore
1550
   */
1551
  public function __construct(H5PFrameworkInterface $H5PFramework, H5PCore $H5PCore) {
1552
    $this->h5pF = $H5PFramework;
1553
    $this->h5pC = $H5PCore;
1554
  }
1555
 
1556
  /**
1557
   * Saves a H5P file
1558
   *
1559
   * @param null $content
1560
   * @param int $contentMainId
1561
   *  The main id for the content we are saving. This is used if the framework
1562
   *  we're integrating with uses content id's and version id's
1563
   * @param bool $skipContent
1564
   * @param array $options
1565
   * @return bool TRUE if one or more libraries were updated
1566
   * TRUE if one or more libraries were updated
1567
   * FALSE otherwise
1568
   */
1569
  public function savePackage($content = NULL, $contentMainId = NULL, $skipContent = FALSE, $options = array()) {
1570
    if ($this->h5pC->mayUpdateLibraries()) {
1571
      // Save the libraries we processed during validation
1572
      $this->saveLibraries();
1573
    }
1574
 
1575
    if (!$skipContent) {
1576
      $basePath = $this->h5pF->getUploadedH5pFolderPath();
1577
      $current_path = $basePath . '/' . 'content';
1578
 
1579
      // Save content
1580
      if ($content === NULL) {
1581
        $content = array();
1582
      }
1583
      if (!is_array($content)) {
1584
        $content = array('id' => $content);
1585
      }
1586
 
1587
      // Find main library version
1588
      foreach ($this->h5pC->mainJsonData['preloadedDependencies'] as $dep) {
1589
        if ($dep['machineName'] === $this->h5pC->mainJsonData['mainLibrary']) {
1590
          $dep['libraryId'] = $this->h5pC->getLibraryId($dep);
1591
          $content['library'] = $dep;
1592
          break;
1593
        }
1594
      }
1595
 
1596
      $content['params'] = file_get_contents($current_path . '/' . 'content.json');
1597
 
1598
      if (isset($options['disable'])) {
1599
        $content['disable'] = $options['disable'];
1600
      }
1601
      $content['id'] = $this->h5pC->saveContent($content, $contentMainId);
1602
      $this->contentId = $content['id'];
1603
 
1604
      try {
1605
        // Save content folder contents
1606
        $this->h5pC->fs->saveContent($current_path, $content);
1607
      }
1608
      catch (Exception $e) {
1609
        $this->h5pF->setErrorMessage($e->getMessage(), 'save-content-failed');
1610
      }
1611
 
1612
      // Remove temp content folder
1613
      H5PCore::deleteFileTree($basePath);
1614
    }
1615
  }
1616
 
1617
  /**
1618
   * Helps savePackage.
1619
   *
1620
   * @return int Number of libraries saved
1621
   */
1622
  private function saveLibraries() {
1623
    // Keep track of the number of libraries that have been saved
1624
    $newOnes = 0;
1625
    $oldOnes = 0;
1626
 
1627
    // Go through libraries that came with this package
1628
    foreach ($this->h5pC->librariesJsonData as $libString => &$library) {
1629
      // Find local library with same major + minor
1630
      $existingLibrary = $this->h5pC->loadLibrary($library['machineName'], $library['majorVersion'], $library['minorVersion']);
1631
 
1632
      // Assume new library
1633
      $new = TRUE;
1634
      if (isset($existingLibrary['libraryId'])) {
1635
        $new = false;
1636
        // We have the library installed already (with the same major + minor)
1637
 
1638
        $library['libraryId'] = $existingLibrary['libraryId'];
1639
 
1640
        // Is this a newer patchVersion?
1641
        $newerPatchVersion = $existingLibrary['patchVersion'] < $library['patchVersion'];
1642
 
1643
        if (!$newerPatchVersion) {
1644
          $library['saveDependencies'] = FALSE;
1645
          // This is an older version, no need to save.
1646
          continue;
1647
        }
1648
      }
1649
 
1650
      // Indicate that the dependencies of this library should be saved.
1651
      $library['saveDependencies'] = TRUE;
1652
 
1653
      // Convert metadataSettings values to boolean & json_encode it before saving
1654
      $library['metadataSettings'] = isset($library['metadataSettings']) ?
1655
        H5PMetadata::boolifyAndEncodeSettings($library['metadataSettings']) :
1656
        NULL;
1657
 
1658
      // MOODLE PATCH: The library needs to be saved in database first before creating the files, because the libraryid is used
1659
      // as itemid for the files.
1660
      // Update our DB
1661
      $this->h5pF->saveLibraryData($library, $new);
1662
 
1663
      // Save library folder
1664
      $this->h5pC->fs->saveLibrary($library);
1665
 
1666
      // Remove cached assets that uses this library
1667
      if ($this->h5pC->aggregateAssets && isset($library['libraryId'])) {
1668
        $removedKeys = $this->h5pF->deleteCachedAssets($library['libraryId']);
1669
        $this->h5pC->fs->deleteCachedAssets($removedKeys);
1670
      }
1671
 
1672
      // Remove tmp folder
1673
      H5PCore::deleteFileTree($library['uploadDirectory']);
1674
 
1675
      if ($existingLibrary) {
1676
        $this->h5pC->fs->deleteLibrary($existingLibrary);
1677
      }
1678
 
1679
      if ($new) {
1680
        $newOnes++;
1681
      }
1682
      else {
1683
        $oldOnes++;
1684
      }
1685
    }
1686
 
1687
    // Go through the libraries again to save dependencies.
1688
    $library_ids = array();
1689
    foreach ($this->h5pC->librariesJsonData as &$library) {
1690
      if (!$library['saveDependencies']) {
1691
        continue;
1692
      }
1693
 
1694
      // TODO: Should the table be locked for this operation?
1695
 
1696
      // Remove any old dependencies
1697
      $this->h5pF->deleteLibraryDependencies($library['libraryId']);
1698
 
1699
      // Insert the different new ones
1700
      if (isset($library['preloadedDependencies'])) {
1701
        $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['preloadedDependencies'], 'preloaded');
1702
      }
1703
      if (isset($library['dynamicDependencies'])) {
1704
        $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['dynamicDependencies'], 'dynamic');
1705
      }
1706
      if (isset($library['editorDependencies'])) {
1707
        $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['editorDependencies'], 'editor');
1708
      }
1709
 
1710
      $library_ids[] = $library['libraryId'];
1711
    }
1712
 
1713
    // Make sure libraries dependencies, parameter filtering and export files gets regenerated for all content who uses these libraries.
1714
    if (!empty($library_ids)) {
1715
      $this->h5pF->clearFilteredParameters($library_ids);
1716
    }
1717
 
1718
    // Tell the user what we've done.
1719
    if ($newOnes && $oldOnes) {
1720
      if ($newOnes === 1)  {
1721
        if ($oldOnes === 1)  {
1722
          // Singular Singular
1723
          $message = $this->h5pF->t('Added %new new H5P library and updated %old old one.', array('%new' => $newOnes, '%old' => $oldOnes));
1724
        }
1725
        else {
1726
          // Singular Plural
1727
          $message = $this->h5pF->t('Added %new new H5P library and updated %old old ones.', array('%new' => $newOnes, '%old' => $oldOnes));
1728
        }
1729
      }
1730
      else {
1731
        // Plural
1732
        if ($oldOnes === 1)  {
1733
          // Plural Singular
1734
          $message = $this->h5pF->t('Added %new new H5P libraries and updated %old old one.', array('%new' => $newOnes, '%old' => $oldOnes));
1735
        }
1736
        else {
1737
          // Plural Plural
1738
          $message = $this->h5pF->t('Added %new new H5P libraries and updated %old old ones.', array('%new' => $newOnes, '%old' => $oldOnes));
1739
        }
1740
      }
1741
    }
1742
    elseif ($newOnes) {
1743
      if ($newOnes === 1)  {
1744
        // Singular
1745
        $message = $this->h5pF->t('Added %new new H5P library.', array('%new' => $newOnes));
1746
      }
1747
      else {
1748
        // Plural
1749
        $message = $this->h5pF->t('Added %new new H5P libraries.', array('%new' => $newOnes));
1750
      }
1751
    }
1752
    elseif ($oldOnes) {
1753
      if ($oldOnes === 1)  {
1754
        // Singular
1755
        $message = $this->h5pF->t('Updated %old H5P library.', array('%old' => $oldOnes));
1756
      }
1757
      else {
1758
        // Plural
1759
        $message = $this->h5pF->t('Updated %old H5P libraries.', array('%old' => $oldOnes));
1760
      }
1761
    }
1762
 
1763
    if (isset($message)) {
1764
      $this->h5pF->setInfoMessage($message);
1765
    }
1766
  }
1767
 
1768
  /**
1769
   * Delete an H5P package
1770
   *
1771
   * @param $content
1772
   */
1773
  public function deletePackage($content) {
1774
    $this->h5pC->fs->deleteContent($content);
1775
    $this->h5pC->fs->deleteExport(($content['slug'] ? $content['slug'] . '-' : '') . $content['id'] . '.h5p');
1776
    $this->h5pF->deleteContentData($content['id']);
1777
  }
1778
 
1779
  /**
1780
   * Copy/clone an H5P package
1781
   *
1782
   * May for instance be used if the content is being revisioned without
1783
   * uploading a new H5P package
1784
   *
1785
   * @param int $contentId
1786
   *  The new content id
1787
   * @param int $copyFromId
1788
   *  The content id of the content that should be cloned
1789
   * @param int $contentMainId
1790
   *  The main id of the new content (used in frameworks that support revisioning)
1791
   */
1792
  public function copyPackage($contentId, $copyFromId, $contentMainId = NULL) {
1793
    $this->h5pC->fs->cloneContent($copyFromId, $contentId);
1794
    $this->h5pF->copyLibraryUsage($contentId, $copyFromId, $contentMainId);
1795
  }
1796
}
1797
 
1798
/**
1799
* This class is used for exporting zips
1800
*/
1801
Class H5PExport {
1802
  public $h5pF;
1803
  public $h5pC;
1804
 
1805
  /**
1806
   * Constructor for the H5PExport
1807
   *
1808
   * @param H5PFrameworkInterface|object $H5PFramework
1809
   *  The frameworks implementation of the H5PFrameworkInterface
1810
   * @param H5PCore $H5PCore
1811
   *  Reference to an instance of H5PCore
1812
   */
1813
  public function __construct(H5PFrameworkInterface $H5PFramework, H5PCore $H5PCore) {
1814
    $this->h5pF = $H5PFramework;
1815
    $this->h5pC = $H5PCore;
1816
  }
1817
 
1818
  /**
1819
   * Reverts the replace pattern used by the text editor
1820
   *
1821
   * @param string $value
1822
   * @return string
1823
   */
1824
  private static function revertH5PEditorTextEscape($value) {
1825
    return str_replace('&lt;', '<', str_replace('&gt;', '>', str_replace('&#039;', "'", str_replace('&quot;', '"', $value))));
1826
  }
1827
 
1828
  /**
1829
   * Return path to h5p package.
1830
   *
1831
   * Creates package if not already created
1832
   *
1833
   * @param array $content
1834
   * @return string
1835
   */
1836
  public function createExportFile($content) {
1837
 
1838
    // Get path to temporary folder, where export will be contained
1839
    $tmpPath = $this->h5pC->fs->getTmpPath();
1840
    mkdir($tmpPath, 0777, true);
1841
 
1842
    try {
1843
      // Create content folder and populate with files
1844
      $this->h5pC->fs->exportContent($content['id'], "{$tmpPath}/content");
1845
    }
1846
    catch (Exception $e) {
1847
      $this->h5pF->setErrorMessage($this->h5pF->t($e->getMessage()), 'failed-creating-export-file');
1848
      H5PCore::deleteFileTree($tmpPath);
1849
      return FALSE;
1850
    }
1851
 
1852
    // Update content.json with content from database
1853
    file_put_contents("{$tmpPath}/content/content.json", $content['filtered']);
1854
 
1855
    // Make embedType into an array
1856
    $embedTypes = explode(', ', $content['embedType']);
1857
 
1858
    // Build h5p.json, the en-/de-coding will ensure proper escaping
1859
    $h5pJson = array (
1860
      'title' => self::revertH5PEditorTextEscape($content['title']),
1861
      'language' => (isset($content['language']) && strlen(trim($content['language'])) !== 0) ? $content['language'] : 'und',
1862
      'mainLibrary' => $content['library']['name'],
1863
      'embedTypes' => $embedTypes
1864
    );
1865
 
1866
    foreach(array('authors', 'source', 'license', 'licenseVersion', 'licenseExtras' ,'yearFrom', 'yearTo', 'changes', 'authorComments', 'defaultLanguage') as $field) {
1867
      if (isset($content['metadata'][$field]) && $content['metadata'][$field] !== '') {
1868
        if (($field !== 'authors' && $field !== 'changes') || (!empty($content['metadata'][$field]))) {
1869
          $h5pJson[$field] = json_decode(json_encode($content['metadata'][$field], TRUE));
1870
        }
1871
      }
1872
    }
1873
 
1874
    // Remove all values that are not set
1875
    foreach ($h5pJson as $key => $value) {
1876
      if (!isset($value)) {
1877
        unset($h5pJson[$key]);
1878
      }
1879
    }
1880
 
1881
    // Add dependencies to h5p
1882
    foreach ($content['dependencies'] as $dependency) {
1883
      $library = $dependency['library'];
1884
 
1885
      try {
1886
        $exportFolder = NULL;
1887
 
1888
        // Determine path of export library
1889
        if (isset($this->h5pC) && isset($this->h5pC->h5pD)) {
1890
 
1891
          // Tries to find library in development folder
1892
          $isDevLibrary = $this->h5pC->h5pD->getLibrary(
1893
              $library['machineName'],
1894
              $library['majorVersion'],
1895
              $library['minorVersion']
1896
          );
1897
 
1898
          if ($isDevLibrary !== NULL && isset($library['path'])) {
1899
            $exportFolder = "/" . $library['path'];
1900
          }
1901
        }
1902
 
1903
        // Export required libraries
1904
        $this->h5pC->fs->exportLibrary($library, $tmpPath, $exportFolder);
1905
      }
1906
      catch (Exception $e) {
1907
        $this->h5pF->setErrorMessage($this->h5pF->t($e->getMessage()), 'failed-creating-export-file');
1908
        H5PCore::deleteFileTree($tmpPath);
1909
        return FALSE;
1910
      }
1911
 
1912
      // Do not add editor dependencies to h5p json.
1913
      if ($dependency['type'] === 'editor') {
1914
        continue;
1915
      }
1916
 
1917
      // Add to h5p.json dependencies
1918
      $h5pJson[$dependency['type'] . 'Dependencies'][] = array(
1919
        'machineName' => $library['machineName'],
1920
        'majorVersion' => $library['majorVersion'],
1921
        'minorVersion' => $library['minorVersion']
1922
      );
1923
    }
1924
 
1925
    // Save h5p.json
1926
    $results = print_r(json_encode($h5pJson), true);
1927
    file_put_contents("{$tmpPath}/h5p.json", $results);
1928
 
1929
    // Get a complete file list from our tmp dir
1930
    $files = array();
1931
    self::populateFileList($tmpPath, $files);
1932
 
1933
    // Get path to temporary export target file
1934
    $tmpFile = $this->h5pC->fs->getTmpPath();
1935
 
1936
    // Create new zip instance.
1937
    $zip = new ZipArchive();
1938
    $zip->open($tmpFile, ZipArchive::CREATE | ZipArchive::OVERWRITE);
1939
 
1940
    // Add all the files from the tmp dir.
1941
    foreach ($files as $file) {
1942
      // Please note that the zip format has no concept of folders, we must
1943
      // use forward slashes to separate our directories.
1944
      if (file_exists(realpath($file->absolutePath))) {
1945
        $zip->addFile(realpath($file->absolutePath), $file->relativePath);
1946
      }
1947
    }
1948
 
1949
    // Close zip and remove tmp dir
1950
    $zip->close();
1951
    H5PCore::deleteFileTree($tmpPath);
1952
 
1953
    $filename = $content['slug'] . '-' . $content['id'] . '.h5p';
1954
    try {
1955
      // Save export
1956
      $this->h5pC->fs->saveExport($tmpFile, $filename);
1957
    }
1958
    catch (Exception $e) {
1959
      $this->h5pF->setErrorMessage($this->h5pF->t($e->getMessage()), 'failed-creating-export-file');
1960
      return false;
1961
    }
1962
 
1963
    unlink($tmpFile);
1964
    $this->h5pF->afterExportCreated($content, $filename);
1965
 
1966
    return true;
1967
  }
1968
 
1969
  /**
1970
   * Recursive function the will add the files of the given directory to the
1971
   * given files list. All files are objects with an absolute path and
1972
   * a relative path. The relative path is forward slashes only! Great for
1973
   * use in zip files and URLs.
1974
   *
1975
   * @param string $dir path
1976
   * @param array $files list
1977
   * @param string $relative prefix. Optional
1978
   */
1979
  private static function populateFileList($dir, &$files, $relative = '') {
1980
    $strip = strlen($dir) + 1;
1981
    $contents = glob($dir . '/' . '*');
1982
    if (!empty($contents)) {
1983
      foreach ($contents as $file) {
1984
        $rel = $relative . substr($file, $strip);
1985
        if (is_dir($file)) {
1986
          self::populateFileList($file, $files, $rel . '/');
1987
        }
1988
        else {
1989
          $files[] = (object) array(
1990
            'absolutePath' => $file,
1991
            'relativePath' => $rel
1992
          );
1993
        }
1994
      }
1995
    }
1996
  }
1997
 
1998
  /**
1999
   * Delete .h5p file
2000
   *
2001
   * @param array $content object
2002
   */
2003
  public function deleteExport($content) {
2004
    $this->h5pC->fs->deleteExport(($content['slug'] ? $content['slug'] . '-' : '') . $content['id'] . '.h5p');
2005
  }
2006
 
2007
  /**
2008
   * Add editor libraries to the list of libraries
2009
   *
2010
   * These are not supposed to go into h5p.json, but must be included with the rest
2011
   * of the libraries
2012
   *
2013
   * TODO This is a private function that is not currently being used
2014
   *
2015
   * @param array $libraries
2016
   *  List of libraries keyed by machineName
2017
   * @param array $editorLibraries
2018
   *  List of libraries keyed by machineName
2019
   * @return array List of libraries keyed by machineName
2020
   */
2021
  private function addEditorLibraries($libraries, $editorLibraries) {
2022
    foreach ($editorLibraries as $editorLibrary) {
2023
      $libraries[$editorLibrary['machineName']] = $editorLibrary;
2024
    }
2025
    return $libraries;
2026
  }
2027
}
2028
 
2029
abstract class H5PPermission {
2030
  const DOWNLOAD_H5P = 0;
2031
  const EMBED_H5P = 1;
2032
  const CREATE_RESTRICTED = 2;
2033
  const UPDATE_LIBRARIES = 3;
2034
  const INSTALL_RECOMMENDED = 4;
2035
  const COPY_H5P = 8;
2036
}
2037
 
2038
abstract class H5PDisplayOptionBehaviour {
2039
  const NEVER_SHOW = 0;
2040
  const CONTROLLED_BY_AUTHOR_DEFAULT_ON = 1;
2041
  const CONTROLLED_BY_AUTHOR_DEFAULT_OFF = 2;
2042
  const ALWAYS_SHOW = 3;
2043
  const CONTROLLED_BY_PERMISSIONS = 4;
2044
}
2045
 
2046
abstract class H5PContentHubSyncStatus {
2047
  const NOT_SYNCED = 0;
2048
  const SYNCED = 1;
2049
  const WAITING = 2;
2050
  const FAILED = 3;
2051
}
2052
 
2053
abstract class H5PContentStatus {
2054
  const STATUS_UNPUBLISHED = 0;
2055
  const STATUS_DOWNLOADED = 1;
2056
  const STATUS_WAITING = 2;
2057
  const STATUS_FAILED_DOWNLOAD = 3;
2058
  const STATUS_FAILED_VALIDATION = 4;
2059
  const STATUS_SUSPENDED = 5;
2060
}
2061
 
2062
abstract class H5PHubEndpoints {
2063
  const CONTENT_TYPES = 'api.h5p.org/v1/content-types/';
2064
  const SITES = 'api.h5p.org/v1/sites';
2065
  const METADATA = 'hub-api.h5p.org/v1/metadata';
2066
  const CONTENT = 'hub-api.h5p.org/v1/contents';
2067
  const REGISTER = 'hub-api.h5p.org/v1/accounts';
2068
 
2069
  public static function createURL($endpoint) {
2070
    $protocol = (extension_loaded('openssl') ? 'https' : 'http');
2071
    return "{$protocol}://{$endpoint}";
2072
  }
2073
}
2074
 
2075
/**
2076
 * Functions and storage shared by the other H5P classes
2077
 */
2078
class H5PCore {
2079
 
2080
  public static $coreApi = array(
2081
    'majorVersion' => 1,
2082
    'minorVersion' => 26
2083
  );
2084
  public static $styles = array(
2085
    'styles/h5p.css',
2086
    'styles/h5p-confirmation-dialog.css',
2087
    'styles/h5p-core-button.css',
2088
    'styles/h5p-tooltip.css',
2089
  );
2090
  public static $scripts = array(
2091
    'js/jquery.js',
2092
    'js/h5p.js',
2093
    'js/h5p-event-dispatcher.js',
2094
    'js/h5p-x-api-event.js',
2095
    'js/h5p-x-api.js',
2096
    'js/h5p-content-type.js',
2097
    'js/h5p-confirmation-dialog.js',
2098
    'js/h5p-action-bar.js',
2099
    'js/request-queue.js',
2100
    'js/h5p-tooltip.js',
2101
  );
2102
  public static $adminScripts = array(
2103
    'js/jquery.js',
2104
    'js/h5p-utils.js',
2105
  );
2106
 
2107
  public static $defaultContentWhitelist = 'json png jpg jpeg gif bmp tif tiff svg eot ttf woff woff2 otf webm mp4 ogg mp3 m4a wav txt pdf rtf doc docx xls xlsx ppt pptx odt ods odp xml csv diff patch swf md textile vtt webvtt gltf glb';
2108
  public static $defaultLibraryWhitelistExtras = 'js css';
2109
 
2110
  public $librariesJsonData, $contentJsonData, $mainJsonData, $h5pF, $fs, $h5pD, $disableFileCheck;
2111
  const SECONDS_IN_WEEK = 604800;
2112
 
2113
  private $exportEnabled;
2114
 
2115
  // Disable flags
2116
  const DISABLE_NONE = 0;
2117
  const DISABLE_FRAME = 1;
2118
  const DISABLE_DOWNLOAD = 2;
2119
  const DISABLE_EMBED = 4;
2120
  const DISABLE_COPYRIGHT = 8;
2121
  const DISABLE_ABOUT = 16;
2122
 
2123
  const DISPLAY_OPTION_FRAME = 'frame';
2124
  const DISPLAY_OPTION_DOWNLOAD = 'export';
2125
  const DISPLAY_OPTION_EMBED = 'embed';
2126
  const DISPLAY_OPTION_COPYRIGHT = 'copyright';
2127
  const DISPLAY_OPTION_ABOUT = 'icon';
2128
  const DISPLAY_OPTION_COPY = 'copy';
2129
 
2130
  // Map flags to string
2131
  public static $disable = array(
2132
    self::DISABLE_FRAME => self::DISPLAY_OPTION_FRAME,
2133
    self::DISABLE_DOWNLOAD => self::DISPLAY_OPTION_DOWNLOAD,
2134
    self::DISABLE_EMBED => self::DISPLAY_OPTION_EMBED,
2135
    self::DISABLE_COPYRIGHT => self::DISPLAY_OPTION_COPYRIGHT
2136
  );
2137
 
2138
  /** @var string */
2139
  public $url;
2140
 
2141
  /** @var int evelopment mode. */
2142
  public $development_mode;
2143
 
2144
  /** @var bool aggregated files for assets. */
2145
  public $aggregateAssets;
2146
 
2147
  /** @var string full path of plugin. */
2148
  protected $fullPluginPath;
2149
 
2150
  /** @var string regex for converting copied files paths. */
2151
  public $relativePathRegExp;
2152
 
2153
  /**
2154
   * Constructor for the H5PCore
2155
   *
2156
   * @param H5PFrameworkInterface $H5PFramework
2157
   *  The frameworks implementation of the H5PFrameworkInterface
2158
   * @param string|H5PFileStorage $path H5P file storage directory or class.
2159
   * @param string $url To file storage directory.
2160
   * @param string $language code. Defaults to english.
2161
   * @param boolean $export enabled?
2162
   */
2163
  public function __construct(H5PFrameworkInterface $H5PFramework, $path, $url, $language = 'en', $export = FALSE) {
2164
    $this->h5pF = $H5PFramework;
2165
 
2166
    $this->fs = ($path instanceof H5PFileStorage ? $path : new H5PDefaultStorage($path));
2167
 
2168
    $this->url = $url;
2169
    $this->exportEnabled = $export;
2170
    $this->development_mode = H5PDevelopment::MODE_NONE;
2171
 
2172
    $this->aggregateAssets = FALSE; // Off by default.. for now
2173
 
2174
    $this->detectSiteType();
2175
    $this->fullPluginPath = preg_replace('/\/[^\/]+[\/]?$/', '' , dirname(__FILE__));
2176
 
2177
    // Standard regex for converting copied files paths
2178
    $this->relativePathRegExp = '/^((\.\.\/){1,2})(.*content\/)?(\d+|editor)\/(.+)$/';
2179
  }
2180
 
2181
  /**
2182
   * Save content and clear cache.
2183
   *
2184
   * @param array $content
2185
   * @param null|int $contentMainId
2186
   * @return int Content ID
2187
   */
2188
  public function saveContent($content, $contentMainId = NULL) {
2189
    if (isset($content['id'])) {
2190
      $this->h5pF->updateContent($content, $contentMainId);
2191
    }
2192
    else {
2193
      $content['id'] = $this->h5pF->insertContent($content, $contentMainId);
2194
    }
2195
 
2196
    // Some user data for content has to be reset when the content changes.
2197
    $this->h5pF->resetContentUserData($contentMainId ? $contentMainId : $content['id']);
2198
 
2199
    return $content['id'];
2200
  }
2201
 
2202
  /**
2203
   * Load content.
2204
   *
2205
   * @param int $id for content.
2206
   * @return object
2207
   */
2208
  public function loadContent($id) {
2209
    $content = $this->h5pF->loadContent($id);
2210
 
2211
    if ($content !== NULL) {
2212
      // Validate main content's metadata
2213
      $validator = new H5PContentValidator($this->h5pF, $this);
2214
      $content['metadata'] = $validator->validateMetadata($content['metadata']);
2215
 
2216
      $content['library'] = array(
2217
        'id' => $content['libraryId'],
2218
        'name' => $content['libraryName'],
2219
        'majorVersion' => $content['libraryMajorVersion'],
2220
        'minorVersion' => $content['libraryMinorVersion'],
2221
        'embedTypes' => $content['libraryEmbedTypes'],
2222
        'fullscreen' => $content['libraryFullscreen'],
2223
      );
2224
      unset($content['libraryId'], $content['libraryName'], $content['libraryEmbedTypes'], $content['libraryFullscreen']);
2225
 
2226
//      // TODO: Move to filterParameters?
2227
//      if (isset($this->h5pD)) {
2228
//        // TODO: Remove Drupal specific stuff
2229
//        $json_content_path = file_create_path(file_directory_path() . '/' . variable_get('h5p_default_path', 'h5p') . '/content/' . $id . '/content.json');
2230
//        if (file_exists($json_content_path) === TRUE) {
2231
//          $json_content = file_get_contents($json_content_path);
2232
//          if (json_decode($json_content, TRUE) !== FALSE) {
2233
//            drupal_set_message(t('Invalid json in json content'), 'warning');
2234
//          }
2235
//          $content['params'] = $json_content;
2236
//        }
2237
//      }
2238
    }
2239
 
2240
    return $content;
2241
  }
2242
 
2243
  /**
2244
   * Filter content run parameters, rebuild content dependency cache and export file.
2245
   *
2246
   * @param Object|array $content
2247
   * @return Object NULL on failure.
2248
   */
2249
  public function filterParameters(&$content) {
2250
    if (!empty($content['filtered']) &&
2251
        (!$this->exportEnabled ||
2252
         ($content['slug'] &&
2253
          $this->fs->hasExport($content['slug'] . '-' . $content['id'] . '.h5p')))) {
2254
      return $content['filtered'];
2255
    }
2256
 
2257
    if (!(isset($content['library']) && isset($content['params']))) {
2258
      return NULL;
2259
    }
2260
 
2261
    // Validate and filter against main library semantics.
2262
    $validator = new H5PContentValidator($this->h5pF, $this);
2263
    $params = (object) array(
2264
      'library' => H5PCore::libraryToString($content['library']),
2265
      'params' => json_decode($content['params'])
2266
    );
2267
    if (!$params->params) {
2268
      return NULL;
2269
    }
2270
    $validator->validateLibrary($params, (object) array('options' => array($params->library)));
2271
 
2272
    // Handle addons:
2273
    $addons = $this->h5pF->loadAddons();
2274
    foreach ($addons as $addon) {
2275
      $add_to = json_decode($addon['addTo']);
2276
 
2277
      if (isset($add_to->content->types)) {
2278
        foreach($add_to->content->types as $type) {
2279
 
2280
          if (isset($type->text->regex) &&
2281
              $this->textAddonMatches($params->params, $type->text->regex)) {
2282
            $validator->addon($addon);
2283
 
2284
            // An addon shall only be added once
2285
            break;
2286
          }
2287
        }
2288
      }
2289
    }
2290
 
2291
    $params = json_encode($params->params);
2292
 
2293
    // Update content dependencies.
2294
    $content['dependencies'] = $validator->getDependencies();
2295
 
2296
    // Sometimes the parameters are filtered before content has been created
2297
    if ($content['id']) {
2298
      $this->h5pF->deleteLibraryUsage($content['id']);
2299
      $this->h5pF->saveLibraryUsage($content['id'], $content['dependencies']);
2300
 
2301
      if (!$content['slug']) {
2302
        $content['slug'] = $this->generateContentSlug($content);
2303
 
2304
        // Remove old export file
2305
        $this->fs->deleteExport($content['id'] . '.h5p');
2306
      }
2307
 
2308
      if ($this->exportEnabled) {
2309
        // Recreate export file
2310
        $exporter = new H5PExport($this->h5pF, $this);
2311
        $content['filtered'] = $params;
2312
        $exporter->createExportFile($content);
2313
      }
2314
 
2315
      // Cache.
2316
      $this->h5pF->updateContentFields($content['id'], array(
2317
        'filtered' => $params,
2318
        'slug' => $content['slug']
2319
      ));
2320
    }
2321
    return $params;
2322
  }
2323
 
2324
  /**
2325
   * Retrieve a value from a nested mixed array structure.
2326
   *
2327
   * @param Array $params Array to be looked in.
2328
   * @param String $path Supposed path to the value.
2329
   * @param String [$delimiter='.'] Property delimiter within the path.
2330
   * @return Object|NULL The object found or NULL.
2331
   */
2332
  private function retrieveValue ($params, $path, $delimiter='.') {
2333
    $path = explode($delimiter, $path);
2334
 
2335
    // Property not found
2336
    if (!isset($params[$path[0]])) {
2337
      return NULL;
2338
    }
2339
 
2340
    $first = $params[$path[0]];
2341
 
2342
    // End of path, done
2343
    if (sizeof($path) === 1) {
2344
      return $first;
2345
    }
2346
 
2347
    // We cannot go deeper
2348
    if (!is_array($first)) {
2349
      return NULL;
2350
    }
2351
 
2352
    // Regular Array
2353
    if (isset($first[0])) {
2354
      foreach($first as $number => $object) {
2355
        $found = $this->retrieveValue($object, implode($delimiter, array_slice($path, 1)));
2356
        if (isset($found)) {
2357
          return $found;
2358
        }
2359
      }
2360
      return NULL;
2361
    }
2362
 
2363
    // Associative Array
2364
    return $this->retrieveValue($first, implode('.', array_slice($path, 1)));
2365
  }
2366
 
2367
  /**
2368
   * Determine if params contain any match.
2369
   *
2370
   * @param {object} params - Parameters.
2371
   * @param {string} [pattern] - Regular expression to identify pattern.
2372
   * @param {boolean} [found] - Used for recursion.
2373
   * @return {boolean} True, if params matches pattern.
2374
   */
2375
  private function textAddonMatches($params, $pattern, $found = false) {
2376
    $type = gettype($params);
2377
    if ($type === 'string') {
2378
      if (preg_match($pattern, $params) === 1) {
2379
        return true;
2380
      }
2381
    }
2382
    elseif ($type === 'array' || $type === 'object') {
2383
      foreach ($params as $value) {
2384
        $found = $this->textAddonMatches($value, $pattern, $found);
2385
        if ($found === true) {
2386
          return true;
2387
        }
2388
      }
2389
    }
2390
    return false;
2391
  }
2392
 
2393
  /**
2394
   * Generate content slug
2395
   *
2396
   * @param array $content object
2397
   * @return string unique content slug
2398
   */
2399
  private function generateContentSlug($content) {
2400
    $slug = H5PCore::slugify($content['title']);
2401
 
2402
    $available = NULL;
2403
    while (!$available) {
2404
      if ($available === FALSE) {
2405
        // If not available, add number suffix.
2406
        $matches = array();
2407
        if (preg_match('/(.+-)([0-9]+)$/', $slug, $matches)) {
2408
          $slug = $matches[1] . (intval($matches[2]) + 1);
2409
        }
2410
        else {
2411
          $slug .=  '-2';
2412
        }
2413
      }
2414
      $available = $this->h5pF->isContentSlugAvailable($slug);
2415
    }
2416
 
2417
    return $slug;
2418
  }
2419
 
2420
  /**
2421
   * Find the files required for this content to work.
2422
   *
2423
   * @param int $id for content.
2424
   * @param null $type
2425
   * @return array
2426
   */
2427
  public function loadContentDependencies($id, $type = NULL) {
2428
    $dependencies = $this->h5pF->loadContentDependencies($id, $type);
2429
 
2430
    if (isset($this->h5pD)) {
2431
      $developmentLibraries = $this->h5pD->getLibraries();
2432
 
2433
      foreach ($dependencies as $key => $dependency) {
2434
        $libraryString = H5PCore::libraryToString($dependency);
2435
        if (isset($developmentLibraries[$libraryString])) {
2436
          $developmentLibraries[$libraryString]['dependencyType'] = $dependencies[$key]['dependencyType'];
2437
          $dependencies[$key] = $developmentLibraries[$libraryString];
2438
        }
2439
      }
2440
    }
2441
 
2442
    return $dependencies;
2443
  }
2444
 
2445
  /**
2446
   * Get all dependency assets of the given type
2447
   *
2448
   * @param array $dependency
2449
   * @param string $type
2450
   * @param array $assets
2451
   * @param string $prefix Optional. Make paths relative to another dir.
2452
   */
2453
  private function getDependencyAssets($dependency, $type, &$assets, $prefix = '') {
2454
    // Check if dependency has any files of this type
2455
    if (empty($dependency[$type]) || $dependency[$type][0] === '') {
2456
      return;
2457
    }
2458
 
2459
    // Check if we should skip CSS.
2460
    if ($type === 'preloadedCss' && (isset($dependency['dropCss']) && $dependency['dropCss'] === '1')) {
2461
      return;
2462
    }
2463
    foreach ($dependency[$type] as $file) {
2464
      $assets[] = (object) array(
2465
        'path' => $prefix . '/' . $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file),
2466
        'version' => $dependency['version']
2467
      );
2468
    }
2469
  }
2470
 
2471
  /**
2472
   * Combines path with cache buster / version.
2473
   *
2474
   * @param array $assets
2475
   * @return array
2476
   */
2477
  public function getAssetsUrls($assets) {
2478
    $urls = array();
2479
 
2480
    foreach ($assets as $asset) {
2481
      $url = $asset->path;
2482
 
2483
      // Add URL prefix if not external
2484
      if (strpos($asset->path, '://') === FALSE) {
2485
        $url = $this->url . $url;
2486
      }
2487
 
2488
      // Add version/cache buster if set
2489
      if (isset($asset->version)) {
2490
        $url .= $asset->version;
2491
      }
2492
 
2493
      $urls[] = $url;
2494
    }
2495
 
2496
    return $urls;
2497
  }
2498
 
2499
  /**
2500
   * Return file paths for all dependencies files.
2501
   *
2502
   * @param array $dependencies
2503
   * @param string $prefix Optional. Make paths relative to another dir.
2504
   * @return array files.
2505
   */
2506
  public function getDependenciesFiles($dependencies, $prefix = '') {
2507
    // Build files list for assets
2508
    $files = array(
2509
      'scripts' => array(),
2510
      'styles' => array()
2511
    );
2512
 
2513
    $key = null;
2514
 
2515
    // Avoid caching empty files
2516
    if (empty($dependencies)) {
2517
      return $files;
2518
    }
2519
 
2520
    if ($this->aggregateAssets) {
2521
      // Get aggregated files for assets
2522
      $key = self::getDependenciesHash($dependencies);
2523
 
2524
      $cachedAssets = $this->fs->getCachedAssets($key);
2525
      if ($cachedAssets !== NULL) {
2526
        return array_merge($files, $cachedAssets); // Using cached assets
2527
      }
2528
    }
2529
 
2530
    // Using content dependencies
2531
    foreach ($dependencies as $dependency) {
2532
      if (isset($dependency['path']) === FALSE) {
2533
        $dependency['path'] = $this->getDependencyPath($dependency);
2534
        $dependency['preloadedJs'] = explode(',', $dependency['preloadedJs']);
2535
        $dependency['preloadedCss'] = explode(',', $dependency['preloadedCss']);
2536
      }
2537
      $dependency['version'] = "?ver={$dependency['majorVersion']}.{$dependency['minorVersion']}.{$dependency['patchVersion']}";
2538
      $this->getDependencyAssets($dependency, 'preloadedJs', $files['scripts'], $prefix);
2539
      $this->getDependencyAssets($dependency, 'preloadedCss', $files['styles'], $prefix);
2540
    }
2541
 
2542
    if ($this->aggregateAssets) {
2543
      // Aggregate and store assets
2544
      $this->fs->cacheAssets($files, $key);
2545
 
2546
      // Keep track of which libraries have been cached in case they are updated
2547
      $this->h5pF->saveCachedAssets($key, $dependencies);
2548
    }
2549
 
2550
    return $files;
2551
  }
2552
 
2553
  /**
2554
   * Get the path to the dependency.
2555
   *
2556
   * @param array $dependency
2557
   * @return string
2558
   */
2559
  protected function getDependencyPath(array $dependency) {
2560
    return 'libraries/' . H5PCore::libraryToFolderName($dependency);
2561
  }
2562
 
2563
  private static function getDependenciesHash(&$dependencies) {
2564
    // Build hash of dependencies
2565
    $toHash = array();
2566
 
2567
    // Use unique identifier for each library version
2568
    foreach ($dependencies as $dep) {
2569
      $toHash[] = "{$dep['machineName']}-{$dep['majorVersion']}.{$dep['minorVersion']}.{$dep['patchVersion']}";
2570
    }
2571
 
2572
    // Sort in case the same dependencies comes in a different order
2573
    sort($toHash);
2574
 
2575
    // Calculate hash sum
2576
    return hash('sha1', implode('', $toHash));
2577
  }
2578
 
2579
  /**
2580
   * Load library semantics.
2581
   *
2582
   * @param $name
2583
   * @param $majorVersion
2584
   * @param $minorVersion
2585
   * @return string
2586
   */
2587
  public function loadLibrarySemantics($name, $majorVersion, $minorVersion) {
2588
    $semantics = NULL;
2589
    if (isset($this->h5pD)) {
2590
      // Try to load from dev lib
2591
      $semantics = $this->h5pD->getSemantics($name, $majorVersion, $minorVersion);
2592
    }
2593
 
2594
    if ($semantics === NULL) {
2595
      // Try to load from DB.
2596
      $semantics = $this->h5pF->loadLibrarySemantics($name, $majorVersion, $minorVersion);
2597
    }
2598
 
2599
    if ($semantics !== NULL) {
2600
      $semantics = json_decode($semantics);
2601
      $this->h5pF->alterLibrarySemantics($semantics, $name, $majorVersion, $minorVersion);
2602
    }
2603
 
2604
    return $semantics;
2605
  }
2606
 
2607
  /**
2608
   * Load library.
2609
   *
2610
   * @param $name
2611
   * @param $majorVersion
2612
   * @param $minorVersion
2613
   * @return array or null.
2614
   */
2615
  public function loadLibrary($name, $majorVersion, $minorVersion) {
2616
    $library = NULL;
2617
    if (isset($this->h5pD)) {
2618
      // Try to load from dev
2619
      $library = $this->h5pD->getLibrary($name, $majorVersion, $minorVersion);
2620
      if ($library !== NULL) {
2621
        $library['semantics'] = $this->h5pD->getSemantics($name, $majorVersion, $minorVersion);
2622
      }
2623
    }
2624
 
2625
    if ($library === NULL) {
2626
      // Try to load from DB.
2627
      $library = $this->h5pF->loadLibrary($name, $majorVersion, $minorVersion);
2628
    }
2629
 
2630
    return $library;
2631
  }
2632
 
2633
  /**
2634
   * Deletes a library
2635
   *
2636
   * @param stdClass $libraryId
2637
   */
2638
  public function deleteLibrary($libraryId) {
2639
    $this->h5pF->deleteLibrary($libraryId);
2640
  }
2641
 
2642
  /**
2643
   * Recursive. Goes through the dependency tree for the given library and
2644
   * adds all the dependencies to the given array in a flat format.
2645
   *
2646
   * @param $dependencies
2647
   * @param array $library To find all dependencies for.
2648
   * @param int $nextWeight An integer determining the order of the libraries
2649
   *  when they are loaded
2650
   * @param bool $editor Used internally to force all preloaded sub dependencies
2651
   *  of an editor dependency to be editor dependencies.
2652
   * @return int
2653
   */
2654
  public function findLibraryDependencies(&$dependencies, $library, $nextWeight = 1, $editor = FALSE) {
2655
    foreach (array('dynamic', 'preloaded', 'editor') as $type) {
2656
      $property = $type . 'Dependencies';
2657
      if (!isset($library[$property])) {
2658
        continue; // Skip, no such dependencies.
2659
      }
2660
 
2661
      if ($type === 'preloaded' && $editor === TRUE) {
2662
        // All preloaded dependencies of an editor library is set to editor.
2663
        $type = 'editor';
2664
      }
2665
 
2666
      foreach ($library[$property] as $dependency) {
2667
        $dependencyKey = $type . '-' . $dependency['machineName'];
2668
        if (isset($dependencies[$dependencyKey]) === TRUE) {
2669
          continue; // Skip, already have this.
2670
        }
2671
 
2672
        $dependencyLibrary = $this->loadLibrary($dependency['machineName'], $dependency['majorVersion'], $dependency['minorVersion']);
2673
        if ($dependencyLibrary) {
2674
          $dependencies[$dependencyKey] = array(
2675
            'library' => $dependencyLibrary,
2676
            'type' => $type
2677
          );
2678
          $nextWeight = $this->findLibraryDependencies($dependencies, $dependencyLibrary, $nextWeight, $type === 'editor');
2679
          $dependencies[$dependencyKey]['weight'] = $nextWeight++;
2680
        }
2681
        else {
2682
          // This site is missing a dependency!
2683
          $this->h5pF->setErrorMessage($this->h5pF->t('Missing dependency @dep required by @lib.', array('@dep' => H5PCore::libraryToString($dependency), '@lib' => H5PCore::libraryToString($library))), 'missing-library-dependency');
2684
        }
2685
      }
2686
    }
2687
    return $nextWeight;
2688
  }
2689
 
2690
  /**
2691
   * Check if a library is of the version we're looking for
2692
   *
2693
   * Same version means that the majorVersion and minorVersion is the same
2694
   *
2695
   * @param array $library
2696
   *  Data from library.json
2697
   * @param array $dependency
2698
   *  Definition of what library we're looking for
2699
   * @return boolean
2700
   *  TRUE if the library is the same version as the dependency
2701
   *  FALSE otherwise
2702
   */
2703
  public function isSameVersion($library, $dependency) {
2704
    if ($library['machineName'] != $dependency['machineName']) {
2705
      return FALSE;
2706
    }
2707
    if ($library['majorVersion'] != $dependency['majorVersion']) {
2708
      return FALSE;
2709
    }
2710
    if ($library['minorVersion'] != $dependency['minorVersion']) {
2711
      return FALSE;
2712
    }
2713
    return TRUE;
2714
  }
2715
 
2716
  /**
2717
   * Recursive function for removing directories.
2718
   *
2719
   * @param string $dir
2720
   *  Path to the directory we'll be deleting
2721
   * @return boolean
2722
   *  Indicates if the directory existed.
2723
   */
2724
  public static function deleteFileTree($dir) {
2725
    if (!is_dir($dir)) {
2726
      return false;
2727
    }
2728
    if (is_link($dir)) {
2729
      // Do not traverse and delete linked content, simply unlink.
2730
      unlink($dir);
2731
      return;
2732
    }
2733
    $files = array_diff(scandir($dir), array('.','..'));
2734
    foreach ($files as $file) {
2735
      $filepath = "$dir/$file";
2736
      // Note that links may resolve as directories
2737
      if (!is_dir($filepath) || is_link($filepath)) {
2738
        // Unlink files and links
2739
        unlink($filepath);
2740
      }
2741
      else {
2742
        // Traverse subdir and delete files
2743
        self::deleteFileTree($filepath);
2744
      }
2745
    }
2746
    return rmdir($dir);
2747
  }
2748
 
2749
  /**
2750
   * Writes library data as string on the form {machineName} {majorVersion}.{minorVersion}
2751
   *
2752
   * @param array $library
2753
   *  With keys (machineName and/or name), majorVersion and minorVersion
2754
   * @return string
2755
   *  On the form {machineName} {majorVersion}.{minorVersion}
2756
   */
2757
  public static function libraryToString($library) {
2758
    $name = $library['machineName'] ?? $library['name'];
2759
 
2760
    return "{$name} {$library['majorVersion']}.{$library['minorVersion']}";
2761
  }
2762
 
2763
  /**
2764
   * Get the name of a library's folder name
2765
   *
2766
   * @return string
2767
   */
2768
  public static function libraryToFolderName($library) {
2769
    $name = $library['machineName'] ?? $library['name'];
2770
    $includePatchVersion = $library['patchVersionInFolderName'] ?? false;
2771
 
2772
    return "{$name}-{$library['majorVersion']}.{$library['minorVersion']}" . ($includePatchVersion ? ".{$library['patchVersion']}" : '');
2773
  }
2774
 
2775
  /**
2776
   * Parses library data from a string on the form {machineName} {majorVersion}.{minorVersion}
2777
   *
2778
   * @param string $libraryString
2779
   *  On the form {machineName} {majorVersion}.{minorVersion}
2780
   * @return array|FALSE
2781
   *  With keys machineName, majorVersion and minorVersion.
2782
   *  Returns FALSE only if string is not parsable in the normal library
2783
   *  string formats "Lib.Name-x.y" or "Lib.Name x.y"
2784
   */
2785
  public static function libraryFromString($libraryString) {
2786
    $re = '/^([\w0-9\-\.]{1,255})[\-\ ]([0-9]{1,5})\.([0-9]{1,5})$/i';
2787
    $matches = array();
2788
    $res = preg_match($re, $libraryString, $matches);
2789
    if ($res) {
2790
      return array(
2791
        'machineName' => $matches[1],
2792
        'majorVersion' => $matches[2],
2793
        'minorVersion' => $matches[3]
2794
      );
2795
    }
2796
    return FALSE;
2797
  }
2798
 
2799
  /**
2800
   * Determine the correct embed type to use.
2801
   *
2802
   * @param $contentEmbedType
2803
   * @param $libraryEmbedTypes
2804
   * @return string 'div' or 'iframe'.
2805
   */
2806
  public static function determineEmbedType($contentEmbedType, $libraryEmbedTypes) {
2807
    // Detect content embed type
2808
    $embedType = strpos(strtolower($contentEmbedType), 'div') !== FALSE ? 'div' : 'iframe';
2809
 
2810
    if ($libraryEmbedTypes !== NULL && $libraryEmbedTypes !== '') {
2811
      // Check that embed type is available for library
2812
      $embedTypes = strtolower($libraryEmbedTypes);
2813
      if (strpos($embedTypes, $embedType) === FALSE) {
2814
        // Not available, pick default.
2815
        $embedType = strpos($embedTypes, 'div') !== FALSE ? 'div' : 'iframe';
2816
      }
2817
    }
2818
 
2819
    return $embedType;
2820
  }
2821
 
2822
  /**
2823
   * Get the absolute version for the library as a human readable string.
2824
   *
2825
   * @param object $library
2826
   * @return string
2827
   */
2828
  public static function libraryVersion($library) {
2829
    return $library->major_version . '.' . $library->minor_version . '.' . $library->patch_version;
2830
  }
2831
 
2832
  /**
2833
   * Determine which versions content with the given library can be upgraded to.
2834
   *
2835
   * @param object $library
2836
   * @param array $versions
2837
   * @return array
2838
   */
2839
  public function getUpgrades($library, $versions) {
2840
   $upgrades = array();
2841
 
2842
   foreach ($versions as $upgrade) {
2843
     if ($upgrade->major_version > $library->major_version || $upgrade->major_version === $library->major_version && $upgrade->minor_version > $library->minor_version) {
2844
       $upgrades[$upgrade->id] = H5PCore::libraryVersion($upgrade);
2845
     }
2846
   }
2847
 
2848
   return $upgrades;
2849
  }
2850
 
2851
  /**
2852
   * Converts all the properties of the given object or array from
2853
   * snake_case to camelCase. Useful after fetching data from the database.
2854
   *
2855
   * Note that some databases does not support camelCase.
2856
   *
2857
   * @param mixed $arr input
2858
   * @param boolean $obj return object
2859
   * @return mixed object or array
2860
   */
2861
  public static function snakeToCamel($arr, $obj = false) {
2862
    $newArr = array();
2863
 
2864
    foreach ($arr as $key => $val) {
2865
      $next = -1;
2866
      while (($next = strpos($key, '_', $next + 1)) !== FALSE) {
2867
        $key = substr_replace($key, strtoupper($key[$next + 1]), $next, 2);
2868
      }
2869
 
2870
      $newArr[$key] = $val;
2871
    }
2872
 
2873
    return $obj ? (object) $newArr : $newArr;
2874
  }
2875
 
2876
  /**
2877
   * Detects if the site was accessed from localhost,
2878
   * through a local network or from the internet.
2879
   */
2880
  public function detectSiteType() {
2881
    $type = $this->h5pF->getOption('site_type', 'local');
2882
 
2883
    // Determine remote/visitor origin
2884
    if ($type === 'network' ||
2885
        ($type === 'local' &&
2886
         isset($_SERVER['REMOTE_ADDR']) &&
2887
         !preg_match('/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/i', $_SERVER['REMOTE_ADDR']))) {
2888
      if (isset($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) {
2889
        // Internet
2890
        $this->h5pF->setOption('site_type', 'internet');
2891
      }
2892
      elseif ($type === 'local') {
2893
        // Local network
2894
        $this->h5pF->setOption('site_type', 'network');
2895
      }
2896
    }
2897
  }
2898
 
2899
  /**
2900
   * Get a list of installed libraries, different minor versions will
2901
   * return separate entries.
2902
   *
2903
   * @return array
2904
   *  A distinct array of installed libraries
2905
   */
2906
  public function getLibrariesInstalled() {
2907
    $librariesInstalled = array();
2908
    $libs = $this->h5pF->loadLibraries();
2909
 
2910
    foreach($libs as $libName => $library) {
2911
      foreach($library as $libVersion) {
2912
        $librariesInstalled[$libName.' '.$libVersion->major_version.'.'.$libVersion->minor_version] = $libVersion->patch_version;
2913
      }
2914
    }
2915
 
2916
    return $librariesInstalled;
2917
  }
2918
 
2919
  /**
2920
   * Easy way to combine similar data sets.
2921
   *
2922
   * @param array $inputs Multiple arrays with data
2923
   * @return array
2924
   */
2925
  public function combineArrayValues($inputs) {
2926
    $results = array();
2927
    foreach ($inputs as $index => $values) {
2928
      foreach ($values as $key => $value) {
2929
        $results[$key][$index] = $value;
2930
      }
2931
    }
2932
    return $results;
2933
  }
2934
 
2935
  /**
2936
   * Communicate with H5P.org and get content type cache. Each platform
2937
   * implementation is responsible for invoking this, eg using cron
2938
   *
2939
   * @param bool $fetchingDisabled
2940
   * @param bool $onlyRegister Only register site with H5P.org
2941
   *
2942
   * @return bool|object Returns endpoint data if found, otherwise FALSE
2943
   */
2944
  public function fetchLibrariesMetadata($fetchingDisabled = FALSE, $onlyRegister = false) {
2945
    // Gather data
2946
    $uuid = $this->h5pF->getOption('site_uuid', '');
2947
    $platform = $this->h5pF->getPlatformInfo();
2948
    $registrationData = array(
2949
      'uuid' => $uuid,
2950
      'platform_name' => $platform['name'],
2951
      'platform_version' => $platform['version'],
2952
      'h5p_version' => $platform['h5pVersion'],
2953
      'disabled' => $fetchingDisabled ? 1 : 0,
2954
      'local_id' => hash('crc32', $this->fullPluginPath),
2955
      'type' => $this->h5pF->getOption('site_type', 'local'),
2956
      'core_api_version' => H5PCore::$coreApi['majorVersion'] . '.' .
2957
                            H5PCore::$coreApi['minorVersion']
2958
    );
2959
 
2960
    // Register site if it is not registered
2961
    if (empty($uuid)) {
2962
      $registration = $this->h5pF->fetchExternalData(H5PHubEndpoints::createURL(H5PHubEndpoints::SITES), $registrationData);
2963
 
2964
      // Failed retrieving uuid
2965
      if (!$registration) {
2966
        $errorMessage = $this->h5pF->t('Site could not be registered with the hub. Please contact your site administrator.');
2967
        $this->h5pF->setErrorMessage($errorMessage);
2968
        $this->h5pF->setErrorMessage(
2969
          $this->h5pF->t('The H5P Hub has been disabled until this problem can be resolved. You may still upload libraries through the "H5P Libraries" page.'),
2970
          'registration-failed-hub-disabled'
2971
        );
2972
        return FALSE;
2973
      }
2974
 
2975
      // Successfully retrieved new uuid
2976
      $json = json_decode($registration);
2977
      $registrationData['uuid'] = $json->uuid;
2978
      $this->h5pF->setOption('site_uuid', $json->uuid);
2979
      $this->h5pF->setInfoMessage(
2980
        $this->h5pF->t('Your site was successfully registered with the H5P Hub.')
2981
      );
2982
      $uuid = $json->uuid;
2983
      // TODO: Uncomment when key is once again available in H5P Settings
2984
//      $this->h5pF->setInfoMessage(
2985
//        $this->h5pF->t('You have been provided a unique key that identifies you with the Hub when receiving new updates. The key is available for viewing in the "H5P Settings" page.')
2986
//      );
2987
    }
2988
 
2989
    if ($onlyRegister) {
2990
      return $uuid;
2991
    }
2992
 
2993
    if ($this->h5pF->getOption('send_usage_statistics', TRUE)) {
2994
      $siteData = array_merge(
2995
        $registrationData,
2996
        array(
2997
          'num_authors' => $this->h5pF->getNumAuthors(),
2998
          'libraries'   => json_encode($this->combineArrayValues(array(
2999
            'patch'            => $this->getLibrariesInstalled(),
3000
            'content'          => $this->h5pF->getLibraryContentCount(),
3001
            'loaded'           => $this->h5pF->getLibraryStats('library'),
3002
            'created'          => $this->h5pF->getLibraryStats('content create'),
3003
            'createdUpload'    => $this->h5pF->getLibraryStats('content create upload'),
3004
            'deleted'          => $this->h5pF->getLibraryStats('content delete'),
3005
            'resultViews'      => $this->h5pF->getLibraryStats('results content'),
3006
            'shortcodeInserts' => $this->h5pF->getLibraryStats('content shortcode insert')
3007
          )))
3008
        )
3009
      );
3010
    }
3011
    else {
3012
      $siteData = $registrationData;
3013
    }
3014
 
3015
    $result = $this->updateContentTypeCache($siteData);
3016
 
3017
    // No data received
3018
    if (!$result || empty($result)) {
3019
      return FALSE;
3020
    }
3021
 
3022
    // Handle libraries metadata
3023
    if (isset($result->libraries)) {
3024
      foreach ($result->libraries as $library) {
3025
        if (isset($library->tutorialUrl) && isset($library->machineName)) {
3026
          $this->h5pF->setLibraryTutorialUrl($library->machineNamee, $library->tutorialUrl);
3027
        }
3028
      }
3029
    }
3030
 
3031
    return $result;
3032
  }
3033
 
3034
  /**
3035
   * Create representation of display options as int
3036
   *
3037
   * @param array $sources
3038
   * @param int $current
3039
   * @return int
3040
   */
3041
  public function getStorableDisplayOptions(&$sources, $current) {
3042
    // Download - force setting it if always on or always off
3043
    $download = $this->h5pF->getOption(self::DISPLAY_OPTION_DOWNLOAD, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3044
    if ($download == H5PDisplayOptionBehaviour::ALWAYS_SHOW ||
3045
        $download == H5PDisplayOptionBehaviour::NEVER_SHOW) {
3046
      $sources[self::DISPLAY_OPTION_DOWNLOAD] = ($download == H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3047
    }
3048
 
3049
    // Embed - force setting it if always on or always off
3050
    $embed = $this->h5pF->getOption(self::DISPLAY_OPTION_EMBED, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3051
    if ($embed == H5PDisplayOptionBehaviour::ALWAYS_SHOW ||
3052
        $embed == H5PDisplayOptionBehaviour::NEVER_SHOW) {
3053
      $sources[self::DISPLAY_OPTION_EMBED] = ($embed == H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3054
    }
3055
 
3056
    foreach (H5PCore::$disable as $bit => $option) {
3057
      if (!isset($sources[$option]) || !$sources[$option]) {
3058
        $current |= $bit; // Disable
3059
      }
3060
      else {
3061
        $current &= ~$bit; // Enable
3062
      }
3063
    }
3064
    return $current;
3065
  }
3066
 
3067
  /**
3068
   * Determine display options visibility and value on edit
3069
   *
3070
   * @param int $disable
3071
   * @return array
3072
   */
3073
  public function getDisplayOptionsForEdit($disable = NULL) {
3074
    $display_options = array();
3075
 
3076
    $current_display_options = $disable === NULL ? array() : $this->getDisplayOptionsAsArray($disable);
3077
 
3078
    if ($this->h5pF->getOption(self::DISPLAY_OPTION_FRAME, TRUE)) {
3079
      $display_options[self::DISPLAY_OPTION_FRAME] =
3080
        isset($current_display_options[self::DISPLAY_OPTION_FRAME]) ?
3081
        $current_display_options[self::DISPLAY_OPTION_FRAME] :
3082
        TRUE;
3083
 
3084
      // Download
3085
      $export = $this->h5pF->getOption(self::DISPLAY_OPTION_DOWNLOAD, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3086
      if ($export == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_ON ||
3087
          $export == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_OFF) {
3088
        $display_options[self::DISPLAY_OPTION_DOWNLOAD] =
3089
          isset($current_display_options[self::DISPLAY_OPTION_DOWNLOAD]) ?
3090
          $current_display_options[self::DISPLAY_OPTION_DOWNLOAD] :
3091
          ($export == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_ON);
3092
      }
3093
 
3094
      // Embed
3095
      $embed = $this->h5pF->getOption(self::DISPLAY_OPTION_EMBED, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3096
      if ($embed == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_ON ||
3097
          $embed == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_OFF) {
3098
        $display_options[self::DISPLAY_OPTION_EMBED] =
3099
          isset($current_display_options[self::DISPLAY_OPTION_EMBED]) ?
3100
          $current_display_options[self::DISPLAY_OPTION_EMBED] :
3101
          ($embed == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_ON);
3102
      }
3103
 
3104
      // Copyright
3105
      if ($this->h5pF->getOption(self::DISPLAY_OPTION_COPYRIGHT, TRUE)) {
3106
        $display_options[self::DISPLAY_OPTION_COPYRIGHT] =
3107
          isset($current_display_options[self::DISPLAY_OPTION_COPYRIGHT]) ?
3108
          $current_display_options[self::DISPLAY_OPTION_COPYRIGHT] :
3109
          TRUE;
3110
      }
3111
    }
3112
 
3113
    return $display_options;
3114
  }
3115
 
3116
  /**
3117
   * Helper function used to figure out embed & download behaviour
3118
   *
3119
   * @param string $option_name
3120
   * @param H5PPermission $permission
3121
   * @param int $id
3122
   * @param bool &$value
3123
   */
3124
  private function setDisplayOptionOverrides($option_name, $permission, $id, &$value) {
3125
    $behaviour = $this->h5pF->getOption($option_name, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3126
    // If never show globally, force hide
3127
    if ($behaviour == H5PDisplayOptionBehaviour::NEVER_SHOW) {
3128
      $value = false;
3129
    }
3130
    elseif ($behaviour == H5PDisplayOptionBehaviour::ALWAYS_SHOW) {
3131
      // If always show or permissions say so, force show
3132
      $value = true;
3133
    }
3134
    elseif ($behaviour == H5PDisplayOptionBehaviour::CONTROLLED_BY_PERMISSIONS) {
3135
      $value = $this->h5pF->hasPermission($permission, $id);
3136
    }
3137
  }
3138
 
3139
  /**
3140
   * Determine display option visibility when viewing H5P
3141
   *
3142
   * @param int $display_options
3143
   * @param int  $id Might be content id or user id.
3144
   * Depends on what the platform needs to be able to determine permissions.
3145
   * @return array
3146
   */
3147
  public function getDisplayOptionsForView($disable, $id) {
3148
    $display_options = $this->getDisplayOptionsAsArray($disable);
3149
 
3150
    if ($this->h5pF->getOption(self::DISPLAY_OPTION_FRAME, TRUE) == FALSE) {
3151
      $display_options[self::DISPLAY_OPTION_FRAME] = false;
3152
    }
3153
    else {
3154
      $this->setDisplayOptionOverrides(self::DISPLAY_OPTION_DOWNLOAD, H5PPermission::DOWNLOAD_H5P, $id, $display_options[self::DISPLAY_OPTION_DOWNLOAD]);
3155
      $this->setDisplayOptionOverrides(self::DISPLAY_OPTION_EMBED, H5PPermission::EMBED_H5P, $id, $display_options[self::DISPLAY_OPTION_EMBED]);
3156
 
3157
      if ($this->h5pF->getOption(self::DISPLAY_OPTION_COPYRIGHT, TRUE) == FALSE) {
3158
        $display_options[self::DISPLAY_OPTION_COPYRIGHT] = false;
3159
      }
3160
    }
3161
    $display_options[self::DISPLAY_OPTION_COPY] = $this->h5pF->hasPermission(H5PPermission::COPY_H5P, $id);
3162
 
3163
    return $display_options;
3164
  }
3165
 
3166
  /**
3167
   * Convert display options as single byte to array
3168
   *
3169
   * @param int $disable
3170
   * @return array
3171
   */
3172
  private function getDisplayOptionsAsArray($disable) {
3173
    return array(
3174
      self::DISPLAY_OPTION_FRAME => !($disable & H5PCore::DISABLE_FRAME),
3175
      self::DISPLAY_OPTION_DOWNLOAD => !($disable & H5PCore::DISABLE_DOWNLOAD),
3176
      self::DISPLAY_OPTION_EMBED => !($disable & H5PCore::DISABLE_EMBED),
3177
      self::DISPLAY_OPTION_COPYRIGHT => !($disable & H5PCore::DISABLE_COPYRIGHT),
3178
      self::DISPLAY_OPTION_ABOUT => !!$this->h5pF->getOption(self::DISPLAY_OPTION_ABOUT, TRUE),
3179
    );
3180
  }
3181
 
3182
  /**
3183
   * Small helper for getting the library's ID.
3184
   *
3185
   * @param array $library
3186
   * @param string [$libString]
3187
   * @return int Identifier, or FALSE if non-existent
3188
   */
3189
  public function getLibraryId($library, $libString = NULL) {
3190
    static $libraryIdMap = [];
3191
 
3192
    if (!$libString) {
3193
      $libString = self::libraryToString($library);
3194
    }
3195
 
3196
    if (!isset($libraryIdMap[$libString])) {
3197
      $libraryIdMap[$libString] = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']);
3198
    }
3199
 
3200
    return $libraryIdMap[$libString];
3201
  }
3202
 
3203
  /**
3204
   * Convert strings of text into simple kebab case slugs.
3205
   * Very useful for readable urls etc.
3206
   *
3207
   * @param string $input
3208
   * @return string
3209
   */
3210
  public static function slugify($input) {
3211
    // Down low
3212
    $input = strtolower($input);
3213
 
3214
    // Replace common chars
3215
    $input = str_replace(
3216
      array('æ',  'ø',  'ö', 'ó', 'ô', 'Ò',  'Õ', 'Ý', 'ý', 'ÿ', 'ā', 'ă', 'ą', 'œ', 'å', 'ä', 'á', 'à', 'â', 'ã', 'ç', 'ć', 'ĉ', 'ċ', 'č', 'é', 'è', 'ê', 'ë', 'í', 'ì', 'î', 'ï', 'ú', 'ñ', 'ü', 'ù', 'û', 'ß',  'ď', 'đ', 'ē', 'ĕ', 'ė', 'ę', 'ě', 'ĝ', 'ğ', 'ġ', 'ģ', 'ĥ', 'ħ', 'ĩ', 'ī', 'ĭ', 'į', 'ı', 'ij',  'ĵ', 'ķ', 'ĺ', 'ļ', 'ľ', 'ŀ', 'ł', 'ń', 'ņ', 'ň', 'ʼn', 'ō', 'ŏ', 'ő', 'ŕ', 'ŗ', 'ř', 'ś', 'ŝ', 'ş', 'š', 'ţ', 'ť', 'ŧ', 'ũ', 'ū', 'ŭ', 'ů', 'ű', 'ų', 'ŵ', 'ŷ', 'ź', 'ż', 'ž', 'ſ', 'ƒ', 'ơ', 'ư', 'ǎ', 'ǐ', 'ǒ', 'ǔ', 'ǖ', 'ǘ', 'ǚ', 'ǜ', 'ǻ', 'ǽ',  'ǿ'),
3217
      array('ae', 'oe', 'o', 'o', 'o', 'oe', 'o', 'o', 'y', 'y', 'y', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'c', 'c', 'c', 'c', 'c', 'e', 'e', 'e', 'e', 'i', 'i', 'i', 'i', 'u', 'n', 'u', 'u', 'u', 'es', 'd', 'd', 'e', 'e', 'e', 'e', 'e', 'g', 'g', 'g', 'g', 'h', 'h', 'i', 'i', 'i', 'i', 'i', 'ij', 'j', 'k', 'l', 'l', 'l', 'l', 'l', 'n', 'n', 'n', 'n', 'o', 'o', 'o', 'r', 'r', 'r', 's', 's', 's', 's', 't', 't', 't', 'u', 'u', 'u', 'u', 'u', 'u', 'w', 'y', 'z', 'z', 'z', 's', 'f', 'o', 'u', 'a', 'i', 'o', 'u', 'u', 'u', 'u', 'u', 'a', 'ae', 'oe'),
3218
      $input);
3219
 
3220
    // Replace everything else
3221
    $input = preg_replace('/[^a-z0-9]/', '-', $input);
3222
 
3223
    // Prevent double hyphen
3224
    $input = preg_replace('/-{2,}/', '-', $input);
3225
 
3226
    // Prevent hyphen in beginning or end
3227
    $input = trim($input, '-');
3228
 
3229
    // Prevent to long slug
3230
    if (strlen($input) > 91) {
3231
      $input = substr($input, 0, 92);
3232
    }
3233
 
3234
    // Prevent empty slug
3235
    if ($input === '') {
3236
      $input = 'interactive';
3237
    }
3238
 
3239
    return $input;
3240
  }
3241
 
3242
  /**
3243
   * Makes it easier to print response when AJAX request succeeds.
3244
   *
3245
   * @param mixed $data
3246
   * @since 1.6.0
3247
   */
3248
  public static function ajaxSuccess($data = NULL, $only_data = FALSE) {
3249
    $response = array(
3250
      'success' => TRUE
3251
    );
3252
    if ($data !== NULL) {
3253
      $response['data'] = $data;
3254
 
3255
      // Pass data flatly to support old methods
3256
      if ($only_data) {
3257
        $response = $data;
3258
      }
3259
    }
3260
    self::printJson($response);
3261
  }
3262
 
3263
  /**
3264
   * Makes it easier to print response when AJAX request fails.
3265
   * Will exit after printing error.
3266
   *
3267
   * @param string $message A human readable error message
3268
   * @param string $error_code An machine readable error code that a client
3269
   * should be able to interpret
3270
   * @param null|int $status_code Http response code
3271
   * @param array [$details=null] Better description of the error and possible which action to take
3272
   * @since 1.6.0
3273
   */
3274
  public static function ajaxError($message = NULL, $error_code = NULL, $status_code = NULL, $details = NULL) {
3275
    $response = array(
3276
      'success' => FALSE
3277
    );
3278
    if ($message !== NULL) {
3279
      $response['message'] = $message;
3280
    }
3281
 
3282
    if ($error_code !== NULL) {
3283
      $response['errorCode'] = $error_code;
3284
    }
3285
 
3286
    if ($details !== NULL) {
3287
      $response['details'] = $details;
3288
    }
3289
 
3290
    self::printJson($response, $status_code);
3291
  }
3292
 
3293
  /**
3294
   * Print JSON headers with UTF-8 charset and json encode response data.
3295
   * Makes it easier to respond using JSON.
3296
   *
3297
   * @param mixed $data
3298
   * @param null|int $status_code Http response code
3299
   */
3300
  private static function printJson($data, $status_code = NULL) {
3301
    header('Cache-Control: no-cache');
3302
    header('Content-Type: application/json; charset=utf-8');
3303
    print json_encode($data);
3304
  }
3305
 
3306
  /**
3307
   * Get a new H5P security token for the given action.
3308
   *
3309
   * @param string $action
3310
   * @return string token
3311
   */
3312
  public static function createToken($action) {
3313
    // Create and return token
3314
    return self::hashToken($action, self::getTimeFactor());
3315
  }
3316
 
3317
  /**
3318
   * Create a time based number which is unique for each 12 hour.
3319
   * @return int
3320
   */
3321
  private static function getTimeFactor() {
3322
    return ceil(time() / (86400 / 2));
3323
  }
3324
 
3325
  /**
3326
   * Generate a unique hash string based on action, time and token
3327
   *
3328
   * @param string $action
3329
   * @param int $time_factor
3330
   * @return string
3331
   */
3332
  private static function hashToken($action, $time_factor) {
3333
    global $SESSION;
3334
    if (!isset($SESSION->h5p_token)) {
3335
      // Create an unique key which is used to create action tokens for this session.
3336
      if (function_exists('random_bytes')) {
3337
        $SESSION->h5p_token = base64_encode(random_bytes(15));
3338
      }
3339
      else if (function_exists('openssl_random_pseudo_bytes')) {
3340
        $SESSION->h5p_token = base64_encode(openssl_random_pseudo_bytes(15));
3341
      }
3342
      else {
3343
        $SESSION->h5p_token = uniqid('', TRUE);
3344
      }
3345
    }
3346
 
3347
    // Create hash and return
3348
    return substr(hash('md5', $action . $time_factor . $SESSION->h5p_token), -16, 13);
3349
  }
3350
 
3351
  /**
3352
   * Verify if the given token is valid for the given action.
3353
   *
3354
   * @param string $action
3355
   * @param string $token
3356
   * @return boolean valid token
3357
   */
3358
  public static function validToken($action, $token) {
3359
    // Get the timefactor
3360
    $time_factor = self::getTimeFactor();
3361
 
3362
    // Check token to see if it's valid
3363
    return $token === self::hashToken($action, $time_factor) || // Under 12 hours
3364
           $token === self::hashToken($action, $time_factor - 1); // Between 12-24 hours
3365
  }
3366
 
3367
  /**
3368
   * Update content type cache
3369
   *
3370
   * @param object $postData Data sent to the hub
3371
   *
3372
   * @return bool|object Returns endpoint data if found, otherwise FALSE
3373
   */
3374
  public function updateContentTypeCache($postData = NULL) {
3375
    $interface = $this->h5pF;
3376
 
3377
    // Make sure data is sent!
3378
    if (!isset($postData) || !isset($postData['uuid'])) {
3379
      return $this->fetchLibrariesMetadata();
3380
    }
3381
 
3382
    $postData['current_cache'] = $this->h5pF->getOption('content_type_cache_updated_at', 0);
3383
 
3384
    $data = $interface->fetchExternalData(H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT_TYPES), $postData);
3385
 
3386
    if (! $this->h5pF->getOption('hub_is_enabled', TRUE)) {
3387
      return TRUE;
3388
    }
3389
 
3390
    // No data received
3391
    if (!$data) {
3392
      $interface->setErrorMessage(
3393
        $interface->t("Couldn't communicate with the H5P Hub. Please try again later."),
3394
        'failed-communicationg-with-hub'
3395
      );
3396
      return FALSE;
3397
    }
3398
 
3399
    $json = json_decode($data);
3400
 
3401
    // No libraries received
3402
    if (!isset($json->contentTypes) || empty($json->contentTypes)) {
3403
      $interface->setErrorMessage(
3404
        $interface->t('No content types were received from the H5P Hub. Please try again later.'),
3405
        'no-content-types-from-hub'
3406
      );
3407
      return FALSE;
3408
    }
3409
 
3410
    // Replace content type cache
3411
    $interface->replaceContentTypeCache($json);
3412
 
3413
    // Inform of the changes and update timestamp
3414
    $interface->setInfoMessage($interface->t('Library cache was successfully updated!'));
3415
    $interface->setOption('content_type_cache_updated_at', time());
3416
    return $data;
3417
  }
3418
 
3419
  /**
3420
   * Update content hub metadata cache
3421
   */
3422
  public function updateContentHubMetadataCache($lang = 'en') {
3423
    $url          = H5PHubEndpoints::createURL(H5PHubEndpoints::METADATA);
3424
    $lastModified = $this->h5pF->getContentHubMetadataChecked($lang);
3425
 
3426
    $headers = array();
3427
    if (!empty($lastModified)) {
3428
      $headers['If-Modified-Since'] = $lastModified;
3429
    }
3430
    $data = $this->h5pF->fetchExternalData("{$url}?lang={$lang}", NULL, TRUE, NULL, TRUE, $headers, NULL, 'GET');
3431
    $lastChecked = new DateTime('now', new DateTimeZone('GMT'));
3432
 
3433
    if ($data['status'] !== 200 && $data['status'] !== 304) {
3434
      // If this was not a success, set the error message and return
3435
      $this->h5pF->setErrorMessage(
3436
        $this->h5pF->t('No metadata was received from the H5P Hub. Please try again later.')
3437
      );
3438
      return null;
3439
    }
3440
 
3441
    // Update timestamp
3442
    $this->h5pF->setContentHubMetadataChecked($lastChecked->getTimestamp(), $lang);
3443
 
3444
    // Not modified
3445
    if ($data['status'] === 304) {
3446
      return null;
3447
    }
3448
    $this->h5pF->replaceContentHubMetadataCache($data['data'], $lang);
3449
    // TODO: If 200 should we have checked if it decodes? Or 'success'? Not sure if necessary though
3450
    return $data['data'];
3451
  }
3452
 
3453
  /**
3454
   * Get updated content hub metadata cache
3455
   *
3456
   * @param  string  $lang Language as ISO 639-1 code
3457
   *
3458
   * @return JsonSerializable|string
3459
   */
3460
  public function getUpdatedContentHubMetadataCache($lang = 'en') {
3461
    $lastUpdate = $this->h5pF->getContentHubMetadataChecked($lang);
3462
    if (!$lastUpdate) {
3463
      return $this->updateContentHubMetadataCache($lang);
3464
    }
3465
 
3466
    $lastUpdate = new DateTime($lastUpdate);
3467
    $expirationTime = $lastUpdate->getTimestamp() + (60 * 60 * 24); // Check once per day
3468
    if (time() > $expirationTime) {
3469
      $update = $this->updateContentHubMetadataCache($lang);
3470
      if (!empty($update)) {
3471
        return $update;
3472
      }
3473
    }
3474
 
3475
    $storedCache = $this->h5pF->getContentHubMetadataCache($lang);
3476
    if (!$storedCache) {
3477
      // We don't have the value stored for some reason, reset last update and re-fetch
3478
      $this->h5pF->setContentHubMetadataChecked(null, $lang);
3479
      return $this->updateContentHubMetadataCache($lang);
3480
    }
3481
 
3482
    return $storedCache;
3483
  }
3484
 
3485
  /**
3486
   * Check if the current server setup is valid and set error messages
3487
   *
3488
   * @return object Setup object with errors and disable hub properties
3489
   */
3490
  public function checkSetupErrorMessage() {
3491
    $setup = (object) array(
3492
      'errors' => array(),
3493
      'disable_hub' => FALSE
3494
    );
3495
 
3496
    if (!class_exists('ZipArchive')) {
3497
      $setup->errors[] = $this->h5pF->t('Your PHP version does not support ZipArchive.');
3498
      $setup->disable_hub = TRUE;
3499
    }
3500
 
3501
    if (!extension_loaded('mbstring')) {
3502
      $setup->errors[] = $this->h5pF->t(
3503
        'The mbstring PHP extension is not loaded. H5P needs this to function properly'
3504
      );
3505
      $setup->disable_hub = TRUE;
3506
    }
3507
 
3508
    // Check php version >= 5.2
3509
    $php_version = explode('.', phpversion());
3510
    if ($php_version[0] < 5 || ($php_version[0] === 5 && $php_version[1] < 2)) {
3511
      $setup->errors[] = $this->h5pF->t('Your PHP version is outdated. H5P requires version 5.2 to function properly. Version 5.6 or later is recommended.');
3512
      $setup->disable_hub = TRUE;
3513
    }
3514
 
3515
    // Check write access
3516
    if (!$this->fs->hasWriteAccess()) {
3517
      $setup->errors[] = $this->h5pF->t('A problem with the server write access was detected. Please make sure that your server can write to your data folder.');
3518
      $setup->disable_hub = TRUE;
3519
    }
3520
 
3521
    $max_upload_size = self::returnBytes(ini_get('upload_max_filesize'));
3522
    $max_post_size   = self::returnBytes(ini_get('post_max_size'));
3523
    $byte_threshold  = 5000000; // 5MB
3524
    if ($max_upload_size < $byte_threshold) {
3525
      $setup->errors[] =
3526
        $this->h5pF->t('Your PHP max upload size is quite small. With your current setup, you may not upload files larger than %number MB. This might be a problem when trying to upload H5Ps, images and videos. Please consider to increase it to more than 5MB.', array('%number' => number_format($max_upload_size / 1024 / 1024, 2, '.', ' ')));
3527
    }
3528
 
3529
    if ($max_post_size < $byte_threshold) {
3530
      $setup->errors[] =
3531
        $this->h5pF->t('Your PHP max post size is quite small. With your current setup, you may not upload files larger than %number MB. This might be a problem when trying to upload H5Ps, images and videos. Please consider to increase it to more than 5MB', array('%number' => number_format($max_upload_size / 1024 / 1024, 2, '.', ' ')));
3532
    }
3533
 
3534
    if ($max_upload_size > $max_post_size) {
3535
      $setup->errors[] =
3536
        $this->h5pF->t('Your PHP max upload size is bigger than your max post size. This is known to cause issues in some installations.');
3537
    }
3538
 
3539
    // Check SSL
3540
    if (!extension_loaded('openssl')) {
3541
      $setup->errors[] =
3542
        $this->h5pF->t('Your server does not have SSL enabled. SSL should be enabled to ensure a secure connection with the H5P hub.');
3543
      $setup->disable_hub = TRUE;
3544
    }
3545
 
3546
    return $setup;
3547
  }
3548
 
3549
  /**
3550
   * Check that all H5P requirements for the server setup is met.
3551
   */
3552
  public function checkSetupForRequirements() {
3553
    $setup = $this->checkSetupErrorMessage();
3554
 
3555
    $this->h5pF->setOption('hub_is_enabled', !$setup->disable_hub);
3556
    if (!empty($setup->errors)) {
3557
      foreach ($setup->errors as $err) {
3558
        $this->h5pF->setErrorMessage($err);
3559
      }
3560
    }
3561
 
3562
    if ($setup->disable_hub) {
3563
      // Inform how to re-enable hub
3564
      $this->h5pF->setErrorMessage(
3565
        $this->h5pF->t('H5P hub communication has been disabled because one or more H5P requirements failed.')
3566
      );
3567
      $this->h5pF->setErrorMessage(
3568
        $this->h5pF->t('When you have revised your server setup you may re-enable H5P hub communication in H5P Settings.')
3569
      );
3570
    }
3571
  }
3572
 
3573
  /**
3574
   * Return bytes from php_ini string value
3575
   *
3576
   * @param string $val
3577
   *
3578
   * @return int|string
3579
   */
3580
  public static function returnBytes($val) {
3581
    $val  = trim($val);
3582
    $last = strtolower($val[strlen($val) - 1]);
3583
    $bytes = (int) $val;
3584
 
3585
    switch ($last) {
3586
      case 'g':
3587
        $bytes *= 1024;
3588
      case 'm':
3589
        $bytes *= 1024;
3590
      case 'k':
3591
        $bytes *= 1024;
3592
    }
3593
 
3594
    return $bytes;
3595
  }
3596
 
3597
  /**
3598
   * Check if the current user has permission to update and install new
3599
   * libraries.
3600
   *
3601
   * @param bool [$set] Optional, sets the permission
3602
   * @return bool
3603
   */
3604
  public function mayUpdateLibraries($set = null) {
3605
    static $can;
3606
 
3607
    if ($set !== null) {
3608
      // Use value set
3609
      $can = $set;
3610
    }
3611
 
3612
    if ($can === null) {
3613
      // Ask our framework
3614
      $can = $this->h5pF->mayUpdateLibraries();
3615
    }
3616
 
3617
    return $can;
3618
  }
3619
 
3620
  /**
3621
   * Provide localization for the Core JS
3622
   * @return array
3623
   */
3624
  public function getLocalization() {
3625
    return array(
3626
      'fullscreen' => $this->h5pF->t('Fullscreen'),
3627
      'disableFullscreen' => $this->h5pF->t('Disable fullscreen'),
3628
      'download' => $this->h5pF->t('Download'),
3629
      'copyrights' => $this->h5pF->t('Rights of use'),
3630
      'embed' => $this->h5pF->t('Embed'),
3631
      'size' => $this->h5pF->t('Size'),
3632
      'showAdvanced' => $this->h5pF->t('Show advanced'),
3633
      'hideAdvanced' => $this->h5pF->t('Hide advanced'),
3634
      'advancedHelp' => $this->h5pF->t('Include this script on your website if you want dynamic sizing of the embedded content:'),
3635
      'copyrightInformation' => $this->h5pF->t('Rights of use'),
3636
      'close' => $this->h5pF->t('Close'),
3637
      'title' => $this->h5pF->t('Title'),
3638
      'author' => $this->h5pF->t('Author'),
3639
      'year' => $this->h5pF->t('Year'),
3640
      'source' => $this->h5pF->t('Source'),
3641
      'license' => $this->h5pF->t('License'),
3642
      'thumbnail' => $this->h5pF->t('Thumbnail'),
3643
      'noCopyrights' => $this->h5pF->t('No copyright information available for this content.'),
3644
      'reuse' => $this->h5pF->t('Reuse'),
3645
      'reuseContent' => $this->h5pF->t('Reuse Content'),
3646
      'reuseDescription' => $this->h5pF->t('Reuse this content.'),
3647
      'downloadDescription' => $this->h5pF->t('Download this content as a H5P file.'),
3648
      'copyrightsDescription' => $this->h5pF->t('View copyright information for this content.'),
3649
      'embedDescription' => $this->h5pF->t('View the embed code for this content.'),
3650
      'h5pDescription' => $this->h5pF->t('Visit H5P.org to check out more cool content.'),
3651
      'contentChanged' => $this->h5pF->t('This content has changed since you last used it.'),
3652
      'startingOver' => $this->h5pF->t("You'll be starting over."),
3653
      'by' => $this->h5pF->t('by'),
3654
      'showMore' => $this->h5pF->t('Show more'),
3655
      'showLess' => $this->h5pF->t('Show less'),
3656
      'subLevel' => $this->h5pF->t('Sublevel'),
3657
      'confirmDialogHeader' => $this->h5pF->t('Confirm action'),
3658
      'confirmDialogBody' => $this->h5pF->t('Please confirm that you wish to proceed. This action is not reversible.'),
3659
      'cancelLabel' => $this->h5pF->t('Cancel'),
3660
      'confirmLabel' => $this->h5pF->t('Confirm'),
3661
      'licenseU' => $this->h5pF->t('Undisclosed'),
3662
      'licenseCCBY' => $this->h5pF->t('Attribution'),
3663
      'licenseCCBYSA' => $this->h5pF->t('Attribution-ShareAlike'),
3664
      'licenseCCBYND' => $this->h5pF->t('Attribution-NoDerivs'),
3665
      'licenseCCBYNC' => $this->h5pF->t('Attribution-NonCommercial'),
3666
      'licenseCCBYNCSA' => $this->h5pF->t('Attribution-NonCommercial-ShareAlike'),
3667
      'licenseCCBYNCND' => $this->h5pF->t('Attribution-NonCommercial-NoDerivs'),
3668
      'licenseCC40' => $this->h5pF->t('4.0 International'),
3669
      'licenseCC30' => $this->h5pF->t('3.0 Unported'),
3670
      'licenseCC25' => $this->h5pF->t('2.5 Generic'),
3671
      'licenseCC20' => $this->h5pF->t('2.0 Generic'),
3672
      'licenseCC10' => $this->h5pF->t('1.0 Generic'),
3673
      'licenseGPL' => $this->h5pF->t('General Public License'),
3674
      'licenseV3' => $this->h5pF->t('Version 3'),
3675
      'licenseV2' => $this->h5pF->t('Version 2'),
3676
      'licenseV1' => $this->h5pF->t('Version 1'),
3677
      'licensePD' => $this->h5pF->t('Public Domain'),
3678
      'licenseCC010' => $this->h5pF->t('CC0 1.0 Universal (CC0 1.0) Public Domain Dedication'),
3679
      'licensePDM' => $this->h5pF->t('Public Domain Mark'),
3680
      'licenseC' => $this->h5pF->t('Copyright'),
3681
      'contentType' => $this->h5pF->t('Content Type'),
3682
      'licenseExtras' => $this->h5pF->t('License Extras'),
3683
      'changes' => $this->h5pF->t('Changelog'),
3684
      'contentCopied' => $this->h5pF->t('Content is copied to the clipboard'),
3685
      'connectionLost' => $this->h5pF->t('Connection lost. Results will be stored and sent when you regain connection.'),
3686
      'connectionReestablished' => $this->h5pF->t('Connection reestablished.'),
3687
      'resubmitScores' => $this->h5pF->t('Attempting to submit stored results.'),
3688
      'offlineDialogHeader' => $this->h5pF->t('Your connection to the server was lost'),
3689
      'offlineDialogBody' => $this->h5pF->t('We were unable to send information about your completion of this task. Please check your internet connection.'),
3690
      'offlineDialogRetryMessage' => $this->h5pF->t('Retrying in :num....'),
3691
      'offlineDialogRetryButtonLabel' => $this->h5pF->t('Retry now'),
3692
      'offlineSuccessfulSubmit' => $this->h5pF->t('Successfully submitted results.'),
3693
      'mainTitle' => $this->h5pF->t('Sharing <strong>:title</strong>'),
3694
      'editInfoTitle' => $this->h5pF->t('Edit info for <strong>:title</strong>'),
3695
      'cancel' => $this->h5pF->t('Cancel'),
3696
      'back' => $this->h5pF->t('Back'),
3697
      'next' => $this->h5pF->t('Next'),
3698
      'reviewInfo' => $this->h5pF->t('Review info'),
3699
      'share' => $this->h5pF->t('Share'),
3700
      'saveChanges' => $this->h5pF->t('Save changes'),
3701
      'registerOnHub' => $this->h5pF->t('Register on the H5P Hub'),
3702
      'updateRegistrationOnHub' => $this->h5pF->t('Save account settings'),
3703
      'requiredInfo' => $this->h5pF->t('Required Info'),
3704
      'optionalInfo' => $this->h5pF->t('Optional Info'),
3705
      'reviewAndShare' => $this->h5pF->t('Review & Share'),
3706
      'reviewAndSave' => $this->h5pF->t('Review & Save'),
3707
      'shared' => $this->h5pF->t('Shared'),
3708
      'currentStep' => $this->h5pF->t('Step :step of :total'),
3709
      'sharingNote' => $this->h5pF->t('All content details can be edited after sharing'),
3710
      'licenseDescription' => $this->h5pF->t('Select a license for your content'),
3711
      'licenseVersion' => $this->h5pF->t('License Version'),
3712
      'licenseVersionDescription' => $this->h5pF->t('Select a license version'),
3713
      'disciplineLabel' => $this->h5pF->t('Disciplines'),
3714
      'disciplineDescription' => $this->h5pF->t('You can select multiple disciplines'),
3715
      'disciplineLimitReachedMessage' => $this->h5pF->t('You can select up to :numDisciplines disciplines'),
3716
      'discipline' => array(
3717
        'searchPlaceholder' => $this->h5pF->t('Type to search for disciplines'),
3718
        'in' => $this->h5pF->t('in'),
3719
        'dropdownButton' => $this->h5pF->t('Dropdown button'),
3720
      ),
3721
      'removeChip' => $this->h5pF->t('Remove :chip from the list'),
3722
      'keywordsPlaceholder' => $this->h5pF->t('Add keywords'),
3723
      'keywords' => $this->h5pF->t('Keywords'),
3724
      'keywordsDescription' => $this->h5pF->t('You can add multiple keywords separated by commas. Press "Enter" or "Add" to confirm keywords'),
3725
      'altText' => $this->h5pF->t('Alt text'),
3726
      'reviewMessage' => $this->h5pF->t('Please review the info below before you share'),
3727
      'subContentWarning' => $this->h5pF->t('Sub-content (images, questions etc.) will be shared under :license unless otherwise specified in the authoring tool'),
3728
      'disciplines' => $this->h5pF->t('Disciplines'),
3729
      'shortDescription' => $this->h5pF->t('Short description'),
3730
      'longDescription' => $this->h5pF->t('Long description'),
3731
      'icon' => $this->h5pF->t('Icon'),
3732
      'screenshots' => $this->h5pF->t('Screenshots'),
3733
      'helpChoosingLicense' => $this->h5pF->t('Help me choose a license'),
3734
      'shareFailed' => $this->h5pF->t('Share failed.'),
3735
      'editingFailed' => $this->h5pF->t('Editing failed.'),
3736
      'shareTryAgain' => $this->h5pF->t('Something went wrong, please try to share again.'),
3737
      'pleaseWait' => $this->h5pF->t('Please wait...'),
3738
      'language' => $this->h5pF->t('Language'),
3739
      'level' => $this->h5pF->t('Level'),
3740
      'shortDescriptionPlaceholder' => $this->h5pF->t('Short description of your content'),
3741
      'longDescriptionPlaceholder' => $this->h5pF->t('Long description of your content'),
3742
      'description' => $this->h5pF->t('Description'),
3743
      'iconDescription' => $this->h5pF->t('640x480px. If not selected content will use category icon'),
3744
      'screenshotsDescription' => $this->h5pF->t('Add up to five screenshots of your content'),
3745
      'submitted' => $this->h5pF->t('Submitted!'),
3746
      'isNowSubmitted' => $this->h5pF->t('Is now submitted to H5P Hub'),
3747
      'changeHasBeenSubmitted' => $this->h5pF->t('A change has been submited for'),
3748
      'contentAvailable' => $this->h5pF->t('Your content will normally be available in the Hub within one business day.'),
3749
      'contentUpdateSoon' => $this->h5pF->t('Your content will update soon'),
3750
      'contentLicenseTitle' => $this->h5pF->t('Content License Info'),
3751
      'licenseDialogDescription' => $this->h5pF->t('Click on a specific license to get info about proper usage'),
3752
      'publisherFieldTitle' => $this->h5pF->t('Publisher'),
3753
      'publisherFieldDescription' => $this->h5pF->t('This will display as the "Publisher name" on shared content'),
3754
      'emailAddress' => $this->h5pF->t('Email Address'),
3755
      'publisherDescription' => $this->h5pF->t('Publisher description'),
3756
      'publisherDescriptionText' => $this->h5pF->t('This will be displayed under "Publisher info" on shared content'),
3757
      'contactPerson' => $this->h5pF->t('Contact Person'),
3758
      'phone' => $this->h5pF->t('Phone'),
3759
      'address' => $this->h5pF->t('Address'),
3760
      'city' => $this->h5pF->t('City'),
3761
      'zip' => $this->h5pF->t('Zip'),
3762
      'country' => $this->h5pF->t('Country'),
3763
      'logoUploadText' => $this->h5pF->t('Organization logo or avatar'),
3764
      'acceptTerms' => $this->h5pF->t('I accept the <a href=":url" target="_blank">terms of use</a>'),
3765
      'successfullyRegistred' => $this->h5pF->t('You have successfully registered an account on the H5P Hub'),
3766
      'successfullyRegistredDescription' => $this->h5pF->t('You account details can be changed'),
3767
      'successfullyUpdated' => $this->h5pF->t('Your H5P Hub account settings have successfully been changed'),
3768
      'accountDetailsLinkText' => $this->h5pF->t('here'),
3769
      'registrationTitle' => $this->h5pF->t('H5P Hub Registration'),
3770
      'registrationFailed' => $this->h5pF->t('An error occurred'),
3771
      'registrationFailedDescription' => $this->h5pF->t('We were not able to create an account at this point. Something went wrong. Try again later.'),
3772
      'maxLength' => $this->h5pF->t(':length is the maximum number of characters'),
3773
      'keywordExists' => $this->h5pF->t('Keyword already exists!'),
3774
      'licenseDetails' => $this->h5pF->t('License details'),
3775
      'remove' => $this->h5pF->t('Remove'),
3776
      'removeImage' => $this->h5pF->t('Remove image'),
3777
      'cancelPublishConfirmationDialogTitle' => $this->h5pF->t('Cancel sharing'),
3778
      'cancelPublishConfirmationDialogDescription' => $this->h5pF->t('Are you sure you want to cancel the sharing process?'),
3779
      'cancelPublishConfirmationDialogCancelButtonText' => $this->h5pF->t('No'),
3780
      'cancelPublishConfirmationDialogConfirmButtonText' => $this->h5pF->t('Yes'),
3781
      'add' => $this->h5pF->t('Add'),
3782
      'age' => $this->h5pF->t('Typical age'),
3783
      'ageDescription' => $this->h5pF->t('The target audience of this content. Possible input formats separated by commas: "1,34-45,-50,59-".'),
3784
      'invalidAge' => $this->h5pF->t('Invalid input format for Typical age. Possible input formats separated by commas: "1, 34-45, -50, -59-".'),
3785
      'contactPersonDescription' => $this->h5pF->t('H5P will reach out to the contact person in case there are any issues with the content shared by the publisher. The contact person\'s name or other information will not be published or shared with third parties'),
3786
      'emailAddressDescription' => $this->h5pF->t('The email address will be used by H5P to reach out to the publisher in case of any issues with the content or in case the publisher needs to recover their account. It will not be published or shared with any third parties'),
3787
      'copyrightWarning' => $this->h5pF->t('Copyrighted material cannot be shared in the H5P Content Hub. If the content is licensed with a OER friendly license like Creative Commons, please choose the appropriate license. If not this content cannot be shared.'),
3788
      'keywordsExits' => $this->h5pF->t('Keywords already exists!'),
3789
      'someKeywordsExits' => $this->h5pF->t('Some of these keywords already exist'),
3790
      'width' => $this->h5pF->t('width'),
3791
      'height' => $this->h5pF->t('height')
3792
    );
3793
  }
3794
 
3795
  /**
3796
   * Publish content on the H5P Hub.
3797
   *
3798
   * @param bigint $id
3799
   * @return stdClass
3800
   */
3801
  public function hubRetrieveContent($id) {
3802
    $headers = array(
3803
      'Authorization' => $this->hubGetAuthorizationHeader(),
3804
      'Accept' => 'application/json',
3805
    );
3806
 
3807
    $response = $this->h5pF->fetchExternalData(
3808
      H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT . "/{$id}"),
3809
      NULL, TRUE, NULL, TRUE, $headers
3810
    );
3811
 
3812
    if (empty($response['data'])) {
3813
      throw new Exception($this->h5pF->t('Unable to authorize with the H5P Hub. Please check your Hub registration and connection.'));
3814
    }
3815
 
3816
    if (isset($response['status']) && $response['status'] !== 200) {
3817
      if ($response['status'] === 404) {
3818
        $this->h5pF->setErrorMessage($this->h5pF->t('Content is not shared on the H5P OER Hub.'));
3819
        return NULL;
3820
      }
3821
      throw new Exception($this->h5pF->t("Couldn't communicate with the H5P Hub. Please try again later."));
3822
    }
3823
 
3824
    $hub_content = json_decode($response['data'])->data;
3825
    $hub_content->id = "$hub_content->id";
3826
    return $hub_content;
3827
  }
3828
 
3829
  /**
3830
   * Publish content on the H5P Hub.
3831
   *
3832
   * @param array $data Data from content publishing process
3833
   * @param array $files Files to upload with the content publish
3834
   * @param bigint $content_hub_id For updating existing content
3835
   * @return stdClass
3836
   */
3837
  public function hubPublishContent($data, $files, $content_hub_id = NULL) {
3838
    $headers = array(
3839
      'Authorization' => $this->hubGetAuthorizationHeader(),
3840
      'Accept' => 'application/json',
3841
    );
3842
 
3843
    $data['published'] = '1';
3844
    $endpoint = H5PHubEndpoints::CONTENT;
3845
    if ($content_hub_id !== NULL) {
3846
      $endpoint .= "/{$content_hub_id}";
3847
      $data['_method'] = 'PUT';
3848
    }
3849
 
3850
    $response = $this->h5pF->fetchExternalData(
3851
      H5PHubEndpoints::createURL($endpoint),
3852
      $data, TRUE, NULL, TRUE, $headers, $files
3853
    );
3854
 
3855
    if (empty($response['data']) || $response['status'] === 403) {
3856
      throw new Exception($this->h5pF->t('Unable to authorize with the H5P Hub. Please check your Hub registration and connection.'));
3857
    }
3858
 
3859
    if (isset($response['status']) && $response['status'] !== 200) {
3860
      throw new Exception($this->h5pF->t('Connecting to the content hub failed, please try again later.'));
3861
    }
3862
 
3863
    $result = json_decode($response['data']);
3864
    if (isset($result->success) && $result->success === TRUE) {
3865
      return $result;
3866
    }
3867
    elseif (!empty($result->errors)) {
3868
      // Relay any error messages
3869
      $e = new Exception($this->h5pF->t('Validation failed.'));
3870
      $e->errors = $result->errors;
3871
      throw $e;
3872
    }
3873
  }
3874
 
3875
  /**
3876
   * Creates the authorization header needed to access the private parts of
3877
   * the H5P Hub.
3878
   *
3879
   * @return string
3880
   */
3881
  public function hubGetAuthorizationHeader() {
3882
    $site_uuid = $this->h5pF->getOption('site_uuid', '');
3883
    $hub_secret = $this->h5pF->getOption('hub_secret', '');
3884
    if (empty($site_uuid)) {
3885
      $this->h5pF->setErrorMessage($this->h5pF->t('Missing Site UUID. Please check your Hub registration.'));
3886
    }
3887
    elseif (empty($hub_secret)) {
3888
      $this->h5pF->setErrorMessage($this->h5pF->t('Missing Hub Secret. Please check your Hub registration.'));
3889
    }
3890
    return 'Basic ' . base64_encode("$site_uuid:$hub_secret");
3891
  }
3892
 
3893
  /**
3894
   * Unpublish content from content hub
3895
   *
3896
   * @param  integer  $hubId  Content hub id
3897
   *
3898
   * @return bool True if successful
3899
   */
3900
  public function hubUnpublishContent($hubId) {
3901
    $headers = array(
3902
      'Authorization' => $this->hubGetAuthorizationHeader(),
3903
      'Accept' => 'application/json',
3904
    );
3905
 
3906
    $url = H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT);
3907
    $response = $this->h5pF->fetchExternalData("{$url}/{$hubId}", array(
3908
      'published' => '0',
3909
    ), true, null, true, $headers, array(), 'PUT');
3910
 
3911
    // Remove shared status if successful
3912
    if (!empty($response) && $response['status'] === 200) {
3913
      $msg = $this->h5pF->t('Content successfully unpublished');
3914
      $this->h5pF->setInfoMessage($msg);
3915
 
3916
      return true;
3917
    }
3918
    $msg = $this->h5pF->t('Content unpublish failed');
3919
    $this->h5pF->setErrorMessage($msg);
3920
 
3921
    return false;
3922
  }
3923
 
3924
  /**
3925
   * Sync content with content hub
3926
   *
3927
   * @param integer $hubId Content hub id
3928
   * @param string $exportPath Export path where .h5p for content can be found
3929
   *
3930
   * @return bool
3931
   */
3932
  public function hubSyncContent($hubId, $exportPath) {
3933
    $headers = array(
3934
      'Authorization' => $this->hubGetAuthorizationHeader(),
3935
      'Accept' => 'application/json',
3936
    );
3937
 
3938
    $url = H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT);
3939
    $response = $this->h5pF->fetchExternalData("{$url}/{$hubId}", array(
3940
      'download_url' => $exportPath,
3941
      'resync' => '1',
3942
    ), true, null, true, $headers, array(), 'PUT');
3943
 
3944
    if (!empty($response) && $response['status'] === 200) {
3945
      $msg = $this->h5pF->t('Content sync queued');
3946
      $this->h5pF->setInfoMessage($msg);
3947
      return true;
3948
    }
3949
 
3950
    $msg = $this->h5pF->t('Content sync failed');
3951
    $this->h5pF->setErrorMessage($msg);
3952
    return false;
3953
  }
3954
 
3955
  /**
3956
   * Fetch account info for our site from the content hub
3957
   *
3958
   * @return array|bool|string False if account is not setup, otherwise data
3959
   */
3960
  public function hubAccountInfo() {
3961
    $siteUuid = $this->h5pF->getOption('site_uuid', null);
3962
    $secret   = $this->h5pF->getOption('hub_secret', null);
3963
    if (empty($siteUuid) && !empty($secret)) {
3964
      $this->h5pF->setErrorMessage($this->h5pF->t('H5P Hub secret is set without a site uuid. This may be fixed by restoring the site uuid or removing the hub secret and registering a new account with the content hub.'));
3965
      throw new Exception('Hub secret not set');
3966
    }
3967
 
3968
    if (empty($siteUuid) || empty($secret)) {
3969
      $this->h5pF->setErrorMessage($this->h5pF->t('Missing Site UUID or Hub Secret. Please check your Hub registration.'));
3970
      return false;
3971
    }
3972
 
3973
    $headers = array(
3974
      'Authorization' => $this->hubGetAuthorizationHeader(),
3975
      'Accept' => 'application/json',
3976
    );
3977
 
3978
    $url = H5PHubEndpoints::createURL(H5PHubEndpoints::REGISTER);
3979
    $accountInfo = $this->h5pF->fetchExternalData("{$url}/{$siteUuid}",
3980
      null, true, null, true, $headers, array(), 'GET');
3981
 
3982
    if ($accountInfo['status'] === 401) {
3983
      // Unauthenticated, invalid hub secret and site uuid combination
3984
      $this->h5pF->setErrorMessage($this->h5pF->t('Hub account authentication info is invalid. This may be fixed by an admin by restoring the hub secret or register a new account with the content hub.'));
3985
      return false;
3986
    }
3987
 
3988
    if ($accountInfo['status'] !== 200) {
3989
      $this->h5pF->setErrorMessage($this->h5pF->t('Unable to retrieve HUB account information. Please contact support.'));
3990
      return false;
3991
    }
3992
 
3993
    return json_decode($accountInfo['data'])->data;
3994
  }
3995
 
3996
  /**
3997
   * Register account
3998
   *
3999
   * @param array $formData Form data. Should include: name, email, description,
4000
   *    contact_person, phone, address, city, zip, country, remove_logo
4001
   * @param object $logo Input image
4002
   *
4003
   * @return array
4004
   */
4005
  public function hubRegisterAccount($formData, $logo) {
4006
 
4007
    $uuid = $this->h5pF->getOption('site_uuid', '');
4008
    if (empty($uuid)) {
4009
      // Attempt to fetch a new site uuid
4010
      $uuid = $this->fetchLibrariesMetadata(false, true);
4011
      if (!$uuid) {
4012
        return [
4013
          'message'     => $this->h5pF->t('Site is missing a unique site uuid and was unable to set a new one. The H5P Content Hub is disabled until this problem can be resolved. Please make sure the H5P Hub is enabled in the H5P settings and try again later.'),
4014
          'status_code' => 403,
4015
          'error_code'  => 'MISSING_SITE_UUID',
4016
          'success'     => FALSE,
4017
        ];
4018
      }
4019
    }
4020
 
4021
    $formData['site_uuid'] = $uuid;
4022
 
4023
    $headers  = [];
4024
    $endpoint = H5PHubEndpoints::REGISTER;
4025
    // Update if already registered
4026
    $hasRegistered = $this->h5pF->getOption('hub_secret');
4027
    if ($hasRegistered) {
4028
      $endpoint            .= "/{$uuid}";
4029
      $formData['_method'] = 'PUT';
4030
      $headers             = [
4031
        'Authorization' => $this->hubGetAuthorizationHeader(),
4032
      ];
4033
    }
4034
 
4035
    $url          = H5PHubEndpoints::createURL($endpoint);
4036
    $registration = $this->h5pF->fetchExternalData(
4037
      $url,
4038
      $formData,
4039
      NULL,
4040
      NULL,
4041
      TRUE,
4042
      $headers,
4043
      isset($logo) ? ['logo' => $logo] : []
4044
    );
4045
 
4046
    try {
4047
      $results = json_decode($registration['data']);
4048
    } catch (Exception $e) {
4049
      return [
4050
        'message'     => 'Could not parse json response.',
4051
        'status_code' => 424,
4052
        'error_code'  => 'COULD_NOT_PARSE_RESPONSE',
4053
        'success'     => FALSE,
4054
      ];
4055
    }
4056
 
4057
    if (isset($results->errors->site_uuid)) {
4058
      return [
4059
        'message'     => 'Site UUID is not unique. This must be fixed by an admin by restoring the hub secret or remove the site uuid and register as a new account with the content hub.',
4060
        'status_code' => 403,
4061
        'error_code'  => 'SITE_UUID_NOT_UNIQUE',
4062
        'success'     => FALSE,
4063
      ];
4064
    }
4065
 
4066
    if (isset($results->errors->logo)) {
4067
      return [
4068
        'message' => $results->errors->logo[0],
4069
        'status_code' => 400,
4070
        'success' => FALSE,
4071
      ];
4072
    }
4073
 
4074
    if (
4075
      !isset($results->success)
4076
      || $results->success === FALSE
4077
      || !$hasRegistered && !isset($results->account->secret)
4078
      || $registration['status'] !== 200
4079
    ) {
4080
      return [
4081
        'message'     => 'Unable to register the account. Please contact support team.',
4082
        'status_code' => 422,
4083
        'error_code'  => 'REGISTRATION_FAILED',
4084
        'success'     => FALSE,
4085
      ];
4086
    }
4087
 
4088
    if (!$hasRegistered) {
4089
      $this->h5pF->setOption('hub_secret', $results->account->secret);
4090
    }
4091
 
4092
    return [
4093
      'message'     => $this->h5pF->t('Account successfully registered.'),
4094
      'status_code' => 200,
4095
      'success'     => TRUE,
4096
    ];
4097
  }
4098
 
4099
  /**
4100
   * Get status of content from content hub
4101
   *
4102
   * @param string $hubContentId
4103
   * @param int $syncStatus
4104
   *
4105
   * @return false|int Returns a new H5PContentStatus if successful, else false
4106
   */
4107
  public function getHubContentStatus($hubContentId, $syncStatus) {
4108
    $headers = array(
4109
      'Authorization' => $this->hubGetAuthorizationHeader(),
4110
      'Accept' => 'application/json',
4111
    );
4112
 
4113
    $url     = H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT);
4114
    $response = $this->h5pF->fetchExternalData("{$url}/{$hubContentId}/status",
4115
      null, true, null, true, $headers);
4116
 
4117
    if (isset($response['status']) && $response['status'] === 403) {
4118
      $msg = $this->h5pF->t('The request for content status was unauthorized. This could be because the content belongs to a different account, or your account is not setup properly.');
4119
      $this->h5pF->setErrorMessage($msg);
4120
      return false;
4121
    }
4122
    if (empty($response) || $response['status'] !== 200) {
4123
      $msg = $this->h5pF->t('Could not get content hub sync status for content.');
4124
      $this->h5pF->setErrorMessage($msg);
4125
      return false;
4126
    }
4127
 
4128
    $data = json_decode($response['data']);
4129
 
4130
    if (isset($data->messages)) {
4131
      // TODO: Is this the right place/way to display them?
4132
 
4133
      if (!empty($data->messages->info)) {
4134
        foreach ($data->messages->info as $info) {
4135
          $this->h5pF->setInfoMessage($info);
4136
        }
4137
      }
4138
      if (!empty($data->messages->error)) {
4139
        foreach ($data->messages->error as $error) {
4140
          $this->h5pF->setErrorMessage($error->message, $error->code);
4141
        }
4142
      }
4143
    }
4144
 
4145
    $contentStatus = intval($data->status);
4146
    // Content status updated
4147
    if ($contentStatus !== H5PContentStatus::STATUS_WAITING) {
4148
      $newState = H5PContentHubSyncStatus::SYNCED;
4149
      if ($contentStatus !== H5PContentStatus::STATUS_DOWNLOADED) {
4150
        $newState = H5PContentHubSyncStatus::FAILED;
4151
      }
4152
      else if (intval($syncStatus) !== $contentStatus) {
4153
        // Content status successfully transitioned to synced/downloaded
4154
        $successMsg = $this->h5pF->t('Content was successfully shared on the content hub.');
4155
        $this->h5pF->setInfoMessage($successMsg);
4156
      }
4157
 
4158
      return $newState;
4159
    }
4160
 
4161
    return false;
4162
  }
4163
}
4164
 
4165
/**
4166
 * Functions for validating basic types from H5P library semantics.
4167
 * @property bool allowedStyles
4168
 */
4169
class H5PContentValidator {
4170
  public $h5pF;
4171
  public $h5pC;
4172
  private $typeMap, $libraries, $dependencies, $nextWeight;
4173
  private static $allowed_styleable_tags = array('span', 'p', 'div','h1','h2','h3', 'td');
4174
 
4175
  /** @var bool Allowed styles status. */
4176
  protected $allowedStyles;
4177
 
4178
  /**
4179
   * Constructor for the H5PContentValidator
4180
   *
4181
   * @param object $H5PFramework
4182
   *  The frameworks implementation of the H5PFrameworkInterface
4183
   * @param object $H5PCore
4184
   *  The main H5PCore instance
4185
   */
4186
  public function __construct($H5PFramework, $H5PCore) {
4187
    $this->h5pF = $H5PFramework;
4188
    $this->h5pC = $H5PCore;
4189
    $this->typeMap = array(
4190
      'text' => 'validateText',
4191
      'number' => 'validateNumber',
4192
      'boolean' => 'validateBoolean',
4193
      'list' => 'validateList',
4194
      'group' => 'validateGroup',
4195
      'file' => 'validateFile',
4196
      'image' => 'validateImage',
4197
      'video' => 'validateVideo',
4198
      'audio' => 'validateAudio',
4199
      'select' => 'validateSelect',
4200
      'library' => 'validateLibrary',
4201
    );
4202
    $this->nextWeight = 1;
4203
 
4204
    // Keep track of the libraries we load to avoid loading it multiple times.
4205
    $this->libraries = array();
4206
 
4207
    // Keep track of all dependencies for the given content.
4208
    $this->dependencies = array();
4209
  }
4210
 
4211
  /**
4212
   * Add Addon library.
4213
   */
4214
  public function addon($library) {
4215
    $depKey = 'preloaded-' . $library['machineName'];
4216
    $this->dependencies[$depKey] = array(
4217
      'library' => $library,
4218
      'type' => 'preloaded'
4219
    );
4220
    $this->nextWeight = $this->h5pC->findLibraryDependencies($this->dependencies, $library, $this->nextWeight);
4221
    $this->dependencies[$depKey]['weight'] = $this->nextWeight++;
4222
  }
4223
 
4224
  /**
4225
   * Get the flat dependency tree.
4226
   *
4227
   * @return array
4228
   */
4229
  public function getDependencies() {
4230
    return $this->dependencies;
4231
  }
4232
 
4233
  /**
4234
   * Validate metadata
4235
   *
4236
   * @param array $metadata
4237
   * @return array Validated & filtered
4238
   */
4239
  public function validateMetadata($metadata) {
4240
    $semantics = $this->getMetadataSemantics();
4241
    $group = (object)$metadata;
4242
 
4243
    // Stop complaining about "invalid selected option in select" for
4244
    // old content without license chosen.
4245
    if (!isset($group->license)) {
4246
      $group->license = 'U';
4247
    }
4248
 
4249
    $this->validateGroup($group, (object) array(
4250
      'type' => 'group',
4251
      'fields' => $semantics,
4252
    ), FALSE);
4253
 
4254
    return (array)$group;
4255
  }
4256
 
4257
  /**
4258
   * Validate given text value against text semantics.
4259
   * @param $text
4260
   * @param $semantics
4261
   */
4262
  public function validateText(&$text, $semantics) {
4263
    if (!is_string($text)) {
4264
      $text = '';
4265
    }
4266
    if (isset($semantics->tags)) {
4267
      // Not testing for empty array allows us to use the 4 defaults without
4268
      // specifying them in semantics.
4269
      $tags = array_merge(array('div', 'span', 'p', 'br'), $semantics->tags);
4270
 
4271
      // Add related tags for table etc.
4272
      if (in_array('table', $tags)) {
4273
        $tags = array_merge($tags, array('tr', 'td', 'th', 'colgroup', 'thead', 'tbody', 'tfoot'));
4274
      }
4275
      if (in_array('b', $tags) && ! in_array('strong', $tags)) {
4276
        $tags[] = 'strong';
4277
      }
4278
      if (in_array('i', $tags) && ! in_array('em', $tags)) {
4279
        $tags[] = 'em';
4280
      }
4281
      if (in_array('ul', $tags) || in_array('ol', $tags) && ! in_array('li', $tags)) {
4282
        $tags[] = 'li';
4283
      }
4284
      if (in_array('del', $tags) || in_array('strike', $tags) && ! in_array('s', $tags)) {
4285
        $tags[] = 's';
4286
      }
4287
 
4288
      // Determine allowed style tags
4289
      $stylePatterns = array();
4290
      // All styles must be start to end patterns (^...$)
4291
      if (isset($semantics->font)) {
4292
        if (isset($semantics->font->size) && $semantics->font->size) {
4293
          $stylePatterns[] = '/^font-size: *[0-9.]+(em|px|%) *;?$/i';
4294
        }
4295
        if (isset($semantics->font->family) && $semantics->font->family) {
4296
          $stylePatterns[] = '/^font-family: *[-a-z0-9," ]+;?$/i';
4297
        }
4298
        if (isset($semantics->font->color) && $semantics->font->color) {
4299
          $stylePatterns[] = '/^color: *(#[a-f0-9]{3}[a-f0-9]{3}?|rgba?\([0-9, ]+\)) *;?$/i';
4300
        }
4301
        if (isset($semantics->font->background) && $semantics->font->background) {
4302
          $stylePatterns[] = '/^background-color: *(#[a-f0-9]{3}[a-f0-9]{3}?|rgba?\([0-9, ]+\)) *;?$/i';
4303
        }
4304
        if (isset($semantics->font->spacing) && $semantics->font->spacing) {
4305
          $stylePatterns[] = '/^letter-spacing: *[0-9.]+(em|px|%) *;?$/i';
4306
        }
4307
        if (isset($semantics->font->height) && $semantics->font->height) {
4308
          $stylePatterns[] = '/^line-height: *[0-9.]+(em|px|%|) *;?$/i';
4309
        }
4310
      }
4311
 
4312
      // Alignment is allowed for all wysiwyg texts
4313
      $stylePatterns[] = '/^text-align: *(center|left|right);?$/i';
4314
 
4315
      // Strip invalid HTML tags.
4316
      $text = $this->filter_xss($text, $tags, $stylePatterns);
4317
    }
4318
    else {
4319
      // Filter text to plain text.
4320
      $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8', FALSE);
4321
    }
4322
 
4323
    // Check if string is within allowed length
4324
    if (isset($semantics->maxLength)) {
4325
      if (!extension_loaded('mbstring')) {
4326
        $this->h5pF->setErrorMessage($this->h5pF->t('The mbstring PHP extension is not loaded. H5P need this to function properly'), 'mbstring-unsupported');
4327
      }
4328
      else {
4329
        $text = mb_substr($text, 0, $semantics->maxLength);
4330
      }
4331
    }
4332
 
4333
    // Check if string is according to optional regexp in semantics
4334
    if (!($text === '' && isset($semantics->optional) && $semantics->optional) && isset($semantics->regexp)) {
4335
      // Escaping '/' found in patterns, so that it does not break regexp fencing.
4336
      $pattern = '/' . str_replace('/', '\\/', $semantics->regexp->pattern) . '/';
4337
      $pattern .= isset($semantics->regexp->modifiers) ? $semantics->regexp->modifiers : '';
4338
      if (preg_match($pattern, $text) === 0) {
4339
        // Note: explicitly ignore return value FALSE, to avoid removing text
4340
        // if regexp is invalid...
4341
        $this->h5pF->setErrorMessage($this->h5pF->t('Provided string is not valid according to regexp in semantics. (value: "%value", regexp: "%regexp")', array('%value' => $text, '%regexp' => $pattern)), 'semantics-invalid-according-regexp');
4342
        $text = '';
4343
      }
4344
    }
4345
  }
4346
 
4347
  /**
4348
   * Validates content files
4349
   *
4350
   * @param string $contentPath
4351
   *  The path containing content files to validate.
4352
   * @param bool $isLibrary
4353
   * @return bool TRUE if all files are valid
4354
   * TRUE if all files are valid
4355
   * FALSE if one or more files fail validation. Error message should be set accordingly by validator.
4356
   */
4357
  public function validateContentFiles($contentPath, $isLibrary = FALSE) {
4358
    if ($this->h5pC->disableFileCheck === TRUE) {
4359
      return TRUE;
4360
    }
4361
 
4362
    // Scan content directory for files, recurse into sub directories.
4363
    $files = array_diff(scandir($contentPath), array('.','..'));
4364
    $valid = TRUE;
4365
    $whitelist = $this->h5pF->getWhitelist($isLibrary, H5PCore::$defaultContentWhitelist, H5PCore::$defaultLibraryWhitelistExtras);
4366
 
4367
    $wl_regex = '/\.(' . preg_replace('/ +/i', '|', preg_quote($whitelist)) . ')$/i';
4368
 
4369
    foreach ($files as $file) {
4370
      $filePath = $contentPath . '/' . $file;
4371
      if (is_dir($filePath)) {
4372
        $valid = $this->validateContentFiles($filePath, $isLibrary) && $valid;
4373
      }
4374
      else {
4375
        // Snipped from drupal 6 "file_validate_extensions".  Using own code
4376
        // to avoid 1. creating a file-like object just to test for the known
4377
        // file name, 2. testing against a returned error array that could
4378
        // never be more than 1 element long anyway, 3. recreating the regex
4379
        // for every file.
4380
        if (!extension_loaded('mbstring')) {
4381
          $this->h5pF->setErrorMessage($this->h5pF->t('The mbstring PHP extension is not loaded. H5P need this to function properly'), 'mbstring-unsupported');
4382
          $valid = FALSE;
4383
        }
4384
        else if (!preg_match($wl_regex, mb_strtolower($file))) {
4385
          $this->h5pF->setErrorMessage($this->h5pF->t('File "%filename" not allowed. Only files with the following extensions are allowed: %files-allowed.', array('%filename' => $file, '%files-allowed' => $whitelist)), 'not-in-whitelist');
4386
          $valid = FALSE;
4387
        }
4388
      }
4389
    }
4390
    return $valid;
4391
  }
4392
 
4393
  /**
4394
   * Validate given value against number semantics
4395
   * @param $number
4396
   * @param $semantics
4397
   */
4398
  public function validateNumber(&$number, $semantics) {
4399
    // Validate that $number is indeed a number
4400
    if (!is_numeric($number)) {
4401
      $number = 0;
4402
    }
4403
    // Check if number is within valid bounds. Move within bounds if not.
4404
    if (isset($semantics->min) && $number < $semantics->min) {
4405
      $number = $semantics->min;
4406
    }
4407
    if (isset($semantics->max) && $number > $semantics->max) {
4408
      $number = $semantics->max;
4409
    }
4410
    // Check if number is within allowed bounds even if step value is set.
4411
    if (isset($semantics->step)) {
4412
      $testNumber = $number - (isset($semantics->min) ? $semantics->min : 0);
4413
      $rest = $testNumber % $semantics->step;
4414
      if ($rest !== 0) {
4415
        $number -= $rest;
4416
      }
4417
    }
4418
    // Check if number has proper number of decimals.
4419
    if (isset($semantics->decimals)) {
4420
      $number = round($number, $semantics->decimals);
4421
    }
4422
  }
4423
 
4424
  /**
4425
   * Validate given value against boolean semantics
4426
   * @param $bool
4427
   * @return bool
4428
   */
4429
  public function validateBoolean(&$bool) {
4430
    return is_bool($bool);
4431
  }
4432
 
4433
  /**
4434
   * Validate select values
4435
   * @param $select
4436
   * @param $semantics
4437
   */
4438
  public function validateSelect(&$select, $semantics) {
4439
    $optional = isset($semantics->optional) && $semantics->optional;
4440
    $strict = FALSE;
4441
    if (isset($semantics->options) && !empty($semantics->options)) {
4442
      // We have a strict set of options to choose from.
4443
      $strict = TRUE;
4444
      $options = array();
4445
 
4446
      foreach ($semantics->options as $option) {
4447
        // Support optgroup - just flatten options into one
4448
        if (isset($option->type) && $option->type === 'optgroup') {
4449
          foreach ($option->options as $suboption) {
4450
            $options[$suboption->value] = TRUE;
4451
          }
4452
        }
4453
        elseif (isset($option->value)) {
4454
          $options[$option->value] = TRUE;
4455
        }
4456
      }
4457
    }
4458
 
4459
    if (isset($semantics->multiple) && $semantics->multiple) {
4460
      // Multi-choice generates array of values. Test each one against valid
4461
      // options, if we are strict.  First make sure we are working on an
4462
      // array.
4463
      if (!is_array($select)) {
4464
        $select = array($select);
4465
      }
4466
 
4467
      foreach ($select as $key => &$value) {
4468
        if ($strict && !$optional && !isset($options[$value])) {
4469
          $this->h5pF->setErrorMessage($this->h5pF->t('Invalid selected option in multi-select.'));
4470
          unset($select[$key]);
4471
        }
4472
        else {
4473
          $select[$key] = htmlspecialchars($value, ENT_QUOTES, 'UTF-8', FALSE);
4474
        }
4475
      }
4476
    }
4477
    else {
4478
      // Single mode.  If we get an array in here, we chop off the first
4479
      // element and use that instead.
4480
      if (is_array($select)) {
4481
        $select = $select[0];
4482
      }
4483
 
4484
      if ($strict && !$optional && !isset($options[$select])) {
4485
        $this->h5pF->setErrorMessage($this->h5pF->t('Invalid selected option in select.'));
4486
        $select = $semantics->options[0]->value;
4487
      }
4488
      $select = htmlspecialchars($select, ENT_QUOTES, 'UTF-8', FALSE);
4489
    }
4490
  }
4491
 
4492
  /**
4493
   * Validate given list value against list semantics.
4494
   * Will recurse into validating each item in the list according to the type.
4495
   * @param $list
4496
   * @param $semantics
4497
   */
4498
  public function validateList(&$list, $semantics) {
4499
    $field = $semantics->field;
4500
    $function = $this->typeMap[$field->type];
4501
 
4502
    // Check that list is not longer than allowed length. We do this before
4503
    // iterating to avoid unnecessary work.
4504
    if (isset($semantics->max)) {
4505
      array_splice($list, $semantics->max);
4506
    }
4507
 
4508
    if (!is_array($list)) {
4509
      $list = array();
4510
    }
4511
 
4512
    // Validate each element in list.
4513
    foreach ($list as $key => &$value) {
4514
      if (!is_int($key)) {
4515
        array_splice($list, $key, 1);
4516
        continue;
4517
      }
4518
      $this->$function($value, $field);
4519
      if ($value === NULL) {
4520
        array_splice($list, $key, 1);
4521
      }
4522
    }
4523
 
4524
    if (count($list) === 0) {
4525
      $list = NULL;
4526
    }
4527
  }
4528
 
4529
  /**
4530
   * Validate a file like object, such as video, image, audio and file.
4531
   * @param $file
4532
   * @param $semantics
4533
   * @param array $typeValidKeys
4534
   */
4535
  private function _validateFilelike(&$file, $semantics, $typeValidKeys = array()) {
4536
    // Do not allow to use files from other content folders.
4537
    $matches = array();
4538
    if (preg_match($this->h5pC->relativePathRegExp, $file->path, $matches)) {
4539
      $file->path = $matches[5];
4540
    }
4541
 
4542
    // Remove temporary files suffix
4543
    if (substr($file->path, -4, 4) === '#tmp') {
4544
      $file->path = substr($file->path, 0, strlen($file->path) - 4);
4545
    }
4546
 
4547
    // Make sure path and mime does not have any special chars
4548
    $file->path = htmlspecialchars($file->path, ENT_QUOTES, 'UTF-8', FALSE);
4549
    if (isset($file->mime)) {
4550
      $file->mime = htmlspecialchars($file->mime, ENT_QUOTES, 'UTF-8', FALSE);
4551
    }
4552
 
4553
    // Remove attributes that should not exist, they may contain JSON escape
4554
    // code.
4555
    $validKeys = array_merge(array('path', 'mime', 'copyright'), $typeValidKeys);
4556
    if (isset($semantics->extraAttributes)) {
4557
      $validKeys = array_merge($validKeys, $semantics->extraAttributes); // TODO: Validate extraAttributes
4558
    }
4559
    $this->filterParams($file, $validKeys);
4560
 
4561
    if (isset($file->width)) {
4562
      $file->width = intval($file->width);
4563
    }
4564
 
4565
    if (isset($file->height)) {
4566
      $file->height = intval($file->height);
4567
    }
4568
 
4569
    if (isset($file->codecs)) {
4570
      $file->codecs = htmlspecialchars($file->codecs, ENT_QUOTES, 'UTF-8', FALSE);
4571
    }
4572
 
4573
    if (isset($file->bitrate)) {
4574
      $file->bitrate = intval($file->bitrate);
4575
    }
4576
 
4577
    if (isset($file->quality)) {
4578
      if (!is_object($file->quality) || !isset($file->quality->level) || !isset($file->quality->label)) {
4579
        unset($file->quality);
4580
      }
4581
      else {
4582
        $this->filterParams($file->quality, array('level', 'label'));
4583
        $file->quality->level = intval($file->quality->level);
4584
        $file->quality->label = htmlspecialchars($file->quality->label, ENT_QUOTES, 'UTF-8', FALSE);
4585
      }
4586
    }
4587
 
4588
    if (isset($file->copyright)) {
4589
      $this->validateGroup($file->copyright, $this->getCopyrightSemantics());
4590
    }
4591
  }
4592
 
4593
  /**
4594
   * Validate given file data
4595
   * @param $file
4596
   * @param $semantics
4597
   */
4598
  public function validateFile(&$file, $semantics) {
4599
    $this->_validateFilelike($file, $semantics);
4600
  }
4601
 
4602
  /**
4603
   * Validate given image data
4604
   * @param $image
4605
   * @param $semantics
4606
   */
4607
  public function validateImage(&$image, $semantics) {
4608
    $this->_validateFilelike($image, $semantics, array('width', 'height', 'originalImage'));
4609
  }
4610
 
4611
  /**
4612
   * Validate given video data
4613
   * @param $video
4614
   * @param $semantics
4615
   */
4616
  public function validateVideo(&$video, $semantics) {
4617
    foreach ($video as &$variant) {
4618
      $this->_validateFilelike($variant, $semantics, array('width', 'height', 'codecs', 'quality', 'bitrate'));
4619
    }
4620
  }
4621
 
4622
  /**
4623
   * Validate given audio data
4624
   * @param $audio
4625
   * @param $semantics
4626
   */
4627
  public function validateAudio(&$audio, $semantics) {
4628
    foreach ($audio as &$variant) {
4629
      $this->_validateFilelike($variant, $semantics);
4630
    }
4631
  }
4632
 
4633
  /**
4634
   * Validate given group value against group semantics.
4635
   * Will recurse into validating each group member.
4636
   * @param $group
4637
   * @param $semantics
4638
   * @param bool $flatten
4639
   */
4640
  public function validateGroup(&$group, $semantics, $flatten = TRUE) {
4641
    // Groups with just one field are compressed in the editor to only output
4642
    // the child content. (Exemption for fake groups created by
4643
    // "validateBySemantics" above)
4644
    $function = null;
4645
    $field = null;
4646
 
4647
    $isSubContent = isset($semantics->isSubContent) && $semantics->isSubContent === TRUE;
4648
 
4649
    if (count($semantics->fields) == 1 && $flatten && !$isSubContent) {
4650
      $field = $semantics->fields[0];
4651
      $function = $this->typeMap[$field->type];
4652
      $this->$function($group, $field);
4653
    }
4654
    else {
4655
      foreach ($group as $key => &$value) {
4656
        // If subContentId is set, keep value
4657
        if($isSubContent && ($key == 'subContentId')){
4658
          continue;
4659
        }
4660
 
4661
        // Find semantics for name=$key
4662
        $found = FALSE;
4663
        foreach ($semantics->fields as $field) {
4664
          if ($field->name == $key) {
4665
            if (isset($semantics->optional) && $semantics->optional) {
4666
              $field->optional = TRUE;
4667
            }
4668
            $function = $this->typeMap[$field->type];
4669
            $found = TRUE;
4670
            break;
4671
          }
4672
        }
4673
        if ($found) {
4674
          if ($function) {
4675
            $this->$function($value, $field);
4676
            if ($value === NULL) {
4677
              unset($group->$key);
4678
            }
4679
          }
4680
          else {
4681
            // We have a field type in semantics for which we don't have a
4682
            // known validator.
4683
            $this->h5pF->setErrorMessage($this->h5pF->t('H5P internal error: unknown content type "@type" in semantics. Removing content!', array('@type' => $field->type)), 'semantics-unknown-type');
4684
            unset($group->$key);
4685
          }
4686
        }
4687
        else {
4688
          // If validator is not found, something exists in content that does
4689
          // not have a corresponding semantics field. Remove it.
4690
          // $this->h5pF->setErrorMessage($this->h5pF->t('H5P internal error: no validator exists for @key', array('@key' => $key)));
4691
          unset($group->$key);
4692
        }
4693
      }
4694
    }
4695
  }
4696
 
4697
  /**
4698
   * Validate given library value against library semantics.
4699
   * Check if provided library is within allowed options.
4700
   *
4701
   * Will recurse into validating the library's semantics too.
4702
   * @param $value
4703
   * @param $semantics
4704
   */
4705
  public function validateLibrary(&$value, $semantics) {
4706
    if (!isset($value->library)) {
4707
      $value = NULL;
4708
      return;
4709
    }
4710
 
4711
    // Check for array of objects or array of strings
4712
    if (is_object($semantics->options[0])) {
4713
      $getLibraryNames = function ($item) {
4714
        return $item->name;
4715
      };
4716
      $libraryNames = array_map($getLibraryNames, $semantics->options);
4717
    }
4718
    else {
4719
      $libraryNames = $semantics->options;
4720
    }
4721
 
4722
    if (!in_array($value->library, $libraryNames)) {
4723
      $message = NULL;
4724
      // Create an understandable error message:
4725
      $machineNameArray = explode(' ', $value->library);
4726
      $machineName = $machineNameArray[0];
4727
      foreach ($libraryNames as $semanticsLibrary) {
4728
        $semanticsMachineNameArray = explode(' ', $semanticsLibrary);
4729
        $semanticsMachineName = $semanticsMachineNameArray[0];
4730
        if ($machineName === $semanticsMachineName) {
4731
          // Using the wrong version of the library in the content
4732
          $message = $this->h5pF->t('The version of the H5P library %machineName used in this content is not valid. Content contains %contentLibrary, but it should be %semanticsLibrary.', array(
4733
            '%machineName' => $machineName,
4734
            '%contentLibrary' => $value->library,
4735
            '%semanticsLibrary' => $semanticsLibrary
4736
          ));
4737
          break;
4738
        }
4739
      }
4740
 
4741
      // Using a library in content that is not present at all in semantics
4742
      if ($message === NULL) {
4743
        $message = $this->h5pF->t('The H5P library %library used in the content is not valid', array(
4744
          '%library' => $value->library
4745
        ));
4746
      }
4747
 
4748
      $this->h5pF->setErrorMessage($message);
4749
      $value = NULL;
4750
      return;
4751
    }
4752
 
4753
    if (!isset($this->libraries[$value->library])) {
4754
      $libSpec = H5PCore::libraryFromString($value->library);
4755
      $library = $this->h5pC->loadLibrary($libSpec['machineName'], $libSpec['majorVersion'], $libSpec['minorVersion']);
4756
      $library['semantics'] = $this->h5pC->loadLibrarySemantics($libSpec['machineName'], $libSpec['majorVersion'], $libSpec['minorVersion']);
4757
      $this->libraries[$value->library] = $library;
4758
    }
4759
    else {
4760
      $library = $this->libraries[$value->library];
4761
    }
4762
 
4763
    // Validate parameters
4764
    $this->validateGroup($value->params, (object) array(
4765
      'type' => 'group',
4766
      'fields' => $library['semantics'],
4767
    ), FALSE);
4768
 
4769
    // Validate subcontent's metadata
4770
    if (isset($value->metadata)) {
4771
      $value->metadata = $this->validateMetadata($value->metadata);
4772
    }
4773
 
4774
    $validKeys = array('library', 'params', 'subContentId', 'metadata');
4775
    if (isset($semantics->extraAttributes)) {
4776
      $validKeys = array_merge($validKeys, $semantics->extraAttributes);
4777
    }
4778
 
4779
    $this->filterParams($value, $validKeys);
4780
    if (isset($value->subContentId) && ! preg_match('/^\{?[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\}?$/', $value->subContentId)) {
4781
      unset($value->subContentId);
4782
    }
4783
 
4784
    // Find all dependencies for this library
4785
    $depKey = 'preloaded-' . $library['machineName'];
4786
    if (!isset($this->dependencies[$depKey])) {
4787
      $this->dependencies[$depKey] = array(
4788
        'library' => $library,
4789
        'type' => 'preloaded'
4790
      );
4791
 
4792
      $this->nextWeight = $this->h5pC->findLibraryDependencies($this->dependencies, $library, $this->nextWeight);
4793
      $this->dependencies[$depKey]['weight'] = $this->nextWeight++;
4794
    }
4795
  }
4796
 
4797
  /**
4798
   * Check params for a whitelist of allowed properties
4799
   *
4800
   * @param array/object $params
4801
   * @param array $whitelist
4802
   */
4803
  public function filterParams(&$params, $whitelist) {
4804
    foreach ($params as $key => $value) {
4805
      if (!in_array($key, $whitelist)) {
4806
        unset($params->{$key});
4807
      }
4808
    }
4809
  }
4810
 
4811
  // XSS filters copied from drupal 7 common.inc. Some modifications done to
4812
  // replace Drupal one-liner functions with corresponding flat PHP.
4813
 
4814
  /**
4815
   * Filters HTML to prevent cross-site-scripting (XSS) vulnerabilities.
4816
   *
4817
   * Based on kses by Ulf Harnhammar, see http://sourceforge.net/projects/kses.
4818
   * For examples of various XSS attacks, see: http://ha.ckers.org/xss.html.
4819
   *
4820
   * This code does four things:
4821
   * - Removes characters and constructs that can trick browsers.
4822
   * - Makes sure all HTML entities are well-formed.
4823
   * - Makes sure all HTML tags and attributes are well-formed.
4824
   * - Makes sure no HTML tags contain URLs with a disallowed protocol (e.g.
4825
   *   javascript:).
4826
   *
4827
   * @param $string
4828
   *   The string with raw HTML in it. It will be stripped of everything that can
4829
   *   cause an XSS attack.
4830
   * @param array $allowed_tags
4831
   *   An array of allowed tags.
4832
   *
4833
   * @param bool $allowedStyles
4834
   * @return mixed|string An XSS safe version of $string, or an empty string if $string is not
4835
   * An XSS safe version of $string, or an empty string if $string is not
4836
   * valid UTF-8.
4837
   * @ingroup sanitation
4838
   */
4839
  private function filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd'), $allowedStyles = FALSE) {
4840
    if (strlen($string) == 0) {
4841
      return $string;
4842
    }
4843
    // Only operate on valid UTF-8 strings. This is necessary to prevent cross
4844
    // site scripting issues on Internet Explorer 6. (Line copied from
4845
    // drupal_validate_utf8)
4846
    if (preg_match('/^./us', $string) != 1) {
4847
      return '';
4848
    }
4849
 
4850
    $this->allowedStyles = $allowedStyles;
4851
 
4852
    // Store the text format.
4853
    $this->_filter_xss_split($allowed_tags, TRUE);
4854
    // Remove NULL characters (ignored by some browsers).
4855
    $string = str_replace(chr(0), '', $string);
4856
    // Remove Netscape 4 JS entities.
4857
    $string = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string);
4858
 
4859
    // Defuse all HTML entities.
4860
    $string = str_replace('&', '&amp;', $string);
4861
    // Change back only well-formed entities in our whitelist:
4862
    // Decimal numeric entities.
4863
    $string = preg_replace('/&amp;#([0-9]+;)/', '&#\1', $string);
4864
    // Hexadecimal numeric entities.
4865
    $string = preg_replace('/&amp;#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '&#x\1', $string);
4866
    // Named entities.
4867
    $string = preg_replace('/&amp;([A-Za-z][A-Za-z0-9]*;)/', '&\1', $string);
4868
    return preg_replace_callback('%
4869
      (
4870
      <(?=[^a-zA-Z!/])  # a lone <
4871
      |                 # or
4872
      <!--.*?-->        # a comment
4873
      |                 # or
4874
      <[^>]*(>|$)       # a string that starts with a <, up until the > or the end of the string
4875
      |                 # or
4876
      >                 # just a >
4877
      )%x', array($this, '_filter_xss_split'), $string);
4878
  }
4879
 
4880
  /**
4881
   * Processes an HTML tag.
4882
   *
4883
   * @param $m
4884
   *   An array with various meaning depending on the value of $store.
4885
   *   If $store is TRUE then the array contains the allowed tags.
4886
   *   If $store is FALSE then the array has one element, the HTML tag to process.
4887
   * @param bool $store
4888
   *   Whether to store $m.
4889
   * @return string If the element isn't allowed, an empty string. Otherwise, the cleaned up
4890
   * If the element isn't allowed, an empty string. Otherwise, the cleaned up
4891
   * version of the HTML element.
4892
   */
4893
  private function _filter_xss_split($m, $store = FALSE) {
4894
    static $allowed_html;
4895
 
4896
    if ($store) {
4897
      $allowed_html = array_flip($m);
4898
      return $allowed_html;
4899
    }
4900
 
4901
    $string = $m[1];
4902
 
4903
    if (substr($string, 0, 1) != '<') {
4904
      // We matched a lone ">" character.
4905
      return '&gt;';
4906
    }
4907
    elseif (strlen($string) == 1) {
4908
      // We matched a lone "<" character.
4909
      return '&lt;';
4910
    }
4911
 
4912
    if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9\-]+)\s*([^>]*)>?|(<!--.*?-->)$%', $string, $matches)) {
4913
      // Seriously malformed.
4914
      return '';
4915
    }
4916
 
4917
    $slash = trim($matches[1]);
4918
    $elem = &$matches[2];
4919
    $attrList = &$matches[3];
4920
    $comment = &$matches[4];
4921
 
4922
    if ($comment) {
4923
      $elem = '!--';
4924
    }
4925
 
4926
    if (!isset($allowed_html[strtolower($elem)])) {
4927
      // Disallowed HTML element.
4928
      return '';
4929
    }
4930
 
4931
    if ($comment) {
4932
      return $comment;
4933
    }
4934
 
4935
    if ($slash != '') {
4936
      return "</$elem>";
4937
    }
4938
 
4939
    // Is there a closing XHTML slash at the end of the attributes?
4940
    $attrList = preg_replace('%(\s?)/\s*$%', '\1', $attrList, -1, $count);
4941
    $xhtml_slash = $count ? ' /' : '';
4942
 
4943
    // Clean up attributes.
4944
 
4945
    $attr2 = implode(' ', $this->_filter_xss_attributes($attrList, (in_array($elem, self::$allowed_styleable_tags) ? $this->allowedStyles : FALSE)));
4946
    $attr2 = preg_replace('/[<>]/', '', $attr2);
4947
    $attr2 = strlen($attr2) ? ' ' . $attr2 : '';
4948
 
4949
    return "<$elem$attr2$xhtml_slash>";
4950
  }
4951
 
4952
  /**
4953
   * Processes a string of HTML attributes.
4954
   *
4955
   * @param $attr
4956
   * @param array|bool|object $allowedStyles
4957
   * @return array Cleaned up version of the HTML attributes.
4958
   * Cleaned up version of the HTML attributes.
4959
   */
4960
  private function _filter_xss_attributes($attr, $allowedStyles = FALSE) {
4961
    $attrArr = array();
4962
    $mode = 0;
4963
    $attrName = '';
4964
    $skip = false;
4965
 
4966
    while (strlen($attr) != 0) {
4967
      // Was the last operation successful?
4968
      $working = 0;
4969
      switch ($mode) {
4970
        case 0:
4971
          // Attribute name, href for instance.
4972
          if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) {
4973
            $attrName = strtolower($match[1]);
4974
            $skip = (
4975
              $attrName == 'style' ||
4976
              substr($attrName, 0, 2) == 'on' ||
4977
              substr($attrName, 0, 1) == '-' ||
4978
              // Ignore long attributes to avoid unnecessary processing overhead.
4979
              strlen($attrName) > 96
4980
            );
4981
            $working = $mode = 1;
4982
            $attr = preg_replace('/^[-a-zA-Z]+/', '', $attr);
4983
          }
4984
          break;
4985
 
4986
        case 1:
4987
          // Equals sign or valueless ("selected").
4988
          if (preg_match('/^\s*=\s*/', $attr)) {
4989
            $working = 1; $mode = 2;
4990
            $attr = preg_replace('/^\s*=\s*/', '', $attr);
4991
            break;
4992
          }
4993
 
4994
          if (preg_match('/^\s+/', $attr)) {
4995
            $working = 1; $mode = 0;
4996
            if (!$skip) {
4997
              $attrArr[] = $attrName;
4998
            }
4999
            $attr = preg_replace('/^\s+/', '', $attr);
5000
          }
5001
          break;
5002
 
5003
        case 2:
5004
          // Attribute value, a URL after href= for instance.
5005
          if (preg_match('/^"([^"]*)"(\s+|$)/', $attr, $match)) {
5006
            if ($allowedStyles && $attrName === 'style') {
5007
              // Allow certain styles
5008
              foreach ($allowedStyles as $pattern) {
5009
                if (preg_match($pattern, $match[1])) {
5010
                  // All patterns are start to end patterns, and CKEditor adds one span per style
5011
                  $attrArr[] = 'style="' . $match[1] . '"';
5012
                  break;
5013
                }
5014
              }
5015
              break;
5016
            }
5017
 
5018
            $thisVal = $this->filter_xss_bad_protocol($match[1]);
5019
 
5020
            if (!$skip) {
5021
              $attrArr[] = "$attrName=\"$thisVal\"";
5022
            }
5023
            $working = 1;
5024
            $mode = 0;
5025
            $attr = preg_replace('/^"[^"]*"(\s+|$)/', '', $attr);
5026
            break;
5027
          }
5028
 
5029
          if (preg_match("/^'([^']*)'(\s+|$)/", $attr, $match)) {
5030
            $thisVal = $this->filter_xss_bad_protocol($match[1]);
5031
 
5032
            if (!$skip) {
5033
              $attrArr[] = "$attrName='$thisVal'";
5034
            }
5035
            $working = 1; $mode = 0;
5036
            $attr = preg_replace("/^'[^']*'(\s+|$)/", '', $attr);
5037
            break;
5038
          }
5039
 
5040
          if (preg_match("%^([^\s\"']+)(\s+|$)%", $attr, $match)) {
5041
            $thisVal = $this->filter_xss_bad_protocol($match[1]);
5042
 
5043
            if (!$skip) {
5044
              $attrArr[] = "$attrName=\"$thisVal\"";
5045
            }
5046
            $working = 1; $mode = 0;
5047
            $attr = preg_replace("%^[^\s\"']+(\s+|$)%", '', $attr);
5048
          }
5049
          break;
5050
      }
5051
 
5052
      if ($working == 0) {
5053
        // Not well formed; remove and try again.
5054
        $attr = preg_replace('/
5055
          ^
5056
          (
5057
          "[^"]*("|$)     # - a string that starts with a double quote, up until the next double quote or the end of the string
5058
          |               # or
5059
          \'[^\']*(\'|$)| # - a string that starts with a quote, up until the next quote or the end of the string
5060
          |               # or
5061
          \S              # - a non-whitespace character
5062
          )*              # any number of the above three
5063
          \s*             # any number of whitespaces
5064
          /x', '', $attr);
5065
        $mode = 0;
5066
      }
5067
    }
5068
 
5069
    // The attribute list ends with a valueless attribute like "selected".
5070
    if ($mode == 1 && !$skip) {
5071
      $attrArr[] = $attrName;
5072
    }
5073
    return $attrArr;
5074
  }
5075
 
5076
// TODO: Remove Drupal related stuff in docs.
5077
 
5078
  /**
5079
   * Processes an HTML attribute value and strips dangerous protocols from URLs.
5080
   *
5081
   * @param $string
5082
   *   The string with the attribute value.
5083
   * @param bool $decode
5084
   *   (deprecated) Whether to decode entities in the $string. Set to FALSE if the
5085
   *   $string is in plain text, TRUE otherwise. Defaults to TRUE. This parameter
5086
   *   is deprecated and will be removed in Drupal 8. To process a plain-text URI,
5087
   *   call _strip_dangerous_protocols() or check_url() instead.
5088
   * @return string Cleaned up and HTML-escaped version of $string.
5089
   * Cleaned up and HTML-escaped version of $string.
5090
   */
5091
  private function filter_xss_bad_protocol($string, $decode = TRUE) {
5092
    // Get the plain text representation of the attribute value (i.e. its meaning).
5093
    // @todo Remove the $decode parameter in Drupal 8, and always assume an HTML
5094
    //   string that needs decoding.
5095
    if ($decode) {
5096
      $string = html_entity_decode($string, ENT_QUOTES, 'UTF-8');
5097
    }
5098
    return htmlspecialchars($this->_strip_dangerous_protocols($string), ENT_QUOTES, 'UTF-8', FALSE);
5099
  }
5100
 
5101
  /**
5102
   * Strips dangerous protocols (e.g. 'javascript:') from a URI.
5103
   *
5104
   * This function must be called for all URIs within user-entered input prior
5105
   * to being output to an HTML attribute value. It is often called as part of
5106
   * check_url() or filter_xss(), but those functions return an HTML-encoded
5107
   * string, so this function can be called independently when the output needs to
5108
   * be a plain-text string for passing to t(), l(), drupal_attributes(), or
5109
   * another function that will call check_plain() separately.
5110
   *
5111
   * @param $uri
5112
   *   A plain-text URI that might contain dangerous protocols.
5113
   * @return string A plain-text URI stripped of dangerous protocols. As with all plain-text
5114
   * A plain-text URI stripped of dangerous protocols. As with all plain-text
5115
   * strings, this return value must not be output to an HTML page without
5116
   * check_plain() being called on it. However, it can be passed to functions
5117
   * expecting plain-text strings.
5118
   * @see check_url()
5119
   */
5120
  private function _strip_dangerous_protocols($uri) {
5121
    static $allowed_protocols;
5122
 
5123
    if (!isset($allowed_protocols)) {
5124
      $allowed_protocols = array_flip(array('ftp', 'http', 'https', 'mailto'));
5125
    }
5126
 
5127
    // Iteratively remove any invalid protocol found.
5128
    do {
5129
      $before = $uri;
5130
      $colonPos = strpos($uri, ':');
5131
      if ($colonPos > 0) {
5132
        // We found a colon, possibly a protocol. Verify.
5133
        $protocol = substr($uri, 0, $colonPos);
5134
        // If a colon is preceded by a slash, question mark or hash, it cannot
5135
        // possibly be part of the URL scheme. This must be a relative URL, which
5136
        // inherits the (safe) protocol of the base document.
5137
        if (preg_match('![/?#]!', $protocol)) {
5138
          break;
5139
        }
5140
        // Check if this is a disallowed protocol. Per RFC2616, section 3.2.3
5141
        // (URI Comparison) scheme comparison must be case-insensitive.
5142
        if (!isset($allowed_protocols[strtolower($protocol)])) {
5143
          $uri = substr($uri, $colonPos + 1);
5144
        }
5145
      }
5146
    } while ($before != $uri);
5147
 
5148
    return $uri;
5149
  }
5150
 
5151
  public function getMetadataSemantics() {
5152
    static $semantics;
5153
 
5154
    $cc_versions = array(
5155
      (object) array(
5156
        'value' => '4.0',
5157
        'label' => $this->h5pF->t('4.0 International')
5158
      ),
5159
      (object) array(
5160
        'value' => '3.0',
5161
        'label' => $this->h5pF->t('3.0 Unported')
5162
      ),
5163
      (object) array(
5164
        'value' => '2.5',
5165
        'label' => $this->h5pF->t('2.5 Generic')
5166
      ),
5167
      (object) array(
5168
        'value' => '2.0',
5169
        'label' => $this->h5pF->t('2.0 Generic')
5170
      ),
5171
      (object) array(
5172
        'value' => '1.0',
5173
        'label' => $this->h5pF->t('1.0 Generic')
5174
      )
5175
    );
5176
 
5177
    $semantics = array(
5178
      (object) array(
5179
        'name' => 'title',
5180
        'type' => 'text',
5181
        'label' => $this->h5pF->t('Title'),
5182
        'placeholder' => 'La Gioconda'
5183
      ),
5184
      (object) array(
5185
        'name' => 'a11yTitle',
5186
        'type' => 'text',
5187
        'label' => $this->h5pF->t('Assistive Technologies label'),
5188
        'optional' => TRUE,
5189
      ),
5190
      (object) array(
5191
        'name' => 'license',
5192
        'type' => 'select',
5193
        'label' => $this->h5pF->t('License'),
5194
        'default' => 'U',
5195
        'options' => array(
5196
          (object) array(
5197
            'value' => 'U',
5198
            'label' => $this->h5pF->t('Undisclosed')
5199
          ),
5200
          (object) array(
5201
            'type' => 'optgroup',
5202
            'label' => $this->h5pF->t('Creative Commons'),
5203
            'options' => array(
5204
              (object) array(
5205
                'value' => 'CC BY',
5206
                'label' => $this->h5pF->t('Attribution (CC BY)'),
5207
                'versions' => $cc_versions
5208
              ),
5209
              (object) array(
5210
                'value' => 'CC BY-SA',
5211
                'label' => $this->h5pF->t('Attribution-ShareAlike (CC BY-SA)'),
5212
                'versions' => $cc_versions
5213
              ),
5214
              (object) array(
5215
                'value' => 'CC BY-ND',
5216
                'label' => $this->h5pF->t('Attribution-NoDerivs (CC BY-ND)'),
5217
                'versions' => $cc_versions
5218
              ),
5219
              (object) array(
5220
                'value' => 'CC BY-NC',
5221
                'label' => $this->h5pF->t('Attribution-NonCommercial (CC BY-NC)'),
5222
                'versions' => $cc_versions
5223
              ),
5224
              (object) array(
5225
                'value' => 'CC BY-NC-SA',
5226
                'label' => $this->h5pF->t('Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)'),
5227
                'versions' => $cc_versions
5228
              ),
5229
              (object) array(
5230
                'value' => 'CC BY-NC-ND',
5231
                'label' => $this->h5pF->t('Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)'),
5232
                'versions' => $cc_versions
5233
              ),
5234
              (object) array(
5235
                'value' => 'CC0 1.0',
5236
                'label' => $this->h5pF->t('Public Domain Dedication (CC0)')
5237
              ),
5238
              (object) array(
5239
                'value' => 'CC PDM',
5240
                'label' => $this->h5pF->t('Public Domain Mark (PDM)')
5241
              ),
5242
            )
5243
          ),
5244
          (object) array(
5245
            'value' => 'GNU GPL',
5246
            'label' => $this->h5pF->t('General Public License v3')
5247
          ),
5248
          (object) array(
5249
            'value' => 'PD',
5250
            'label' => $this->h5pF->t('Public Domain')
5251
          ),
5252
          (object) array(
5253
            'value' => 'ODC PDDL',
5254
            'label' => $this->h5pF->t('Public Domain Dedication and Licence')
5255
          ),
5256
          (object) array(
5257
            'value' => 'C',
5258
            'label' => $this->h5pF->t('Copyright')
5259
          )
5260
        )
5261
      ),
5262
      (object) array(
5263
        'name' => 'licenseVersion',
5264
        'type' => 'select',
5265
        'label' => $this->h5pF->t('License Version'),
5266
        'options' => $cc_versions,
5267
        'optional' => TRUE
5268
      ),
5269
      (object) array(
5270
        'name' => 'yearFrom',
5271
        'type' => 'number',
5272
        'label' => $this->h5pF->t('Years (from)'),
5273
        'placeholder' => '1991',
5274
        'min' => '-9999',
5275
        'max' => '9999',
5276
        'optional' => TRUE
5277
      ),
5278
      (object) array(
5279
        'name' => 'yearTo',
5280
        'type' => 'number',
5281
        'label' => $this->h5pF->t('Years (to)'),
5282
        'placeholder' => '1992',
5283
        'min' => '-9999',
5284
        'max' => '9999',
5285
        'optional' => TRUE
5286
      ),
5287
      (object) array(
5288
        'name' => 'source',
5289
        'type' => 'text',
5290
        'label' => $this->h5pF->t('Source'),
5291
        'placeholder' => 'https://',
5292
        'optional' => TRUE
5293
      ),
5294
      (object) array(
5295
        'name' => 'authors',
5296
        'type' => 'list',
5297
        'field' => (object) array (
5298
          'name' => 'author',
5299
          'type' => 'group',
5300
          'fields'=> array(
5301
            (object) array(
5302
              'label' => $this->h5pF->t("Author's name"),
5303
              'name' => 'name',
5304
              'optional' => TRUE,
5305
              'type' => 'text'
5306
            ),
5307
            (object) array(
5308
              'name' => 'role',
5309
              'type' => 'select',
5310
              'label' => $this->h5pF->t("Author's role"),
5311
              'default' => 'Author',
5312
              'options' => array(
5313
                (object) array(
5314
                  'value' => 'Author',
5315
                  'label' => $this->h5pF->t('Author')
5316
                ),
5317
                (object) array(
5318
                  'value' => 'Editor',
5319
                  'label' => $this->h5pF->t('Editor')
5320
                ),
5321
                (object) array(
5322
                  'value' => 'Licensee',
5323
                  'label' => $this->h5pF->t('Licensee')
5324
                ),
5325
                (object) array(
5326
                  'value' => 'Originator',
5327
                  'label' => $this->h5pF->t('Originator')
5328
                )
5329
              )
5330
            )
5331
          )
5332
        )
5333
      ),
5334
      (object) array(
5335
        'name' => 'licenseExtras',
5336
        'type' => 'text',
5337
        'widget' => 'textarea',
5338
        'label' => $this->h5pF->t('License Extras'),
5339
        'optional' => TRUE,
5340
        'description' => $this->h5pF->t('Any additional information about the license')
5341
      ),
5342
      (object) array(
5343
        'name' => 'changes',
5344
        'type' => 'list',
5345
        'field' => (object) array(
5346
          'name' => 'change',
5347
          'type' => 'group',
5348
          'label' => $this->h5pF->t('Changelog'),
5349
          'fields' => array(
5350
            (object) array(
5351
              'name' => 'date',
5352
              'type' => 'text',
5353
              'label' => $this->h5pF->t('Date'),
5354
              'optional' => TRUE
5355
            ),
5356
            (object) array(
5357
              'name' => 'author',
5358
              'type' => 'text',
5359
              'label' => $this->h5pF->t('Changed by'),
5360
              'optional' => TRUE
5361
            ),
5362
            (object) array(
5363
              'name' => 'log',
5364
              'type' => 'text',
5365
              'widget' => 'textarea',
5366
              'label' => $this->h5pF->t('Description of change'),
5367
              'placeholder' => $this->h5pF->t('Photo cropped, text changed, etc.'),
5368
              'optional' => TRUE
5369
            )
5370
          )
5371
        )
5372
      ),
5373
      (object) array (
5374
        'name' => 'authorComments',
5375
        'type' => 'text',
5376
        'widget' => 'textarea',
5377
        'label' => $this->h5pF->t('Author comments'),
5378
        'description' => $this->h5pF->t('Comments for the editor of the content (This text will not be published as a part of copyright info)'),
5379
        'optional' => TRUE
5380
      ),
5381
      (object) array(
5382
        'name' => 'contentType',
5383
        'type' => 'text',
5384
        'widget' => 'none'
5385
      ),
5386
      (object) array(
5387
        'name' => 'defaultLanguage',
5388
        'type' => 'text',
5389
        'widget' => 'none'
5390
      )
5391
    );
5392
 
5393
    return $semantics;
5394
  }
5395
 
5396
  public function getCopyrightSemantics() {
5397
    static $semantics;
5398
 
5399
    if ($semantics === NULL) {
5400
      $cc_versions = array(
5401
        (object) array(
5402
          'value' => '4.0',
5403
          'label' => $this->h5pF->t('4.0 International')
5404
        ),
5405
        (object) array(
5406
          'value' => '3.0',
5407
          'label' => $this->h5pF->t('3.0 Unported')
5408
        ),
5409
        (object) array(
5410
          'value' => '2.5',
5411
          'label' => $this->h5pF->t('2.5 Generic')
5412
        ),
5413
        (object) array(
5414
          'value' => '2.0',
5415
          'label' => $this->h5pF->t('2.0 Generic')
5416
        ),
5417
        (object) array(
5418
          'value' => '1.0',
5419
          'label' => $this->h5pF->t('1.0 Generic')
5420
        )
5421
      );
5422
 
5423
      $semantics = (object) array(
5424
        'name' => 'copyright',
5425
        'type' => 'group',
5426
        'label' => $this->h5pF->t('Copyright information'),
5427
        'fields' => array(
5428
          (object) array(
5429
            'name' => 'title',
5430
            'type' => 'text',
5431
            'label' => $this->h5pF->t('Title'),
5432
            'placeholder' => 'La Gioconda',
5433
            'optional' => TRUE
5434
          ),
5435
          (object) array(
5436
            'name' => 'author',
5437
            'type' => 'text',
5438
            'label' => $this->h5pF->t('Author'),
5439
            'placeholder' => 'Leonardo da Vinci',
5440
            'optional' => TRUE
5441
          ),
5442
          (object) array(
5443
            'name' => 'year',
5444
            'type' => 'text',
5445
            'label' => $this->h5pF->t('Year(s)'),
5446
            'placeholder' => '1503 - 1517',
5447
            'optional' => TRUE
5448
          ),
5449
          (object) array(
5450
            'name' => 'source',
5451
            'type' => 'text',
5452
            'label' => $this->h5pF->t('Source'),
5453
            'placeholder' => 'http://en.wikipedia.org/wiki/Mona_Lisa',
5454
            'optional' => true,
5455
            'regexp' => (object) array(
5456
              'pattern' => '^http[s]?://.+',
5457
              'modifiers' => 'i'
5458
            )
5459
          ),
5460
          (object) array(
5461
            'name' => 'license',
5462
            'type' => 'select',
5463
            'label' => $this->h5pF->t('License'),
5464
            'default' => 'U',
5465
            'options' => array(
5466
              (object) array(
5467
                'value' => 'U',
5468
                'label' => $this->h5pF->t('Undisclosed')
5469
              ),
5470
              (object) array(
5471
                'value' => 'CC BY',
5472
                'label' => $this->h5pF->t('Attribution'),
5473
                'versions' => $cc_versions
5474
              ),
5475
              (object) array(
5476
                'value' => 'CC BY-SA',
5477
                'label' => $this->h5pF->t('Attribution-ShareAlike'),
5478
                'versions' => $cc_versions
5479
              ),
5480
              (object) array(
5481
                'value' => 'CC BY-ND',
5482
                'label' => $this->h5pF->t('Attribution-NoDerivs'),
5483
                'versions' => $cc_versions
5484
              ),
5485
              (object) array(
5486
                'value' => 'CC BY-NC',
5487
                'label' => $this->h5pF->t('Attribution-NonCommercial'),
5488
                'versions' => $cc_versions
5489
              ),
5490
              (object) array(
5491
                'value' => 'CC BY-NC-SA',
5492
                'label' => $this->h5pF->t('Attribution-NonCommercial-ShareAlike'),
5493
                'versions' => $cc_versions
5494
              ),
5495
              (object) array(
5496
                'value' => 'CC BY-NC-ND',
5497
                'label' => $this->h5pF->t('Attribution-NonCommercial-NoDerivs'),
5498
                'versions' => $cc_versions
5499
              ),
5500
              (object) array(
5501
                'value' => 'GNU GPL',
5502
                'label' => $this->h5pF->t('General Public License'),
5503
                'versions' => array(
5504
                  (object) array(
5505
                    'value' => 'v3',
5506
                    'label' => $this->h5pF->t('Version 3')
5507
                  ),
5508
                  (object) array(
5509
                    'value' => 'v2',
5510
                    'label' => $this->h5pF->t('Version 2')
5511
                  ),
5512
                  (object) array(
5513
                    'value' => 'v1',
5514
                    'label' => $this->h5pF->t('Version 1')
5515
                  )
5516
                )
5517
              ),
5518
              (object) array(
5519
                'value' => 'PD',
5520
                'label' => $this->h5pF->t('Public Domain'),
5521
                'versions' => array(
5522
                  (object) array(
5523
                    'value' => '-',
5524
                    'label' => '-'
5525
                  ),
5526
                  (object) array(
5527
                    'value' => 'CC0 1.0',
5528
                    'label' => $this->h5pF->t('CC0 1.0 Universal')
5529
                  ),
5530
                  (object) array(
5531
                    'value' => 'CC PDM',
5532
                    'label' => $this->h5pF->t('Public Domain Mark')
5533
                  )
5534
                )
5535
              ),
5536
              (object) array(
5537
                'value' => 'C',
5538
                'label' => $this->h5pF->t('Copyright')
5539
              )
5540
            )
5541
          ),
5542
          (object) array(
5543
            'name' => 'version',
5544
            'type' => 'select',
5545
            'label' => $this->h5pF->t('License Version'),
5546
            'options' => array()
5547
          )
5548
        )
5549
      );
5550
    }
5551
 
5552
    return $semantics;
5553
  }
5554
}