Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
 
3
namespace Moodle;
4
 
5
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' => 27
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
    'styles/h5p-table.css',
2090
  );
2091
  public static $scripts = array(
2092
    'js/jquery.js',
2093
    'js/h5p.js',
2094
    'js/h5p-event-dispatcher.js',
2095
    'js/h5p-x-api-event.js',
2096
    'js/h5p-x-api.js',
2097
    'js/h5p-content-type.js',
2098
    'js/h5p-confirmation-dialog.js',
2099
    'js/h5p-action-bar.js',
2100
    'js/request-queue.js',
2101
    'js/h5p-tooltip.js',
2102
  );
2103
  public static $adminScripts = array(
2104
    'js/jquery.js',
2105
    'js/h5p-utils.js',
2106
  );
2107
 
2108
  public static $defaultContentWhitelist = 'json png jpg jpeg gif bmp tif tiff eot ttf woff woff2 otf webm mp4 ogg mp3 m4a wav txt pdf rtf doc docx xls xlsx ppt pptx odt ods odp csv diff patch swf md textile vtt webvtt gltf glb';
2109
  public static $defaultLibraryWhitelistExtras = 'js css svg xml';
2110
 
2111
  public $librariesJsonData, $contentJsonData, $mainJsonData, $h5pF, $fs, $h5pD, $disableFileCheck;
2112
  const SECONDS_IN_WEEK = 604800;
2113
 
2114
  private $exportEnabled;
2115
 
2116
  // Disable flags
2117
  const DISABLE_NONE = 0;
2118
  const DISABLE_FRAME = 1;
2119
  const DISABLE_DOWNLOAD = 2;
2120
  const DISABLE_EMBED = 4;
2121
  const DISABLE_COPYRIGHT = 8;
2122
  const DISABLE_ABOUT = 16;
2123
 
2124
  const DISPLAY_OPTION_FRAME = 'frame';
2125
  const DISPLAY_OPTION_DOWNLOAD = 'export';
2126
  const DISPLAY_OPTION_EMBED = 'embed';
2127
  const DISPLAY_OPTION_COPYRIGHT = 'copyright';
2128
  const DISPLAY_OPTION_ABOUT = 'icon';
2129
  const DISPLAY_OPTION_COPY = 'copy';
2130
 
2131
  // Map flags to string
2132
  public static $disable = array(
2133
    self::DISABLE_FRAME => self::DISPLAY_OPTION_FRAME,
2134
    self::DISABLE_DOWNLOAD => self::DISPLAY_OPTION_DOWNLOAD,
2135
    self::DISABLE_EMBED => self::DISPLAY_OPTION_EMBED,
2136
    self::DISABLE_COPYRIGHT => self::DISPLAY_OPTION_COPYRIGHT
2137
  );
2138
 
2139
  /** @var string */
2140
  public $url;
2141
 
2142
  /** @var int evelopment mode. */
2143
  public $development_mode;
2144
 
2145
  /** @var bool aggregated files for assets. */
2146
  public $aggregateAssets;
2147
 
2148
  /** @var string full path of plugin. */
2149
  protected $fullPluginPath;
2150
 
2151
  /** @var string regex for converting copied files paths. */
2152
  public $relativePathRegExp;
2153
 
2154
  /**
2155
   * Constructor for the H5PCore
2156
   *
2157
   * @param H5PFrameworkInterface $H5PFramework
2158
   *  The frameworks implementation of the H5PFrameworkInterface
2159
   * @param string|H5PFileStorage $path H5P file storage directory or class.
2160
   * @param string $url To file storage directory.
2161
   * @param string $language code. Defaults to english.
2162
   * @param boolean $export enabled?
2163
   */
2164
  public function __construct(H5PFrameworkInterface $H5PFramework, $path, $url, $language = 'en', $export = FALSE) {
2165
    $this->h5pF = $H5PFramework;
2166
 
2167
    $this->fs = ($path instanceof H5PFileStorage ? $path : new H5PDefaultStorage($path));
2168
 
2169
    $this->url = $url;
2170
    $this->exportEnabled = $export;
2171
    $this->development_mode = H5PDevelopment::MODE_NONE;
2172
 
2173
    $this->aggregateAssets = FALSE; // Off by default.. for now
2174
 
2175
    $this->detectSiteType();
2176
    $this->fullPluginPath = preg_replace('/\/[^\/]+[\/]?$/', '' , dirname(__FILE__));
2177
 
2178
    // Standard regex for converting copied files paths
2179
    $this->relativePathRegExp = '/^((\.\.\/){1,2})(.*content\/)?(\d+|editor)\/(.+)$/';
2180
  }
2181
 
2182
  /**
2183
   * Save content and clear cache.
2184
   *
2185
   * @param array $content
2186
   * @param null|int $contentMainId
2187
   * @return int Content ID
2188
   */
2189
  public function saveContent($content, $contentMainId = NULL) {
2190
    if (isset($content['id'])) {
2191
      $this->h5pF->updateContent($content, $contentMainId);
2192
    }
2193
    else {
2194
      $content['id'] = $this->h5pF->insertContent($content, $contentMainId);
2195
    }
2196
 
2197
    // Some user data for content has to be reset when the content changes.
2198
    $this->h5pF->resetContentUserData($contentMainId ? $contentMainId : $content['id']);
2199
 
2200
    return $content['id'];
2201
  }
2202
 
2203
  /**
2204
   * Load content.
2205
   *
2206
   * @param int $id for content.
2207
   * @return object
2208
   */
2209
  public function loadContent($id) {
2210
    $content = $this->h5pF->loadContent($id);
2211
 
2212
    if ($content !== NULL) {
2213
      // Validate main content's metadata
2214
      $validator = new H5PContentValidator($this->h5pF, $this);
2215
      $content['metadata'] = $validator->validateMetadata($content['metadata']);
2216
 
2217
      $content['library'] = array(
2218
        'id' => $content['libraryId'],
2219
        'name' => $content['libraryName'],
2220
        'majorVersion' => $content['libraryMajorVersion'],
2221
        'minorVersion' => $content['libraryMinorVersion'],
2222
        'embedTypes' => $content['libraryEmbedTypes'],
2223
        'fullscreen' => $content['libraryFullscreen'],
2224
      );
2225
      unset($content['libraryId'], $content['libraryName'], $content['libraryEmbedTypes'], $content['libraryFullscreen']);
2226
 
2227
//      // TODO: Move to filterParameters?
2228
//      if (isset($this->h5pD)) {
2229
//        // TODO: Remove Drupal specific stuff
2230
//        $json_content_path = file_create_path(file_directory_path() . '/' . variable_get('h5p_default_path', 'h5p') . '/content/' . $id . '/content.json');
2231
//        if (file_exists($json_content_path) === TRUE) {
2232
//          $json_content = file_get_contents($json_content_path);
2233
//          if (json_decode($json_content, TRUE) !== FALSE) {
2234
//            drupal_set_message(t('Invalid json in json content'), 'warning');
2235
//          }
2236
//          $content['params'] = $json_content;
2237
//        }
2238
//      }
2239
    }
2240
 
2241
    return $content;
2242
  }
2243
 
2244
  /**
2245
   * Filter content run parameters, rebuild content dependency cache and export file.
2246
   *
2247
   * @param Object|array $content
2248
   * @return Object NULL on failure.
2249
   */
2250
  public function filterParameters(&$content) {
2251
    if (!empty($content['filtered']) &&
2252
        (!$this->exportEnabled ||
2253
         ($content['slug'] &&
2254
          $this->fs->hasExport($content['slug'] . '-' . $content['id'] . '.h5p')))) {
2255
      return $content['filtered'];
2256
    }
2257
 
2258
    if (!(isset($content['library']) && isset($content['params']))) {
2259
      return NULL;
2260
    }
2261
 
2262
    // Validate and filter against main library semantics.
2263
    $validator = new H5PContentValidator($this->h5pF, $this);
2264
    $params = (object) array(
2265
      'library' => H5PCore::libraryToString($content['library']),
2266
      'params' => json_decode($content['params'])
2267
    );
2268
    if (!$params->params) {
2269
      return NULL;
2270
    }
2271
    $validator->validateLibrary($params, (object) array('options' => array($params->library)));
2272
 
2273
    // Handle addons:
2274
    $addons = $this->h5pF->loadAddons();
2275
    foreach ($addons as $addon) {
2276
      $add_to = json_decode($addon['addTo']);
2277
 
2278
      if (isset($add_to->content->types)) {
2279
        foreach($add_to->content->types as $type) {
2280
 
2281
          if (isset($type->text->regex) &&
2282
              $this->textAddonMatches($params->params, $type->text->regex)) {
2283
            $validator->addon($addon);
2284
 
2285
            // An addon shall only be added once
2286
            break;
2287
          }
2288
        }
2289
      }
2290
    }
2291
 
2292
    $params = json_encode($params->params);
2293
 
2294
    // Update content dependencies.
2295
    $content['dependencies'] = $validator->getDependencies();
2296
 
2297
    // Sometimes the parameters are filtered before content has been created
2298
    if ($content['id']) {
2299
      $this->h5pF->deleteLibraryUsage($content['id']);
2300
      $this->h5pF->saveLibraryUsage($content['id'], $content['dependencies']);
2301
 
2302
      if (!$content['slug']) {
2303
        $content['slug'] = $this->generateContentSlug($content);
2304
 
2305
        // Remove old export file
2306
        $this->fs->deleteExport($content['id'] . '.h5p');
2307
      }
2308
 
2309
      if ($this->exportEnabled) {
2310
        // Recreate export file
2311
        $exporter = new H5PExport($this->h5pF, $this);
2312
        $content['filtered'] = $params;
2313
        $exporter->createExportFile($content);
2314
      }
2315
 
2316
      // Cache.
2317
      $this->h5pF->updateContentFields($content['id'], array(
2318
        'filtered' => $params,
2319
        'slug' => $content['slug']
2320
      ));
2321
    }
2322
    return $params;
2323
  }
2324
 
2325
  /**
2326
   * Retrieve a value from a nested mixed array structure.
2327
   *
2328
   * @param Array $params Array to be looked in.
2329
   * @param String $path Supposed path to the value.
2330
   * @param String [$delimiter='.'] Property delimiter within the path.
2331
   * @return Object|NULL The object found or NULL.
2332
   */
2333
  private function retrieveValue ($params, $path, $delimiter='.') {
2334
    $path = explode($delimiter, $path);
2335
 
2336
    // Property not found
2337
    if (!isset($params[$path[0]])) {
2338
      return NULL;
2339
    }
2340
 
2341
    $first = $params[$path[0]];
2342
 
2343
    // End of path, done
2344
    if (sizeof($path) === 1) {
2345
      return $first;
2346
    }
2347
 
2348
    // We cannot go deeper
2349
    if (!is_array($first)) {
2350
      return NULL;
2351
    }
2352
 
2353
    // Regular Array
2354
    if (isset($first[0])) {
2355
      foreach($first as $number => $object) {
2356
        $found = $this->retrieveValue($object, implode($delimiter, array_slice($path, 1)));
2357
        if (isset($found)) {
2358
          return $found;
2359
        }
2360
      }
2361
      return NULL;
2362
    }
2363
 
2364
    // Associative Array
2365
    return $this->retrieveValue($first, implode('.', array_slice($path, 1)));
2366
  }
2367
 
2368
  /**
2369
   * Determine if params contain any match.
2370
   *
2371
   * @param {object} params - Parameters.
2372
   * @param {string} [pattern] - Regular expression to identify pattern.
2373
   * @param {boolean} [found] - Used for recursion.
2374
   * @return {boolean} True, if params matches pattern.
2375
   */
2376
  private function textAddonMatches($params, $pattern, $found = false) {
2377
    $type = gettype($params);
2378
    if ($type === 'string') {
2379
      if (preg_match($pattern, $params) === 1) {
2380
        return true;
2381
      }
2382
    }
2383
    elseif ($type === 'array' || $type === 'object') {
2384
      foreach ($params as $value) {
2385
        $found = $this->textAddonMatches($value, $pattern, $found);
2386
        if ($found === true) {
2387
          return true;
2388
        }
2389
      }
2390
    }
2391
    return false;
2392
  }
2393
 
2394
  /**
2395
   * Generate content slug
2396
   *
2397
   * @param array $content object
2398
   * @return string unique content slug
2399
   */
2400
  private function generateContentSlug($content) {
2401
    $slug = H5PCore::slugify($content['title']);
2402
 
2403
    $available = NULL;
2404
    while (!$available) {
2405
      if ($available === FALSE) {
2406
        // If not available, add number suffix.
2407
        $matches = array();
2408
        if (preg_match('/(.+-)([0-9]+)$/', $slug, $matches)) {
2409
          $slug = $matches[1] . (intval($matches[2]) + 1);
2410
        }
2411
        else {
2412
          $slug .=  '-2';
2413
        }
2414
      }
2415
      $available = $this->h5pF->isContentSlugAvailable($slug);
2416
    }
2417
 
2418
    return $slug;
2419
  }
2420
 
2421
  /**
2422
   * Find the files required for this content to work.
2423
   *
2424
   * @param int $id for content.
2425
   * @param null $type
2426
   * @return array
2427
   */
2428
  public function loadContentDependencies($id, $type = NULL) {
2429
    $dependencies = $this->h5pF->loadContentDependencies($id, $type);
2430
 
2431
    if (isset($this->h5pD)) {
2432
      $developmentLibraries = $this->h5pD->getLibraries();
2433
 
2434
      foreach ($dependencies as $key => $dependency) {
2435
        $libraryString = H5PCore::libraryToString($dependency);
2436
        if (isset($developmentLibraries[$libraryString])) {
2437
          $developmentLibraries[$libraryString]['dependencyType'] = $dependencies[$key]['dependencyType'];
2438
          $dependencies[$key] = $developmentLibraries[$libraryString];
2439
        }
2440
      }
2441
    }
2442
 
2443
    return $dependencies;
2444
  }
2445
 
2446
  /**
2447
   * Get all dependency assets of the given type
2448
   *
2449
   * @param array $dependency
2450
   * @param string $type
2451
   * @param array $assets
2452
   * @param string $prefix Optional. Make paths relative to another dir.
2453
   */
2454
  private function getDependencyAssets($dependency, $type, &$assets, $prefix = '') {
2455
    // Check if dependency has any files of this type
2456
    if (empty($dependency[$type]) || $dependency[$type][0] === '') {
2457
      return;
2458
    }
2459
 
2460
    // Check if we should skip CSS.
2461
    if ($type === 'preloadedCss' && (isset($dependency['dropCss']) && $dependency['dropCss'] === '1')) {
2462
      return;
2463
    }
2464
    foreach ($dependency[$type] as $file) {
2465
      $assets[] = (object) array(
2466
        'path' => $prefix . '/' . $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file),
2467
        'version' => $dependency['version']
2468
      );
2469
    }
2470
  }
2471
 
2472
  /**
2473
   * Combines path with cache buster / version.
2474
   *
2475
   * @param array $assets
2476
   * @return array
2477
   */
2478
  public function getAssetsUrls($assets) {
2479
    $urls = array();
2480
 
2481
    foreach ($assets as $asset) {
2482
      $url = $asset->path;
2483
 
2484
      // Add URL prefix if not external
2485
      if (strpos($asset->path, '://') === FALSE) {
2486
        $url = $this->url . $url;
2487
      }
2488
 
2489
      // Add version/cache buster if set
2490
      if (isset($asset->version)) {
2491
        $url .= $asset->version;
2492
      }
2493
 
2494
      $urls[] = $url;
2495
    }
2496
 
2497
    return $urls;
2498
  }
2499
 
2500
  /**
2501
   * Return file paths for all dependencies files.
2502
   *
2503
   * @param array $dependencies
2504
   * @param string $prefix Optional. Make paths relative to another dir.
2505
   * @return array files.
2506
   */
2507
  public function getDependenciesFiles($dependencies, $prefix = '') {
2508
    // Build files list for assets
2509
    $files = array(
2510
      'scripts' => array(),
2511
      'styles' => array()
2512
    );
2513
 
2514
    $key = null;
2515
 
2516
    // Avoid caching empty files
2517
    if (empty($dependencies)) {
2518
      return $files;
2519
    }
2520
 
2521
    if ($this->aggregateAssets) {
2522
      // Get aggregated files for assets
2523
      $key = self::getDependenciesHash($dependencies);
2524
 
2525
      $cachedAssets = $this->fs->getCachedAssets($key);
2526
      if ($cachedAssets !== NULL) {
2527
        return array_merge($files, $cachedAssets); // Using cached assets
2528
      }
2529
    }
2530
 
2531
    // Using content dependencies
2532
    foreach ($dependencies as $dependency) {
2533
      if (isset($dependency['path']) === FALSE) {
2534
        $dependency['path'] = $this->getDependencyPath($dependency);
2535
        $dependency['preloadedJs'] = explode(',', $dependency['preloadedJs']);
2536
        $dependency['preloadedCss'] = explode(',', $dependency['preloadedCss']);
2537
      }
2538
      $dependency['version'] = "?ver={$dependency['majorVersion']}.{$dependency['minorVersion']}.{$dependency['patchVersion']}";
2539
      $this->getDependencyAssets($dependency, 'preloadedJs', $files['scripts'], $prefix);
2540
      $this->getDependencyAssets($dependency, 'preloadedCss', $files['styles'], $prefix);
2541
    }
2542
 
2543
    if ($this->aggregateAssets) {
2544
      // Aggregate and store assets
2545
      $this->fs->cacheAssets($files, $key);
2546
 
2547
      // Keep track of which libraries have been cached in case they are updated
2548
      $this->h5pF->saveCachedAssets($key, $dependencies);
2549
    }
2550
 
2551
    return $files;
2552
  }
2553
 
2554
  /**
2555
   * Get the path to the dependency.
2556
   *
2557
   * @param array $dependency
2558
   * @return string
2559
   */
2560
  protected function getDependencyPath(array $dependency) {
2561
    return 'libraries/' . H5PCore::libraryToFolderName($dependency);
2562
  }
2563
 
