Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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