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