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