2564
  private static function getDependenciesHash(&$dependencies) {
2565
    // Build hash of dependencies
2566
    $toHash = array();
2567
 
2568
    // Use unique identifier for each library version
2569
    foreach ($dependencies as $dep) {
2570
      $toHash[] = "{$dep['machineName']}-{$dep['majorVersion']}.{$dep['minorVersion']}.{$dep['patchVersion']}";
2571
    }
2572
 
2573
    // Sort in case the same dependencies comes in a different order
2574
    sort($toHash);
2575
 
2576
    // Calculate hash sum
2577
    return hash('sha1', implode('', $toHash));
2578
  }
2579
 
2580
  /**
2581
   * Load library semantics.
2582
   *
2583
   * @param $name
2584
   * @param $majorVersion
2585
   * @param $minorVersion
2586
   * @return string
2587
   */
2588
  public function loadLibrarySemantics($name, $majorVersion, $minorVersion) {
2589
    $semantics = NULL;
2590
    if (isset($this->h5pD)) {
2591
      // Try to load from dev lib
2592
      $semantics = $this->h5pD->getSemantics($name, $majorVersion, $minorVersion);
2593
    }
2594
 
2595
    if ($semantics === NULL) {
2596
      // Try to load from DB.
2597
      $semantics = $this->h5pF->loadLibrarySemantics($name, $majorVersion, $minorVersion);
2598
    }
2599
 
2600
    if ($semantics !== NULL) {
2601
      $semantics = json_decode($semantics);
2602
      $this->h5pF->alterLibrarySemantics($semantics, $name, $majorVersion, $minorVersion);
2603
    }
2604
 
2605
    return $semantics;
2606
  }
2607
 
2608
  /**
2609
   * Load library.
2610
   *
2611
   * @param $name
2612
   * @param $majorVersion
2613
   * @param $minorVersion
2614
   * @return array or null.
2615
   */
2616
  public function loadLibrary($name, $majorVersion, $minorVersion) {
2617
    $library = NULL;
2618
    if (isset($this->h5pD)) {
2619
      // Try to load from dev
2620
      $library = $this->h5pD->getLibrary($name, $majorVersion, $minorVersion);
2621
      if ($library !== NULL) {
2622
        $library['semantics'] = $this->h5pD->getSemantics($name, $majorVersion, $minorVersion);
2623
      }
2624
    }
2625
 
2626
    if ($library === NULL) {
2627
      // Try to load from DB.
2628
      $library = $this->h5pF->loadLibrary($name, $majorVersion, $minorVersion);
2629
    }
2630
 
2631
    return $library;
2632
  }
2633
 
2634
  /**
2635
   * Deletes a library
2636
   *
2637
   * @param stdClass $libraryId
2638
   */
2639
  public function deleteLibrary($libraryId) {
2640
    $this->h5pF->deleteLibrary($libraryId);
2641
  }
2642
 
2643
  /**
2644
   * Recursive. Goes through the dependency tree for the given library and
2645
   * adds all the dependencies to the given array in a flat format.
2646
   *
2647
   * @param $dependencies
2648
   * @param array $library To find all dependencies for.
2649
   * @param int $nextWeight An integer determining the order of the libraries
2650
   *  when they are loaded
2651
   * @param bool $editor Used internally to force all preloaded sub dependencies
2652
   *  of an editor dependency to be editor dependencies.
2653
   * @return int
2654
   */
2655
  public function findLibraryDependencies(&$dependencies, $library, $nextWeight = 1, $editor = FALSE) {
2656
    foreach (array('dynamic', 'preloaded', 'editor') as $type) {
2657
      $property = $type . 'Dependencies';
2658
      if (!isset($library[$property])) {
2659
        continue; // Skip, no such dependencies.
2660
      }
2661
 
2662
      if ($type === 'preloaded' && $editor === TRUE) {
2663
        // All preloaded dependencies of an editor library is set to editor.
2664
        $type = 'editor';
2665
      }
2666
 
2667
      foreach ($library[$property] as $dependency) {
2668
        $dependencyKey = $type . '-' . $dependency['machineName'];
2669
        if (isset($dependencies[$dependencyKey]) === TRUE) {
2670
          continue; // Skip, already have this.
2671
        }
2672
 
2673
        $dependencyLibrary = $this->loadLibrary($dependency['machineName'], $dependency['majorVersion'], $dependency['minorVersion']);
2674
        if ($dependencyLibrary) {
2675
          $dependencies[$dependencyKey] = array(
2676
            'library' => $dependencyLibrary,
2677
            'type' => $type
2678
          );
2679
          $nextWeight = $this->findLibraryDependencies($dependencies, $dependencyLibrary, $nextWeight, $type === 'editor');
2680
          $dependencies[$dependencyKey]['weight'] = $nextWeight++;
2681
        }
2682
        else {
2683
          // This site is missing a dependency!
2684
          $this->h5pF->setErrorMessage($this->h5pF->t('Missing dependency @dep required by @lib.', array('@dep' => H5PCore::libraryToString($dependency), '@lib' => H5PCore::libraryToString($library))), 'missing-library-dependency');
2685
        }
2686
      }
2687
    }
2688
    return $nextWeight;
2689
  }
2690
 
2691
  /**
2692
   * Check if a library is of the version we're looking for
2693
   *
2694
   * Same version means that the majorVersion and minorVersion is the same
2695
   *
2696
   * @param array $library
2697
   *  Data from library.json
2698
   * @param array $dependency
2699
   *  Definition of what library we're looking for
2700
   * @return boolean
2701
   *  TRUE if the library is the same version as the dependency
2702
   *  FALSE otherwise
2703
   */
2704
  public function isSameVersion($library, $dependency) {
2705
    if ($library['machineName'] != $dependency['machineName']) {
2706
      return FALSE;
2707
    }
2708
    if ($library['majorVersion'] != $dependency['majorVersion']) {
2709
      return FALSE;
2710
    }
2711
    if ($library['minorVersion'] != $dependency['minorVersion']) {
2712
      return FALSE;
2713
    }
2714
    return TRUE;
2715
  }
2716
 
2717
  /**
2718
   * Recursive function for removing directories.
2719
   *
2720
   * @param string $dir
2721
   *  Path to the directory we'll be deleting
2722
   * @return boolean
2723
   *  Indicates if the directory existed.
2724
   */
2725
  public static function deleteFileTree($dir) {
2726
    if (!is_dir($dir)) {
2727
      return false;
2728
    }
2729
    if (is_link($dir)) {
2730
      // Do not traverse and delete linked content, simply unlink.
2731
      unlink($dir);
2732
      return;
2733
    }
2734
    $files = array_diff(scandir($dir), array('.','..'));
2735
    foreach ($files as $file) {
2736
      $filepath = "$dir/$file";
2737
      // Note that links may resolve as directories
2738
      if (!is_dir($filepath) || is_link($filepath)) {
2739
        // Unlink files and links
2740
        unlink($filepath);
2741
      }
2742
      else {
2743
        // Traverse subdir and delete files
2744
        self::deleteFileTree($filepath);
2745
      }
2746
    }
2747
    return rmdir($dir);
2748
  }
2749
 
2750
  /**
2751
   * Writes library data as string on the form {machineName} {majorVersion}.{minorVersion}
2752
   *
2753
   * @param array $library
2754
   *  With keys (machineName and/or name), majorVersion and minorVersion
2755
   * @return string
2756
   *  On the form {machineName} {majorVersion}.{minorVersion}
2757
   */
2758
  public static function libraryToString($library) {
2759
    $name = $library['machineName'] ?? $library['name'];
2760
 
2761
    return "{$name} {$library['majorVersion']}.{$library['minorVersion']}";
2762
  }
2763
 
2764
  /**
2765
   * Get the name of a library's folder name
2766
   *
2767
   * @return string
2768
   */
2769
  public static function libraryToFolderName($library) {
2770
    $name = $library['machineName'] ?? $library['name'];
2771
    $includePatchVersion = $library['patchVersionInFolderName'] ?? false;
2772
 
2773
    return "{$name}-{$library['majorVersion']}.{$library['minorVersion']}" . ($includePatchVersion ? ".{$library['patchVersion']}" : '');
2774
  }
2775
 
2776
  /**
2777
   * Parses library data from a string on the form {machineName} {majorVersion}.{minorVersion}
2778
   *
2779
   * @param string $libraryString
2780
   *  On the form {machineName} {majorVersion}.{minorVersion}
2781
   * @return array|FALSE
2782
   *  With keys machineName, majorVersion and minorVersion.
2783
   *  Returns FALSE only if string is not parsable in the normal library
2784
   *  string formats "Lib.Name-x.y" or "Lib.Name x.y"
2785
   */
2786
  public static function libraryFromString($libraryString) {
2787
    $re = '/^([\w0-9\-\.]{1,255})[\-\ ]([0-9]{1,5})\.([0-9]{1,5})$/i';
2788
    $matches = array();
2789
    $res = preg_match($re, $libraryString, $matches);
2790
    if ($res) {
2791
      return array(
2792
        'machineName' => $matches[1],
2793
        'majorVersion' => $matches[2],
2794
        'minorVersion' => $matches[3]
2795
      );
2796
    }
2797
    return FALSE;
2798
  }
2799
 
2800
  /**
2801
   * Determine the correct embed type to use.
2802
   *
2803
   * @param $contentEmbedType
2804
   * @param $libraryEmbedTypes
2805
   * @return string 'div' or 'iframe'.
2806
   */
2807
  public static function determineEmbedType($contentEmbedType, $libraryEmbedTypes) {
2808
    // Detect content embed type
2809
    $embedType = strpos(strtolower($contentEmbedType), 'div') !== FALSE ? 'div' : 'iframe';
2810
 
2811
    if ($libraryEmbedTypes !== NULL && $libraryEmbedTypes !== '') {
2812
      // Check that embed type is available for library
2813
      $embedTypes = strtolower($libraryEmbedTypes);
2814
      if (strpos($embedTypes, $embedType) === FALSE) {
2815
        // Not available, pick default.
2816
        $embedType = strpos($embedTypes, 'div') !== FALSE ? 'div' : 'iframe';
2817
      }
2818
    }
2819
 
2820
    return $embedType;
2821
  }
2822
 
2823
  /**
2824
   * Get the absolute version for the library as a human readable string.
2825
   *
2826
   * @param object $library
2827
   * @return string
2828
   */
2829
  public static function libraryVersion($library) {
2830
    return $library->major_version . '.' . $library->minor_version . '.' . $library->patch_version;
2831
  }
2832
 
2833
  /**
2834
   * Determine which versions content with the given library can be upgraded to.
2835
   *
2836
   * @param object $library
2837
   * @param array $versions
2838
   * @return array
2839
   */
2840
  public function getUpgrades($library, $versions) {
2841
   $upgrades = array();
2842
 
2843
   foreach ($versions as $upgrade) {
2844
     if ($upgrade->major_version > $library->major_version || $upgrade->major_version === $library->major_version && $upgrade->minor_version > $library->minor_version) {
2845
       $upgrades[$upgrade->id] = H5PCore::libraryVersion($upgrade);
2846
     }
2847
   }
2848
 
2849
   return $upgrades;
2850
  }
2851
 
2852
  /**
2853
   * Converts all the properties of the given object or array from
2854
   * snake_case to camelCase. Useful after fetching data from the database.
2855
   *
2856
   * Note that some databases does not support camelCase.
2857
   *
2858
   * @param mixed $arr input
2859
   * @param boolean $obj return object
2860
   * @return mixed object or array
2861
   */
2862
  public static function snakeToCamel($arr, $obj = false) {
2863
    $newArr = array();
2864
 
2865
    foreach ($arr as $key => $val) {
2866
      $next = -1;
2867
      while (($next = strpos($key, '_', $next + 1)) !== FALSE) {
2868
        $key = substr_replace($key, strtoupper($key[$next + 1]), $next, 2);
2869
      }
2870
 
2871
      $newArr[$key] = $val;
2872
    }
2873
 
2874
    return $obj ? (object) $newArr : $newArr;
2875
  }
2876
 
2877
  /**
2878
   * Detects if the site was accessed from localhost,
2879
   * through a local network or from the internet.
2880
   */
