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