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