2881
  public function detectSiteType() {
2882
    $type = $this->h5pF->getOption('site_type', 'local');
2883
 
2884
    // Determine remote/visitor origin
2885
    if ($type === 'network' ||
2886
        ($type === 'local' &&
2887
         isset($_SERVER['REMOTE_ADDR']) &&
2888
         !preg_match('/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/i', $_SERVER['REMOTE_ADDR']))) {
2889
      if (isset($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) {
2890
        // Internet
2891
        $this->h5pF->setOption('site_type', 'internet');
2892
      }
2893
      elseif ($type === 'local') {
2894
        // Local network
2895
        $this->h5pF->setOption('site_type', 'network');
2896
      }
2897
    }
2898
  }
2899
 
2900
  /**
2901
   * Get a list of installed libraries, different minor versions will
2902
   * return separate entries.
2903
   *
2904
   * @return array
2905
   *  A distinct array of installed libraries
2906
   */
2907
  public function getLibrariesInstalled() {
2908
    $librariesInstalled = array();
2909
    $libs = $this->h5pF->loadLibraries();
2910
 
2911
    foreach($libs as $libName => $library) {
2912
      foreach($library as $libVersion) {
2913
        $librariesInstalled[$libName.' '.$libVersion->major_version.'.'.$libVersion->minor_version] = $libVersion->patch_version;
2914
      }
2915
    }
2916
 
2917
    return $librariesInstalled;
2918
  }
2919
 
2920
  /**
2921
   * Easy way to combine similar data sets.
2922
   *
2923
   * @param array $inputs Multiple arrays with data
2924
   * @return array
2925
   */
2926
  public function combineArrayValues($inputs) {
2927
    $results = array();
2928
    foreach ($inputs as $index => $values) {
2929
      foreach ($values as $key => $value) {
2930
        $results[$key][$index] = $value;
2931
      }
2932
    }
2933
    return $results;
2934
  }
2935
 
2936
  /**
2937
   * Communicate with H5P.org and get content type cache. Each platform
2938
   * implementation is responsible for invoking this, eg using cron
2939
   *
2940
   * @param bool $fetchingDisabled
2941
   * @param bool $onlyRegister Only register site with H5P.org
2942
   *
2943
   * @return bool|object Returns endpoint data if found, otherwise FALSE
2944
   */
2945
  public function fetchLibrariesMetadata($fetchingDisabled = FALSE, $onlyRegister = false) {
2946
    // Gather data
2947
    $uuid = $this->h5pF->getOption('site_uuid', '');
2948
    $platform = $this->h5pF->getPlatformInfo();
2949
    $registrationData = array(
2950
      'uuid' => $uuid,
2951
      'platform_name' => $platform['name'],
2952
      'platform_version' => $platform['version'],
2953
      'h5p_version' => $platform['h5pVersion'],
2954
      'disabled' => $fetchingDisabled ? 1 : 0,
2955
      'local_id' => hash('crc32', $this->fullPluginPath),
2956
      'type' => $this->h5pF->getOption('site_type', 'local'),
2957
      'core_api_version' => H5PCore::$coreApi['majorVersion'] . '.' .
2958
                            H5PCore::$coreApi['minorVersion']
2959
    );
2960
 
2961
    // Register site if it is not registered
2962
    if (empty($uuid)) {
2963
      $registration = $this->h5pF->fetchExternalData(H5PHubEndpoints::createURL(H5PHubEndpoints::SITES), $registrationData);
2964
 
2965
      // Failed retrieving uuid
2966
      if (!$registration) {
2967
        $errorMessage = $this->h5pF->t('Site could not be registered with the hub. Please contact your site administrator.');
2968
        $this->h5pF->setErrorMessage($errorMessage);
2969
        $this->h5pF->setErrorMessage(
2970
          $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.'),
2971
          'registration-failed-hub-disabled'
2972
        );
2973
        return FALSE;
2974
      }
2975
 
2976
      // Successfully retrieved new uuid
2977
      $json = json_decode($registration);
2978
      $registrationData['uuid'] = $json->uuid;
2979
      $this->h5pF->setOption('site_uuid', $json->uuid);
2980
      $this->h5pF->setInfoMessage(
2981
        $this->h5pF->t('Your site was successfully registered with the H5P Hub.')
2982
      );
2983
      $uuid = $json->uuid;
2984
      // TODO: Uncomment when key is once again available in H5P Settings
2985
//      $this->h5pF->setInfoMessage(
2986
//        $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.')
2987
//      );
2988
    }
2989
 
2990
    if ($onlyRegister) {
2991
      return $uuid;
2992
    }
2993
 
2994
    if ($this->h5pF->getOption('send_usage_statistics', TRUE)) {
2995
      $siteData = array_merge(
2996
        $registrationData,
2997
        array(
2998
          'num_authors' => $this->h5pF->getNumAuthors(),
2999
          'libraries'   => json_encode($this->combineArrayValues(array(
3000
            'patch'            => $this->getLibrariesInstalled(),
3001
            'content'          => $this->h5pF->getLibraryContentCount(),
3002
            'loaded'           => $this->h5pF->getLibraryStats('library'),
3003
            'created'          => $this->h5pF->getLibraryStats('content create'),
3004
            'createdUpload'    => $this->h5pF->getLibraryStats('content create upload'),
3005
            'deleted'          => $this->h5pF->getLibraryStats('content delete'),
3006
            'resultViews'      => $this->h5pF->getLibraryStats('results content'),
3007
            'shortcodeInserts' => $this->h5pF->getLibraryStats('content shortcode insert')
3008
          )))
3009
        )
3010
      );
3011
    }
3012
    else {
3013
      $siteData = $registrationData;
3014
    }
3015
 
3016
    $result = $this->updateContentTypeCache($siteData);
3017
 
3018
    // No data received
3019
    if (!$result || empty($result)) {
3020
      return FALSE;
3021
    }
3022
 
3023
    // Handle libraries metadata
3024
    if (isset($result->libraries)) {
3025
      foreach ($result->libraries as $library) {
3026
        if (isset($library->tutorialUrl) && isset($library->machineName)) {
3027
          $this->h5pF->setLibraryTutorialUrl($library->machineNamee, $library->tutorialUrl);
3028
        }
3029
      }
3030
    }
3031
 
3032
    return $result;
3033
  }
3034
 
3035
  /**
3036
   * Create representation of display options as int
3037
   *
3038
   * @param array $sources
3039
   * @param int $current
3040
   * @return int
3041
   */
3042
  public function getStorableDisplayOptions(&$sources, $current) {
3043
    // Download - force setting it if always on or always off
3044
    $download = $this->h5pF->getOption(self::DISPLAY_OPTION_DOWNLOAD, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3045
    if ($download == H5PDisplayOptionBehaviour::ALWAYS_SHOW ||
3046
        $download == H5PDisplayOptionBehaviour::NEVER_SHOW) {
3047
      $sources[self::DISPLAY_OPTION_DOWNLOAD] = ($download == H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3048
    }
3049
 
3050
    // Embed - force setting it if always on or always off
3051
    $embed = $this->h5pF->getOption(self::DISPLAY_OPTION_EMBED, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3052
    if ($embed == H5PDisplayOptionBehaviour::ALWAYS_SHOW ||
3053
        $embed == H5PDisplayOptionBehaviour::NEVER_SHOW) {
3054
      $sources[self::DISPLAY_OPTION_EMBED] = ($embed == H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3055
    }
3056
 
3057
    foreach (H5PCore::$disable as $bit => $option) {
3058
      if (!isset($sources[$option]) || !$sources[$option]) {
3059
        $current |= $bit; // Disable
3060
      }
3061
      else {
3062
        $current &= ~$bit; // Enable
3063
      }
3064
    }
3065
    return $current;
3066
  }
3067
 
3068
  /**
3069
   * Determine display options visibility and value on edit
3070
   *
3071
   * @param int $disable
3072
   * @return array
3073
   */
3074
  public function getDisplayOptionsForEdit($disable = NULL) {
3075
    $display_options = array();
3076
 
3077
    $current_display_options = $disable === NULL ? array() : $this->getDisplayOptionsAsArray($disable);
3078
 
3079
    if ($this->h5pF->getOption(self::DISPLAY_OPTION_FRAME, TRUE)) {
3080
      $display_options[self::DISPLAY_OPTION_FRAME] =
3081
        isset($current_display_options[self::DISPLAY_OPTION_FRAME]) ?
3082
        $current_display_options[self::DISPLAY_OPTION_FRAME] :
3083
        TRUE;
3084
 
3085
      // Download
3086
      $export = $this->h5pF->getOption(self::DISPLAY_OPTION_DOWNLOAD, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3087
      if ($export == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_ON ||
3088
          $export == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_OFF) {
3089
        $display_options[self::DISPLAY_OPTION_DOWNLOAD] =
3090
          isset($current_display_options[self::DISPLAY_OPTION_DOWNLOAD]) ?
3091
          $current_display_options[self::DISPLAY_OPTION_DOWNLOAD] :
3092
          ($export == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_ON);
3093
      }
3094
 
3095
      // Embed
3096
      $embed = $this->h5pF->getOption(self::DISPLAY_OPTION_EMBED, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3097
      if ($embed == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_ON ||
3098
          $embed == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_OFF) {
3099
        $display_options[self::DISPLAY_OPTION_EMBED] =
3100
          isset($current_display_options[self::DISPLAY_OPTION_EMBED]) ?
3101
          $current_display_options[self::DISPLAY_OPTION_EMBED] :
3102
          ($embed == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_ON);
3103
      }
3104
 
3105
      // Copyright
3106
      if ($this->h5pF->getOption(self::DISPLAY_OPTION_COPYRIGHT, TRUE)) {
3107
        $display_options[self::DISPLAY_OPTION_COPYRIGHT] =
3108
          isset($current_display_options[self::DISPLAY_OPTION_COPYRIGHT]) ?
3109
          $current_display_options[self::DISPLAY_OPTION_COPYRIGHT] :
3110
          TRUE;
3111
      }
3112
    }
3113
 
3114
    return $display_options;
3115
  }
3116
 
3117
  /**
3118
   * Helper function used to figure out embed & download behaviour
3119
   *
3120
   * @param string $option_name
3121
   * @param H5PPermission $permission
3122
   * @param int $id
3123
   * @param bool &$value
3124
   */
3125
  private function setDisplayOptionOverrides($option_name, $permission, $id, &$value) {
3126
    $behaviour = $this->h5pF->getOption($option_name, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3127
    // If never show globally, force hide
3128
    if ($behaviour == H5PDisplayOptionBehaviour::NEVER_SHOW) {
3129
      $value = false;
3130
    }
3131
    elseif ($behaviour == H5PDisplayOptionBehaviour::ALWAYS_SHOW) {
3132
      // If always show or permissions say so, force show
3133
      $value = true;
3134
    }
3135
    elseif ($behaviour == H5PDisplayOptionBehaviour::CONTROLLED_BY_PERMISSIONS) {
3136
      $value = $this->h5pF->hasPermission($permission, $id);
3137
    }
3138
  }
3139
 
3140
  /**
3141
   * Determine display option visibility when viewing H5P
3142
   *
3143
   * @param int $display_options
3144
   * @param int  $id Might be content id or user id.
3145
   * Depends on what the platform needs to be able to determine permissions.
3146
   * @return array
3147
   */
3148
  public function getDisplayOptionsForView($disable, $id) {
3149
    $display_options = $this->getDisplayOptionsAsArray($disable);
3150
 
3151
    if ($this->h5pF->getOption(self::DISPLAY_OPTION_FRAME, TRUE) == FALSE) {
3152
      $display_options[self::DISPLAY_OPTION_FRAME] = false;
3153
    }
3154
    else {
3155
      $this->setDisplayOptionOverrides(self::DISPLAY_OPTION_DOWNLOAD, H5PPermission::DOWNLOAD_H5P, $id, $display_options[self::DISPLAY_OPTION_DOWNLOAD]);
3156
      $this->setDisplayOptionOverrides(self::DISPLAY_OPTION_EMBED, H5PPermission::EMBED_H5P, $id, $display_options[self::DISPLAY_OPTION_EMBED]);
3157
 
3158
      if ($this->h5pF->getOption(self::DISPLAY_OPTION_COPYRIGHT, TRUE) == FALSE) {
3159
        $display_options[self::DISPLAY_OPTION_COPYRIGHT] = false;
3160
      }
3161
    }
3162
    $display_options[self::DISPLAY_OPTION_COPY] = $this->h5pF->hasPermission(H5PPermission::COPY_H5P, $id);
3163
 
3164
    return $display_options;
3165
  }
3166
 
3167
  /**
3168
   * Convert display options as single byte to array
3169
   *
3170
   * @param int $disable
3171
   * @return array
3172
   */
3173
  private function getDisplayOptionsAsArray($disable) {
3174
    return array(
3175
      self::DISPLAY_OPTION_FRAME => !($disable & H5PCore::DISABLE_FRAME),
3176
      self::DISPLAY_OPTION_DOWNLOAD => !($disable & H5PCore::DISABLE_DOWNLOAD),
3177
      self::DISPLAY_OPTION_EMBED => !($disable & H5PCore::DISABLE_EMBED),
3178
      self::DISPLAY_OPTION_COPYRIGHT => !($disable & H5PCore::DISABLE_COPYRIGHT),
3179
      self::DISPLAY_OPTION_ABOUT => !!$this->h5pF->getOption(self::DISPLAY_OPTION_ABOUT, TRUE),
3180
    );
3181
  }
3182
 
3183
  /**
3184
   * Small helper for getting the library's ID.
3185
   *
3186
   * @param array $library
3187
   * @param string [$libString]
3188
   * @return int Identifier, or FALSE if non-existent
3189
   */
3190
  public function getLibraryId($library, $libString = NULL) {
3191
    static $libraryIdMap = [];
3192
 
3193
    if (!$libString) {
3194
      $libString = self::libraryToString($library);
3195
    }
3196
 
3197
    if (!isset($libraryIdMap[$libString])) {
3198
      $libraryIdMap[$libString] = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']);
3199
    }
3200
 
3201
    return $libraryIdMap[$libString];
3202
  }
3203
 
3204
  /**
3205
   * Convert strings of text into simple kebab case slugs.
3206
   * Very useful for readable urls etc.
3207
   *
3208
   * @param string $input
3209
   * @return string
3210
   */
3211
  public static function slugify($input) {
3212
    // Down low
3213
    $input = strtolower($input);
3214
 
3215
    // Replace common chars
3216
    $input = str_replace(
3217
      array('æ',  'ø',  'ö', 'ó', 'ô', 'Ò',  'Õ', 'Ý', 'ý', 'ÿ', 'ā', 'ă', 'ą', 'œ', 'å', 'ä', 'á', 'à', 'â', 'ã', 'ç', 'ć', 'ĉ', 'ċ', 'č', 'é', 'è', 'ê', 'ë', 'í', 'ì', 'î', 'ï', 'ú', 'ñ', 'ü', 'ù', 'û', 'ß',  'ď', 'đ', 'ē', 'ĕ', 'ė', 'ę', 'ě', 'ĝ', 'ğ', 'ġ', 'ģ', 'ĥ', 'ħ', 'ĩ', 'ī', 'ĭ', 'į', 'ı', 'ij',  'ĵ', 'ķ', 'ĺ', 'ļ', 'ľ', 'ŀ', 'ł', 'ń', 'ņ', 'ň', 'ʼn', 'ō', 'ŏ', 'ő', 'ŕ', 'ŗ', 'ř', 'ś', 'ŝ', 'ş', 'š', 'ţ', 'ť', 'ŧ', 'ũ', 'ū', 'ŭ', 'ů', 'ű', 'ų', 'ŵ', 'ŷ', 'ź', 'ż', 'ž', 'ſ', 'ƒ', 'ơ', 'ư', 'ǎ', 'ǐ', 'ǒ', 'ǔ', 'ǖ', 'ǘ', 'ǚ', 'ǜ', 'ǻ', 'ǽ',  'ǿ'),
3218
      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'),
3219
      $input);
3220
 
3221
    // Replace everything else
3222
    $input = preg_replace('/[^a-z0-9]/', '-', $input);
3223
 
3224
    // Prevent double hyphen
3225
    $input = preg_replace('/-{2,}/', '-', $input);
3226
 
3227
    // Prevent hyphen in beginning or end
3228
    $input = trim($input, '-');
3229
 
3230
    // Prevent to long slug
3231
    if (strlen($input) > 91) {
3232
      $input = substr($input, 0, 92);
3233
    }
3234
 
3235
    // Prevent empty slug
3236
    if ($input === '') {
3237
      $input = 'interactive';
3238
    }
3239
 
3240
    return $input;
3241
  }
3242
 
3243
  /**
3244
   * Makes it easier to print response when AJAX request succeeds.
3245
   *
3246
   * @param mixed $data
3247
   * @since 1.6.0
3248
   */
3249
  public static function ajaxSuccess($data = NULL, $only_data = FALSE) {
3250
    $response = array(
3251
      'success' => TRUE
3252
    );
3253
    if ($data !== NULL) {
3254
      $response['data'] = $data;
3255
 
3256
      // Pass data flatly to support old methods
3257
      if ($only_data) {
3258
        $response = $data;
3259
      }
3260
    }
3261
    self::printJson($response);
3262
  }
3263
 
3264
  /**
3265
   * Makes it easier to print response when AJAX request fails.
3266
   * Will exit after printing error.
3267
   *
3268
   * @param string $message A human readable error message
3269
   * @param string $error_code An machine readable error code that a client
3270
   * should be able to interpret
3271
   * @param null|int $status_code Http response code
3272
   * @param array [$details=null] Better description of the error and possible which action to take
3273
   * @since 1.6.0
3274
   */
3275
  public static function ajaxError($message = NULL, $error_code = NULL, $status_code = NULL, $details = NULL) {
3276
    $response = array(
3277
      'success' => FALSE
3278
    );
3279
    if ($message !== NULL) {
3280
      $response['message'] = $message;
3281
    }
3282
 
3283
    if ($error_code !== NULL) {
3284
      $response['errorCode'] = $error_code;
3285
    }
3286
 
3287
    if ($details !== NULL) {
3288
      $response['details'] = $details;
3289
    }
3290
 
3291
    self::printJson($response, $status_code);
3292
  }
3293
 
3294
  /**
3295
   * Print JSON headers with UTF-8 charset and json encode response data.
3296
   * Makes it easier to respond using JSON.
3297
   *
3298
   * @param mixed $data
3299
   * @param null|int $status_code Http response code
3300
   */
3301
  private static function printJson($data, $status_code = NULL) {
3302
    header('Cache-Control: no-cache');
3303
    header('Content-Type: application/json; charset=utf-8');
3304
    print json_encode($data);
3305
  }
3306
 
3307
  /**
3308
   * Get a new H5P security token for the given action.
3309
   *
3310
   * @param string $action
3311
   * @return string token
3312
   */
3313
  public static function createToken($action) {
3314
    // Create and return token
3315
    return self::hashToken($action, self::getTimeFactor());
3316
  }
3317
 
3318
  /**
3319
   * Create a time based number which is unique for each 12 hour.
3320
   * @return int
3321
   */
3322
  private static function getTimeFactor() {
3323
    return ceil(time() / (86400 / 2));
3324
  }
3325
 
3326
  /**
3327
   * Generate a unique hash string based on action, time and token
3328
   *
3329
   * @param string $action
3330
   * @param int $time_factor
3331
   * @return string
3332
   */
3333
  private static function hashToken($action, $time_factor) {
3334
    global $SESSION;
3335
 
3336
    if (!isset($SESSION->h5p_token)) {
3337
      // Create an unique key which is used to create action tokens for this session.
3338
      if (function_exists('random_bytes')) {
3339
        $SESSION->h5p_token = base64_encode(random_bytes(15));
3340
      }
3341
      else if (function_exists('openssl_random_pseudo_bytes')) {
3342
        $SESSION->h5p_token = base64_encode(openssl_random_pseudo_bytes(15));
3343
      }
3344
      else {
3345
        $SESSION->h5p_token = uniqid('', TRUE);
3346
      }
3347
    }
3348
 
3349
    // Create hash and return
3350
    return substr(hash('md5', $action . $time_factor . $SESSION->h5p_token), -16, 13);
3351
  }
3352
 
3353
  /**
3354
   * Verify if the given token is valid for the given action.
3355
   *
3356
   * @param string $action
3357
   * @param string $token
3358
   * @return boolean valid token
3359
   */
3360
  public static function validToken($action, $token) {
3361
    // Get the timefactor
3362
    $time_factor = self::getTimeFactor();
3363
 
3364
    // Check token to see if it's valid
3365
    return $token === self::hashToken($action, $time_factor) || // Under 12 hours
3366
           $token === self::hashToken($action, $time_factor - 1); // Between 12-24 hours
3367
  }
3368
 
3369
  /**
3370
   * Update content type cache
3371
   *
3372
   * @param object $postData Data sent to the hub
3373
   *
3374
   * @return bool|object Returns endpoint data if found, otherwise FALSE
3375
   */
3376
  public function updateContentTypeCache($postData = NULL) {
3377
    $interface = $this->h5pF;
3378
 
3379
    // Make sure data is sent!
3380
    if (!isset($postData) || !isset($postData['uuid'])) {
3381
      return $this->fetchLibrariesMetadata();
3382
    }
3383
 
3384
    $postData['current_cache'] = $this->h5pF->getOption('content_type_cache_updated_at', 0);
3385
 
3386
    $data = $interface->fetchExternalData(H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT_TYPES), $postData);
3387
 
3388
    if (! $this->h5pF->getOption('hub_is_enabled', TRUE)) {
3389
      return TRUE;
3390
    }
3391
 
3392
    // No data received
3393
    if (!$data) {
3394
      $interface->setErrorMessage(
3395
        $interface->t("Couldn't communicate with the H5P Hub. Please try again later."),
3396
        'failed-communicationg-with-hub'
3397
      );
3398
      return FALSE;
3399
    }
3400
 
3401
    $json = json_decode($data);
3402
 
3403
    // No libraries received
3404
    if (!isset($json->contentTypes) || empty($json->contentTypes)) {
3405
      $interface->setErrorMessage(
3406
        $interface->t('No content types were received from the H5P Hub. Please try again later.'),
3407
        'no-content-types-from-hub'
3408
      );
3409
      return FALSE;
3410
    }
3411
 
3412
    // Replace content type cache
3413
    $interface->replaceContentTypeCache($json);
3414
 
3415
    // Inform of the changes and update timestamp
3416
    $interface->setInfoMessage($interface->t('Library cache was successfully updated!'));
3417
    $interface->setOption('content_type_cache_updated_at', time());
3418
    return $data;
3419
  }
3420
 
3421
  /**
3422
   * Update content hub metadata cache
3423
   */
3424
  public function updateContentHubMetadataCache($lang = 'en') {
3425
    $url          = H5PHubEndpoints::createURL(H5PHubEndpoints::METADATA);
3426
    $lastModified = $this->h5pF->getContentHubMetadataChecked($lang);
3427
 
3428
    $headers = array();
3429
    if (!empty($lastModified)) {
3430
      $headers['If-Modified-Since'] = $lastModified;
3431
    }
3432
    $data = $this->h5pF->fetchExternalData("{$url}?lang={$lang}", NULL, TRUE, NULL, TRUE, $headers, NULL, 'GET');
3433
    $lastChecked = new DateTime('now', new DateTimeZone('GMT'));
3434
 
3435
    if ($data['status'] !== 200 && $data['status'] !== 304) {
3436
      // If this was not a success, set the error message and return
3437
      $this->h5pF->setErrorMessage(
3438
        $this->h5pF->t('No metadata was received from the H5P Hub. Please try again later.')
3439
      );
3440
      return null;
3441
    }
3442
 
3443
    // Update timestamp
3444
    $this->h5pF->setContentHubMetadataChecked($lastChecked->getTimestamp(), $lang);
3445
 
3446
    // Not modified
3447
    if ($data['status'] === 304) {
3448
      return null;
3449
    }
3450
    $this->h5pF->replaceContentHubMetadataCache($data['data'], $lang);
3451
    // TODO: If 200 should we have checked if it decodes? Or 'success'? Not sure if necessary though
3452
    return $data['data'];
3453
  }
3454
 
3455
  /**
3456
   * Get updated content hub metadata cache
3457
   *
3458
   * @param  string  $lang Language as ISO 639-1 code
3459
   *
3460
   * @return JsonSerializable|string
3461
   */
3462
  public function getUpdatedContentHubMetadataCache($lang = 'en') {
3463
    $lastUpdate = $this->h5pF->getContentHubMetadataChecked($lang);
3464
    if (!$lastUpdate) {
3465
      return $this->updateContentHubMetadataCache($lang);
3466
    }
3467
 
3468
    $lastUpdate = new DateTime($lastUpdate);
3469
    $expirationTime = $lastUpdate->getTimestamp() + (60 * 60 * 24); // Check once per day
3470
    if (time() > $expirationTime) {
3471
      $update = $this->updateContentHubMetadataCache($lang);
3472
      if (!empty($update)) {
3473
        return $update;
3474
      }
3475
    }
3476
 
3477
    $storedCache = $this->h5pF->getContentHubMetadataCache($lang);
3478
    if (!$storedCache) {
3479
      // We don't have the value stored for some reason, reset last update and re-fetch
3480
      $this->h5pF->setContentHubMetadataChecked(null, $lang);
3481
      return $this->updateContentHubMetadataCache($lang);
3482
    }
3483
 
3484
    return $storedCache;
3485
  }
3486
 
3487
  /**
3488
   * Check if the current server setup is valid and set error messages
3489
   *
3490
   * @return object Setup object with errors and disable hub properties
3491
   */
3492
  public function checkSetupErrorMessage() {
3493
    $setup = (object) array(
3494
      'errors' => array(),
3495
      'disable_hub' => FALSE
3496
    );
3497
 
3498
    if (!class_exists('ZipArchive')) {
3499
      $setup->errors[] = $this->h5pF->t('Your PHP version does not support ZipArchive.');
3500
      $setup->disable_hub = TRUE;
3501
    }
3502
 
3503
    if (!extension_loaded('mbstring')) {
3504
      $setup->errors[] = $this->h5pF->t(
3505
        'The mbstring PHP extension is not loaded. H5P needs this to function properly'
3506
      );
3507
      $setup->disable_hub = TRUE;
3508
    }
3509
 
3510
    // Check php version >= 5.2
3511
    $php_version = explode('.', phpversion());
3512
    if ($php_version[0] < 5 || ($php_version[0] === 5 && $php_version[1] < 2)) {
3513
      $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.');
3514
      $setup->disable_hub = TRUE;
3515
    }
3516
 
3517
    // Check write access
3518
    if (!$this->fs->hasWriteAccess()) {
3519
      $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.');
3520
      $setup->disable_hub = TRUE;
3521
    }
3522
 
3523
    $max_upload_size = self::returnBytes(ini_get('upload_max_filesize'));
3524
    $max_post_size   = self::returnBytes(ini_get('post_max_size'));
3525
    $byte_threshold  = 5000000; // 5MB
3526
    if ($max_upload_size < $byte_threshold) {
3527
      $setup->errors[] =
3528
        $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, '.', ' ')));
3529
    }
3530
 
3531
    if ($max_post_size < $byte_threshold) {
3532
      $setup->errors[] =
3533
        $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, '.', ' ')));
3534
    }
3535
 
3536
    if ($max_upload_size > $max_post_size) {
3537
      $setup->errors[] =
3538
        $this->h5pF->t('Your PHP max upload size is bigger than your max post size. This is known to cause issues in some installations.');
3539
    }
3540
 
3541
    // Check SSL
3542
    if (!extension_loaded('openssl')) {
3543
      $setup->errors[] =
3544
        $this->h5pF->t('Your server does not have SSL enabled. SSL should be enabled to ensure a secure connection with the H5P hub.');
3545
      $setup->disable_hub = TRUE;
3546
    }
3547
 
3548
    return $setup;
3549
  }
3550
 
3551
  /**
3552
   * Check that all H5P requirements for the server setup is met.
3553
   */
3554
  public function checkSetupForRequirements() {
3555
    $setup = $this->checkSetupErrorMessage();
3556
 
3557
    $this->h5pF->setOption('hub_is_enabled', !$setup->disable_hub);
3558
    if (!empty($setup->errors)) {
3559
      foreach ($setup->errors as $err) {
3560
        $this->h5pF->setErrorMessage($err);
3561
      }
3562
    }
3563
 
3564
    if ($setup->disable_hub) {
3565
      // Inform how to re-enable hub
3566
      $this->h5pF->setErrorMessage(
3567
        $this->h5pF->t('H5P hub communication has been disabled because one or more H5P requirements failed.')
3568
      );
3569
      $this->h5pF->setErrorMessage(
3570
        $this->h5pF->t('When you have revised your server setup you may re-enable H5P hub communication in H5P Settings.')
3571
      );
3572
    }
3573
  }
3574
 
3575
  /**
3576
   * Return bytes from php_ini string value
3577
   *
3578
   * @param string $val
3579
   *
3580
   * @return int|string
3581
   */
3582
  public static function returnBytes($val) {
3583
    $val  = trim($val);
3584
    $last = strtolower($val[strlen($val) - 1]);
3585
    $bytes = (int) $val;
3586
 
3587
    switch ($last) {
3588
      case 'g':
3589
        $bytes *= 1024;
3590
      case 'm':
3591
        $bytes *= 1024;
3592
      case 'k':
3593
        $bytes *= 1024;
3594
    }
3595
 
3596
    return $bytes;
3597
  }
3598
 
3599
  /**
3600
   * Check if the current user has permission to update and install new
3601
   * libraries.
3602
   *
3603
   * @param bool [$set] Optional, sets the permission
3604
   * @return bool
3605
   */
3606
  public function mayUpdateLibraries($set = null) {
3607
    static $can;
3608
 
3609
    if ($set !== null) {
3610
      // Use value set
3611
      $can = $set;
3612
    }
3613
 
3614
    if ($can === null) {
3615
      // Ask our framework
3616
      $can = $this->h5pF->mayUpdateLibraries();
3617
    }
3618
 
3619
    return $can;
3620
  }
3621
 
3622
  /**
3623
   * Provide localization for the Core JS
3624
   * @return array
3625
   */
3626
  public function getLocalization() {
3627
    return array(
3628
      'fullscreen' => $this->h5pF->t('Fullscreen'),
3629
      'disableFullscreen' => $this->h5pF->t('Disable fullscreen'),
3630
      'download' => $this->h5pF->t('Download'),
3631
      'copyrights' => $this->h5pF->t('Rights of use'),
3632
      'embed' => $this->h5pF->t('Embed'),
3633
      'size' => $this->h5pF->t('Size'),
3634
      'showAdvanced' => $this->h5pF->t('Show advanced'),
3635
      'hideAdvanced' => $this->h5pF->t('Hide advanced'),
3636
      'advancedHelp' => $this->h5pF->t('Include this script on your website if you want dynamic sizing of the embedded content:'),
3637
      'copyrightInformation' => $this->h5pF->t('Rights of use'),
3638
      'close' => $this->h5pF->t('Close'),
3639
      'title' => $this->h5pF->t('Title'),
3640
      'author' => $this->h5pF->t('Author'),
3641
      'year' => $this->h5pF->t('Year'),
3642
      'source' => $this->h5pF->t('Source'),
3643
      'license' => $this->h5pF->t('License'),
3644
      'thumbnail' => $this->h5pF->t('Thumbnail'),
3645
      'noCopyrights' => $this->h5pF->t('No copyright information available for this content.'),
3646
      'reuse' => $this->h5pF->t('Reuse'),
3647
      'reuseContent' => $this->h5pF->t('Reuse Content'),
3648
      'reuseDescription' => $this->h5pF->t('Reuse this content.'),
3649
      'downloadDescription' => $this->h5pF->t('Download this content as a H5P file.'),
3650
      'copyrightsDescription' => $this->h5pF->t('View copyright information for this content.'),
3651
      'embedDescription' => $this->h5pF->t('View the embed code for this content.'),
3652
      'h5pDescription' => $this->h5pF->t('Visit H5P.org to check out more cool content.'),
3653
      'contentChanged' => $this->h5pF->t('This content has changed since you last used it.'),
3654
      'startingOver' => $this->h5pF->t("You'll be starting over."),
3655
      'by' => $this->h5pF->t('by'),
3656
      'showMore' => $this->h5pF->t('Show more'),
3657
      'showLess' => $this->h5pF->t('Show less'),
3658
      'subLevel' => $this->h5pF->t('Sublevel'),
3659
      'confirmDialogHeader' => $this->h5pF->t('Confirm action'),
3660
      'confirmDialogBody' => $this->h5pF->t('Please confirm that you wish to proceed. This action is not reversible.'),
3661
      'cancelLabel' => $this->h5pF->t('Cancel'),
3662
      'confirmLabel' => $this->h5pF->t('Confirm'),
3663
      'licenseU' => $this->h5pF->t('Undisclosed'),
3664
      'licenseCCBY' => $this->h5pF->t('Attribution'),
3665
      'licenseCCBYSA' => $this->h5pF->t('Attribution-ShareAlike'),
3666
      'licenseCCBYND' => $this->h5pF->t('Attribution-NoDerivs'),
3667
      'licenseCCBYNC' => $this->h5pF->t('Attribution-NonCommercial'),
3668
      'licenseCCBYNCSA' => $this->h5pF->t('Attribution-NonCommercial-ShareAlike'),
3669
      'licenseCCBYNCND' => $this->h5pF->t('Attribution-NonCommercial-NoDerivs'),
3670
      'licenseCC40' => $this->h5pF->t('4.0 International'),
3671
      'licenseCC30' => $this->h5pF->t('3.0 Unported'),
3672
      'licenseCC25' => $this->h5pF->t('2.5 Generic'),
3673
      'licenseCC20' => $this->h5pF->t('2.0 Generic'),
3674
      'licenseCC10' => $this->h5pF->t('1.0 Generic'),
3675
      'licenseGPL' => $this->h5pF->t('General Public License'),
3676
      'licenseV3' => $this->h5pF->t('Version 3'),
3677
      'licenseV2' => $this->h5pF->t('Version 2'),
3678
      'licenseV1' => $this->h5pF->t('Version 1'),
3679
      'licensePD' => $this->h5pF->t('Public Domain'),
3680
      'licenseCC010' => $this->h5pF->t('CC0 1.0 Universal (CC0 1.0) Public Domain Dedication'),
3681
      'licensePDM' => $this->h5pF->t('Public Domain Mark'),
3682
      'licenseC' => $this->h5pF->t('Copyright'),
3683
      'contentType' => $this->h5pF->t('Content Type'),
3684
      'licenseExtras' => $this->h5pF->t('License Extras'),
3685
      'changes' => $this->h5pF->t('Changelog'),
3686
      'contentCopied' => $this->h5pF->t('Content is copied to the clipboard'),
3687
      'connectionLost' => $this->h5pF->t('Connection lost. Results will be stored and sent when you regain connection.'),
3688
      'connectionReestablished' => $this->h5pF->t('Connection reestablished.'),
3689
      'resubmitScores' => $this->h5pF->t('Attempting to submit stored results.'),
3690
      'offlineDialogHeader' => $this->h5pF->t('Your connection to the server was lost'),
3691
      'offlineDialogBody' => $this->h5pF->t('We were unable to send information about your completion of this task. Please check your internet connection.'),
3692
      'offlineDialogRetryMessage' => $this->h5pF->t('Retrying in :num....'),
3693
      'offlineDialogRetryButtonLabel' => $this->h5pF->t('Retry now'),
3694
      'offlineSuccessfulSubmit' => $this->h5pF->t('Successfully submitted results.'),
3695
      'mainTitle' => $this->h5pF->t('Sharing <strong>:title</strong>'),
3696
      'editInfoTitle' => $this->h5pF->t('Edit info for <strong>:title</strong>'),
3697
      'cancel' => $this->h5pF->t('Cancel'),
3698
      'back' => $this->h5pF->t('Back'),
3699
      'next' => $this->h5pF->t('Next'),
3700
      'reviewInfo' => $this->h5pF->t('Review info'),
3701
      'share' => $this->h5pF->t('Share'),
3702
      'saveChanges' => $this->h5pF->t('Save changes'),
3703
      'registerOnHub' => $this->h5pF->t('Register on the H5P Hub'),
3704
      'updateRegistrationOnHub' => $this->h5pF->t('Save account settings'),
3705
      'requiredInfo' => $this->h5pF->t('Required Info'),
3706
      'optionalInfo' => $this->h5pF->t('Optional Info'),
3707
      'reviewAndShare' => $this->h5pF->t('Review & Share'),
3708
      'reviewAndSave' => $this->h5pF->t('Review & Save'),
3709
      'shared' => $this->h5pF->t('Shared'),
3710
      'currentStep' => $this->h5pF->t('Step :step of :total'),
3711
      'sharingNote' => $this->h5pF->t('All content details can be edited after sharing'),
3712
      'licenseDescription' => $this->h5pF->t('Select a license for your content'),
3713
      'licenseVersion' => $this->h5pF->t('License Version'),
3714
      'licenseVersionDescription' => $this->h5pF->t('Select a license version'),
3715
      'disciplineLabel' => $this->h5pF->t('Disciplines'),
3716
      'disciplineDescription' => $this->h5pF->t('You can select multiple disciplines'),
3717
      'disciplineLimitReachedMessage' => $this->h5pF->t('You can select up to :numDisciplines disciplines'),
3718
      'discipline' => array(
3719
        'searchPlaceholder' => $this->h5pF->t('Type to search for disciplines'),
3720
        'in' => $this->h5pF->t('in'),
3721
        'dropdownButton' => $this->h5pF->t('Dropdown button'),
3722
      ),
3723
      'removeChip' => $this->h5pF->t('Remove :chip from the list'),
3724
      'keywordsPlaceholder' => $this->h5pF->t('Add keywords'),
3725
      'keywords' => $this->h5pF->t('Keywords'),
3726
      'keywordsDescription' => $this->h5pF->t('You can add multiple keywords separated by commas. Press "Enter" or "Add" to confirm keywords'),
3727
      'altText' => $this->h5pF->t('Alt text'),
3728
      'reviewMessage' => $this->h5pF->t('Please review the info below before you share'),
3729
      'subContentWarning' => $this->h5pF->t('Sub-content (images, questions etc.) will be shared under :license unless otherwise specified in the authoring tool'),
3730
      'disciplines' => $this->h5pF->t('Disciplines'),
3731
      'shortDescription' => $this->h5pF->t('Short description'),
3732
      'longDescription' => $this->h5pF->t('Long description'),
3733
      'icon' => $this->h5pF->t('Icon'),
3734
      'screenshots' => $this->h5pF->t('Screenshots'),
3735
      'helpChoosingLicense' => $this->h5pF->t('Help me choose a license'),
3736
      'shareFailed' => $this->h5pF->t('Share failed.'),
3737
      'editingFailed' => $this->h5pF->t('Editing failed.'),
3738
      'shareTryAgain' => $this->h5pF->t('Something went wrong, please try to share again.'),
3739
      'pleaseWait' => $this->h5pF->t('Please wait...'),
3740
      'language' => $this->h5pF->t('Language'),
3741
      'level' => $this->h5pF->t('Level'),
3742
      'shortDescriptionPlaceholder' => $this->h5pF->t('Short description of your content'),
3743
      'longDescriptionPlaceholder' => $this->h5pF->t('Long description of your content'),
3744
      'description' => $this->h5pF->t('Description'),
3745
      'iconDescription' => $this->h5pF->t('640x480px. If not selected content will use category icon'),
3746
      'screenshotsDescription' => $this->h5pF->t('Add up to five screenshots of your content'),
3747
      'submitted' => $this->h5pF->t('Submitted!'),
3748
      'isNowSubmitted' => $this->h5pF->t('Is now submitted to H5P Hub'),
3749
      'changeHasBeenSubmitted' => $this->h5pF->t('A change has been submited for'),
3750
      'contentAvailable' => $this->h5pF->t('Your content will normally be available in the Hub within one business day.'),
3751
      'contentUpdateSoon' => $this->h5pF->t('Your content will update soon'),
3752
      'contentLicenseTitle' => $this->h5pF->t('Content License Info'),
3753
      'licenseDialogDescription' => $this->h5pF->t('Click on a specific license to get info about proper usage'),
3754
      'publisherFieldTitle' => $this->h5pF->t('Publisher'),
3755
      'publisherFieldDescription' => $this->h5pF->t('This will display as the "Publisher name" on shared content'),
3756
      'emailAddress' => $this->h5pF->t('Email Address'),
3757
      'publisherDescription' => $this->h5pF->t('Publisher description'),
3758
      'publisherDescriptionText' => $this->h5pF->t('This will be displayed under "Publisher info" on shared content'),
3759
      'contactPerson' => $this->h5pF->t('Contact Person'),
3760
      'phone' => $this->h5pF->t('Phone'),
3761
      'address' => $this->h5pF->t('Address'),
3762
      'city' => $this->h5pF->t('City'),
3763
      'zip' => $this->h5pF->t('Zip'),
3764
      'country' => $this->h5pF->t('Country'),
3765
      'logoUploadText' => $this->h5pF->t('Organization logo or avatar'),
3766
      'acceptTerms' => $this->h5pF->t('I accept the <a href=":url" target="_blank">terms of use</a>'),
3767
      'successfullyRegistred' => $this->h5pF->t('You have successfully registered an account on the H5P Hub'),
3768
      'successfullyRegistredDescription' => $this->h5pF->t('You account details can be changed'),
3769
      'successfullyUpdated' => $this->h5pF->t('Your H5P Hub account settings have successfully been changed'),
3770
      'accountDetailsLinkText' => $this->h5pF->t('here'),
3771
      'registrationTitle' => $this->h5pF->t('H5P Hub Registration'),
3772
      'registrationFailed' => $this->h5pF->t('An error occurred'),
3773
      'registrationFailedDescription' => $this->h5pF->t('We were not able to create an account at this point. Something went wrong. Try again later.'),
3774
      'maxLength' => $this->h5pF->t(':length is the maximum number of characters'),
3775
      'keywordExists' => $this->h5pF->t('Keyword already exists!'),
3776
      'licenseDetails' => $this->h5pF->t('License details'),
3777
      'remove' => $this->h5pF->t('Remove'),
3778
      'removeImage' => $this->h5pF->t('Remove image'),
3779
      'cancelPublishConfirmationDialogTitle' => $this->h5pF->t('Cancel sharing'),
3780
      'cancelPublishConfirmationDialogDescription' => $this->h5pF->t('Are you sure you want to cancel the sharing process?'),
3781
      'cancelPublishConfirmationDialogCancelButtonText' => $this->h5pF->t('No'),
3782
      'cancelPublishConfirmationDialogConfirmButtonText' => $this->h5pF->t('Yes'),
3783
      'add' => $this->h5pF->t('Add'),
3784
      'age' => $this->h5pF->t('Typical age'),
3785
      'ageDescription' => $this->h5pF->t('The target audience of this content. Possible input formats separated by commas: "1,34-45,-50,59-".'),
3786
      'invalidAge' => $this->h5pF->t('Invalid input format for Typical age. Possible input formats separated by commas: "1, 34-45, -50, -59-".'),
3787
      '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'),
3788
      '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'),
3789
      '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.'),
