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