/libraries/src/Microdata/Microdata.php

https://github.com/joomla/joomla-cms · PHP · 835 lines · 340 code · 100 blank · 395 comment · 58 complexity · c0454cd4e181b9219cdb586008b6728e MD5 · raw file

  1. <?php
  2. /**
  3. * Joomla! Content Management System
  4. *
  5. * @copyright (C) 2013 Open Source Matters, Inc. <https://www.joomla.org>
  6. * @license GNU General Public License version 2 or later; see LICENSE.txt
  7. */
  8. namespace Joomla\CMS\Microdata;
  9. /**
  10. * Joomla Platform class for interacting with Microdata semantics.
  11. *
  12. * @since 3.2
  13. */
  14. class Microdata
  15. {
  16. /**
  17. * Array with all available Types and Properties from the http://schema.org vocabulary
  18. *
  19. * @var array
  20. * @since 3.2
  21. */
  22. protected static $types = null;
  23. /**
  24. * The Type
  25. *
  26. * @var string
  27. * @since 3.2
  28. */
  29. protected $type = null;
  30. /**
  31. * The Property
  32. *
  33. * @var string
  34. * @since 3.2
  35. */
  36. protected $property = null;
  37. /**
  38. * The Human content
  39. *
  40. * @var string
  41. * @since 3.2
  42. */
  43. protected $content = null;
  44. /**
  45. * The Machine content
  46. *
  47. * @var string
  48. * @since 3.2
  49. */
  50. protected $machineContent = null;
  51. /**
  52. * The Fallback Type
  53. *
  54. * @var string
  55. * @since 3.2
  56. */
  57. protected $fallbackType = null;
  58. /**
  59. * The Fallback Property
  60. *
  61. * @var string
  62. * @since 3.2
  63. */
  64. protected $fallbackProperty = null;
  65. /**
  66. * Used for checking if the library output is enabled or disabled
  67. *
  68. * @var boolean
  69. * @since 3.2
  70. */
  71. protected $enabled = true;
  72. /**
  73. * Initialize the class and setup the default $Type
  74. *
  75. * @param string $type Optional, fallback to 'Thing' Type
  76. * @param boolean $flag Enable or disable the library output
  77. *
  78. * @since 3.2
  79. */
  80. public function __construct($type = '', $flag = true)
  81. {
  82. if ($this->enabled = (bool) $flag) {
  83. // Fallback to 'Thing' Type
  84. if (!$type) {
  85. $type = 'Thing';
  86. }
  87. $this->setType($type);
  88. }
  89. }
  90. /**
  91. * Load all available Types and Properties from the http://schema.org vocabulary contained in the types.json file
  92. *
  93. * @return void
  94. *
  95. * @since 3.2
  96. */
  97. protected static function loadTypes()
  98. {
  99. // Load the JSON
  100. if (!static::$types) {
  101. $path = __DIR__ . '/types.json';
  102. static::$types = json_decode(file_get_contents($path), true);
  103. }
  104. }
  105. /**
  106. * Reset all params
  107. *
  108. * @return void
  109. *
  110. * @since 3.2
  111. */
  112. protected function resetParams()
  113. {
  114. $this->content = null;
  115. $this->machineContent = null;
  116. $this->property = null;
  117. $this->fallbackProperty = null;
  118. $this->fallbackType = null;
  119. }
  120. /**
  121. * Enable or Disable the library output
  122. *
  123. * @param boolean $flag Enable or disable the library output
  124. *
  125. * @return Microdata Instance of $this
  126. *
  127. * @since 3.2
  128. */
  129. public function enable($flag = true)
  130. {
  131. $this->enabled = (bool) $flag;
  132. return $this;
  133. }
  134. /**
  135. * Return 'true' if the library output is enabled
  136. *
  137. * @return boolean
  138. *
  139. * @since 3.2
  140. */
  141. public function isEnabled()
  142. {
  143. return $this->enabled;
  144. }
  145. /**
  146. * Set a new http://schema.org Type
  147. *
  148. * @param string $type The $Type to be setup
  149. *
  150. * @return Microdata Instance of $this
  151. *
  152. * @since 3.2
  153. */
  154. public function setType($type)
  155. {
  156. if (!$this->enabled) {
  157. return $this;
  158. }
  159. // Sanitize the Type
  160. $this->type = static::sanitizeType($type);
  161. // If the given $Type isn't available, fallback to 'Thing' Type
  162. if (!static::isTypeAvailable($this->type)) {
  163. $this->type = 'Thing';
  164. }
  165. return $this;
  166. }
  167. /**
  168. * Return the current $Type name
  169. *
  170. * @return string
  171. *
  172. * @since 3.2
  173. */
  174. public function getType()
  175. {
  176. return $this->type;
  177. }
  178. /**
  179. * Setup a $Property
  180. *
  181. * @param string $property The Property
  182. *
  183. * @return Microdata Instance of $this
  184. *
  185. * @since 3.2
  186. */
  187. public function property($property)
  188. {
  189. if (!$this->enabled) {
  190. return $this;
  191. }
  192. // Sanitize the $Property
  193. $property = static::sanitizeProperty($property);
  194. // Control if the $Property exists in the given $Type and setup it, otherwise leave it 'NULL'
  195. if (static::isPropertyInType($this->type, $property)) {
  196. $this->property = $property;
  197. }
  198. return $this;
  199. }
  200. /**
  201. * Return the current $Property name
  202. *
  203. * @return string
  204. *
  205. * @since 3.2
  206. */
  207. public function getProperty()
  208. {
  209. return $this->property;
  210. }
  211. /**
  212. * Setup a Human content or content for the Machines
  213. *
  214. * @param string $content The human content or machine content to be used
  215. * @param string $machineContent The machine content
  216. *
  217. * @return Microdata Instance of $this
  218. *
  219. * @since 3.2
  220. */
  221. public function content($content, $machineContent = null)
  222. {
  223. $this->content = $content;
  224. $this->machineContent = $machineContent;
  225. return $this;
  226. }
  227. /**
  228. * Return the current $content
  229. *
  230. * @return string
  231. *
  232. * @since 3.2
  233. */
  234. public function getContent()
  235. {
  236. return $this->content;
  237. }
  238. /**
  239. * Return the current $machineContent
  240. *
  241. * @return string
  242. *
  243. * @since 3.3
  244. */
  245. public function getMachineContent()
  246. {
  247. return $this->machineContent;
  248. }
  249. /**
  250. * Setup a Fallback Type and Property
  251. *
  252. * @param string $type The Fallback Type
  253. * @param string $property The Fallback Property
  254. *
  255. * @return Microdata Instance of $this
  256. *
  257. * @since 3.2
  258. */
  259. public function fallback($type, $property)
  260. {
  261. if (!$this->enabled) {
  262. return $this;
  263. }
  264. // Sanitize the $Type
  265. $this->fallbackType = static::sanitizeType($type);
  266. // If the given $Type isn't available, fallback to 'Thing' Type
  267. if (!static::isTypeAvailable($this->fallbackType)) {
  268. $this->fallbackType = 'Thing';
  269. }
  270. // Control if the $Property exist in the given $Type and setup it, otherwise leave it 'NULL'
  271. if (static::isPropertyInType($this->fallbackType, $property)) {
  272. $this->fallbackProperty = $property;
  273. } else {
  274. $this->fallbackProperty = null;
  275. }
  276. return $this;
  277. }
  278. /**
  279. * Return the current $fallbackType
  280. *
  281. * @return string
  282. *
  283. * @since 3.2
  284. */
  285. public function getFallbackType()
  286. {
  287. return $this->fallbackType;
  288. }
  289. /**
  290. * Return the current $fallbackProperty
  291. *
  292. * @return string
  293. *
  294. * @since 3.2
  295. */
  296. public function getFallbackProperty()
  297. {
  298. return $this->fallbackProperty;
  299. }
  300. /**
  301. * This function handles the display logic.
  302. * It checks if the Type, Property are available, if not check for a Fallback,
  303. * then reset all params for the next use and return the HTML.
  304. *
  305. * @param string $displayType Optional, 'inline', available options ['inline'|'span'|'div'|meta]
  306. * @param boolean $emptyOutput Return an empty string if the library output is disabled and there is a $content value
  307. *
  308. * @return string
  309. *
  310. * @since 3.2
  311. */
  312. public function display($displayType = '', $emptyOutput = false)
  313. {
  314. // Initialize the HTML to output
  315. $html = ($this->content !== null && !$emptyOutput) ? $this->content : '';
  316. // Control if the library output is enabled, otherwise return the $content or an empty string
  317. if (!$this->enabled) {
  318. // Reset params
  319. $this->resetParams();
  320. return $html;
  321. }
  322. // If the $property is wrong for the current $Type check if a Fallback is available, otherwise return an empty HTML
  323. if ($this->property) {
  324. // Process and return the HTML the way the user expects to
  325. if ($displayType) {
  326. switch ($displayType) {
  327. case 'span':
  328. $html = static::htmlSpan($html, $this->property);
  329. break;
  330. case 'div':
  331. $html = static::htmlDiv($html, $this->property);
  332. break;
  333. case 'meta':
  334. $html = $this->machineContent ?? $html;
  335. $html = static::htmlMeta($html, $this->property);
  336. break;
  337. default:
  338. // Default $displayType = 'inline'
  339. $html = static::htmlProperty($this->property);
  340. break;
  341. }
  342. } else {
  343. /*
  344. * Process and return the HTML in an automatic way,
  345. * with the $Property expected Types and display everything in the right way,
  346. * check if the $Property is 'normal', 'nested' or must be rendered in a metadata tag
  347. */
  348. switch (static::getExpectedDisplayType($this->type, $this->property)) {
  349. case 'nested':
  350. // Retrieve the expected 'nested' Type of the $Property
  351. $nestedType = static::getExpectedTypes($this->type, $this->property);
  352. $nestedProperty = '';
  353. // If there is a Fallback Type then probably it could be the expectedType
  354. if (\in_array($this->fallbackType, $nestedType)) {
  355. $nestedType = $this->fallbackType;
  356. if ($this->fallbackProperty) {
  357. $nestedProperty = $this->fallbackProperty;
  358. }
  359. } else {
  360. $nestedType = $nestedType[0];
  361. }
  362. // Check if a $content is available, otherwise fallback to an 'inline' display type
  363. if ($this->content !== null) {
  364. if ($nestedProperty) {
  365. $html = static::htmlSpan(
  366. $this->content,
  367. $nestedProperty
  368. );
  369. }
  370. $html = static::htmlSpan(
  371. $html,
  372. $this->property,
  373. $nestedType,
  374. true
  375. );
  376. } else {
  377. $html = static::htmlProperty($this->property) . ' ' . static::htmlScope($nestedType);
  378. if ($nestedProperty) {
  379. $html .= ' ' . static::htmlProperty($nestedProperty);
  380. }
  381. }
  382. break;
  383. case 'meta':
  384. // Check if a $content is available, otherwise fallback to an 'inline' display type
  385. if ($this->content !== null) {
  386. $html = $this->machineContent ?? $this->content;
  387. $html = static::htmlMeta($html, $this->property) . $this->content;
  388. } else {
  389. $html = static::htmlProperty($this->property);
  390. }
  391. break;
  392. default:
  393. /*
  394. * Default expected display type = 'normal'
  395. * Check if a $content is available,
  396. * otherwise fallback to an 'inline' display type
  397. */
  398. if ($this->content !== null) {
  399. $html = static::htmlSpan($this->content, $this->property);
  400. } else {
  401. $html = static::htmlProperty($this->property);
  402. }
  403. break;
  404. }
  405. }
  406. } elseif ($this->fallbackProperty) {
  407. // Process and return the HTML the way the user expects to
  408. if ($displayType) {
  409. switch ($displayType) {
  410. case 'span':
  411. $html = static::htmlSpan($html, $this->fallbackProperty, $this->fallbackType);
  412. break;
  413. case 'div':
  414. $html = static::htmlDiv($html, $this->fallbackProperty, $this->fallbackType);
  415. break;
  416. case 'meta':
  417. $html = $this->machineContent ?? $html;
  418. $html = static::htmlMeta($html, $this->fallbackProperty, $this->fallbackType);
  419. break;
  420. default:
  421. // Default $displayType = 'inline'
  422. $html = static::htmlScope($this->fallbackType) . ' ' . static::htmlProperty($this->fallbackProperty);
  423. break;
  424. }
  425. } else {
  426. /*
  427. * Process and return the HTML in an automatic way,
  428. * with the $Property expected Types and display everything in the right way,
  429. * check if the Property is 'nested' or must be rendered in a metadata tag
  430. */
  431. switch (static::getExpectedDisplayType($this->fallbackType, $this->fallbackProperty)) {
  432. case 'meta':
  433. // Check if a $content is available, otherwise fallback to an 'inline' display Type
  434. if ($this->content !== null) {
  435. $html = $this->machineContent ?? $this->content;
  436. $html = static::htmlMeta($html, $this->fallbackProperty, $this->fallbackType);
  437. } else {
  438. $html = static::htmlScope($this->fallbackType) . ' ' . static::htmlProperty($this->fallbackProperty);
  439. }
  440. break;
  441. default:
  442. /*
  443. * Default expected display type = 'normal'
  444. * Check if a $content is available,
  445. * otherwise fallback to an 'inline' display Type
  446. */
  447. if ($this->content !== null) {
  448. $html = static::htmlSpan($this->content, $this->fallbackProperty);
  449. $html = static::htmlSpan($html, '', $this->fallbackType);
  450. } else {
  451. $html = static::htmlScope($this->fallbackType) . ' ' . static::htmlProperty($this->fallbackProperty);
  452. }
  453. break;
  454. }
  455. }
  456. } elseif (!$this->fallbackProperty && $this->fallbackType !== null) {
  457. $html = static::htmlScope($this->fallbackType);
  458. }
  459. // Reset params
  460. $this->resetParams();
  461. return $html;
  462. }
  463. /**
  464. * Return the HTML of the current Scope
  465. *
  466. * @return string
  467. *
  468. * @since 3.2
  469. */
  470. public function displayScope()
  471. {
  472. // Control if the library output is enabled, otherwise return the $content or empty string
  473. if (!$this->enabled) {
  474. return '';
  475. }
  476. return static::htmlScope($this->type);
  477. }
  478. /**
  479. * Return the sanitized $Type
  480. *
  481. * @param string $type The Type to sanitize
  482. *
  483. * @return string
  484. *
  485. * @since 3.2
  486. */
  487. public static function sanitizeType($type)
  488. {
  489. return ucfirst(trim($type));
  490. }
  491. /**
  492. * Return the sanitized $Property
  493. *
  494. * @param string $property The Property to sanitize
  495. *
  496. * @return string
  497. *
  498. * @since 3.2
  499. */
  500. public static function sanitizeProperty($property)
  501. {
  502. return lcfirst(trim($property));
  503. }
  504. /**
  505. * Return an array with all available Types and Properties from the http://schema.org vocabulary
  506. *
  507. * @return array
  508. *
  509. * @since 3.2
  510. */
  511. public static function getTypes()
  512. {
  513. static::loadTypes();
  514. return static::$types;
  515. }
  516. /**
  517. * Return an array with all available Types from the http://schema.org vocabulary
  518. *
  519. * @return array
  520. *
  521. * @since 3.2
  522. */
  523. public static function getAvailableTypes()
  524. {
  525. static::loadTypes();
  526. return array_keys(static::$types);
  527. }
  528. /**
  529. * Return the expected Types of the given Property
  530. *
  531. * @param string $type The Type to process
  532. * @param string $property The Property to process
  533. *
  534. * @return array
  535. *
  536. * @since 3.2
  537. */
  538. public static function getExpectedTypes($type, $property)
  539. {
  540. static::loadTypes();
  541. $tmp = static::$types[$type]['properties'];
  542. // Check if the $Property is in the $Type
  543. if (isset($tmp[$property])) {
  544. return $tmp[$property]['expectedTypes'];
  545. }
  546. // Check if the $Property is inherit
  547. $extendedType = static::$types[$type]['extends'];
  548. // Recursive
  549. if (!empty($extendedType)) {
  550. return static::getExpectedTypes($extendedType, $property);
  551. }
  552. return array();
  553. }
  554. /**
  555. * Return the expected display type: [normal|nested|meta]
  556. * In which way to display the Property:
  557. * normal -> itemprop="name"
  558. * nested -> itemprop="director" itemscope itemtype="https://schema.org/Person"
  559. * meta -> `<meta itemprop="datePublished" content="1991-05-01">`
  560. *
  561. * @param string $type The Type where to find the Property
  562. * @param string $property The Property to process
  563. *
  564. * @return string
  565. *
  566. * @since 3.2
  567. */
  568. protected static function getExpectedDisplayType($type, $property)
  569. {
  570. $expectedTypes = static::getExpectedTypes($type, $property);
  571. // Retrieve the first expected type
  572. $type = $expectedTypes[0];
  573. // Check if it's a 'meta' display
  574. if ($type === 'Date' || $type === 'DateTime' || $property === 'interactionCount') {
  575. return 'meta';
  576. }
  577. // Check if it's a 'normal' display
  578. if ($type === 'Text' || $type === 'URL' || $type === 'Boolean' || $type === 'Number') {
  579. return 'normal';
  580. }
  581. // Otherwise it's a 'nested' display
  582. return 'nested';
  583. }
  584. /**
  585. * Recursive function, control if the given Type has the given Property
  586. *
  587. * @param string $type The Type where to check
  588. * @param string $property The Property to check
  589. *
  590. * @return boolean
  591. *
  592. * @since 3.2
  593. */
  594. public static function isPropertyInType($type, $property)
  595. {
  596. if (!static::isTypeAvailable($type)) {
  597. return false;
  598. }
  599. // Control if the $Property exists, and return 'true'
  600. if (\array_key_exists($property, static::$types[$type]['properties'])) {
  601. return true;
  602. }
  603. // Recursive: Check if the $Property is inherit
  604. $extendedType = static::$types[$type]['extends'];
  605. if (!empty($extendedType)) {
  606. return static::isPropertyInType($extendedType, $property);
  607. }
  608. return false;
  609. }
  610. /**
  611. * Control if the given Type class is available
  612. *
  613. * @param string $type The Type to check
  614. *
  615. * @return boolean
  616. *
  617. * @since 3.2
  618. */
  619. public static function isTypeAvailable($type)
  620. {
  621. static::loadTypes();
  622. return \array_key_exists($type, static::$types);
  623. }
  624. /**
  625. * Return Microdata semantics in a `<meta>` tag with content for machines.
  626. *
  627. * @param string $content The machine content to display
  628. * @param string $property The Property
  629. * @param string $scope Optional, the Type scope to display
  630. * @param boolean $invert Optional, default = false, invert the $scope with the $property
  631. *
  632. * @return string
  633. *
  634. * @since 3.2
  635. */
  636. public static function htmlMeta($content, $property, $scope = '', $invert = false)
  637. {
  638. return static::htmlTag('meta', $content, $property, $scope, $invert);
  639. }
  640. /**
  641. * Return Microdata semantics in a `<span>` tag.
  642. *
  643. * @param string $content The human content
  644. * @param string $property Optional, the human content to display
  645. * @param string $scope Optional, the Type scope to display
  646. * @param boolean $invert Optional, default = false, invert the $scope with the $property
  647. *
  648. * @return string
  649. *
  650. * @since 3.2
  651. */
  652. public static function htmlSpan($content, $property = '', $scope = '', $invert = false)
  653. {
  654. return static::htmlTag('span', $content, $property, $scope, $invert);
  655. }
  656. /**
  657. * Return Microdata semantics in a `<div>` tag.
  658. *
  659. * @param string $content The human content
  660. * @param string $property Optional, the human content to display
  661. * @param string $scope Optional, the Type scope to display
  662. * @param boolean $invert Optional, default = false, invert the $scope with the $property
  663. *
  664. * @return string
  665. *
  666. * @since 3.2
  667. */
  668. public static function htmlDiv($content, $property = '', $scope = '', $invert = false)
  669. {
  670. return static::htmlTag('div', $content, $property, $scope, $invert);
  671. }
  672. /**
  673. * Return Microdata semantics in a specified tag.
  674. *
  675. * @param string $tag The HTML tag
  676. * @param string $content The human content
  677. * @param string $property Optional, the human content to display
  678. * @param string $scope Optional, the Type scope to display
  679. * @param boolean $invert Optional, default = false, invert the $scope with the $property
  680. *
  681. * @return string
  682. *
  683. * @since 3.3
  684. */
  685. public static function htmlTag($tag, $content, $property = '', $scope = '', $invert = false)
  686. {
  687. // Control if the $Property has already the 'itemprop' prefix
  688. if (!empty($property) && stripos($property, 'itemprop') !== 0) {
  689. $property = static::htmlProperty($property);
  690. }
  691. // Control if the $Scope have already the 'itemscope' prefix
  692. if (!empty($scope) && stripos($scope, 'itemscope') !== 0) {
  693. $scope = static::htmlScope($scope);
  694. }
  695. // Depending on the case, the $scope must precede the $property, or otherwise
  696. if ($invert) {
  697. $tmp = implode(' ', array($property, $scope));
  698. } else {
  699. $tmp = implode(' ', array($scope, $property));
  700. }
  701. $tmp = trim($tmp);
  702. $tmp = ($tmp) ? ' ' . $tmp : '';
  703. // Control if it is an empty element without a closing tag
  704. if ($tag === 'meta') {
  705. return "<meta$tmp content='$content'>";
  706. }
  707. return '<' . $tag . $tmp . '>' . $content . '</' . $tag . '>';
  708. }
  709. /**
  710. * Return the HTML Scope
  711. *
  712. * @param string $scope The Scope to process
  713. *
  714. * @return string
  715. *
  716. * @since 3.2
  717. */
  718. public static function htmlScope($scope)
  719. {
  720. return "itemscope itemtype='https://schema.org/" . static::sanitizeType($scope) . "'";
  721. }
  722. /**
  723. * Return the HTML Property
  724. *
  725. * @param string $property The Property to process
  726. *
  727. * @return string
  728. *
  729. * @since 3.2
  730. */
  731. public static function htmlProperty($property)
  732. {
  733. return "itemprop='$property'";
  734. }
  735. }