3790
      'keywordsExits' => $this->h5pF->t('Keywords already exists!'),
3791
      'someKeywordsExits' => $this->h5pF->t('Some of these keywords already exist'),
3792
      'width' => $this->h5pF->t('width'),
3793
      'height' => $this->h5pF->t('height'),
3794
      'rotateLeft' => $this->h5pF->t('Rotate Left'),
3795
      'rotateRight' => $this->h5pF->t('Rotate Right'),
3796
      'cropImage' => $this->h5pF->t('Crop Image'),
3797
      'confirmCrop' => $this->h5pF->t('Confirm Crop'),
3798
      'cancelCrop' => $this->h5pF->t('Cancel Crop')
3799
    );
3800
  }
3801
 
3802
  /**
3803
   * Publish content on the H5P Hub.
3804
   *
3805
   * @param bigint $id
3806
   * @return stdClass
3807
   */
3808
  public function hubRetrieveContent($id) {
3809
    $headers = array(
3810
      'Authorization' => $this->hubGetAuthorizationHeader(),
3811
      'Accept' => 'application/json',
3812
    );
3813
 
3814
    $response = $this->h5pF->fetchExternalData(
3815
      H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT . "/{$id}"),
3816
      NULL, TRUE, NULL, TRUE, $headers
3817
    );
