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