3818
 
3819
    if (empty($response['data'])) {
3820
      throw new Exception($this->h5pF->t('Unable to authorize with the H5P Hub. Please check your Hub registration and connection.'));
3821
    }
3822
 
3823
    if (isset($response['status']) && $response['status'] !== 200) {
3824
      if ($response['status'] === 404) {
3825
        $this->h5pF->setErrorMessage($this->h5pF->t('Content is not shared on the H5P OER Hub.'));
3826
        return NULL;
3827
      }
3828
      throw new Exception($this->h5pF->t("Couldn't communicate with the H5P Hub. Please try again later."));
3829
    }
3830
 
3831
    $hub_content = json_decode($response['data'])->data;
3832
    $hub_content->id = "$hub_content->id";
3833
    return $hub_content;
3834
  }
3835
 
3836
  /**
3837
   * Publish content on the H5P Hub.
3838
   *
3839
   * @param array $data Data from content publishing process
3840
   * @param array $files Files to upload with the content publish
3841
   * @param bigint $content_hub_id For updating existing content
3842
   * @return stdClass
3843
   */
3844
  public function hubPublishContent($data, $files, $content_hub_id = NULL) {
3845
    $headers = array(
3846
      'Authorization' => $this->hubGetAuthorizationHeader(),
3847
      'Accept' => 'application/json',
3848
    );
3849
 
3850
    $data['published'] = '1';
3851
    $endpoint = H5PHubEndpoints::CONTENT;
3852
    if ($content_hub_id !== NULL) {
3853
      $endpoint .= "/{$content_hub_id}";
3854
      $data['_method'] = 'PUT';
3855
    }
3856
 
3857
    $response = $this->h5pF->fetchExternalData(
3858
      H5PHubEndpoints::createURL($endpoint),
3859
      $data, TRUE, NULL, TRUE, $headers, $files
3860
    );
3861
 
3862
    if (empty($response['data']) || $response['status'] === 403) {
3863
      throw new Exception($this->h5pF->t('Unable to authorize with the H5P Hub. Please check your Hub registration and connection.'));
3864
    }
3865
 
3866
    if (isset($response['status']) && $response['status'] !== 200) {
3867
      throw new Exception($this->h5pF->t('Connecting to the content hub failed, please try again later.'));
3868
    }
3869
 
3870
    $result = json_decode($response['data']);
3871
    if (isset($result->success) && $result->success === TRUE) {
3872
      return $result;
3873
    }
3874
    elseif (!empty($result->errors)) {
3875
      // Relay any error messages
3876
      $e = new Exception($this->h5pF->t('Validation failed.'));
3877
      $e->errors = $result->errors;
3878
      throw $e;
3879
    }
3880
  }
3881
 
3882
  /**
3883
   * Creates the authorization header needed to access the private parts of
3884
   * the H5P Hub.
3885
   *
3886
   * @return string
3887
   */
3888
  public function hubGetAuthorizationHeader() {
3889
    $site_uuid = $this->h5pF->getOption('site_uuid', '');
3890
    $hub_secret = $this->h5pF->getOption('hub_secret', '');
3891
    if (empty($site_uuid)) {
3892
      $this->h5pF->setErrorMessage($this->h5pF->t('Missing Site UUID. Please check your Hub registration.'));
3893
    }
3894
    elseif (empty($hub_secret)) {
3895
      $this->h5pF->setErrorMessage($this->h5pF->t('Missing Hub Secret. Please check your Hub registration.'));
3896
    }
3897
    return 'Basic ' . base64_encode("$site_uuid:$hub_secret");
3898
  }
3899
 
3900
  /**
3901
   * Unpublish content from content hub
3902
   *
3903
   * @param  integer  $hubId  Content hub id
3904
   *
3905
   * @return bool True if successful
3906
   */
3907
  public function hubUnpublishContent($hubId) {
3908
    $headers = array(
3909
      'Authorization' => $this->hubGetAuthorizationHeader(),
3910
      'Accept' => 'application/json',
3911
    );
3912
 
3913
    $url = H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT);
3914
    $response = $this->h5pF->fetchExternalData("{$url}/{$hubId}", array(
3915
      'published' => '0',
3916
    ), true, null, true, $headers, array(), 'PUT');
3917
 
3918
    // Remove shared status if successful
3919
    if (!empty($response) && $response['status'] === 200) {
3920
      $msg = $this->h5pF->t('Content successfully unpublished');
3921
      $this->h5pF->setInfoMessage($msg);
3922
 
3923
      return true;
3924
    }
3925
    $msg = $this->h5pF->t('Content unpublish failed');
3926
    $this->h5pF->setErrorMessage($msg);
3927
 
3928
    return false;
3929
  }
3930
 
3931
  /**
3932
   * Sync content with content hub
3933
   *
3934
   * @param integer $hubId Content hub id
3935
   * @param string $exportPath Export path where .h5p for content can be found
3936
   *
3937
   * @return bool
3938
   */
3939
  public function hubSyncContent($hubId, $exportPath) {
3940
    $headers = array(
3941
      'Authorization' => $this->hubGetAuthorizationHeader(),
3942
      'Accept' => 'application/json',
3943
    );
3944
 
3945
    $url = H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT);
3946
    $response = $this->h5pF->fetchExternalData("{$url}/{$hubId}", array(
3947
      'download_url' => $exportPath,
3948
      'resync' => '1',
3949
    ), true, null, true, $headers, array(), 'PUT');
3950
 
3951
    if (!empty($response) && $response['status'] === 200) {
3952
      $msg = $this->h5pF->t('Content sync queued');
3953
      $this->h5pF->setInfoMessage($msg);
3954
      return true;
3955
    }
3956
 
3957
    $msg = $this->h5pF->t('Content sync failed');
3958
    $this->h5pF->setErrorMessage($msg);
3959
    return false;
3960
  }
3961
 
3962
  /**
3963
   * Fetch account info for our site from the content hub
3964
   *
3965
   * @return array|bool|string False if account is not setup, otherwise data
3966
   */
3967
  public function hubAccountInfo() {
3968
    $siteUuid = $this->h5pF->getOption('site_uuid', null);
3969
    $secret   = $this->h5pF->getOption('hub_secret', null);
3970
    if (empty($siteUuid) && !empty($secret)) {
3971
      $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.'));
3972
      throw new Exception('Hub secret not set');
3973
    }
3974
 
3975
    if (empty($siteUuid) || empty($secret)) {
3976
      return false;
3977
    }
3978
 
3979
    $headers = array(
3980
      'Authorization' => $this->hubGetAuthorizationHeader(),
3981
      'Accept' => 'application/json',
3982
    );
3983
 
3984
    $url = H5PHubEndpoints::createURL(H5PHubEndpoints::REGISTER);
3985
    $accountInfo = $this->h5pF->fetchExternalData("{$url}/{$siteUuid}",
3986
      null, true, null, true, $headers, array(), 'GET');
3987
 
3988
    if ($accountInfo['status'] === 401) {
3989
      // Unauthenticated, invalid hub secret and site uuid combination
3990
      $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.'));
3991
      return false;
3992
    }
3993
 
3994
    if ($accountInfo['status'] !== 200) {
3995
      $this->h5pF->setErrorMessage($this->h5pF->t('Unable to retrieve HUB account information. Please contact support.'));
3996
      return false;
3997
    }
3998
 
3999
    return json_decode($accountInfo['data'])->data;
4000
  }
4001
 
4002
  /**
4003
   * Register account
4004
   *
4005
   * @param array $formData Form data. Should include: name, email, description,
4006
   *    contact_person, phone, address, city, zip, country, remove_logo
4007
   * @param object $logo Input image
4008
   *
4009
   * @return array
4010
   */
4011
  public function hubRegisterAccount($formData, $logo) {
4012
 
4013
    $uuid = $this->h5pF->getOption('site_uuid', '');
4014
    if (empty($uuid)) {
4015
      // Attempt to fetch a new site uuid
4016
      $uuid = $this->fetchLibrariesMetadata(false, true);
4017
      if (!$uuid) {
4018
        return [
4019
          '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.'),
4020
          'status_code' => 403,
4021
          'error_code'  => 'MISSING_SITE_UUID',
4022
          'success'     => FALSE,
4023
        ];
4024
      }
4025
    }
4026
 
4027
    $formData['site_uuid'] = $uuid;
4028
 
4029
    $headers  = [];
4030
    $endpoint = H5PHubEndpoints::REGISTER;
4031
    // Update if already registered
4032
    $hasRegistered = $this->h5pF->getOption('hub_secret');
4033
    if ($hasRegistered) {
4034
      $endpoint            .= "/{$uuid}";
4035
      $formData['_method'] = 'PUT';
4036
      $headers             = [
4037
        'Authorization' => $this->hubGetAuthorizationHeader(),
4038
      ];
4039
    }
4040
 
4041
    $url          = H5PHubEndpoints::createURL($endpoint);
4042
    $registration = $this->h5pF->fetchExternalData(
4043
      $url,
4044
      $formData,
4045
      NULL,
4046
      NULL,
4047
      TRUE,
4048
      $headers,
4049
      isset($logo) ? ['logo' => $logo] : []
4050
    );
4051
 
4052
    try {
4053
      $results = json_decode($registration['data']);
4054
    } catch (Exception $e) {
4055
      return [
4056
        'message'     => 'Could not parse json response.',
4057
        'status_code' => 424,
4058
        'error_code'  => 'COULD_NOT_PARSE_RESPONSE',
4059
        'success'     => FALSE,
4060
      ];
4061
    }
4062
 
4063
    if (isset($results->errors->site_uuid)) {
4064
      return [
4065
        '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.',
4066
        'status_code' => 403,
4067
        'error_code'  => 'SITE_UUID_NOT_UNIQUE',
4068
        'success'     => FALSE,
4069
      ];
4070
    }
4071
 
4072
    if (isset($results->errors->logo)) {
4073
      return [
4074
        'message' => $results->errors->logo[0],
4075
        'status_code' => 400,
4076
        'success' => FALSE,
4077
      ];
4078
    }
4079
 
4080
    if (
4081
      !isset($results->success)
4082
      || $results->success === FALSE
4083
      || !$hasRegistered && !isset($results->account->secret)
4084
      || $registration['status'] !== 200
4085
    ) {
4086
      return [
4087
        'message'     => 'Unable to register the account. Please contact support team.',
4088
        'status_code' => 422,
4089
        'error_code'  => 'REGISTRATION_FAILED',
4090
        'success'     => FALSE,
4091
      ];
4092
    }
4093
 
4094
    if (!$hasRegistered) {
4095
      $this->h5pF->setOption('hub_secret', $results->account->secret);
4096
    }
4097
 
4098
    return [
4099
      'message'     => $this->h5pF->t('Account successfully registered.'),
4100
      'status_code' => 200,
4101
      'success'     => TRUE,
4102
    ];
4103
  }
4104
 
4105
  /**
4106
   * Get status of content from content hub
4107
   *
4108
   * @param string $hubContentId
4109
   * @param int $syncStatus
4110
   *
4111
   * @return false|int Returns a new H5PContentStatus if successful, else false
4112
   */
4113
  public function getHubContentStatus($hubContentId, $syncStatus) {
4114
    $headers = array(
4115
      'Authorization' => $this->hubGetAuthorizationHeader(),
4116
      'Accept' => 'application/json',
4117
    );
4118
 
4119
    $url     = H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT);
4120
    $response = $this->h5pF->fetchExternalData("{$url}/{$hubContentId}/status",
4121
      null, true, null, true, $headers);
4122
 
4123
    if (isset($response['status']) && $response['status'] === 403) {
4124
      $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.');
4125
      $this->h5pF->setErrorMessage($msg);
4126
      return false;
4127
    }
4128
    if (empty($response) || $response['status'] !== 200) {
4129
      $msg = $this->h5pF->t('Could not get content hub sync status for content.');
4130
      $this->h5pF->setErrorMessage($msg);
4131
      return false;
4132
    }
4133
 
4134
    $data = json_decode($response['data']);
4135
 
4136
    if (isset($data->messages)) {
4137
      // TODO: Is this the right place/way to display them?
4138
 
4139
      if (!empty($data->messages->info)) {
4140
        foreach ($data->messages->info as $info) {
4141
          $this->h5pF->setInfoMessage($info);
4142
        }
4143
      }
4144
      if (!empty($data->messages->error)) {
4145
        foreach ($data->messages->error as $error) {
4146
          $this->h5pF->setErrorMessage($error->message, $error->code);
4147
        }
4148
      }
4149
    }
4150
 
4151
    $contentStatus = intval($data->status);
4152
    // Content status updated
4153
    if ($contentStatus !== H5PContentStatus::STATUS_WAITING) {
4154
      $newState = H5PContentHubSyncStatus::SYNCED;
4155
      if ($contentStatus !== H5PContentStatus::STATUS_DOWNLOADED) {
4156
        $newState = H5PContentHubSyncStatus::FAILED;
4157
      }
4158
      else if (intval($syncStatus) !== $contentStatus) {
4159
        // Content status successfully transitioned to synced/downloaded
4160
        $successMsg = $this->h5pF->t('Content was successfully shared on the content hub.');
4161
        $this->h5pF->setInfoMessage($successMsg);
4162
      }
4163
 
4164
      return $newState;
4165
    }
4166
 
4167
    return false;
4168
  }
4169
}
4170
 
4171
/**
4172
 * Functions for validating basic types from H5P library semantics.
4173
 * @property bool allowedStyles
4174
 */
4175
class H5PContentValidator {
4176
  public $h5pF;
4177
  public $h5pC;
4178
  private $typeMap, $libraries, $dependencies, $nextWeight;
4179
  private static $allowed_styleable_tags = [
4180
    'span',
4181
    'p',
4182
    'div',
4183
    'h1',
4184
    'h2',
4185
    'h3',
4186
    'table',
4187
    'col',
4188
    'figure',
4189
    'td',
4190
    'th',
4191
    'li'
4192
  ];
4193
 
4194
  /** @var bool Allowed styles status. */
4195
  protected $allowedStyles;
4196
 
4197
  /**
4198
   * Constructor for the H5PContentValidator
4199
   *
4200
   * @param object $H5PFramework
4201
   *  The frameworks implementation of the H5PFrameworkInterface
4202
   * @param object $H5PCore
4203
   *  The main H5PCore instance
4204
   */
4205
  public function __construct($H5PFramework, $H5PCore) {
4206
    $this->h5pF = $H5PFramework;
4207
    $this->h5pC = $H5PCore;
4208
    $this->typeMap = array(
4209
      'text' => 'validateText',
4210
      'number' => 'validateNumber',
4211
      'boolean' => 'validateBoolean',
4212
      'list' => 'validateList',
4213
      'group' => 'validateGroup',
4214
      'file' => 'validateFile',
4215
      'image' => 'validateImage',
4216
      'video' => 'validateVideo',
4217
      'audio' => 'validateAudio',
4218
      'select' => 'validateSelect',
4219
      'library' => 'validateLibrary',
4220
    );
4221
    $this->nextWeight = 1;
4222
 
4223
    // Keep track of the libraries we load to avoid loading it multiple times.
4224
    $this->libraries = array();
4225
 
4226
    // Keep track of all dependencies for the given content.
4227
    $this->dependencies = array();
4228
  }
4229
 
4230
  /**
4231
   * Add Addon library.
4232
   */
4233
  public function addon($library) {
4234
    $depKey = 'preloaded-' . $library['machineName'];
4235
    $this->dependencies[$depKey] = array(
4236
      'library' => $library,
4237
      'type' => 'preloaded'
4238
    );
4239
    $this->nextWeight = $this->h5pC->findLibraryDependencies($this->dependencies, $library, $this->nextWeight);
4240
    $this->dependencies[$depKey]['weight'] = $this->nextWeight++;
4241
  }
4242
 
4243
  /**
4244
   * Get the flat dependency tree.
4245
   *
4246
   * @return array
4247
   */
4248
  public function getDependencies() {
4249
    return $this->dependencies;
4250
  }
4251
 
4252
  /**
4253
   * Validate metadata
4254
   *
4255
   * @param array $metadata
4256
   * @return array Validated & filtered
4257
   */
4258
  public function validateMetadata($metadata) {
4259
    $semantics = $this->getMetadataSemantics();
4260
    $group = (object)$metadata;
4261
 
4262
    // Stop complaining about "invalid selected option in select" for
4263
    // old content without license chosen.
4264
    if (!isset($group->license)) {
4265
      $group->license = 'U';
4266
    }
4267
 
4268
    $this->validateGroup($group, (object) array(
4269
      'type' => 'group',
4270
      'fields' => $semantics,
4271
    ), FALSE);
4272
 
4273
    return (array)$group;
4274
  }
4275
 
4276
  /**
4277
   * Validate given text value against text semantics.
4278
   * @param $text
4279
   * @param $semantics
4280
   */
4281
  public function validateText(&$text, $semantics) {
4282
    if (!is_string($text)) {
4283
      $text = '';
4284
    }
4285
    if (isset($semantics->tags)) {
4286
      // Not testing for empty array allows us to use the 4 defaults without
4287
      // specifying them in semantics.
4288
      $tags = array_merge(array('div', 'span', 'p', 'br'), $semantics->tags);
4289
 
4290
      // Add related tags for table etc.
4291
      if (in_array('table', $tags)) {
4292
        $tags = array_merge($tags, array('tr', 'td', 'th', 'colgroup', 'col', 'thead', 'tbody', 'tfoot', 'figure', 'figcaption'));
4293
      }
4294
      if (in_array('b', $tags) && ! in_array('strong', $tags)) {
4295
        $tags[] = 'strong';
4296
      }
4297
      if (in_array('i', $tags) && ! in_array('em', $tags)) {
4298
        $tags[] = 'em';
4299
      }
4300
      if (in_array('ul', $tags) || in_array('ol', $tags) && ! in_array('li', $tags)) {
4301
        $tags[] = 'li';
4302
      }
4303
      if (in_array('del', $tags) || in_array('strike', $tags) && ! in_array('s', $tags)) {
4304
        $tags[] = 's';
4305
      }
4306
 
4307
      // Determine allowed style tags
4308
      $stylePatterns = array();
4309
      // All styles must be start to end patterns (^...$)
4310
      if (isset($semantics->font)) {
4311
        if (isset($semantics->font->size) && $semantics->font->size) {
4312
          $stylePatterns[] = '/^font-size: *[0-9.]+(em|px|%) *;?$/i';
4313
        }
4314
        if (isset($semantics->font->family) && $semantics->font->family) {
4315
          $stylePatterns[] = '/^font-family: *[-a-z0-9,\'&; ]+;?$/i';
4316
        }
4317
        if (isset($semantics->font->color) && $semantics->font->color) {
4318
          $stylePatterns[] = '/^color: *(#[a-f0-9]{3}[a-f0-9]{3}?|rgba?\([0-9, ]+\)|hsla?\([0-9,.% ]+\)) *;?$/i';
4319
        }
4320
        if (isset($semantics->font->background) && $semantics->font->background) {
4321
          $stylePatterns[] = '/^background-color: *(#[a-f0-9]{3}[a-f0-9]{3}?|rgba?\([0-9, ]+\)|hsla?\([0-9,.% ]+\)) *;?$/i';
4322
        }
4323
        if (isset($semantics->font->spacing) && $semantics->font->spacing) {
4324
          $stylePatterns[] = '/^letter-spacing: *[0-9.]+(em|px|%) *;?$/i';
4325
        }
4326
        if (isset($semantics->font->height) && $semantics->font->height) {
4327
          $stylePatterns[] = '/^line-height: *[0-9.]+(em|px|%|) *;?$/i';
4328
        }
4329
      }
4330
 
4331
      // Allow styling of tables if they are allowed
4332
      if (isset($semantics->tags) && in_array('table', $semantics->tags)) {
4333
        // CKEditor outputs border as width style color
4334
        $stylePatterns[] = '/^border: *[0-9.]+(em|px|%|) *(none|solid|dotted|dashed|double|groove|ridge|inset|outset) *(#[a-f0-9]{3}[a-f0-9]{3}?|rgba?\([0-9, ]+\)|hsla?\([0-9,.% ]+\)) *;?$/i';
4335
        $stylePatterns[] = '/^border-style: *(none|solid|dotted|dashed|double|groove|ridge|inset|outset) *;?$/i';
4336
        $stylePatterns[] = '/^border-width: *[0-9.]+(em|px|%|) *;?$/i';
4337
        $stylePatterns[] = '/^border-color: *(#[a-f0-9]{3}[a-f0-9]{3}?|rgba?\([0-9, ]+\)|hsla?\([0-9,.% ]+\)) *;?$/i';
4338
 
4339
        $stylePatterns[] = '/^vertical-align: *(middle|top|bottom);?$/i';
4340
        $stylePatterns[] = '/^padding: *[0-9.]+(em|px|%|) *;?$/i';
4341
        $stylePatterns[] = '/^width: *[0-9.]+(em|px|%|) *;?$/i';
4342
        $stylePatterns[] = '/^height: *[0-9.]+(em|px|%|) *;?$/i';
4343
        $stylePatterns[] = '/^float: *(right|left|none) *;?$/i';
4344
 
4345
        // Needed for backwards compatibility
4346
        $stylePatterns[] = '/^border-collapse: *collapse *;?$/i';
4347
 
4348
        // Table can have background color when font bgcolor is disabled
4349
        // Double entry of bgcolor in stylePatterns shouldn't matter
4350
        $stylePatterns[] = '/^background-color: *(#[a-f0-9]{3}[a-f0-9]{3}?|rgba?\([0-9, ]+\)|hsla?\([0-9,.% ]+\)) *;?$/i';
4351
      }
4352
 
4353
      // Alignment is allowed for all wysiwyg texts
4354
      $stylePatterns[] = '/^text-align: *(center|left|right);?$/i';
4355
 
4356
      // Strip invalid HTML tags.
4357
      $text = $this->filter_xss($text, $tags, $stylePatterns);
4358
    }
4359
    else {
4360
      // Filter text to plain text.
4361
      $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8', FALSE);
4362
    }
4363
 
4364
    // Check if string is within allowed length
4365
    if (isset($semantics->maxLength)) {
4366
      if (!extension_loaded('mbstring')) {
4367
        $this->h5pF->setErrorMessage($this->h5pF->t('The mbstring PHP extension is not loaded. H5P need this to function properly'), 'mbstring-unsupported');
4368
      }
4369
      else {
4370
        $text = mb_substr($text, 0, $semantics->maxLength);
4371
      }
4372
    }
4373
 
4374
    // Check if string is according to optional regexp in semantics
4375
    if (!($text === '' && isset($semantics->optional) && $semantics->optional) && isset($semantics->regexp)) {
4376
      // Escaping '/' found in patterns, so that it does not break regexp fencing.
4377
      $pattern = '/' . str_replace('/', '\\/', $semantics->regexp->pattern) . '/';
4378
      $pattern .= isset($semantics->regexp->modifiers) ? $semantics->regexp->modifiers : '';
4379
      if (preg_match($pattern, $text) === 0) {
4380
        // Note: explicitly ignore return value FALSE, to avoid removing text
4381
        // if regexp is invalid...
4382
        $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');
4383
        $text = '';
4384
      }
4385
    }
4386
  }
4387
 
4388
  /**
4389
   * Validates content files
4390
   *
4391
   * @param string $contentPath
4392
   *  The path containing content files to validate.
4393
   * @param bool $isLibrary
4394
   * @return bool TRUE if all files are valid
4395
   * TRUE if all files are valid
4396
   * FALSE if one or more files fail validation. Error message should be set accordingly by validator.
4397
   */
4398
  public function validateContentFiles($contentPath, $isLibrary = FALSE) {
4399
    if ($this->h5pC->disableFileCheck === TRUE) {
4400
      return TRUE;
4401
    }
4402
 
4403
    // Scan content directory for files, recurse into sub directories.
4404
    $files = array_diff(scandir($contentPath), array('.','..'));
4405
    $valid = TRUE;
4406
    $whitelist = $this->h5pF->getWhitelist($isLibrary, H5PCore::$defaultContentWhitelist, H5PCore::$defaultLibraryWhitelistExtras);
4407
 
4408
    $wl_regex = '/\.(' . preg_replace('/ +/i', '|', preg_quote($whitelist)) . ')$/i';
4409
 
4410
    foreach ($files as $file) {
4411
      $filePath = $contentPath . '/' . $file;
4412
      if (is_dir($filePath)) {
4413
        $valid = $this->validateContentFiles($filePath, $isLibrary) && $valid;
4414
      }
4415
      else {
4416
        // Snipped from drupal 6 "file_validate_extensions".  Using own code
4417
        // to avoid 1. creating a file-like object just to test for the known
4418
        // file name, 2. testing against a returned error array that could
4419
        // never be more than 1 element long anyway, 3. recreating the regex
4420
        // for every file.
4421
        if (!extension_loaded('mbstring')) {
4422
          $this->h5pF->setErrorMessage($this->h5pF->t('The mbstring PHP extension is not loaded. H5P need this to function properly'), 'mbstring-unsupported');
4423
          $valid = FALSE;
4424
        }
4425
        else if (!preg_match($wl_regex, mb_strtolower($file))) {
4426
          $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');
4427
          $valid = FALSE;
4428
        }
4429
      }
4430
    }
4431
    return $valid;
4432
  }
4433
 
4434
  /**
4435
   * Validate given value against number semantics
4436
   * @param $number
4437
   * @param $semantics
4438
   */
4439
  public function validateNumber(&$number, $semantics) {
4440
    // Validate that $number is indeed a number
4441
    if (!is_numeric($number)) {
4442
      $number = 0;
4443
    }
4444
    // Check if number is within valid bounds. Move within bounds if not.
4445
    if (isset($semantics->min) && $number < $semantics->min) {
4446
      $number = $semantics->min;
4447
    }
4448
    if (isset($semantics->max) && $number > $semantics->max) {
4449
      $number = $semantics->max;
4450
    }
4451
    // Check if number is within allowed bounds even if step value is set.
4452
    if (isset($semantics->step)) {
4453
      $testNumber = $number - (isset($semantics->min) ? $semantics->min : 0);
4454
      $rest = $testNumber % $semantics->step;
4455
      if ($rest !== 0) {
4456
        $number -= $rest;
4457
      }
4458
    }
4459
    // Check if number has proper number of decimals.
4460
    if (isset($semantics->decimals)) {
4461
      $number = round($number, $semantics->decimals);
4462
    }
4463
  }
4464
 
4465
  /**
4466
   * Validate given value against boolean semantics
4467
   * @param $bool
4468
   * @return bool
4469
   */
4470
  public function validateBoolean(&$bool) {
4471
    return is_bool($bool);
4472
  }
4473
 
4474
  /**
4475
   * Validate select values
4476
   * @param $select
4477
   * @param $semantics
4478
   */
4479
  public function validateSelect(&$select, $semantics) {
4480
    $optional = isset($semantics->optional) && $semantics->optional;
4481
    $strict = FALSE;
4482
    if (isset($semantics->options) && !empty($semantics->options)) {
4483
      // We have a strict set of options to choose from.
4484
      $strict = TRUE;
4485
      $options = array();
4486
 
4487
      foreach ($semantics->options as $option) {
4488
        // Support optgroup - just flatten options into one
4489
        if (isset($option->type) && $option->type === 'optgroup') {
4490
          foreach ($option->options as $suboption) {
4491
            $options[$suboption->value] = TRUE;
4492
          }
4493
        }
4494
        elseif (isset($option->value)) {
4495
          $options[$option->value] = TRUE;
4496
        }
4497
      }
4498
    }
4499
 
4500
    if (isset($semantics->multiple) && $semantics->multiple) {
4501
      // Multi-choice generates array of values. Test each one against valid
4502
      // options, if we are strict.  First make sure we are working on an
4503
      // array.
4504
      if (!is_array($select)) {
4505
        $select = array($select);
4506
      }
4507
 
4508
      foreach ($select as $key => &$value) {
4509
        if ($strict && !$optional && !isset($options[$value])) {
4510
          $this->h5pF->setErrorMessage($this->h5pF->t('Invalid selected option in multi-select.'));
4511
          unset($select[$key]);
4512
        }
4513
        else {
4514
          $select[$key] = htmlspecialchars($value, ENT_QUOTES, 'UTF-8', FALSE);
4515
        }
4516
      }
4517
    }
4518
    else {
4519
      // Single mode.  If we get an array in here, we chop off the first
4520
      // element and use that instead.
4521
      if (is_array($select)) {
4522
        $select = $select[0];
4523
      }
4524
 
4525
      if ($strict && !$optional && !isset($options[$select])) {
4526
        $this->h5pF->setErrorMessage($this->h5pF->t('Invalid selected option in select.'));
4527
        $select = $semantics->options[0]->value;
4528
      }
4529
      $select = htmlspecialchars($select, ENT_QUOTES, 'UTF-8', FALSE);
4530
    }
4531
  }
4532
 
4533
  /**
4534
   * Validate given list value against list semantics.
4535
   * Will recurse into validating each item in the list according to the type.
4536
   * @param $list
4537
   * @param $semantics
4538
   */
4539
  public function validateList(&$list, $semantics) {
4540
    $field = $semantics->field;
4541
    $function = $this->typeMap[$field->type];
4542
 
4543
    // Check that list is not longer than allowed length. We do this before
4544
    // iterating to avoid unnecessary work.
4545
    if (isset($semantics->max)) {
4546
      array_splice($list, $semantics->max);
4547
    }
4548
 
4549
    if (!is_array($list)) {
4550
      $list = array();
4551
    }
4552
 
4553
    // Validate each element in list.
4554
    foreach ($list as $key => &$value) {
4555
      if (!is_int($key)) {
4556
        array_splice($list, $key, 1);
4557
        continue;
4558
      }
4559
      $this->$function($value, $field);
4560
      if ($value === NULL) {
4561
        array_splice($list, $key, 1);
4562
      }
4563
    }
4564
 
4565
    if (count($list) === 0) {
4566
      $list = NULL;
4567
    }
4568
  }
4569
 
4570
  /**
4571
   * Validate a file like object, such as video, image, audio and file.
4572
   * @param $file
4573
   * @param $semantics
4574
   * @param array $typeValidKeys
4575
   */
4576
  private function _validateFilelike(&$file, $semantics, $typeValidKeys = array()) {
4577
    // Do not allow to use files from other content folders.
4578
    $matches = array();
4579
    if (preg_match($this->h5pC->relativePathRegExp, $file->path, $matches)) {
4580
      $file->path = $matches[5];
4581
    }
4582
 
4583
    // Remove temporary files suffix
4584
    if (substr($file->path, -4, 4) === '#tmp') {
4585
      $file->path = substr($file->path, 0, strlen($file->path) - 4);
4586
    }
4587
 
4588
    // Make sure path and mime does not have any special chars
4589
    $file->path = htmlspecialchars($file->path, ENT_QUOTES, 'UTF-8', FALSE);
4590
    if (isset($file->mime)) {
4591
      $file->mime = htmlspecialchars($file->mime, ENT_QUOTES, 'UTF-8', FALSE);
4592
    }
4593
 
4594
    // Remove attributes that should not exist, they may contain JSON escape
4595
    // code.
4596
    $validKeys = array_merge(array('path', 'mime', 'copyright'), $typeValidKeys);
4597
    if (isset($semantics->extraAttributes)) {
4598
      $validKeys = array_merge($validKeys, $semantics->extraAttributes); // TODO: Validate extraAttributes
4599
    }
4600
    $this->filterParams($file, $validKeys);
4601
 
4602
    if (isset($file->width)) {
4603
      $file->width = intval($file->width);
4604
    }
4605
 
4606
    if (isset($file->height)) {
4607
      $file->height = intval($file->height);
4608
    }
4609
 
4610
    if (isset($file->codecs)) {
4611
      $file->codecs = htmlspecialchars($file->codecs, ENT_QUOTES, 'UTF-8', FALSE);
4612
    }
4613
 
4614
    if (isset($file->bitrate)) {
4615
      $file->bitrate = intval($file->bitrate);
4616
    }
4617
 
4618
    if (isset($file->quality)) {
4619
      if (!is_object($file->quality) || !isset($file->quality->level) || !isset($file->quality->label)) {
4620
        unset($file->quality);
4621
      }
4622
      else {
4623
        $this->filterParams($file->quality, array('level', 'label'));
4624
        $file->quality->level = intval($file->quality->level);
4625
        $file->quality->label = htmlspecialchars($file->quality->label, ENT_QUOTES, 'UTF-8', FALSE);
4626
      }
4627
    }
4628
 
4629
    if (isset($file->copyright)) {
4630
      $this->validateGroup($file->copyright, $this->getCopyrightSemantics());
4631
    }
4632
  }
4633
 
4634
  /**
4635
   * Validate given file data
4636
   * @param $file
4637
   * @param $semantics
4638
   */
4639
  public function validateFile(&$file, $semantics) {
4640
    $this->_validateFilelike($file, $semantics);
4641
  }
4642
 
4643
  /**
4644
   * Validate given image data
4645
   * @param $image
4646
   * @param $semantics
4647
   */
4648
  public function validateImage(&$image, $semantics) {
4649
    $this->_validateFilelike($image, $semantics, array('width', 'height', 'originalImage'));
4650
  }
4651
 
4652
  /**
4653
   * Validate given video data
4654
   * @param $video
4655
   * @param $semantics
4656
   */
4657
  public function validateVideo(&$video, $semantics) {
4658
    foreach ($video as &$variant) {
4659
      $this->_validateFilelike($variant, $semantics, array('width', 'height', 'codecs', 'quality', 'bitrate'));
4660
    }
4661
  }
4662
 
4663
  /**
4664
   * Validate given audio data
4665
   * @param $audio
4666
   * @param $semantics
4667
   */
4668
  public function validateAudio(&$audio, $semantics) {
4669
    foreach ($audio as &$variant) {
4670
      $this->_validateFilelike($variant, $semantics);
4671
    }
4672
  }
4673
 
4674
  /**
4675
   * Validate given group value against group semantics.
4676
   * Will recurse into validating each group member.
4677
   * @param $group
4678
   * @param $semantics
4679
   * @param bool $flatten
4680
   */
4681
  public function validateGroup(&$group, $semantics, $flatten = TRUE) {
4682
    // Groups with just one field are compressed in the editor to only output
4683
    // the child content. (Exemption for fake groups created by
4684
    // "validateBySemantics" above)
4685
    $function = null;
4686
    $field = null;
4687
 
4688
    $isSubContent = isset($semantics->isSubContent) && $semantics->isSubContent === TRUE;
4689
 
4690
    if (count($semantics->fields) == 1 && $flatten && !$isSubContent) {
4691
      $field = $semantics->fields[0];
4692
      $function = $this->typeMap[$field->type];
4693
      $this->$function($group, $field);
4694
    }
4695
    else {
4696
      foreach ($group as $key => &$value) {
4697
        // If subContentId is set, keep value
4698
        if($isSubContent && ($key == 'subContentId')){
4699
          continue;
4700
        }
4701
 
4702
        // Find semantics for name=$key
4703
        $found = FALSE;
4704
        foreach ($semantics->fields as $field) {
4705
          if ($field->name == $key) {
4706
            if (isset($semantics->optional) && $semantics->optional) {
4707
              $field->optional = TRUE;
4708
            }
4709
            $function = $this->typeMap[$field->type];
4710
            $found = TRUE;
4711
            break;
4712
          }
4713
        }
4714
        if ($found) {
4715
          if ($function) {
4716
            $this->$function($value, $field);
4717
            if ($value === NULL) {
4718
              unset($group->$key);
4719
            }
4720
          }
4721
          else {
4722
            // We have a field type in semantics for which we don't have a
4723
            // known validator.
4724
            $this->h5pF->setErrorMessage($this->h5pF->t('H5P internal error: unknown content type "@type" in semantics. Removing content!', array('@type' => $field->type)), 'semantics-unknown-type');
4725
            unset($group->$key);
4726
          }
4727
        }
4728
        else {
4729
          // If validator is not found, something exists in content that does
4730
          // not have a corresponding semantics field. Remove it.
4731
          // $this->h5pF->setErrorMessage($this->h5pF->t('H5P internal error: no validator exists for @key', array('@key' => $key)));
4732
          unset($group->$key);
4733
        }
4734
      }
4735
    }
4736
  }
4737
 
4738
  /**
4739
   * Validate given library value against library semantics.
4740
   * Check if provided library is within allowed options.
4741
   *
4742
   * Will recurse into validating the library's semantics too.
4743
   * @param $value
4744
   * @param $semantics
4745
   */
4746
  public function validateLibrary(&$value, $semantics) {
4747
    if (!isset($value->library)) {
4748
      $value = NULL;
4749
      return;
4750
    }
4751
 
4752
    // Check for array of objects or array of strings
4753
    if (is_object($semantics->options[0])) {
4754
      $getLibraryNames = function ($item) {
4755
        return $item->name;
4756
      };
4757
      $libraryNames = array_map($getLibraryNames, $semantics->options);
4758
    }
4759
    else {
4760
      $libraryNames = $semantics->options;
4761
    }
4762
 
4763
    if (!in_array($value->library, $libraryNames)) {
4764
      $message = NULL;
4765
      // Create an understandable error message:
4766
      $machineNameArray = explode(' ', $value->library);
4767
      $machineName = $machineNameArray[0];
4768
      foreach ($libraryNames as $semanticsLibrary) {
4769
        $semanticsMachineNameArray = explode(' ', $semanticsLibrary);
4770
        $semanticsMachineName = $semanticsMachineNameArray[0];
4771
        if ($machineName === $semanticsMachineName) {
4772
          // Using the wrong version of the library in the content
4773
          $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(
4774
            '%machineName' => $machineName,
4775
            '%contentLibrary' => $value->library,
4776
            '%semanticsLibrary' => $semanticsLibrary
4777
          ));
4778
          break;
4779
        }
4780
      }
4781
 
4782
      // Using a library in content that is not present at all in semantics
4783
      if ($message === NULL) {
4784
        $message = $this->h5pF->t('The H5P library %library used in the content is not valid', array(
4785
          '%library' => $value->library
4786
        ));
4787
      }
4788
 
4789
      $this->h5pF->setErrorMessage($message);
4790
      $value = NULL;
4791
      return;
4792
    }
4793
 
4794
    if (!isset($this->libraries[$value->library])) {
4795
      $libSpec = H5PCore::libraryFromString($value->library);
4796
      $library = $this->h5pC->loadLibrary($libSpec['machineName'], $libSpec['majorVersion'], $libSpec['minorVersion']);
4797
      $library['semantics'] = $this->h5pC->loadLibrarySemantics($libSpec['machineName'], $libSpec['majorVersion'], $libSpec['minorVersion']);
4798
      $this->libraries[$value->library] = $library;
4799
    }
4800
    else {
4801
      $library = $this->libraries[$value->library];
4802
    }
4803
 
4804
    // Validate parameters
4805
    $this->validateGroup($value->params, (object) array(
4806
      'type' => 'group',
4807
      'fields' => $library['semantics'],
4808
    ), FALSE);
4809
 
4810
    // Validate subcontent's metadata
4811
    if (isset($value->metadata)) {
4812
      $value->metadata = $this->validateMetadata($value->metadata);
4813
    }
4814
 
4815
    $validKeys = array('library', 'params', 'subContentId', 'metadata');
4816
    if (isset($semantics->extraAttributes)) {
4817
      $validKeys = array_merge($validKeys, $semantics->extraAttributes);
4818
    }
4819
 
4820
    $this->filterParams($value, $validKeys);
4821
    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)) {
4822
      unset($value->subContentId);
4823
    }
4824
 
4825
    // Find all dependencies for this library
4826
    $depKey = 'preloaded-' . $library['machineName'];
4827
    if (!isset($this->dependencies[$depKey])) {
4828
      $this->dependencies[$depKey] = array(
4829
        'library' => $library,
4830
        'type' => 'preloaded'
4831
      );
4832
 
4833
      $this->nextWeight = $this->h5pC->findLibraryDependencies($this->dependencies, $library, $this->nextWeight);
4834
      $this->dependencies[$depKey]['weight'] = $this->nextWeight++;
4835
    }
4836
  }
4837
 
4838
  /**
4839
   * Check params for a whitelist of allowed properties
4840
   *
4841
   * @param array/object $params
4842
   * @param array $whitelist
4843
   */
4844
  public function filterParams(&$params, $whitelist) {
4845
    foreach ($params as $key => $value) {
4846
      if (!in_array($key, $whitelist)) {
4847
        unset($params->{$key});
4848
      }
4849
    }
4850
  }
4851
 
4852
  // XSS filters copied from drupal 7 common.inc. Some modifications done to
4853
  // replace Drupal one-liner functions with corresponding flat PHP.
4854
 
4855
  /**
4856
   * Filters HTML to prevent cross-site-scripting (XSS) vulnerabilities.
4857
   *
4858
   * Based on kses by Ulf Harnhammar, see http://sourceforge.net/projects/kses.
4859
   * For examples of various XSS attacks, see: http://ha.ckers.org/xss.html.
4860
   *
4861
   * This code does four things:
4862
   * - Removes characters and constructs that can trick browsers.
4863
   * - Makes sure all HTML entities are well-formed.
4864
   * - Makes sure all HTML tags and attributes are well-formed.
4865
   * - Makes sure no HTML tags contain URLs with a disallowed protocol (e.g.
4866
   *   javascript:).
4867
   *
4868
   * @param $string
4869
   *   The string with raw HTML in it. It will be stripped of everything that can
4870
   *   cause an XSS attack.
4871
   * @param array $allowed_tags
4872
   *   An array of allowed tags.
4873
   *
4874
   * @param bool $allowedStyles
4875
   * @return mixed|string An XSS safe version of $string, or an empty string if $string is not
4876
   * An XSS safe version of $string, or an empty string if $string is not
4877
   * valid UTF-8.
4878
   * @ingroup sanitation
4879
   */
4880
  private function filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd'), $allowedStyles = FALSE) {
4881
    if (strlen($string) == 0) {
4882
      return $string;
4883
    }
4884
    // Only operate on valid UTF-8 strings. This is necessary to prevent cross
4885
    // site scripting issues on Internet Explorer 6. (Line copied from
4886
    // drupal_validate_utf8)
4887
    if (preg_match('/^./us', $string) != 1) {
4888
      return '';
4889
    }
4890
 
4891
    $this->allowedStyles = $allowedStyles;
4892
 
4893
    // Store the text format.
4894
    $this->_filter_xss_split($allowed_tags, TRUE);
4895
    // Remove NULL characters (ignored by some browsers).
4896
    $string = str_replace(chr(0), '', $string);
4897
    // Remove Netscape 4 JS entities.
4898
    $string = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string);
4899
 
4900
    // Defuse all HTML entities.
4901
    $string = str_replace('&', '&amp;', $string);
4902
    // Change back only well-formed entities in our whitelist:
4903
    // Decimal numeric entities.
4904
    $string = preg_replace('/&amp;#([0-9]+;)/', '&#\1', $string);
4905
    // Hexadecimal numeric entities.
4906
    $string = preg_replace('/&amp;#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '&#x\1', $string);
4907
    // Named entities.
4908
    $string = preg_replace('/&amp;([A-Za-z][A-Za-z0-9]*;)/', '&\1', $string);
4909
    return preg_replace_callback('%
4910
      (
4911
      <(?=[^a-zA-Z!/])  # a lone <
4912
      |                 # or
4913
      <!--.*?-->        # a comment
4914
      |                 # or
4915
      <[^>]*(>|$)       # a string that starts with a <, up until the > or the end of the string
4916
      |                 # or
4917
      >                 # just a >
4918
      )%x', array($this, '_filter_xss_split'), $string);
4919
  }
4920
 
4921
  /**
4922
   * Processes an HTML tag.
4923
   *
4924
   * @param $m
4925
   *   An array with various meaning depending on the value of $store.
4926
   *   If $store is TRUE then the array contains the allowed tags.
4927
   *   If $store is FALSE then the array has one element, the HTML tag to process.
4928
   * @param bool $store
4929
   *   Whether to store $m.
4930
   * @return string If the element isn't allowed, an empty string. Otherwise, the cleaned up
4931
   * If the element isn't allowed, an empty string. Otherwise, the cleaned up
4932
   * version of the HTML element.
4933
   */
4934
  private function _filter_xss_split($m, $store = FALSE) {
4935
    static $allowed_html;
4936
 
4937
    if ($store) {
4938
      $allowed_html = array_flip($m);
4939
      return $allowed_html;
4940
    }
4941
 
4942
    $string = $m[1];
4943
 
4944
    if (substr($string, 0, 1) != '<') {
4945
      // We matched a lone ">" character.
4946
      return '&gt;';
4947
    }
4948
    elseif (strlen($string) == 1) {
4949
      // We matched a lone "<" character.
4950
      return '&lt;';
4951
    }
4952
 
4953
    if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9\-]+)\s*([^>]*)>?|(<!--.*?-->)$%', $string, $matches)) {
4954
      // Seriously malformed.
4955
      return '';
4956
    }
4957
 
4958
    $slash = trim($matches[1]);
4959
    $elem = &$matches[2];
4960
    $attrList = &$matches[3];
4961
    $comment = &$matches[4];
4962
 
4963
    if ($comment) {
4964
      $elem = '!--';
4965
    }
4966
 
4967
    if (!isset($allowed_html[strtolower($elem)])) {
4968
      // Disallowed HTML element.
4969
      return '';
4970
    }
4971
 
4972
    if ($comment) {
4973
      return $comment;
4974
    }
4975
 
4976
    if ($slash != '') {
4977
      return "</$elem>";
4978
    }
4979
 
4980
    // Is there a closing XHTML slash at the end of the attributes?
4981
    $attrList = preg_replace('%(\s?)/\s*$%', '\1', $attrList, -1, $count);
4982
    $xhtml_slash = $count ? ' /' : '';
4983
 
4984
    // Clean up attributes.
4985
 
4986
    $attr2 = implode(' ', $this->_filter_xss_attributes($attrList, (in_array($elem, self::$allowed_styleable_tags) ? $this->allowedStyles : FALSE)));
4987
    $attr2 = preg_replace('/[<>]/', '', $attr2);
4988
    $attr2 = strlen($attr2) ? ' ' . $attr2 : '';
4989
 
4990
    return "<$elem$attr2$xhtml_slash>";
4991
  }
4992
 
4993
  /**
4994
   * Processes a string of HTML attributes.
4995
   *
4996
   * @param $attr
4997
   * @param array|bool|object $allowedStyles
4998
   * @return array Cleaned up version of the HTML attributes.
4999
   * Cleaned up version of the HTML attributes.
5000
   */
5001
  private function _filter_xss_attributes($attr, $allowedStyles = FALSE) {
5002
    $attrArr = array();
5003
    $mode = 0;
5004
    $attrName = '';
5005
    $skip = false;
5006
 
5007
    while (strlen($attr) != 0) {
5008
      // Was the last operation successful?
5009
      $working = 0;
5010
      switch ($mode) {
5011
        case 0:
5012
          // Attribute name, href for instance.
5013
          if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) {
5014
            $attrName = strtolower($match[1]);
5015
            $skip = (
5016
              $attrName == 'style' ||
5017
              substr($attrName, 0, 2) == 'on' ||
5018
              substr($attrName, 0, 1) == '-' ||
5019
              // Ignore long attributes to avoid unnecessary processing overhead.
5020
              strlen($attrName) > 96
5021
            );
5022
            $working = $mode = 1;
5023
            $attr = preg_replace('/^[-a-zA-Z]+/', '', $attr);
5024
          }
5025
          break;
5026
 
5027
        case 1:
5028
          // Equals sign or valueless ("selected").
5029
          if (preg_match('/^\s*=\s*/', $attr)) {
5030
            $working = 1; $mode = 2;
5031
            $attr = preg_replace('/^\s*=\s*/', '', $attr);
5032
            break;
5033
          }
5034
 
5035
          if (preg_match('/^\s+/', $attr)) {
5036
            $working = 1; $mode = 0;
5037
            if (!$skip) {
5038
              $attrArr[] = $attrName;
5039
            }
5040
            $attr = preg_replace('/^\s+/', '', $attr);
5041
          }
5042
          break;
5043
 
5044
        case 2:
5045
          // Attribute value, a URL after href= for instance.
5046
          if (preg_match('/^"([^"]*)"(\s+|$)/', $attr, $match)) {
5047
            if ($allowedStyles && $attrName === 'style') {
5048
              // Allow certain styles
5049
 
5050
              // Prevent font family from getting split wrong because of the ; in &quot;
5051
              if (str_contains($match[1], 'font-family')) {
5052
                $match[1] = str_replace('&quot;', "'", $match[1]);
5053
              }
5054
 
5055
              $validatedStyles = [];
5056
              $styles = explode(';', $match[1]);
5057
 
5058
              foreach ($allowedStyles as $pattern) {
5059
                foreach ($styles as $style) {
5060
                  $style = trim($style);
5061
                  if (preg_match($pattern, $style)) {
5062
                    $validatedStyles[] = $style;
5063
                  }
5064
                }
5065
              }
5066
 
5067
              $attrArr[] = 'style="' . implode(';', $validatedStyles) . ';"';
5068
              break;
5069
            }
5070
 
5071
            $thisVal = $this->filter_xss_bad_protocol($match[1]);
5072
 
5073
            if (!$skip) {
5074
              $attrArr[] = "$attrName=\"$thisVal\"";
5075
            }
5076
            $working = 1;
5077
            $mode = 0;
5078
            $attr = preg_replace('/^"[^"]*"(\s+|$)/', '', $attr);
5079
            break;
5080
          }
5081
 
5082
          if (preg_match("/^'([^']*)'(\s+|$)/", $attr, $match)) {
5083
            $thisVal = $this->filter_xss_bad_protocol($match[1]);
5084
 
5085
            if (!$skip) {
5086
              $attrArr[] = "$attrName='$thisVal'";
5087
            }
5088
            $working = 1; $mode = 0;
5089
            $attr = preg_replace("/^'[^']*'(\s+|$)/", '', $attr);
5090
            break;
5091
          }
5092
 
5093
          if (preg_match("%^([^\s\"']+)(\s+|$)%", $attr, $match)) {
5094
            $thisVal = $this->filter_xss_bad_protocol($match[1]);
5095
 
5096
            if (!$skip) {
5097
              $attrArr[] = "$attrName=\"$thisVal\"";
5098
            }
5099
            $working = 1; $mode = 0;
5100
            $attr = preg_replace("%^[^\s\"']+(\s+|$)%", '', $attr);
5101
          }
5102
          break;
5103
      }
5104
 
5105
      if ($working == 0) {
5106
        // Not well formed; remove and try again.
5107
        $attr = preg_replace('/
5108
          ^
5109
          (
5110
          "[^"]*("|$)     # - a string that starts with a double quote, up until the next double quote or the end of the string
5111
          |               # or
5112
          \'[^\']*(\'|$)| # - a string that starts with a quote, up until the next quote or the end of the string
5113
          |               # or
5114
          \S              # - a non-whitespace character
5115
          )*              # any number of the above three
5116
          \s*             # any number of whitespaces
5117
          /x', '', $attr);
5118
        $mode = 0;
5119
      }
5120
    }
5121
 
5122
    // The attribute list ends with a valueless attribute like "selected".
5123
    if ($mode == 1 && !$skip) {
5124
      $attrArr[] = $attrName;
5125
    }
5126
    return $attrArr;
5127
  }
5128
 
5129
// TODO: Remove Drupal related stuff in docs.
5130
 
5131
  /**
5132
   * Processes an HTML attribute value and strips dangerous protocols from URLs.
5133
   *
5134
   * @param $string
5135
   *   The string with the attribute value.
5136
   * @param bool $decode
5137
   *   (deprecated) Whether to decode entities in the $string. Set to FALSE if the
5138
   *   $string is in plain text, TRUE otherwise. Defaults to TRUE. This parameter
5139
   *   is deprecated and will be removed in Drupal 8. To process a plain-text URI,
5140
   *   call _strip_dangerous_protocols() or check_url() instead.
5141
   * @return string Cleaned up and HTML-escaped version of $string.
5142
   * Cleaned up and HTML-escaped version of $string.
5143
   */
5144
  private function filter_xss_bad_protocol($string, $decode = TRUE) {
5145
    // Get the plain text representation of the attribute value (i.e. its meaning).
5146
    // @todo Remove the $decode parameter in Drupal 8, and always assume an HTML
5147
    //   string that needs decoding.
5148
    if ($decode) {
5149
      $string = html_entity_decode($string, ENT_QUOTES, 'UTF-8');
5150
    }
5151
    return htmlspecialchars($this->_strip_dangerous_protocols($string), ENT_QUOTES, 'UTF-8', FALSE);
5152
  }
5153
 
5154
  /**
5155
   * Strips dangerous protocols (e.g. 'javascript:') from a URI.
5156
   *
5157
   * This function must be called for all URIs within user-entered input prior
5158
   * to being output to an HTML attribute value. It is often called as part of
5159
   * check_url() or filter_xss(), but those functions return an HTML-encoded
5160
   * string, so this function can be called independently when the output needs to
5161
   * be a plain-text string for passing to t(), l(), drupal_attributes(), or
5162
   * another function that will call check_plain() separately.
5163
   *
5164
   * @param $uri
5165
   *   A plain-text URI that might contain dangerous protocols.
5166
   * @return string A plain-text URI stripped of dangerous protocols. As with all plain-text
5167
   * A plain-text URI stripped of dangerous protocols. As with all plain-text
5168
   * strings, this return value must not be output to an HTML page without
5169
   * check_plain() being called on it. However, it can be passed to functions
5170
   * expecting plain-text strings.
5171
   * @see check_url()
5172
   */
5173
  private function _strip_dangerous_protocols($uri) {
5174
    static $allowed_protocols;
5175
 
5176
    if (!isset($allowed_protocols)) {
5177
      $allowed_protocols = array_flip(array('ftp', 'http', 'https', 'mailto'));
5178
    }
5179
 
5180
    // Iteratively remove any invalid protocol found.
5181
    do {
5182
      $before = $uri;
5183
      $colonPos = strpos($uri, ':');
5184
      if ($colonPos > 0) {
5185
        // We found a colon, possibly a protocol. Verify.
5186
        $protocol = substr($uri, 0, $colonPos);
5187
        // If a colon is preceded by a slash, question mark or hash, it cannot
5188
        // possibly be part of the URL scheme. This must be a relative URL, which
5189
        // inherits the (safe) protocol of the base document.
5190
        if (preg_match('![/?#]!', $protocol)) {
5191
          break;
5192
        }
5193
        // Check if this is a disallowed protocol. Per RFC2616, section 3.2.3
5194
        // (URI Comparison) scheme comparison must be case-insensitive.
5195
        if (!isset($allowed_protocols[strtolower($protocol)])) {
5196
          $uri = substr($uri, $colonPos + 1);
5197
        }
5198
      }
5199
    } while ($before != $uri);
5200
 
5201
    return $uri;
5202
  }
5203
 
5204
  public function getMetadataSemantics() {
5205
    static $semantics;
5206
 
5207
    $cc_versions = array(
5208
      (object) array(
5209
        'value' => '4.0',
5210
        'label' => $this->h5pF->t('4.0 International')
5211
      ),
5212
      (object) array(
5213
        'value' => '3.0',
5214
        'label' => $this->h5pF->t('3.0 Unported')
5215
      ),
5216
      (object) array(
5217
        'value' => '2.5',
5218
        'label' => $this->h5pF->t('2.5 Generic')
5219
      ),
5220
      (object) array(
5221
        'value' => '2.0',
5222
        'label' => $this->h5pF->t('2.0 Generic')
5223
      ),
5224
      (object) array(
5225
        'value' => '1.0',
5226
        'label' => $this->h5pF->t('1.0 Generic')
5227
      )
5228
    );
5229
 
5230
    $semantics = array(
5231
      (object) array(
5232
        'name' => 'title',
5233
        'type' => 'text',
5234
        'label' => $this->h5pF->t('Title'),
5235
        'placeholder' => 'La Gioconda'
5236
      ),
5237
      (object) array(
5238
        'name' => 'a11yTitle',
5239
        'type' => 'text',
5240
        'label' => $this->h5pF->t('Assistive Technologies label'),
5241
        'optional' => TRUE,
5242
      ),
5243
      (object) array(
5244
        'name' => 'license',
5245
        'type' => 'select',
5246
        'label' => $this->h5pF->t('License'),
5247
        'default' => 'U',
5248
        'options' => array(
5249
          (object) array(
5250
            'value' => 'U',
5251
            'label' => $this->h5pF->t('Undisclosed')
5252
          ),
5253
          (object) array(
5254
            'type' => 'optgroup',
5255
            'label' => $this->h5pF->t('Creative Commons'),
5256
            'options' => array(
5257
              (object) array(
5258
                'value' => 'CC BY',
5259
                'label' => $this->h5pF->t('Attribution (CC BY)'),
5260
                'versions' => $cc_versions
5261
              ),
5262
              (object) array(
5263
                'value' => 'CC BY-SA',
5264
                'label' => $this->h5pF->t('Attribution-ShareAlike (CC BY-SA)'),
5265
                'versions' => $cc_versions
5266
              ),
5267
              (object) array(
5268
                'value' => 'CC BY-ND',
5269
                'label' => $this->h5pF->t('Attribution-NoDerivs (CC BY-ND)'),
5270
                'versions' => $cc_versions
5271
              ),
5272
              (object) array(
5273
                'value' => 'CC BY-NC',
5274
                'label' => $this->h5pF->t('Attribution-NonCommercial (CC BY-NC)'),
5275
                'versions' => $cc_versions
5276
              ),
5277
              (object) array(
5278
                'value' => 'CC BY-NC-SA',
5279
                'label' => $this->h5pF->t('Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)'),
5280
                'versions' => $cc_versions
5281
              ),
5282
              (object) array(
5283
                'value' => 'CC BY-NC-ND',
5284
                'label' => $this->h5pF->t('Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)'),
5285
                'versions' => $cc_versions
5286
              ),
5287
              (object) array(
5288
                'value' => 'CC0 1.0',
5289
                'label' => $this->h5pF->t('Public Domain Dedication (CC0)')
5290
              ),
5291
              (object) array(
5292
                'value' => 'CC PDM',
5293
                'label' => $this->h5pF->t('Public Domain Mark (PDM)')
5294
              ),
5295
            )
5296
          ),
5297
          (object) array(
5298
            'value' => 'GNU GPL',
5299
            'label' => $this->h5pF->t('General Public License v3')
5300
          ),
5301
          (object) array(
5302
            'value' => 'PD',
5303
            'label' => $this->h5pF->t('Public Domain')
5304
          ),
5305
          (object) array(
5306
            'value' => 'ODC PDDL',
5307
            'label' => $this->h5pF->t('Public Domain Dedication and Licence')
5308
          ),
5309
          (object) array(
5310
            'value' => 'C',
5311
            'label' => $this->h5pF->t('Copyright')
5312
          )
5313
        )
5314
      ),
5315
      (object) array(
5316
        'name' => 'licenseVersion',
5317
        'type' => 'select',
5318
        'label' => $this->h5pF->t('License Version'),
5319
        'options' => $cc_versions,
5320
        'optional' => TRUE
5321
      ),
5322
      (object) array(
5323
        'name' => 'yearFrom',
5324
        'type' => 'number',
5325
        'label' => $this->h5pF->t('Years (from)'),
5326
        'placeholder' => '1991',
5327
        'min' => '-9999',
5328
        'max' => '9999',
5329
        'optional' => TRUE
5330
      ),
5331
      (object) array(
5332
        'name' => 'yearTo',
5333
        'type' => 'number',
5334
        'label' => $this->h5pF->t('Years (to)'),
5335
        'placeholder' => '1992',
5336
        'min' => '-9999',
5337
        'max' => '9999',
5338
        'optional' => TRUE
5339
      ),
5340
      (object) array(
5341
        'name' => 'source',
5342
        'type' => 'text',
5343
        'label' => $this->h5pF->t('Source'),
5344
        'placeholder' => 'https://',
5345
        'optional' => TRUE
5346
      ),
5347
      (object) array(
5348
        'name' => 'authors',
5349
        'type' => 'list',
5350
        'field' => (object) array (
5351
          'name' => 'author',
5352
          'type' => 'group',
5353
          'fields'=> array(
5354
            (object) array(
5355
              'label' => $this->h5pF->t("Author's name"),
5356
              'name' => 'name',
5357
              'optional' => TRUE,
5358
              'type' => 'text'
5359
            ),
5360
            (object) array(
5361
              'name' => 'role',
5362
              'type' => 'select',
5363
              'label' => $this->h5pF->t("Author's role"),
5364
              'default' => 'Author',
5365
              'options' => array(
5366
                (object) array(
5367
                  'value' => 'Author',
5368
                  'label' => $this->h5pF->t('Author')
5369
                ),
5370
                (object) array(
5371
                  'value' => 'Editor',
5372
                  'label' => $this->h5pF->t('Editor')
5373
                ),
5374
                (object) array(
5375
                  'value' => 'Licensee',
5376
                  'label' => $this->h5pF->t('Licensee')
5377
                ),
5378
                (object) array(
5379
                  'value' => 'Originator',
5380
                  'label' => $this->h5pF->t('Originator')
5381
                )
5382
              )
5383
            )
5384
          )
5385
        )
5386
      ),
5387
      (object) array(
5388
        'name' => 'licenseExtras',
5389
        'type' => 'text',
5390
        'widget' => 'textarea',
5391
        'label' => $this->h5pF->t('License Extras'),
5392
        'optional' => TRUE,
5393
        'description' => $this->h5pF->t('Any additional information about the license')
5394
      ),
5395
      (object) array(
5396
        'name' => 'changes',
5397
        'type' => 'list',
5398
        'field' => (object) array(
5399
          'name' => 'change',
5400
          'type' => 'group',
5401
          'label' => $this->h5pF->t('Changelog'),
5402
          'fields' => array(
5403
            (object) array(
5404
              'name' => 'date',
5405
              'type' => 'text',
5406
              'label' => $this->h5pF->t('Date'),
5407
              'optional' => TRUE
5408
            ),
5409
            (object) array(
5410
              'name' => 'author',
5411
              'type' => 'text',
5412
              'label' => $this->h5pF->t('Changed by'),
5413
              'optional' => TRUE
5414
            ),
5415
            (object) array(
5416
              'name' => 'log',
5417
              'type' => 'text',
5418
              'widget' => 'textarea',
5419
              'label' => $this->h5pF->t('Description of change'),
5420
              'placeholder' => $this->h5pF->t('Photo cropped, text changed, etc.'),
5421
              'optional' => TRUE
5422
            )
5423
          )
5424
        )
5425
      ),
5426
      (object) array (
5427
        'name' => 'authorComments',
5428
        'type' => 'text',
5429
        'widget' => 'textarea',
5430
        'label' => $this->h5pF->t('Author comments'),
5431
        'description' => $this->h5pF->t('Comments for the editor of the content (This text will not be published as a part of copyright info)'),
5432
        'optional' => TRUE
5433
      ),
5434
      (object) array(
5435
        'name' => 'contentType',
5436
        'type' => 'text',
5437
        'widget' => 'none'
5438
      ),
5439
      (object) array(
5440
        'name' => 'defaultLanguage',
5441
        'type' => 'text',
5442
        'widget' => 'none'
5443
      )
5444
    );
5445
 
5446
    return $semantics;
5447
  }
5448
 
5449
  public function getCopyrightSemantics() {
5450
    static $semantics;
5451
 
5452
    if ($semantics === NULL) {
5453
      $cc_versions = array(
5454
        (object) array(
5455
          'value' => '4.0',
5456
          'label' => $this->h5pF->t('4.0 International')
5457
        ),
5458
        (object) array(
5459
          'value' => '3.0',
5460
          'label' => $this->h5pF->t('3.0 Unported')
5461
        ),
5462
        (object) array(
5463
          'value' => '2.5',
5464
          'label' => $this->h5pF->t('2.5 Generic')
5465
        ),
5466
        (object) array(
5467
          'value' => '2.0',
5468
          'label' => $this->h5pF->t('2.0 Generic')
5469
        ),
5470
        (object) array(
5471
          'value' => '1.0',
5472
          'label' => $this->h5pF->t('1.0 Generic')
5473
        )
5474
      );
5475
 
5476
      $semantics = (object) array(
5477
        'name' => 'copyright',
5478
        'type' => 'group',
5479
        'label' => $this->h5pF->t('Copyright information'),
5480
        'fields' => array(
5481
          (object) array(
5482
            'name' => 'title',
5483
            'type' => 'text',
5484
            'label' => $this->h5pF->t('Title'),
5485
            'placeholder' => 'La Gioconda',
5486
            'optional' => TRUE
5487
          ),
5488
          (object) array(
5489
            'name' => 'author',
5490
            'type' => 'text',
5491
            'label' => $this->h5pF->t('Author'),
5492
            'placeholder' => 'Leonardo da Vinci',
5493
            'optional' => TRUE
5494
          ),
5495
          (object) array(
5496
            'name' => 'year',
5497
            'type' => 'text',
5498
            'label' => $this->h5pF->t('Year(s)'),
5499
            'placeholder' => '1503 - 1517',
5500
            'optional' => TRUE
5501
          ),
5502
          (object) array(
5503
            'name' => 'source',
5504
            'type' => 'text',
5505
            'label' => $this->h5pF->t('Source'),
5506
            'placeholder' => 'http://en.wikipedia.org/wiki/Mona_Lisa',
5507
            'optional' => true,
5508
            'regexp' => (object) array(
5509
              'pattern' => '^http[s]?://.+',
5510
              'modifiers' => 'i'
5511
            )
5512
          ),
5513
          (object) array(
5514
            'name' => 'license',
5515
            'type' => 'select',
5516
            'label' => $this->h5pF->t('License'),
5517
            'default' => 'U',
5518
            'options' => array(
5519
              (object) array(
5520
                'value' => 'U',
5521
                'label' => $this->h5pF->t('Undisclosed')
5522
              ),
5523
              (object) array(
5524
                'value' => 'CC BY',
5525
                'label' => $this->h5pF->t('Attribution'),
5526
                'versions' => $cc_versions
5527
              ),
5528
              (object) array(
5529
                'value' => 'CC BY-SA',
5530
                'label' => $this->h5pF->t('Attribution-ShareAlike'),
5531
                'versions' => $cc_versions
5532
              ),
5533
              (object) array(
5534
                'value' => 'CC BY-ND',
5535
                'label' => $this->h5pF->t('Attribution-NoDerivs'),
5536
                'versions' => $cc_versions
5537
              ),
5538
              (object) array(
5539
                'value' => 'CC BY-NC',
5540
                'label' => $this->h5pF->t('Attribution-NonCommercial'),
5541
                'versions' => $cc_versions
5542
              ),
5543
              (object) array(
5544
                'value' => 'CC BY-NC-SA',
5545
                'label' => $this->h5pF->t('Attribution-NonCommercial-ShareAlike'),
5546
                'versions' => $cc_versions
5547
              ),
5548
              (object) array(
5549
                'value' => 'CC BY-NC-ND',
5550
                'label' => $this->h5pF->t('Attribution-NonCommercial-NoDerivs'),
5551
                'versions' => $cc_versions
5552
              ),
5553
              (object) array(
5554
                'value' => 'GNU GPL',
5555
                'label' => $this->h5pF->t('General Public License'),
5556
                'versions' => array(
5557
                  (object) array(
5558
                    'value' => 'v3',
5559
                    'label' => $this->h5pF->t('Version 3')
5560
                  ),
5561
                  (object) array(
5562
                    'value' => 'v2',
5563
                    'label' => $this->h5pF->t('Version 2')
5564
                  ),
5565
                  (object) array(
5566
                    'value' => 'v1',
5567
                    'label' => $this->h5pF->t('Version 1')
5568
                  )
5569
                )
5570
              ),
5571
              (object) array(
5572
                'value' => 'PD',
5573
                'label' => $this->h5pF->t('Public Domain'),
5574
                'versions' => array(
5575
                  (object) array(
5576
                    'value' => '-',
5577
                    'label' => '-'
5578
                  ),
5579
                  (object) array(
5580
                    'value' => 'CC0 1.0',
5581
                    'label' => $this->h5pF->t('CC0 1.0 Universal')
5582
                  ),
5583
                  (object) array(
5584
                    'value' => 'CC PDM',
5585
                    'label' => $this->h5pF->t('Public Domain Mark')
5586
                  )
5587
                )
5588
              ),
5589
              (object) array(
5590
                'value' => 'C',
5591
                'label' => $this->h5pF->t('Copyright')
5592
              )
5593
            )
5594
          ),
5595
          (object) array(
5596
            'name' => 'version',
5597
            'type' => 'select',
5598
            'label' => $this->h5pF->t('License Version'),
5599
            'options' => array()
5600
          )
5601
        )
5602
      );
5603
    }
5604
 
5605
    return $semantics;
5606
  }
5607
}