| Current Path : /proc/1908984/root/proc/2603263/cwd/ |
| Current File : //proc/1908984/root/proc/2603263/cwd/GSD.tar |
Helper/JReviews.php 0000644 00000001554 15237362756 0010257 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Helper;
defined('_JEXEC') or die('Restricted access');
use Joomla\CMS\Factory;
class JReviews
{
public static function getListing($id)
{
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from('#__jreviews_content')
->where('contentid = ' . $db->q($id));
$db->setQuery($query);
return $db->loadAssoc();
} catch (\Exception $exception) {}
return null;
}
} Schemas/Schemas/Article.php 0000644 00000001631 15237362756 0011627 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use GSD\Helper;
class Article extends \GSD\Schemas\Base
{
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
$props = [
'publisherName' => $this->data->get('publisher_name', Helper::getSiteName()),
'publisherLogo' => Helper::cleanImage(Helper::absURL($this->data->get('publisher_logo', Helper::getSiteLogo())))
];
$this->data->loadArray($props);
parent::initProps();
}
} Schemas/Schemas/Book.php 0000644 00000000706 15237362756 0011140 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
class Book extends \GSD\Schemas\Base
{} Schemas/Schemas/Course.php 0000644 00000004401 15237362756 0011502 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use GSD\Helper;
use Joomla\String\StringHelper;
class Course extends \GSD\Schemas\Base
{
/**
* A key => value array with schema properties that needs to be renamed.
*
* The left value represents the name of the property as defined in the schema's XML file.
* The right value represents the name of the property as it's expected in JSON class.
*
* @Todo - We should rename all properties directly in each schema XML file and then get rid of this property.
*
* @var array
*/
protected $rename_properties = [
'country' => 'addressCountry',
'address' => 'streetAddress',
'locality' => 'addressLocality',
'region' => 'addressRegion',
'postal_code' => 'postalCode',
'start_date' => 'startDate',
'end_date' => 'endDate'
];
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
$props = [
'courseWorkload' => Helper::convert_to_ISO8601($this->data->get('courseWorkload'), 'H'),
'validFrom' => Helper::date($this->data->get('validFrom'), true),
'start_date' => Helper::date($this->data->get('start_date'), true),
'end_date' => Helper::date($this->data->get('end_date'), true),
];
$this->data->loadArray($props);
parent::initProps();
}
/**
* Beyond the default housekeeping, limit the characters in the headline property to 110 in order to comply with Google's guidelines.
*
* Reference: https://developers.google.com/search/docs/appearance/structured-data/course
*
* @return void
*/
protected function cleanProps()
{
parent::cleanProps();
$this->data->set('description', StringHelper::substr($this->data->get('description'), 0, 500));
}
} Schemas/Schemas/Custom_Code.php 0000644 00000004240 15237362756 0012447 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use Joomla\Registry\Registry;
class Custom_Code extends \GSD\Schemas\Base
{
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
parent::initProps();
// Since v5.3.1, the SchemaCleaner supports removing structured data also from the <head> that does not have the data-type="gsd" property.
// In order to prevent the user defined custom code from being removed, we need to add the data-type property to every custom JSON+LD script.
$safe_custom_code = str_replace('<script type="application', '<script data-type="gsd" type="application', $this->data->get('custom_code'));
$this->data->set('custom_code', $safe_custom_code);
}
/**
* Since in the Custom Code we do not have a real array but a string, the result is not passed into the cleanProps() method, thus, we may end up
* with unescaped characters and HTML that can break the structured data. With this override, we filter all payload props before they get replaced in the snippet.
*
* Consider this as a temporary workaround. In addition, it's worth consideration to filter payload props on all Schemas by default, so we don't need to do so later.
*
* @param object $payload
*
* @return void
*/
public function onPayloadPrepare(&$payload)
{
$props = $payload->toArray();
array_walk_recursive($props, function(&$prop)
{
if (!is_null($prop)) // Make PHP 8.1 happy.
{
$this->cleanProp($prop);
}
});
$payload = new Registry($props);
}
/**
* Do not clean custom script
*
* @return void
*/
protected function cleanProps()
{
}
} Schemas/Schemas/Event.php 0000644 00000004365 15237362756 0011334 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use GSD\Helper;
class Event extends \GSD\Schemas\Base
{
/**
* A key => value array with schema properties that needs to be renamed.
*
* The left value represents the name of the property as defined in the schema's XML file.
* The right value represents the name of the property as it's expected in JSON class.
*
* @Todo - We should rename all properties directly in each schema XML file and then get rid of this property.
*
* @var array
*/
protected $rename_properties = [
'locationAddress' => 'streetAddress'
];
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
$props = [
'startDate' => Helper::date($this->data['startDate'], true),
'endDate' => Helper::date($this->data['endDate'], true),
'startDateTime' => Helper::date($this->data['offerStartDate'], true),
'price' => $this->getPrice()
];
$this->data->loadArray($props);
parent::initProps();
}
/**
* Detect price range or single price.
*
* @return mixed
*/
private function getPrice()
{
$price = $this->data->get('offerPrice');
// The offerPrice should not be included in the structured data only when it's disabled. The price of '0.00' should be still displayed in the structured data.
if ($price === false)
{
return;
}
if (is_scalar($price) && strpos($price, '-') !== false)
{
$price = explode('-', $price, 2);
}
if (is_array($price))
{
return [
Helper::formatPrice($price[0]),
Helper::formatPrice($price[1])
];
}
return Helper::formatPrice($price);
}
} Schemas/Schemas/FAQ.php 0000644 00000012335 15237362756 0010656 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use GSD\Helper;
use NRFramework\DOMCrawler;
class FAQ extends \GSD\Schemas\Base
{
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
$mode = $this->data->get('mode', 'auto');
$faq = $this->data['faq_repeater_fields'];
$allowed_tags = '<h1><h2><h3><h4><h5><h6><br><ol><ul><li><p><a><div><b><strong><i><em>';
$faqData = [];
switch ($mode)
{
// Manual Mode
case 'manual':
foreach ($faq as $item)
{
$question = trim($item->question);
$question = preg_replace('/\s\s+/', ' ', $question);
$question = strip_tags($question);
$answer = trim($item->answer);
$answer = strip_tags($answer, $allowed_tags);
$faqData[] = [
'question' => $question,
'answer' => $answer
];
}
break;
// Auto Mode
case 'auto':
$question_selector = $this->data->get('question_selector', '.question');
$answer_selector = $this->data->get('answer_selector', '.answer');
// Find questions and answersr
$questions = $this->crawl($question_selector);
$answers = $this->crawl($answer_selector);
// Combine the Q&A
if (count($questions) && count($answers))
{
$counter = 0;
foreach ($questions as $q)
{
$question = trim($q['value']);
$answer = isset($answers[$counter]['html']) ? $answers[$counter]['html'] : '';
// Remove spaces, new lines, invalid HTML tags and empty paragraphs.
$answer = preg_replace('/\s\s+/', ' ', $answer);
$answer = strip_tags($answer, $allowed_tags);
$answer = preg_replace('/<p>\s*<\/p>/', '', $answer);
$answer = trim($answer);
$faqData[] = [
'question' => $question,
'answer' => $answer
];
$counter++;
}
} else
{
Helper::log([
'Error' => 'No FAQs found',
'Question Selector' => $question_selector,
'Questions Found' => count($questions),
'Answer Selector' => $answer_selector,
'Answers Found' => count($answers)
]);
}
}
$this->data->set('faqs', $faqData);
parent::initProps();
}
/**
* Find the FAQ content using XPath based on the provided selector
*
* @param string $content The content to search
* @param string $selector The selector used for the search
*
* @return array
*/
private function crawl($selector)
{
$data = [];
$crawler = new DOMCrawler();
if ($nodes = $crawler->filter($selector)->nodes)
{
foreach ($nodes as $node)
{
// Remove attributes from node
$html = $this->removeAttributesFromNode($node);
$data[] = [
'html' => $html,
'value' => $node->nodeValue
];
}
}
return $data;
}
/**
* Loop through all elements in the node that have an attribute
* and remove them
*
* @param DOMElement $domNode
*
* @return string
*/
private function removeAttributesFromNode($domNode)
{
// Loop through all elements that have an attribute and remove it
$dom = new \DOMDocument;
$content = $domNode->ownerDocument->saveHTML($domNode);
$dom->loadHTML(self::stringToUTF8($content));
$xpath = new \DOMXPath($dom);
$nodes = $xpath->query('//@*');
foreach ($nodes as $node)
{
// Skip the href attribute which is allowed
if ($node->nodeName == 'href')
{
// Fix relative URLs.
$url = Helper::absURL($node->nodeValue);
$node->parentNode->setAttribute('href', $url);
continue;
}
$node->parentNode->removeAttribute($node->nodeName);
}
// return inner child only
$html = '';
foreach($dom->getElementsByTagName('body')->item(0)->firstChild->childNodes as $node) {
$html .= $dom->saveHTML($node);
}
return $html;
}
/**
* Convert a string to UTF8 encoding
*
* @param string
*
* @return string
*/
private function stringToUTF8($string)
{
if (!function_exists('mb_convert_encoding'))
{
return $string;
}
return mb_encode_numericentity(iconv('UTF-8', 'UTF-8', $string), [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8');
}
} Schemas/Schemas/FactCheck.php 0000644 00000004375 15237362756 0012067 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use GSD\Helper;
use Joomla\String\StringHelper;
class FactCheck extends \GSD\Schemas\Base
{
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
parent::initProps();
switch ($this->data['factcheckRating'])
{
// there is no textual representation for zero (0)
case '1':
$textRating = 'False';
break;
case '2':
$textRating = 'Mostly false';
break;
case '3':
$textRating = 'Half true';
break;
case '4':
$textRating = 'Mostly true';
break;
case '5':
$textRating = 'True';
break;
default:
$textRating = 'Hard to categorize';
}
$props = [
'claimDatePublished' => Helper::date($this->data['claimDatePublished'], true),
'factcheckURL' => $this->data['multiple'] ? $this->data['url'] . $this->data['anchorName'] : $this->data['url'],
'bestFactcheckRating' => $this->data['factcheckRating'] != '-1' ? '5' : '-1',
'worstFactcheckRating' => $this->data['factcheckRating'] != '-1' ? '1' : '-1',
'alternateName' => $textRating
];
$this->data->loadArray($props);
}
/**
* Beyond the default housekeeping, limit the characters in the headline property to 110 in order to comply with Google's guidelines.
*
* Reference: https://developers.google.com/search/docs/appearance/structured-data/factcheck
*
* @return void
*/
protected function cleanProps()
{
parent::cleanProps();
$this->data->set('title', StringHelper::substr($this->data->get('title'), 0, 75));
}
} Schemas/Schemas/HowTo.php 0000644 00000005607 15237362756 0011313 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2023 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use GSD\Helper;
use GSD\Schemas\Base;
use NRFramework\DOMCrawler;
class HowTo extends Base
{
/**
* The HTML tags allowed to be used in certain schema properties, such as the headline and the description.
*
* @var mixed
*/
protected $allowed_HTML_tags = '<p><br><ul><li><strong><em><b>';
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
$this->data->set('totalTime', $this->data['totalTime'] ? 'PT' . $this->data['totalTime'] . 'M' : null);
$steps = [];
switch ($this->data->get('mode', 'auto'))
{
case 'manual':
$steps = array_values(json_decode(json_encode($this->data['howto_repeater']), true));
break;
// Auto Mode
case 'auto':
$crawler = new DOMCrawler();
$names = $crawler->readCSSSelectorField($this->data->get('name_selector'), false);
$texts = $crawler->readCSSSelectorField($this->data->get('text_selector'), false);
$images = $crawler->readCSSSelectorField($this->data->get('image_selector'), false);
$urls = $crawler->readCSSSelectorField($this->data->get('url_selector'), false);
$steps = array_map(function($name, $text, $image, $url)
{
return [
'name' => $name,
'text' => $text,
'image' => $image,
'url' => $url
];
}, $names, $texts, $images, $urls);
}
// Prepare steps
$steps = array_map(function($step)
{
return [
'name' => isset($step['name']) ? $step['name'] : '',
'text' => isset($step['text']) ? $step['text'] : '',
'image' => isset($step['image']) ? Helper::absURL($step['image']) : '',
'url' => isset($step['url']) ? Helper::absURL($step['url']) : ''
];
}, $steps);
$this->data->set('step', $steps);
parent::initProps();
}
/**
* Convert a string to UTF8 encoding
*
* @param string
*
* @return string
*/
private function stringToUTF8($string)
{
if (!function_exists('mb_convert_encoding'))
{
return $string;
}
return mb_encode_numericentity(iconv('UTF-8', 'UTF-8', $string), [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8');
}
} Schemas/Schemas/JobPosting.php 0000644 00000003620 15237362756 0012322 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use GSD\Helper;
class JobPosting extends \GSD\Schemas\Base
{
/**
* A key => value array with schema properties that needs to be renamed.
*
* The left value represents the name of the property as defined in the schema's XML file.
* The right value represents the name of the property as it's expected in JSON class.
*
* @Todo - We should rename all properties directly in each schema XML file and then get rid of this property.
*
* @var array
*/
protected $rename_properties = [
'locality' => 'addressLocality',
'region' => 'addressRegion',
'postal_code' => 'postalCode'
];
/**
* The HTML tags allowed to be used in certain schema properties, such as the headline and the description.
*
* @var mixed
*/
protected $allowed_HTML_tags = '<p><br><ul><li>';
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
$props = [
'hiring_organization_logo' => Helper::cleanImage(Helper::absURL($this->data['hiring_organization_logo'])),
'valid_through' => Helper::date($this->data['valid_through'], true),
'salary' => $this->data['salary'] ? (strpos($this->data['salary'], '-') === false ? Helper::formatPrice($this->data['salary']) : explode('-', $this->data['salary'])) : '',
];
$this->data->loadArray($props);
parent::initProps();
}
} Schemas/Schemas/LocalBusiness.php 0000644 00000001636 15237362756 0013017 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use GSD\Helper;
class LocalBusiness extends \GSD\Schemas\Base
{
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
$props = [
'name' => $this->data->get('name', Helper::getSiteName()),
'geo' => array_map('trim', explode(',', $this->data->get('geo', ''), 2)),
'review' => $this->data->get('reviews')
];
$this->data->loadArray($props);
parent::initProps();
}
} Schemas/Schemas/Movie.php 0000644 00000003105 15237362756 0011321 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use GSD\Helper;
class Movie extends \GSD\Schemas\Base
{
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
$props = [
'genre' => $this->readRepeatableValue('genre'),
'creators' => $this->readRepeatableValue('creators'),
'directors' => $this->readRepeatableValue('directors'),
'actors' => $this->readRepeatableValue('actors'),
'duration' => !empty($this->data['duration']) ? 'PT' . $this->data['duration'] . 'M' : null,
'review' => $this->data['reviews'],
];
$this->data->loadArray($props);
parent::initProps();
}
private function readRepeatableValue($prop)
{
$items = $this->data->get($prop, '');
$found = [];
if (!empty($items) && is_string($items))
{
$items = explode(',', $items);
foreach ($items as $item)
{
$found[] = (object) [
'name' => $item
];
}
} else
{
$found = $items;
}
return $found;
}
} Schemas/Schemas/Person.php 0000644 00000000711 15237362756 0011510 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
class Person extends \GSD\Schemas\Base
{
} Schemas/Schemas/Product.php 0000644 00000004067 15237362756 0011672 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use GSD\Helper;
use NRFramework\Functions;
class Product extends \GSD\Schemas\Base
{
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
$props = [
'offerPrice' => $this->getPrice(),
'priceValidUntil' => Helper::date($this->data->get('priceValidUntil', '2100-12-31T10:00:00')),
'weight' => $this->data->get('weight'),
'weightUnit' => $this->data->get('weight_unit'),
'brand' => $this->data->get('brand', Helper::getSiteName()),
'gtin' => $this->data->get('gtin'),
// Fallback to 'sku' property to prevent structured data warning.
'mpn' => $this->data->get('mpn', $this->data->get('sku')),
];
$this->data->loadArray($props);
parent::initProps();
}
/**
* Detect price range or single price.
*
* @return mixed
*/
private function getPrice()
{
$price = $this->data->get('offerPrice');
// The offerPrice should not be included in the structured data only when it's disabled. The price of '0.00' should be still displayed in the structured data.
if ($price === false)
{
return;
}
if (is_scalar($price) && strpos($price, '-') !== false)
{
$price = explode('-', $price, 2);
}
if (is_array($price))
{
return [
Helper::formatPrice($price[0]),
Helper::formatPrice($price[1])
];
}
return Helper::formatPrice($price);
}
} Schemas/Schemas/Recipe.php 0000644 00000002375 15237362756 0011461 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use GSD\Helper;
class Recipe extends \GSD\Schemas\Base
{
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
$props = [
'prepTime' => $this->data['prepTime'] ? 'PT' . $this->data['prepTime'] . 'M' : null,
'cookTime' => $this->data['cookTime'] ? 'PT' . $this->data['cookTime'] . 'M' : null,
'totalTime' => $this->data['totalTime'] ? 'PT' . $this->data['totalTime'] . 'M' : null,
'ingredient' => Helper::makeArrayFromNewLine(strip_tags($this->data['ingredient'] ? $this->data['ingredient'] : '')),
'instructions' => Helper::makeArrayFromNewLine(strip_tags($this->data['instructions'] ? $this->data['instructions'] : ''))
];
$this->data->loadArray($props);
parent::initProps();
}
} Schemas/Schemas/Review.php 0000644 00000007050 15237362756 0011506 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use GSD\Helper;
use NRFramework\Functions;
use Joomla\CMS\Factory;
class Review extends \GSD\Schemas\Base
{
/**
* A key => value array with schema properties that needs to be renamed.
*
* The left value represents the name of the property as defined in the schema's XML file.
* The right value represents the name of the property as it's expected in JSON class.
*
* @Todo - We should rename all properties directly in each schema XML file and then get rid of this property.
*
* @var array
*/
protected $rename_properties = [
'address' => 'streetAddress'
];
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
// We need a better and more dynamic way to handle Repeatable Field values.
// We can move this block to MappingOptions somehow.
$actors = $this->data->get('actors', '');
$actors_ = [];
if (!empty($actors) && is_string($actors))
{
$actors = explode(',', $actors);
foreach ($actors as $actor)
{
$actors_[] = (object)[
'name' => $actor
];
}
} else
{
$actors_ = $actors;
}
$props = [
'itemReviewedPublishedDate' => $this->data['item_reviewed_published_date'],
'movie_director' => $this->data['item_reviewed_movie_director'],
'product_sku' => $this->data['item_reviewed_product_sku'],
'product_brand' => $this->data['item_reviewed_product_brand'],
'product_description' => $this->data['item_reviewed_product_description'],
'currency' => $this->data['item_reviewed_product_currency'],
'condition' => $this->data['item_reviewed_product_offeritemcondition'],
'availability' => $this->data['item_reviewed_product_offeravailability'],
'offerprice' => $this->data['item_reviewed_product_offerprice'],
'pricevaliduntil' => $this->data['item_reviewed_product_pricevaliduntil'],
'book_author' => $this->data['item_reviewed_book_author'],
'book_author_url' => $this->data['item_reviewed_book_author_url'],
'book_isbn' => $this->data['item_reviewed_book_isbn'],
'review' => $this->data['reviews'],
'actors' => $actors_,
'language_code' => explode('-', Factory::getLanguage()->getTag())[0]
];
$this->data->loadArray($props);
parent::initProps();
}
/**
* This method runs everytime a structured data item is saved in the backend.
*
* @param array $data The data to be stored in the database
*
* @return void
*/
public function onSave(&$data)
{
parent::onSave($data);
if ($data['item_reviewed_product_pricevaliduntil']['option'] == 'fixed')
{
$data['item_reviewed_product_pricevaliduntil']['fixed'] = Functions::dateToUTC($data['item_reviewed_product_pricevaliduntil']['fixed']);
}
}
} Schemas/Schemas/Service.php 0000644 00000003641 15237362756 0011647 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use Joomla\CMS\Uri\Uri;
use GSD\Helper;
class Service extends \GSD\Schemas\Base
{
/**
* A key => value array with schema properties that needs to be renamed.
*
* The left value represents the name of the property as defined in the schema's XML file.
* The right value represents the name of the property as it's expected in JSON class.
*
* @Todo - We should rename all properties directly in each schema XML file and then get rid of this property.
*
* @var array
*/
protected $rename_properties = [
'provider_country' => 'addressCountry',
'provider_streetAddress' => 'streetAddress',
'provider_city' => 'addressLocality',
'provider_addressRegion' => 'addressRegion',
'provider_postalCode' => 'postalCode'
];
/**
* The HTML tags allowed to be used in certain schema properties, such as the headline and the description.
*
* @var string
*/
protected $allowed_HTML_tags = '<p><br><ul><li><h1><h2><h3><h4><h5><strong><em><b>';
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
$props = [
'offerPrice' => Helper::formatPrice($this->data['offerPrice']),
'provider_image' => Helper::cleanImage($this->data['provider_image']),
'phone' => $this->data['provider_phone'],
];
$this->data->loadArray($props);
parent::initProps();
}
} Schemas/Schemas/Video.php 0000644 00000002664 15237362756 0011321 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas\Schemas;
// No direct access
defined('_JEXEC') or die;
use GSD\Helper;
class Video extends \GSD\Schemas\Base
{
/**
* A key => value array with schema properties that needs to be renamed.
*
* The left value represents the name of the property as defined in the schema's XML file.
* The right value represents the name of the property as it's expected in JSON class.
*
* @Todo - We should rename all properties directly in each schema XML file and then get rid of this property.
*
* @var array
*/
protected $rename_properties = [
'headline' => 'name',
'image' => 'thumbnailUrl',
'publish_up' => 'uploadDate'
];
/**
* Return all the schema properties
*
* @return void
*/
protected function initProps()
{
$props = [
'publish_up' => Helper::date($this->data->get('publish_up'), true),
'image' => Helper::cleanImage(Helper::absURL($this->data->get('image')))
];
$this->data->loadArray($props);
parent::initProps();
}
} Schemas/Base.php 0000644 00000024272 15237362756 0007541 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas;
// No direct access
defined('_JEXEC') or die;
use GSD\Helper;
use Joomla\Registry\Registry;
use NRFramework\Functions;
use NRFramework\Cache;
use GSD\MappingOptions;
use Joomla\CMS\Uri\Uri;
class Base
{
/**
* The schema properties
*
* @var object
*/
protected $data;
/**
* The HTML tags allowed to be used in certain schema properties, such as the headline and the description.
*
* @var mixed
*/
protected $allowed_HTML_tags = null;
/**
* A key => value array with schema properties that needs to be renamed.
*
* The left value represents the name of the property as defined in the schema's XML file.
* The right value represents the name of the property as it's expected in JSON class.
*
* @Todo - We should rename all properties directly in each schema XML file and then get rid of this property.
*
* @var array
*/
protected $rename_properties;
/**
* Class constructor
*
* @param Registry $data The schema properties
*/
public function __construct($data = null)
{
$this->setData($data);
}
/**
* Return all schema properties
*
* @return Registry
*/
public function get()
{
$this->initProps();
$this->cleanProps();
return $this->data;
}
public function setData($data)
{
$this->data = $data;
return $this;
}
/**
* Run a housekeeping on each property. Remove unwanted HTML tags and whitespace and encode remaining HTML.
*
* @return void
*/
protected function cleanProps()
{
$props = $this->data->toArray();
array_walk_recursive($props, function(&$prop)
{
if (!is_null($prop)) // Make PHP 8.1 happy.
{
$this->cleanProp($prop);
}
});
$this->data = new Registry($props);
}
/**
* Make text safe to be used in a JSON-LD script
*
* @param text $prop The text to clean
*
* @return void
*/
protected function cleanProp(&$prop)
{
// Remove all <script> tags and their content
$prop = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $prop);
// Remove invalid HTML tags
$prop = strip_tags($prop, $this->allowed_HTML_tags);
// Convert remaining HTML tags into HTML entities to prevent structured data errors.
$prop = htmlspecialchars($prop, ENT_QUOTES, 'UTF-8');
// Remove whitespace
$prop = preg_replace('/(\s)+/s', ' ', $prop);
// Remove whitespace from the beginning and end of the prop
$prop = trim($prop);
}
/**
* Prepare common schema properties.
*
* - Rename properties
* - Add timezone offset and format dates to ISO8601
* - Strip HTML tags from certain properties
* - Convert relative paths to absolute URLs
*
* @return void
*/
protected function initProps()
{
$this->renameProperties();
$this->fixMultivalueProperties();
$this->fixPriceRangeProperties();
// Fix dates in the Reviews property. Used in schemas: Product, Movie, Local Business
if ($reviews = $this->data->get('reviews'))
{
foreach ($reviews as &$review)
{
if (!isset($review['datePublished']))
{
continue;
}
// Convert date to ISO8601
$review['datePublished'] = Helper::date($review['datePublished'], true);
}
$this->data->set('reviews', $reviews);
}
// Common properties
$props = [
'contentType' => $this->getName(),
// Make sure the @id property is unique, to prevent structured data awkwardly merged by the Google Structured Data Testing Tool
'id' => Uri::current() . '#' . $this->getName() . $this->data['snippet_id'],
'title' => $this->data['headline'],
'description' => $this->data['description'],
'image' => Helper::cleanImage(Helper::absURL($this->data->get('image'))),
// Author / Publisher
'authorType' => 'Person',
'authorName' => $this->data['author'],
'authorUrl' => isset($this->data['authorUrl']) ? $this->data['authorUrl'] : Uri::current(),
// Rating
'ratingValue' => $this->data['rating_value'],
'reviewCount' => $this->data['review_count'],
'bestRating' => $this->data['bestRating'],
'worstRating' => $this->data['worstRating'],
// Dates
'datePublished' => Helper::date($this->data['publish_up'], true),
'dateCreated' => Helper::date($this->data['created'], true),
'dateModified' => Helper::date($this->data['modified'], true),
// Site based
'url' => Uri::current(),
'siteurl' => Helper::getSiteURL(),
'sitename' => Helper::getSiteName(),
];
$this->data->merge(new Registry($props));
}
/**
* Some schema properties are declared with the wrong name in Schema XML files. With this method, we attemp to rename those properties with the proper name expected by the JSON class.
*
* @todo Rename all properties in XML files and create a migration script that will update users database. Then, we can get get rid of this method.
*
* @return void
*/
private function renameProperties()
{
if (!$this->rename_properties)
{
return;
}
foreach ($this->rename_properties as $old_property_name => $new_property_name)
{
if (!isset($this->data[$old_property_name]))
{
continue;
}
$this->data[$new_property_name] = $this->data[$old_property_name];
// Remove old property as we no longer need it.
unset($this->data[$old_property_name]);
}
}
/**
* Return the name of this schema type
*
* @return string
*/
private function getName()
{
$reflect = new \ReflectionClass($this);
return strtolower($reflect->getShortName());
}
// Temporary workaround. See comments in the Custom_Code class.
public function onPayloadPrepare(&$payload) {}
/**
* This method runs everytime a structured data item is saved in the backend.
*
* @param array $data The data to be stored in the database
*
* @return void
*/
public function onSave(&$data)
{
if (!$data)
{
return;
}
foreach ($data as $optionKey => &$optionValue)
{
$commonDateFieldNames = [
'publish_up',
'modified',
'created',
'valid_through',
'validFrom',
'priceValidUntil'
];
// Skip certain field names
if (in_array($optionKey, ['birthDate']))
{
continue;
}
// Find date fields by their name.
if (strpos(strtolower($optionKey), 'date') === false && !in_array($optionKey, $commonDateFieldNames))
{
continue;
}
// Only when the mapping option is using the "Fixed Dates" option
// The "Custom Option" is ignored as it may include a shortcode or some other formatted value.
if ($optionValue['option'] !== 'fixed' || empty($optionValue['fixed']))
{
continue;
}
$optionValue['fixed'] = Functions::dateToUTC($optionValue['fixed']);
}
}
/**
* Finds all properties that accept multiple values per line and convert the string into an array.
*
* @return void
*/
private function fixPriceRangeProperties()
{
foreach ($this->getXMLFields() as $key => $field)
{
if (!isset($field['real_type']) || $field['real_type'] !== 'pricerange')
{
continue;
}
if (!$currentValue = $this->data->get($key))
{
continue;
}
$newValue = explode('-', $currentValue, 2);
if (count($newValue) == 1)
{
$newValue = [$currentValue, $currentValue];
}
$this->data->set($key, $newValue);
}
}
/**
* Finds all properties that accept multiple values per line and convert the string into an array.
*
* @return void
*/
private function fixMultivalueProperties()
{
foreach ($this->getXMLFields() as $key => $field)
{
if (!isset($field['custom_value_multiple']))
{
continue;
}
$newValue = Helper::makeArrayFromNewLine($this->data->get($key));
if (!$newValue || count($newValue) == 1)
{
continue;
}
$this->data->set($key, $newValue);
}
}
/**
* Returns a list of all schema properties declared in the XML file
*
* @return array
*/
private function getXMLFields()
{
$hash = md5('xmlFields' . $this->getName());
if (Cache::has($hash))
{
return Cache::get($hash);
}
$xmlItems = simplexml_load_file(JPATH_ADMINISTRATOR . '/components/com_gsd/models/forms/contenttypes/' . $this->getName() . '.xml');
$fields = [];
foreach ($xmlItems->fieldset->fields->field as $field)
{
$field = (array) $field;
$field = $field["@attributes"];
$fields[$field['name']] = $field;
}
return Cache::set($hash, $fields);
}
} Schemas/Helper.php 0000644 00000002743 15237362756 0010105 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2022 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD\Schemas;
// No direct access
defined('_JEXEC') or die;
use Joomla\Filesystem\Folder;
class Helper
{
/**
* Bootup a schema type class instance
*
* @param string $type The name of the schema, eg: article, product
* @param Registry $data The schema properties
*
* @return object
*/
public static function getInstance($type, $data = null)
{
$classPath = '\\GSD\\Schemas\\Schemas\\';
$type = strtolower($type);
// Try to find the class using the given name
$className = $classPath . ucfirst($type);
if (!class_exists($className))
{
// Try to find the class by searching all files in the file system
$files = Folder::files(__DIR__ . '/Schemas');
foreach ($files as $file)
{
$fileStripExt = str_replace('.php', '', $file);
if (strtolower($fileStripExt) == $type)
{
$className = $classPath . $fileStripExt;
break;
}
}
}
return new $className($data);
}
} Apps.php 0000644 00000002324 15237362756 0006201 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Language\Text;
defined('_JEXEC') or die('Restricted access');
class Apps
{
public static function getApp($name, $data = null)
{
if (!$plugin = PluginHelper::getPlugin('gsd', $name))
{
throw new \RuntimeException(Text::sprintf('GSD_PLUGIN_NOT_FOUND', $name));
}
// On Joomla 4, use bootPlugin()
if (defined('nrJ4'))
{
$app = Factory::getApplication()->bootPlugin($plugin->name, $plugin->type);
} else
{
// On Joomla 3, use the old classic way to boot up a plugin
// TODO: Remove when J3 support is dropped
$name = 'plg' . $plugin->type . $plugin->name;
require_once JPATH_PLUGINS . '/gsd/' . $plugin->name . '/' . $plugin->name . '.php';
$dispatcher = \JEventDispatcher::getInstance();
$app = new $name($dispatcher, (array) $plugin);
}
return $app;
}
} Helper.php 0000644 00000036301 15237362756 0006517 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD;
defined('_JEXEC') or die('Restricted Access');
use GSD\Json;
use NRFramework\Cache;
use Joomla\Registry\Registry;
use Joomla\String\StringHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Table\Table;
use Joomla\CMS\Object\CMSObject;
use Joomla\CMS\Plugin\PluginHelper;
/**
*
* Google Structured Data Helper Class
*/
class Helper
{
/**
* Plugin Params
*
* @var Registry
*/
public static $params;
/**
* Log Messages
*
* @var array
*/
public static $log;
/**
* Get all available Content Types
*
* @return array
*/
public static function getContentTypes()
{
$json = new Json();
return $json->getContentTypes();
}
/**
* Returns an array with crumbs
*
* @return array
*/
public static function getCrumbs($hometext, $addhome = true)
{
$pathway = Factory::getApplication()->getPathway();
$items = $pathway->getPathWay();
$menu = Factory::getApplication()->getMenu();
$lang = Factory::getLanguage();
$count = count($items);
if (!$count)
{
return false;
}
// We don't use $items here as it references JPathway properties directly
$crumbs = [];
for ($i = 0; $i < $count; $i++)
{
// Note: In some cases, the link in the last crumb (current page) is returned empty by Joomla.
// We don't want to skip over this crumb as it represents the crumb of the current page which needs to be included in the schema.
// Thus, we skip only null link properties, so the empty crumb is included in the list.
if (is_null($items[$i]->link) || !$items[$i]->name)
{
continue;
}
$crumbName = stripslashes(htmlspecialchars(strip_tags($items[$i]->name), ENT_COMPAT, 'UTF-8'));
// Remove [icon] shortcodes added by 3rd party plugins
$crumbName = preg_replace('#\[icon\].*?\[\/icon\]#', '', $crumbName);
$crumbs[$i] = (object) [
'name' => trim($crumbName),
'link' => self::route($items[$i]->link)
];
}
// Add Home item
if ($addhome)
{
// Look for the home menu
$home = Multilanguage::isEnabled() ? $menu->getDefault($lang->getTag()) : $menu->getDefault();
$item = new \stdClass;
$item->name = htmlspecialchars($hometext);
$item->link = self::route('index.php?Itemid=' . $home->id);
array_unshift($crumbs, $item);
}
// Fix last item's missing URL to make Google Markup Tool happy
end($crumbs);
if (empty($crumbs->link))
{
$crumbs[key($crumbs)]->link = Uri::current();
}
// Convert relative URLs to absolute URLs
foreach ($crumbs as $key => &$crumb)
{
// JFilters seems to make the "link" property a \Joomla\CMS\Uri\Uri object in some cases, why?
$link = is_string($crumb->link) ? $crumb->link : ($crumb->link instanceof \Joomla\CMS\Uri\Uri ? $crumb->link->toString() : '');
if (!$link)
{
continue;
}
$crumb->link = self::absURL($crumb->link);
}
return $crumbs;
}
/**
* Makes text safe for JSON outpout
*
* @param string $text The text
* @param integer $limit Limit characters
*
* @return string
*/
public static function makeTextSafe($text, $allowed_tags = null, $limit = 0)
{
if (empty($text))
{
return;
}
// Remove <script> tags
$text = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $text);
// Strip HTML tags/comments and minify
$text = strip_tags($text, $allowed_tags);
// There are some plugins that parse their shortcodes while Joomla! is booting like on the onAfterRender event instead of the onContentPrepare event
// which GSD can control. So, if one of these shortcodes is parsed in the generated structured data, the page is very likely to broke and the only
// way to prevent that from happening is to remove these shortcodes using regex.
$text = preg_replace([
// System - Zen Shortcodes
'/{(\/?)zen-(.*?)}/m',
// System - wbAMP
'/{wbamp-(show|hide) start}(.*?){wbamp-(hide|show) end}/s'
], '', $text);
// Minify Text
$text = self::minify($text);
// Limit characters length
if ($limit > 0)
{
$text = StringHelper::substr($text, 0, $limit);
}
return trim($text);
}
/**
* Minify String
*
* @param string $string The string to be minified
*
* @return string The minified string
*/
public static function minify($string)
{
return preg_replace('/(\s)+/s', ' ', $string);
}
/**
* Returns absolute URL
*
* @param string $url The URL
*
* @return string
*/
public static function absURL($url)
{
if (!is_string($url))
{
return '';
}
$url = Uri::getInstance($url);
// Return the original URL if we're manipulating an external URL
if (in_array($url->getScheme(), ['https', 'http']))
{
return $url->toString();
}
$url = str_replace([Uri::root(), Uri::root(true)], '', $url->toString());
$url = ltrim($url, '/');
return Uri::root() . $url;
}
/**
* Returns URLs based on the Force SSL global configuration
*
* @param string $route The route for which we want a URL
* @param boolean $xhtml If we want the output to be in XHTML
*
* @return string The absolute url
*/
public static function route($route, $xhtml = true)
{
$siteSSL = Factory::getConfig()->get('force_ssl');
$sslFlag = 2;
// the force_ssl value in the global configuration needs
// to be 2 for the frontend to also be under HTTPS
if (($siteSSL == 2) || (isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'))
{
$sslFlag = 1;
}
return Route::_($route, $xhtml, $sslFlag);
}
/**
* Transform a UTC date to ISO8601 format.
*
* The timezone part describes the hours that already have been counted towards the time.
*
* Let's say we have an article published on 2012-04-10 19:31:00 local date Athens +02:00.
* The date should be displayed in the structured data as 2012-04-10T19:31:00+02:00
*
* References:
* https://developers.google.com/search/docs/data-types/event#time-date-incorrect
* https://github.com/Yoast/wordpress-seo/issues/12765
* https://wordpress.org/support/topic/structured-data-wrong-time
*
* @param Date $date
*
* @return Date
*/
public static function date($date, $modify_offset = false)
{
$date = is_string($date) ? trim($date) : $date;
if (empty($date) || is_null($date) || $date == '0000-00-00 00:00:00')
{
return null;
}
// Skip if date is already in ISO8601 format
if (strpos($date, 'T') !== false)
{
return $date;
}
try {
$tz = new \DateTimeZone(Factory::getApplication()->getCfg('offset', 'UTC'));
if ($modify_offset)
{
$date_ = new Date($date);
$date_->setTimezone($tz);
} else
{
$date_ = new Date($date, $tz);
}
return $date_->toISO8601(true);
} catch (\Exception $e) {
return $date;
}
}
/**
* Formats price according to https://schema.org/price
*
* @param mixed $price Use '.' rather than ',' to indicate a decimal point.
*
* @return float 2 Decimal point float price
*/
public static function formatPrice($price)
{
// Prevent "A non well formed numeric value encountered" error.
$price = str_replace(',', '.', (string) $price);
return number_format((float) $price, 2, '.', '');
}
/**
* Determine if the user is viewing the front page
*
* @return boolean
*/
public static function isFrontPage()
{
$menu = Factory::getApplication()->getMenu();
$lang = Factory::getLanguage()->getTag();
return ($menu->getActive() == $menu->getDefault($lang));
}
/**
* Logs messages to log file
*
* @param object $type The log type
*
* @return void
*/
public static function log($msg)
{
self::$log[] = $msg;
}
/**
* Renders Backend's Sidebar
*
* @return string The HTML output
*/
public static function renderSideBar()
{
$data = array(
'view' => Factory::getApplication()->input->get('view', 'gsd'),
'items' => array(
array(
'label' => 'NR_DASHBOARD',
'url' => 'index.php?option=com_gsd',
'icon' => 'dashboard',
'view' => 'gsd'
),
array(
'label' => 'GSD_ITEMS',
'url' => 'index.php?option=com_gsd&view=items',
'icon' => 'list',
'view' => 'items,item'
),
array(
'label' => 'GSD_CONFIG',
'url' => 'index.php?option=com_gsd&view=config&layout=edit',
'icon' => 'options',
'view' => 'config'
),
array(
'label' => 'NR_DOCUMENTATION',
'url' => 'https://www.tassos.gr/joomla-extensions/google-structured-data-markup/docs/',
'icon' => 'file-2',
'target' => 'blank'
),
array(
'label' => 'NR_SUPPORT',
'url' => 'http://www.tassos.gr/contact',
'icon' => 'help',
'target' => 'blank'
)
)
);
return LayoutHelper::render('sidebar', $data);
}
/**
* Get website name
*
* @return string Site URL
*/
public static function getSiteName()
{
return self::getParams()->get('sitename_name', Factory::getConfig()->get('sitename'));
}
/**
* Returns the Site Logo URL
*
* @return string
*/
public static function getSiteLogo()
{
if (!$logo = self::getParams()->get('logo_file', null))
{
return;
}
return Uri::root() . $logo;
}
/**
* Get website URL
*
* @return string Site URL
*/
public static function getSiteURL()
{
return self::getParams()->get('sitename_url', Uri::root());
}
/**
* Get Plugin Parameters
*
* @return Registry
*/
public static function getParams()
{
if (self::$params)
{
return self::$params;
}
Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/tables');
$table = Table::getInstance('Config', 'GSDTable');
$table->load('config');
return (self::$params = new Registry($table->params));
}
/**
* Returns permissions
*
* @return object
*/
public static function getActions()
{
$user = Factory::getUser();
$result = new CMSObject();
$assetName = 'com_gsd';
$actions = array(
'core.admin', 'core.manage', 'core.create', 'core.edit', 'core.edit.state', 'core.delete'
);
foreach ($actions as $action)
{
$result->set($action, $user->authorise($action, $assetName));
}
return $result;
}
/**
* Loads all GSD plugins and triggers an event
*
* @return mixed
*/
public static function event($name, $arguments = [])
{
PluginHelper::importPlugin('gsd');
PluginHelper::importPlugin('system');
return Factory::getApplication()->triggerEvent($name, $arguments);
}
/**
* Get list with all available plugins
*
* @return array
*/
public static function getPlugins()
{
return array_filter(self::event('onGSDGetType'));
}
/**
* Get the 1st found plugin's alias
*
* @return string The plugin's alias
*/
public static function getDefaultPlugin()
{
$plugins = self::getPlugins();
return $plugins[0]['alias'];
}
/**
* Returns active component alias
*
* @return mixed String on success, false on failure
*/
public static function getComponentAlias()
{
if (!$option = Factory::getApplication()->input->get('option'))
{
return;
}
$optionParts = explode('_', $option);
return isset($optionParts[1]) ? $optionParts[1] : false;
}
/**
* Checks whether the plugin is a Pro version
*
* @return boolean
*/
public static function isPro()
{
return \NRFramework\Functions::extensionHasProInstalled('plg_system_gsd');
}
/**
* Split string into array on each new line character
*
* @param string $str The string to split
*
* @return array
*/
public static function makeArrayFromNewLine($str)
{
// Sanity check
if (empty($str))
{
return $str;
}
$array = preg_split("/\\r\\n|\\r|\\n/", $str);
if (!$array)
{
return $str;
}
return array_values(array_filter($array));
}
/**
* Convert an array to UTF8 encoding
*
* @param array The array to convert
*
* @return array
*/
public static function arrayToUTF8($array)
{
if (!is_array($array) || !function_exists('mb_convert_encoding'))
{
return $array;
}
array_walk_recursive($array, function(&$value, $key)
{
if (is_string($value))
{
$value = mb_convert_encoding($value, 'utf8');
}
});
return $array;
}
/**
* Get first image src attribute from a string
*
* @param string $text The text to search for an image
*
* @return mixed string on success, null on failure
*/
public static function getFirstImageFromString($text)
{
if (empty($text))
{
return;
}
preg_match('/<img.+src=[\'"](?P<src>.+?)[\'"].*>/i', $text, $image);
return !empty($image) && isset($image['src']) ? $image['src'] : '';
}
/**
* Returns the weekdays
*
* @param boolean $capitalize Capitalizes the first letter of each day
*
* @return array
*/
public static function getWeekdays($capitalize = false)
{
$days = array('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday');
if ($capitalize)
{
$days = array_map('ucfirst', $days);
}
return $days;
}
/**
* This is a joke. Joomla 4's media field started including width and height information in the path.
* So, we need to clean the path before we can use it.
*
* images/headers/blue-flower.jpg#joomlaImage://local-images/headers/blue-flower.jpg?width=700&height=180)
*
* @param string $path
*
* @return string
*/
public static function cleanImage($path)
{
$path = Helper::absURL($path);
return defined('nrJ4') ? \Joomla\CMS\Helper\MediaHelper::getCleanMediaFieldValue($path) : $path;
}
public static function convert_to_ISO8601($value, $fallback_interval = 'M')
{
// Skip values already in ISO8601 format.
if (substr($value, 0, 1) == 'P')
{
return $value;
}
return 'PT' . $value . $fallback_interval;
}
} Json.php 0000644 00000146620 15237362756 0006217 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD;
defined('_JEXEC') or die('Restricted Access');
use Joomla\Registry\Registry;
use Joomla\CMS\Text\Text;
/**
* Google Structured Data JSON generator
*/
class JSON
{
/**
* Content Type Data
*
* @var object
*/
private $data;
/**
* List of available content types
*
* @var array
*/
private $contentTypes = [
'book',
'course',
'event',
'product',
'movie',
'recipe',
'review',
'factcheck',
'video',
'jobposting',
'custom_code',
'faq',
'howto',
'localbusiness',
'service',
'person',
'article'
];
/**
* Class Constructor
*
* @param object $data
*/
public function __construct($data = null)
{
$this->setData($data);
}
/**
* Get Content Types List
*
* @return array
*/
public function getContentTypes()
{
$types = $this->contentTypes;
asort($types);
// Move Custom Code option to the end
if ($customCodeIndex = array_search('custom_code', $types))
{
unset($types[$customCodeIndex]);
$types[] = 'custom_code';
}
return $types;
}
/**
* Set Data
*
* @param array $data
*/
public function setData($data)
{
if (is_array($data))
{
$this->data = new Registry($data);
} else
{
$this->data = $data;
}
return $this;
}
/**
* Get Content Type result
*
* @return string
*/
public function generate()
{
$contentTypeMethod = 'contentType' . $this->data->get('contentType');
// Make sure we have a valid Content Type
if (!method_exists($this, $contentTypeMethod) || !$content = $this->$contentTypeMethod())
{
return;
}
// In case we have a string (See Custom Code), return the original content.
if (is_string($content))
{
return $content;
}
Helper::event('onGSDSchemaBeforeGenerate', [&$content, $this->data]);
// Sanity check
if (!$content)
{
return;
}
// Remove null and empty properties
$content = $this->clean($content);
// In case we have an array, transform it into JSON-LD format.
// Always prepend the @context property
$content = ['@context' => 'https://schema.org'] + $content;
// We do not use JSON_NUMERIC_CHECK here because it doesn't respect numbers starting with 0.
// Bug: https://bugs.php.net/bug.php?id=70680
$json_string = json_encode($content, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// Detect issues with the encoding
if (json_last_error() !== JSON_ERROR_NONE)
{
$json_string = Text::sprintf('JSON Error: %s.', json_last_error_msg()) . ' ' . $content;
}
return '
<script type="application/ld+json" data-type="gsd">
'
. $json_string .
'
</script>';
}
/**
* Filter resursively an array by removing empty, false and null properties while preserving 0 values.
*
* @param array $input
*
* @return array
*/
private function clean($input)
{
foreach ($input as &$value)
{
if (is_array($value))
{
$value = self::clean($value);
}
}
// We use a custom callback here because the default behavior of array_filter removes 0 values as well.
return array_filter($input, function($value)
{
// Remove also orphan array properties
if (is_array($value) && count($value) == 1 && isset($value['@type']))
{
return false;
}
return ($value !== null && $value !== false && $value !== '');
});
}
/**
* Constructs the HowTo Schema Type
*
* @return array
*/
private function contentTypeHowTo()
{
$steps = array_map(function($step)
{
return array_merge(['@type' => 'HowToStep'], $step);
}, $this->data->get('step'));
$tools = array_map(function($tool)
{
return [
'@type' => 'HowToTool',
'name' => $tool
];
}, (array) $this->data->get('tool'));
$supply = array_map(function($supply)
{
return [
'@type' => 'HowToSupply',
'name' => $supply
];
}, (array) $this->data->get('supply'));
return [
'@type' => 'HowTo',
'image' => [
'@type' => 'ImageObject',
'url' => $this->data->get('image')
],
'name' => $this->data->get('name'),
'totalTime' => $this->data->get('totalTime'),
'estimatedCost' => [
'@type' => 'MonetaryAmount',
'currency' => $this->data->get('estimatedCostCurrency'),
'value' => $this->data->get('estimatedCost')
],
'supply' => $supply,
'tool' => $tools,
'step' => $steps
];
}
/**
* Constructs the FAQ Snippet
*
* @return array
*/
private function contentTypeFAQ()
{
$faq = $this->data->get('faqs');
// If there are no FAQ data, return
if (count($faq) == 0)
{
return;
}
$faqData = [];
foreach ($faq as $item)
{
$faqData[] = [
'@type' => 'Question',
'name' => $item['question'],
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => $item['answer']
]
];
}
return [
'@type' => 'FAQPage',
'mainEntity' => $faqData
];
}
/**
* Constructs the Breadcrumbs Snippet
*
* @return array
*/
private function contentTypeBreadcrumbs()
{
$crumbs = $this->data->get('crumbs');
if (!is_array($crumbs))
{
return;
}
$crumbsData = [];
foreach ($crumbs as $key => $value)
{
$crumbsData[] = [
'@type' => 'ListItem',
'position' => ($key + 1),
'name' => $value->name,
'item' => $value->link
];
}
return [
'@type' => 'BreadcrumbList',
'itemListElement' => $crumbsData
];
}
/**
* Constructs the Website schema with the following info:
*
* Site Name: https://developers.google.com/structured-data/site-name
* Sitelinks Searchbox: https://developers.google.com/search/docs/data-types/sitelinks-searchbox
*
* @return array
*/
private function contentTypeWebsite()
{
$content = [
'@type' => 'WebSite',
'url' => $this->data->get('site_url')
];
// Site Name
if ($this->data->get('site_name_enabled'))
{
$content = array_merge($content, [
'name' => $this->data->get('site_name'),
'alternateName' => $this->data->get('site_name_alt')
]);
}
// Sitelinks Search
if ($this->data->get('site_links_search'))
{
$content = array_merge($content, [
'potentialAction' => [
'@type' => 'SearchAction',
'target' => $this->data->get('site_links_search'),
'query-input' => 'required name=search_term'
]
]);
}
return $content;
}
/**
* Constructs Site Logo Snippet
* https://developers.google.com/search/docs/data-types/logo
*
* @return array
*/
private function contentTypeLogo()
{
return [
'@type' => 'Organization',
'url' => $this->data->get('url'),
'logo' => $this->data->get('logo')
];
}
/**
* Constructs the Article Content Type
*
* @return array
*/
private function contentTypeArticle()
{
$content = [
'@type' => $this->data->get('type', 'Article'),
'mainEntityOfPage' => [
'@type' => 'WebPage',
'@id' => $this->data->get('url')
],
'headline' => $this->data->get('title'),
'description' => $this->data->get('description'),
'image' => [
'@type' => 'ImageObject',
'url' => $this->data->get('image')
]
];
// Publisher
if ($this->data->get('publisherName'))
{
$content = array_merge($content, [
'publisher' => [
'@type' => 'Organization',
'name' => $this->data->get('publisherName'),
'logo' => [
'@type' => 'ImageObject',
'url' => $this->data->get('publisherLogo')
]
]
]);
}
// Add author
$this->addAuthor($content);
return $this->addDate($content);
}
/**
* Constructs the Social Profiles Snippet
* https://developers.google.com/search/docs/data-types/social-profile-links
*
* @return array
*/
private function contentTypeSocialProfiles()
{
return [
'@type' => $this->data->get('type'),
'name' => $this->data->get('sitename'),
'url' => $this->data->get('siteurl'),
'sameAs' => array_values((array) $this->data->get('links'))
];
}
/**
* Constructs the Business Listing Content Type
* https://developers.google.com/search/docs/data-types/local-businesses
*
* @return array
*/
private function contentTypeLocalBusiness()
{
$content = [
'@type' => $this->data->get('type'),
// Neither Rich Results Test or the deprecated tool, Google Structured Data Testing Tool doesn't throw a warning any more if the @id property is missing.
'@id' => $this->data->get('id'),
'name' => $this->data->get('name'),
'image' => $this->data->get('image'),
'url' => $this->data->get('url'),
'telephone' => $this->data->get('telephone'),
'priceRange' => $this->data->get('priceRange'),
'address' => $this->getPostalAddress()
];
// Map coordinates
$coords = $this->data->get('geo');
if ($coords && !empty($coords) && count($coords) == 2)
{
$content['geo'] = [
'@type' => 'GeoCoordinates',
'latitude' => $coords[0],
'longitude' => $coords[1]
];
}
// Opening Hours
if ($this->data->get('openinghours'))
{
$openingHours = $this->getOpeningHours($this->data->get('openinghours'));
$content = array_merge($content, $openingHours);
}
// Food-based business types
$content['servesCuisine'] = $this->data->get('servesCuisine');
$content['menu'] = $this->data->get('menu');
// Add Review
$this->addReview($content);
// Aggregate Rating
$this->addRating($content);
return $content;
}
/**
* Constructs the Preson Content Type
*
* @return array
*/
private function contentTypePerson()
{
$content = [
'@type' => 'Person',
'@id' => $this->data->get('id'),
'url' => $this->data->get('url'),
'name' => $this->data->get('title'),
'description' => $this->data->get('description'),
'honorificPrefix' => $this->data->get('honorificPrefix'),
'honorificSuffix' => $this->data->get('honorificSuffix'),
'alternateName' => $this->data->get('alternateName'),
'additionalName' => $this->data->get('additionalName'),
'givenName' => $this->data->get('givenName'),
'familyName' => $this->data->get('familyName'),
'address' => $this->getPostalAddress(),
'nationality' => $this->data->get('nationality'),
'email' => $this->data->get('email'),
'telephone' => $this->data->get('telephone'),
'gender' => $this->data->get('gender'),
'birthDate' => $this->data->get('birthDate'),
'memberOf' => $this->data->get('memberOf'),
'image' => $this->data->get('image'),
'jobTitle' => $this->data->get('jobTitle'),
'affiliation' => $this->data->get('affiliation'),
'alumniOf' => $this->data->get('alumniOf'),
'award' => $this->data->get('award'),
'knowsAbout' => $this->data->get('knowsAbout'),
'hasCredential' => $this->data->get('hasCredential'),
'hasOccupation' => [
'@type' => 'Occupation',
'name' => $this->data->get('occupationName'),
'description' => $this->data->get('occupationDescription'),
'educationRequirements' => $this->data->get('educationRequirements'),
'experienceRequirements' => $this->data->get('experienceRequirements'),
'occupationLocation' => [
'@type' => 'Country',
'name' => $this->data->get('addressCountry')
],
'estimatedSalary' => [
'@type' => 'MonetaryAmountDistribution',
'name' => 'base',
'duration' => 'P1Y',
'minValue' => is_array($this->data->get('offerPrice')) ? $this->data->get('offerPrice')[0] : null,
'maxValue' => is_array($this->data->get('offerPrice')) ? $this->data->get('offerPrice')[1] : null,
'currency' => $this->data->get('currency'),
]
],
];
if (empty(array_filter([
$this->data->get('occupationName'),
$this->data->get('occupationDescription'),
$this->data->get('educationRequirements'),
$this->data->get('experienceRequirements'),
$this->data->get('offerPrice'),
$this->data->get('currency')
])))
{
unset($content['hasOccupation']);
}
if ($this->data->get('type') !== 'Person')
{
$content['additionalType'] = $this->data->get('type');
}
if ($worksFor = $this->data->get('worksFor', ''))
{
$content['worksFor'] = [
'@type' => 'Organization',
'name' => $worksFor
];
}
if ($sameAs = (array) $this->data->get('sameAs', []))
{
$sameAs = array_column(array_values($sameAs), 'name');
$content['sameAs'] = $sameAs;
}
return $content;
}
/**
* Constructs the Product Content Type
* https://developers.google.com/search/docs/data-types/products
*
* @return array
*/
private function contentTypeProduct()
{
$content = [
'@type' => 'Product',
'productID' => $this->data->get('mpn'),
'name' => $this->data->get('title'),
'image' => $this->data->get('image'),
'description' => $this->data->get('description'),
'sku' => $this->data->get('sku'),
'mpn' => $this->data->get('mpn'),
'gtin' => $this->data->get('gtin'),
// @todo - We should replace "." with "_" in all properties to prevent issues with Registry namespace.
// For instance, all matches of category.id should be renamed to category_id.
'google_product_category' => $this->data->get('category_id')
];
// Weight
if ($this->data->get('weight'))
{
$content = array_merge($content, [
'weight' => [
'@type' => 'QuantitativeValue',
'value' => $this->data->get('weight'),
'unitText' => $this->data->get('weightUnit')
]
]);
}
// Brand
if ($this->data->get('brand'))
{
$content = array_merge($content, [
'brand' => [
'@type' => 'Brand',
'name' => $this->data->get('brand')
]
]);
}
// Offer / Pricing
if ($price = $this->data->get('offerPrice'))
{
$offerCommon = [
'priceCurrency' => $this->data->get('currency'),
'url' => $this->data->get('url'),
'itemCondition' => $this->data->get('offerItemCondition'),
'availability' => $this->data->get('offerAvailability'),
'priceValidUntil' => $this->data->get('priceValidUntil')
];
if (is_array($price))
{
$offer = [
'@type' => 'AggregateOffer',
'offerCount' => $this->data->get('offerCount', 1),
'lowPrice' => $price[0],
'highPrice' => $price[1]
];
} else
{
$offer = [
'@type' => 'Offer',
'price' => $price
];
}
$content['offers'] = array_merge($offer, $offerCommon);
}
// Add Review
$this->addReview($content);
// Aggregate Rating
$this->addRating($content);
return $content;
}
/**
* Adds review data to content
*
* @param array $content
*
* @return void
*/
private function addReview(&$content)
{
if (!$this->data->get('reviews') || !$this->data->get('reviewCount'))
{
return;
}
// Review
$reviews = $this->data->get('reviews');
$bestRating = $this->data->get('bestRating', 5);
$worstRating = $this->data->get('worstRating', 0);
$review_data = [];
foreach ($reviews as $review)
{
$rating = $review['rating'];
$review_data[] = [
'@type' => 'Review',
'author' => [
'@type' => 'Person',
'name' => $review['author'],
],
'datePublished' => $review['datePublished'],
'description' => $review['description'],
'reviewRating' => [
'@type' => 'Rating',
'bestRating' => $bestRating,
'ratingValue' => $rating,
'worstRating' => $worstRating
]
];
}
$content = array_merge($content, [
'review' => $review_data
]);
}
/**
* Constructs the Book Content Type
* https://developers.google.com/search/docs/appearance/structured-data/book
*
* @return array
*/
private function contentTypeBook()
{
$workExample = [
'@type' => 'Book',
'@id' => $this->data->get('id'),
'bookFormat' => $this->data->get('bookFormat'),
'inLanguage' => $this->data->get('inLanguage'),
'isbn' => $this->data->get('isbn'),
'url' => $this->data->get('url'),
'bookEdition' => $this->data->get('edition'),
'datePublished' => $this->data->get('datePublished'),
'potentialAction' => [
'@type' => $this->data->get('potentialAction'),
'target' => [
'@type' => 'EntryPoint',
'urlTemplate' => $this->data->get('actionURL'),
'actionPlatform' => [
'http://schema.org/DesktopWebPlatform',
'http://schema.org/AndroidPlatform',
'http://schema.org/IOSPlatform'
]
]
]
];
// Add author to $workExample
$this->addAuthor($workExample);
$content = [
'@type' => 'Book',
'@id' => $this->data->get('id'),
'url' => $this->data->get('url'),
'name' => $this->data->get('title'),
'image' => $this->data->get('image'),
'sameAs' => $this->data->get('referenceURL'),
'inLanguage' => $this->data->get('inLanguage'),
'workExample' => $workExample
];
// Add author
$this->addAuthor($content);
// Add identifier to workExample property
$identifiers = [];
if ($identifier_oclc_number = $this->data->get('identifier_oclc_number'))
{
$identifiers['OCLC_NUMBER'] = $identifier_oclc_number;
}
if ($identifier_lccn = $this->data->get('identifier_lccn'))
{
$identifiers['LCCN'] = $identifier_lccn;
}
if ($identifier_jp_e_code = $this->data->get('identifier_jp_e_code'))
{
$identifiers['JP_E-CODE'] = $identifier_jp_e_code;
}
if (count($identifiers))
{
$values = [];
foreach ($identifiers as $key => $value)
{
$values[] = [
'@type' => 'PropertyValue',
'propertyID' => $key,
'value' => $value
];
}
if ($values)
{
$content['workExample'] = array_merge($content['workExample'], [
'identifier' => $values
]);
}
}
return $content;
}
/**
* Constructs the Event Content Type
* https://developers.google.com/search/docs/data-types/events
*
* @return array
*/
private function contentTypeEvent()
{
$content = [
'@type' => 'Event',
'name' => $this->data->get('title'),
'image' => $this->data->get('image'),
'description' => $this->data->get('description'),
'url' => $this->data->get('url'),
'startDate' => $this->data->get('startDate'),
'endDate' => $this->data->get('endDate'),
'eventStatus' => 'https://schema.org/EventScheduled',
'eventAttendanceMode' => $this->data->get('eventAttendanceMode', 'https://schema.org/OfflineEventAttendanceMode'),
'offers' => [
'@type' => 'Offer',
'url' => $this->data->get('url'),
'availability' => $this->data->get('offerAvailability'),
'validFrom' => $this->data->get('startDateTime'),
'price' => $this->data->get('price'),
'priceCurrency' => $this->data->get('offerCurrency'),
'inventoryLevel' => [
'@context' => 'https://schema.org',
'@type' => 'QuantitativeValue',
'value' => $this->data->get('offerInventoryLevel'),
'unitText' => 'Tickets'
]
]
];
// Psysical Location
if ($this->data->get('locationName'))
{
$content['location'] = [
'@type' => 'Place',
'name' => $this->data->get('locationName'),
'address' => $this->getPostalAddress()
];
}
// Online Event
if ($online_event_url = $this->data->get('online_url'))
{
$online_event = [
'@type' => 'VirtualLocation',
'url' => $online_event_url
];
if (isset($content['location']))
{
$content['location'] = [$content['location'], $online_event];
} else
{
$content['location'] = $online_event;
}
}
// Performer
if ($this->data->get('performerName'))
{
$content = array_merge($content, [
'performer' => [
'@type' => $this->data->get('performerType'),
'name' => $this->data->get('performerName'),
'url' => $this->data->get('performerURL')
],
]);
}
// Organizer
if ($this->data->get('organizerName'))
{
$content = array_merge($content, [
'organizer' => [
'@type' => $this->data->get('organizerType'),
'name' => $this->data->get('organizerName'),
'url' => $this->data->get('organizerURL')
],
]);
}
return $content;
}
/**
* Constructs the Movie Content Type
* https://developers.google.com/search/docs/data-types/movie
*
* @return array
*/
private function contentTypeMovie()
{
$content = [
'@type' => 'Movie',
'url' => $this->data->get('url'),
'name' => $this->data->get('title'),
'description' => $this->data->get('description'),
'image' => $this->data->get('image'),
'dateCreated' => $this->data->get('datePublished'),
];
// Duration
if ($duration = $this->data->get('duration'))
{
$content['duration'] = $duration;
}
// Genre
if ($genre = $this->data->get('genre'))
{
$genreData = [];
foreach ($genre as $key => $g)
{
if (empty($g->name))
{
continue;
}
$genreData['genre'][] = trim($g->name);
}
$content = array_merge($content, $genreData);
}
// Creators
if ($creators = $this->data->get('creators'))
{
$creatorsData = [];
foreach ($creators as $key => $creator)
{
if (empty($creator->name))
{
continue;
}
$creatorsData['creator'][] = [
'@type' => 'Person',
'name' => trim($creator->name)
];
}
$content = array_merge($content, $creatorsData);
}
// Directors
if ($directors = $this->data->get('directors'))
{
$directorsData = [];
foreach ($directors as $key => $director)
{
if (empty($director->name))
{
continue;
}
$directorsData['director'][] = [
'@type' => 'Person',
'name' => trim($director->name)
];
}
$content = array_merge($content, $directorsData);
}
// Actors
if ($actors = $this->data->get('actors'))
{
$actorsData = [];
foreach ($actors as $key => $actor)
{
if (empty($actor->name))
{
continue;
}
$actorsData['actor'][] = [
'@type' => 'Person',
'name' => trim($actor->name)
];
}
$content = array_merge($content, $actorsData);
}
// Trailer
$content['trailer'] = [
'@type' => 'VideoObject',
'embedUrl' => $this->data->get('trailerUrl'),
'name' => $this->data->get('title'),
'thumbnail' => [
'@type' => 'ImageObject',
'contentUrl' => $this->data->get('image')
],
'thumbnailUrl' => $this->data->get('image'),
'description' => $this->data->get('description'),
'uploadDate' => $this->data->get('datePublished')
];
// Add Aggregate Rating
$this->addRating($content);
$this->addReview($content);
return $content;
}
/**
* Gets the Opening Hours for the Business Listing Type
*
* @param array $openingHours
*
* @return array
*/
private function getOpeningHours($openingHours)
{
$content = [];
// get the hours available
// 0: No hours specified
// 1: Always Open
// 2: Specific Hours
$hoursAvailable = (int) $openingHours->option;
// return if no hours are specified
if ($hoursAvailable == 0)
{
return $content;
}
unset($openingHours->option);
$weekdays = array_map('ucfirst', array_keys((array) $openingHours));
// Always Open
if ($hoursAvailable == 1)
{
$content['openingHoursSpecification'] = [
'@type' => 'OpeningHoursSpecification',
'dayOfWeek' => $weekdays,
'opens' => '00:00',
'closes' => '23:59'
];
return $content;
}
// Selected Dates
$openingHoursData = [];
foreach ($openingHours as $day_name => $day_options)
{
$day_name = ucfirst($day_name);
if (!isset($day_options->enabled) || !$day_options->enabled)
{
continue;
}
// If no hours are set, assume open 24 hours
if (empty($day_options->start) && empty($day_options->end))
{
$openingHoursData[] = [
'@type' => 'OpeningHoursSpecification',
'dayOfWeek' => $day_name,
'opens' => '00:00',
'closes' => '23:59'
];
continue;
}
$openingHoursData[] = [
'@type' => 'OpeningHoursSpecification',
'dayOfWeek' => $day_name,
'opens' => $day_options->start,
'closes' => $day_options->end
];
if (empty($day_options->start1) || empty($day_options->end1))
{
continue;
}
$openingHoursData[] = [
'@type' => 'OpeningHoursSpecification',
'dayOfWeek' => $day_name,
'opens' => $day_options->start1,
'closes' => $day_options->end1
];
}
if ($openingHoursData)
{
$content['openingHoursSpecification'] = $openingHoursData;
}
return $content;
}
/**
* Constructs the Recipe Content Type
* https://developers.google.com/search/docs/data-types/recipes
*
* @return array
*/
private function contentTypeRecipe()
{
$content = [
'@type' => 'Recipe',
'name' => $this->data->get('title'),
'image' => $this->data->get('image'),
'description' => $this->data->get('description'),
'prepTime' => $this->data->get('prepTime'),
'cookTime' => $this->data->get('cookTime'),
'totalTime' => $this->data->get('totalTime'),
'keywords' => $this->data->get('keywords'),
'recipeCuisine' => $this->data->get('cuisine'),
'recipeCategory' => $this->data->get('category'),
'recipeYield' => $this->data->get('yield'),
'recipeIngredient' => $this->data->get('ingredient'),
'recipeInstructions' => $this->data->get('instructions')
];
if ($this->data->get('calories'))
{
$content = array_merge($content, [
'nutrition' => [
'@type' => 'NutritionInformation',
'calories' => $this->data->get('calories')
],
]);
}
if ($this->data->get('video'))
{
$content = array_merge($content, [
'video' => [
'@type' => 'VideoObject',
'name' => $this->data->get('title'),
'description' => $this->data->get('description'),
'thumbnailUrl' => $this->data->get('image'),
'contentUrl' => $this->data->get('video'),
'uploadDate' => $this->data->get('datePublished')
]
]);
}
// Add author
$this->addAuthor($content);
$this->addRating($content);
$this->addDate($content);
return $content;
}
/**
* Constructs the Course Content Type
* https://developers.google.com/search/docs/data-types/courses
*
* @return array
*/
private function contentTypeCourse()
{
$content = [
'@type' => 'Course',
'name' => $this->data->get('title'),
'description' => $this->data->get('description'),
'courseCode' => $this->data->get('course_code'),
'provider' => [
'@type' => 'Organization',
'name' => $this->data->get('sitename')
],
'hasCourseInstance' => [
'@type' => 'CourseInstance',
'name' => $this->data->get('title'),
'description' => $this->data->get('description'),
'courseMode' => $this->data->get('course_mode'),
'startDate' => $this->data->get('startDate'),
'endDate' => $this->data->get('endDate'),
'location' => [
'@type' => 'Place',
'name' => $this->data->get('place_name'),
'address' => $this->getPostalAddress()
],
'image' => [
'@type' => 'ImageObject',
'url' => $this->data->get('image')
],
'performer' => [
'@type' => $this->data->get('performer_type'),
'name' => $this->data->get('performer_name')
],
'courseWorkload' => $this->data->get('courseWorkload')
]
];
if ($price = $this->data->get('price'))
{
$content['offers'] = [
[
'@type' => 'Offer',
'category' => $this->data->get('offerCategory', 'Free'),
'url' => $this->data->get('url'),
'availability' => $this->data->get('availability'),
'price' => $this->data->get('price'),
'priceCurrency' => $this->data->get('priceCurrency'),
'validFrom' => $this->data->get('validFrom')
]
];
}
$this->addRating($content);
$this->addDate($content);
return $content;
}
/**
* Constructs the Review Content Type
* https://developers.google.com/search/docs/data-types/reviews
*
* @return array
*/
private function contentTypeReview()
{
$content = [
'@type' => 'Review',
'description' => $this->data->get('description'),
'url' => $this->data->get('url'),
'datePublished' => $this->data->get('datePublished'),
'publisher' => [
'@type' => 'Organization',
'name' => $this->data->get('sitename'),
'sameAs' => $this->data->get('siteurl')
],
'inLanguage' => $this->data->get('language_code'),
'itemReviewed' => [
'@type' => $this->data->get('itemReviewedType'),
'name' => $this->data->get('title'),
'image' => $this->data->get('image'),
'sameAs' => $this->data->get('itemReviewedURL')
]
];
if ($this->data->get('itemReviewedType') == 'LocalBusiness')
{
$content = array_merge_recursive($content, [
'itemReviewed' => [
'address' => $this->getPostalAddress(),
'priceRange' => $this->data->get('priceRange'),
'telephone' => $this->data->get('telephone')
]
]);
}
if (in_array($this->data->get('itemReviewedType'), ['Movie', 'Book']))
{
$content = array_merge_recursive($content, [
'itemReviewed' => [
'datePublished' => $this->data->get('itemReviewedPublishedDate')
]
]);
}
if ($this->data->get('itemReviewedType') == 'Movie')
{
$movie = [
'itemReviewed' => [
'director' => [
'@type' => 'Person',
'name' => $this->data->get('movie_director')
]
]
];
if ($actors = $this->data->get('actors'))
{
foreach ($actors as $key => $actor)
{
if (empty($actor->name))
{
continue;
}
$movie['itemReviewed']['actor'][] = [
'@type' => 'Person',
'name' => trim($actor->name)
];
}
}
$content = array_merge_recursive($content, $movie);
}
if ($this->data->get('itemReviewedType') == 'Book')
{
$content = array_merge_recursive($content, [
'itemReviewed' => [
'isbn' => $this->data->get('book_isbn'),
'author' => [
'@type' => 'Person',
'name' => $this->data->get('book_author'),
'sameAs' => $this->data->get('book_author_url')
]
]
]);
}
// Handle Product Type
if ($this->data->get('itemReviewedType') == 'Product')
{
$content['itemReviewed']['sku'] = $this->data->get('product_sku');
$content['itemReviewed']['mpn'] = $this->data->get('product_sku');
$content['itemReviewed']['brand'] = [
'@type' => 'Brand',
'name' => $this->data->get('product_brand')
];
$content['itemReviewed']['description'] = $this->data->get('product_description');
// Rating
if ($this->data->get('ratingValue') && $this->data->get('reviewCount'))
{
$content['itemReviewed']['aggregateRating'] = [
'@type' => 'AggregateRating',
'ratingValue' => $this->data->get('ratingValue'),
'reviewCount' => $this->data->get('reviewCount')
];
}
// offers
if ($this->data->get('offerprice'))
{
$content['itemReviewed']['offers'] = [
'@type' => 'Offer',
'priceCurrency' => $this->data->get('currency', 'USD'),
'url' => $this->data->get('itemReviewedURL'),
'itemCondition' => $this->data->get('condition', 'http://schema.org/NewCondition'),
'availability' => $this->data->get('availability', 'http://schema.org/InStock'),
'price' => $this->data->get('offerprice'),
'priceValidUntil' => $this->data->get('pricevaliduntil')
];
}
// Review
if ($reviews = $this->getReviews())
{
$content['itemReviewed']['review'] = $reviews;
}
}
// Add author
$this->addAuthor($content);
if ($this->data->get('ratingValue'))
{
$content = array_merge($content, [
'reviewRating' => [
'@type' => 'Rating',
'ratingValue' => $this->data->get('ratingValue'),
'worstRating' => $this->data->get('worstRating', 0),
'bestRating' => $this->data->get('bestRating', 5)
]
]);
}
return $content;
}
/**
* Returns the reviews of the data
*
* @return array
*/
private function getReviews()
{
if (!$this->data->get('review') || !$this->data->get('reviewCount'))
{
return [];
}
$bestRating = $this->data->get('bestRating', 5);
$worstRating = $this->data->get('worstRating', 0);
$review_data = [];
foreach ($this->data->get('review') as $review)
{
$rating = $review['rating'];
$review_data[] = [
'@type' => 'Review',
'author' => [
'@type' => 'Person',
'name' => $review['author'],
],
'datePublished' => $review['datePublished'],
'description' => $review['description'],
'reviewRating' => [
'@type' => 'Rating',
'bestRating' => $bestRating,
'ratingValue' => $rating,
'worstRating' => $worstRating
]
];
}
return $review_data;
}
/**
* Constructs the Fact Check Content Type
* https://developers.google.com/search/docs/data-types/factcheck
*
* @return array
*/
private function contentTypeFactCheck()
{
$content = [
'@type' => 'ClaimReview',
'url' => $this->data->get('factcheckURL'),
'itemReviewed' => [
'@type' => 'CreativeWork',
'author' => [
'@type' => $this->data->get('claimAuthorType'),
'name' => $this->data->get('claimAuthorName'),
'sameAs' => $this->data->get('claimURL')
],
'datePublished' => $this->data->get('claimDatePublished')
],
'claimReviewed' => $this->data->get('title'),
'author' => [
'@type' => 'Organization',
'name' => $this->data->get('sitename')
],
'reviewRating' => [
'@type' => 'Rating',
'ratingValue' => $this->data->get('factcheckRating'),
'bestRating' => $this->data->get('bestFactcheckRating'),
'worstRating' => $this->data->get('worstFactcheckRating'),
'alternateName' => $this->data->get('alternateName')
]
];
return $this->addDate($content);
}
/**
* Constructs the Service Content Type
* https://schema.org/Service
*
* @return array
*/
private function contentTypeService()
{
$content = [
'@type' => 'Service',
'name' => $this->data->get('title'),
'serviceType' => $this->data->get('title'),
'description' => $this->data->get('description'),
'image' => $this->data->get('image'),
'url' => $this->data->get('url'),
'provider' => [
'@type' => $this->data->get('provider_type'),
'name' => $this->data->get('provider_name'),
'image' => $this->data->get('provider_image'),
'telephone' => $this->data->get('phone'),
'address' => $this->getPostalAddress()
]
];
// Offer / Pricing
if ((float) $this->data->get('offerPrice') > 0)
{
$content = array_merge($content, [
'offers' => [
'@type' => 'Offer',
'priceCurrency' => $this->data->get('currency', 'USD'),
'price' => $this->data->get('offerPrice')
]
]);
}
return $content;
}
/**
* Constructs the Video Content Type
* https://developers.google.com/search/docs/data-types/videos
*
* @return array
*/
private function contentTypeVideo()
{
if (empty($this->data->get('contentUrl')) && empty($this->data->get('embedUrl')))
{
return;
}
return [
'@type' => 'VideoObject',
'name' => $this->data->get('name'),
'description' => $this->data->get('description'),
'thumbnailUrl' => $this->data->get('thumbnailUrl'),
'uploadDate' => $this->data->get('uploadDate'),
'contentUrl' => $this->data->get('contentUrl'),
'embedURL' => $this->data->get('embedUrl'),
'transcript' => $this->data->get('transcript')
];
}
/**
* Generates the Job Posting Content Type
* https://developers.google.com/search/docs/data-types/job-posting
*
* @return void
*/
private function contentTypeJobPosting()
{
$json = [
'@type' => 'JobPosting',
'title' => $this->data->get('title'),
'description' => $this->data->get('description'),
'datePosted' => $this->data->get('datePublished'),
'educationRequirements' => $this->data->get('educationRequirements'),
'employmentType' => $this->data->get('employmenttype'),
'industry' => $this->data->get('industry'),
'jobLocation' => [
'@type' => 'Place',
'address' => $this->getPostalAddress()
],
'hiringOrganization' => [
'@type' => 'Organization',
'name' => $this->data->get('hiring_oprganization_name'),
'sameAs' => $this->data->get('hiring_oprganization_url'),
'logo' => $this->data->get('hiring_organization_logo')
],
'validThrough' => $this->data->get('valid_through')
];
$salary = $this->data->get('salary');
if ($salary > 0)
{
if (is_array($salary) && count($salary) > 1)
{
$salary_value = [
'value' => trim($salary[0]),
'minValue' => trim($salary[0]),
'maxValue' => trim($salary[1])
];
} else
{
$salary_value = ['value' => $salary];
}
$json = array_merge($json, [
'baseSalary' => [
'@type' => 'MonetaryAmount',
'currency' => $this->data->get('currency'),
'value' => [
'@type' => 'QuantitativeValue',
'unitText' => $this->data->get('salary_unit')
]
],
]);
$json = array_merge_recursive($json, [
'baseSalary' => [
'value' => $salary_value
]
]);
}
return $json;
}
/**
* Constructs the Custom Code Content Type
*
* @return string The custom code entered by user
*/
private function contentTypeCustom_Code()
{
return $this->data->get('custom_code', '');
}
/**
* Appends the aggregateRating property to object
*
* @param array &$content
*/
private function addRating(&$content)
{
if (!$this->data->get('ratingValue') || !$this->data->get('reviewCount'))
{
return;
}
return $content = array_merge($content, [
'aggregateRating' => [
'@type' => 'AggregateRating',
'ratingValue' => $this->data->get('ratingValue'),
'reviewCount' => $this->data->get('reviewCount'),
'worstRating' => $this->data->get('worstRating', 0),
'bestRating' => $this->data->get('bestRating', 5)
]
]);
}
/**
* Returns the PostalAddress type used in most of the content types
*
* @return array
*/
private function getPostalAddress()
{
return [
'@type' => 'PostalAddress',
'streetAddress' => $this->data->get('streetAddress'),
'addressCountry' => $this->data->get('addressCountry'),
'addressLocality' => $this->data->get('addressLocality'),
'addressRegion' => $this->data->get('addressRegion'),
'postalCode' => $this->data->get('postalCode')
];
}
/**
* Appends date properties to object
*
* @param array &$content
*/
private function addDate(&$content)
{
return $content = array_merge($content, [
'datePublished' => $this->data->get('datePublished'),
'dateCreated' => $this->data->get('dateCreated'),
'dateModified' => $this->data->get('dateModified')
]);
}
/**
* Adds the author property to the content.
*
* @param array &$content
*
* @return void
*/
private function addAuthor(&$content)
{
if ($this->data->get('authorName'))
{
$content = array_merge($content, [
'author' => [
'@type' => $this->data->get('authorType'),
'name' => $this->data->get('authorName'),
'url' => $this->data->get('authorUrl')
]
]);
}
}
} MappingOptions.php 0000644 00000014020 15237362756 0010241 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD;
defined('_JEXEC') or die('Restricted Access');
use GSD\Helper;
use Joomla\Registry\Registry;
use NRFramework\SmartTags;
use Joomla\CMS\Factory;
/**
* Support SmartTags in the Structured Data Item properties
*/
class MappingOptions
{
/**
* List of available mapping options
*
* @var array
*/
public static $options = [
'GSD_INTEGRATION' => [
'gsd.item.id' => 'ID',
'gsd.item.alias' => 'Alias',
'gsd.item.headline' => 'NR_TITLE',
'gsd.item.description' => 'NR_TEXT',
'gsd.item.introtext' => 'GSD_INTROTEXT',
'gsd.item.fulltext' => 'GSD_FULLTEXT',
'gsd.item.image' => 'NR_IMAGE',
'gsd.item.imagetext' => 'GSD_IMAGE_FROM_TEXT',
'gsd.item.weight' => 'GSD_PRODUCT_WEIGHT',
'gsd.item.weightUnit' => 'GSD_PRODUCT_WEIGHT_UNIT',
'url' => 'NR_URL',
'user.id' => 'Author ID',
'user.name' => 'Author Name',
'user.firstname' => 'Author First Name',
'user.lastname' => 'Author Last Name',
'user.login' => 'Author Username',
'user.email' => 'Author Email',
'gsd.item.created' => 'Date Created',
'gsd.item.publish_up' => 'GSD_DATE_PUBLISH_UP',
'gsd.item.publish_down' => 'GSD_DATE_PUBLISH_DOWN',
'gsd.item.modified' => 'GSD_DATE_MODIFIED',
'gsd.item.ratingValue' => 'Rating Value',
'gsd.item.reviewCount' => 'Review Count',
'gsd.item.metakey' => 'Meta Keywords',
'gsd.item.metadesc' => 'Meta Description'
],
'Page' => [
'page.title' => 'Page Title',
'page.browsertitle' => 'Browser Page Title',
'page.desc' => 'Page Meta Description',
'page.keywords' => 'Page Meta Keywords',
'page.lang' => 'Page Language',
'page.generator' => 'Page Generator'
],
'Site Info' => [
'gsd.sitename' => 'Site Name',
'gsd.siteurl' => 'Site URL',
'gsd.sitelogo' => 'Site Logo',
'site.email' => 'Site Email'
]
];
public static function make($string)
{
if (empty($string))
{
return;
}
return '{' . $string . '}';
}
/**
* Replaces Smart Tags in a snippet.
*
* @param Registry $snippet The snippet data
* @param Registry $payload The tags to use
*
* @return Registry
*/
public static function replace($snippet, $payload)
{
$payload = $payload->toArray();
// Null property must be converted to an empty string in order to replace the respective Smart Tag.
foreach ($payload as $key => $value)
{
if (!is_null($value))
{
continue;
}
$payload[$key] = '';
}
// Initialize SmartTags class
$SmartTags = new SmartTags([
'technology_tags' => false,
'user' => isset($payload['created_by']) ? $payload['created_by'] : null]
);
// Add payload to collection
$SmartTags->add($payload, 'gsd.item.');
// Add extension global settings to collection
$settings = [
'sitename' => Helper::getSiteName(),
'siteurl' => Helper::getSiteURL(),
'sitelogo' => Helper::getSiteLogo()
];
$SmartTags->add($settings, 'gsd.');
// Replace Smart Tags now
$data = $SmartTags->replace($snippet->toArray());
return new Registry($data);
}
public static function prepare(&$properties)
{
foreach ($properties as $key => $property)
{
if (!is_object($property) || !isset($property->option))
{
continue;
}
switch ($property->option)
{
case '_css_selector_':
if ($property->css_selector)
{
$crawler = new \NRFramework\DOMCrawler();
$value = $crawler->readCSSSelectorField($property->css_selector);
}
break;
case 'fixed':
if (in_array($key, ['author', 'publisher_name']))
{
if ($user = Factory::getUser($property->fixed))
{
$property->fixed = $user->name;
}
}
$value = $property->fixed;
break;
case '_custom_':
$value = $property->custom;
break;
case '_disabled_':
$value = false;
break;
default:
$value = self::make($property->option);
break;
}
$properties->set($key, $value);
}
}
/**
* Add mapping options to the collection
*
* @param [type] $options
* @param [type] $newoptions
* @param string $group_name
* @param string $prefix
*
* @return void
*/
public static function add(&$options, $newoptions, $group_name = 'GSD_CUSTOM_FIELDS', $prefix = 'gsd.item.cf.')
{
foreach ($newoptions as $key => $newoption)
{
$new_key = $prefix . $key;
$newoptions[$new_key] = $newoption;
unset($newoptions[$key]);
}
$options = array_merge_recursive($options, [$group_name => $newoptions]);
}
} Migrator.php 0000644 00000022773 15237362756 0007074 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD;
defined('_JEXEC') or die('Restricted Access');
use GSD\Helper;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Table\Table;
use Joomla\CMS\Language\Multilanguage;
/**
* Google Structured Data Migrator Helper
*/
class Migrator
{
/**
* Indicates the current installed version of the extension
*
* @var string
*/
protected $installedVersion;
/**
* Shorthand of the Joomla Application Object
*
* @var object
*/
protected $app;
/**
* Class constructor
*
* @param string $installedVersion The version of the extension
*/
public function __construct($installedVersion)
{
BaseDatabaseModel::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/models');
Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/tables');
$this->installedVersion = $installedVersion;
$this->app = Factory::getApplication();
}
/**
* The main method to run migrations
*
* @return void
*/
public function run()
{
try
{
$this->checkAndAddAppviewColumn();
$this->moveGlobalLocalBusinessToItems();
} catch (\Throwable $th)
{
$this->app->enqueueMessage($th->getMessage(), 'error');
}
}
/**
* Since v4.4.0, the Local Business Schema is available as an indepedent Schema Type. Given than update, the Local Business options
* available in the extension's configuration page are no longer needed and they are migrated as a structured data item in the Items section.
*
* @return mixed Null if the migration doesn't run, True if it does run.
*/
public function moveGlobalLocalBusinessToItems()
{
// Local Business Content Type introduced in v4.4.0
if (version_compare($this->installedVersion, '4.4.0', '>'))
{
return;
}
$params = Helper::getParams();
if (!$params->get('businesslisting_enabled'))
{
return;
}
// Enable Menu Manager Integration
$menu_manager_plugin = \NRFramework\Extension::get('menus', 'plugin', 'gsd');
if ($menu_manager_plugin && !$menu_manager_plugin['enabled'])
{
$table = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\');
$table->load($menu_manager_plugin['extension_id']);
$table->enabled = 1;
$table->store();
}
// Get homepage menu item
$menu = $this->app->getMenu('site');
$lang = Factory::getLanguage();
$home = Multilanguage::isEnabled() ? $menu->getDefault($lang->getTag()) : $menu->getDefault();
$homepage_menuitem = (int) $home->id;
$item = [
'title' => 'Website Local Business',
'contenttype' => 'localbusiness',
'plugin' => 'menus',
'state' => $homepage_menuitem ? 1 : 0,
'note' => 'Moved from extension configruation page',
'localbusiness' => [
'type' => $params->get('businesslisting_type'),
'name' => [
'option' => 'gsd.sitename'
],
'image' => [
'option' => 'gsd.sitelogo'
],
'telephone' => [
'option' => '_custom_',
'custom' => $params->get('businesslisting_telephone')
],
'priceRange' => [
'option' => '_custom_',
'custom' => $params->get('price_range')
],
'openinghours' => [
'option' => 'fixed',
'fixed' => [
'option' => $params->get('businesslisting_hours_available'),
'monday' => [
'enabled' => $params->get('businesslisting_monday'),
'start' => $params->get('businesslisting_monday_start'),
'end' => $params->get('businesslisting_monday_end')
],
'tuesday' => [
'enabled' => $params->get('businesslisting_tuesday'),
'start' => $params->get('businesslisting_tuesday_start'),
'end' => $params->get('businesslisting_tuesday_end')
],
'wednesday' => [
'enabled' => $params->get('businesslisting_wednesday'),
'start' => $params->get('businesslisting_wednesday_start'),
'end' => $params->get('businesslisting_wednesday_end')
],
'thursday' => [
'enabled' => $params->get('businesslisting_thursday'),
'start' => $params->get('businesslisting_thursday_start'),
'end' => $params->get('businesslisting_thursday_end')
],
'friday' => [
'enabled' => $params->get('businesslisting_friday'),
'start' => $params->get('businesslisting_friday_start'),
'end' => $params->get('businesslisting_friday_end')
],
'saturday' => [
'enabled' => $params->get('businesslisting_saturday'),
'start' => $params->get('businesslisting_saturday_start'),
'end' => $params->get('businesslisting_saturday_end')
],
'sunday' => [
'enabled' => $params->get('businesslisting_sunday'),
'start' => $params->get('businesslisting_sunday_start'),
'end' => $params->get('businesslisting_sunday_end')
]
],
],
'addressCountry' => [
'option' => 'fixed',
'fixed' => $params->get('businesslisting_address_country')
],
'addressLocality' => [
'option' => '_custom_',
'custom' => $params->get('businesslisting_address_locality')
],
'streetAddress' => [
'option' => '_custom_',
'custom' => $params->get('businesslisting_street_address')
],
'addressRegion' => [
'option' => '_custom_',
'custom' => $params->get('businesslisting_address_region')
],
'postalCode' => [
'option' => '_custom_',
'custom' => $params->get('businesslisting_postal_code')
],
'geo' => [
'option' => '_custom_',
'custom' => $params->get('businesslisting_latlng')
],
'servesCuisine' => [
'option' => '_custom_',
'custom' => $params->get('servesCuisine')
]
],
'assignments' => [
'menu' => [
'assignment_state' => 1,
'selection' => [$homepage_menuitem]
]
]
];
if (!$this->createItem($item))
{
return;
}
$this->app->enqueueMessage('Your Local Business Listing options previously found in the extension configuration page has been migrated as a Structured Data Item in the Items section.', 'warning');
// To ensure the migration runs once, disable the Local Business option in the configuration
$table = Table::getInstance('Config', 'GSDTable');
$table->load('config');
$p_ = json_decode($table->params);
$p_->businesslisting_enabled = false;
$table->params = json_encode($p_);
$table->store();
return true;
}
/**
* Create a new structured data item
*
* @param array $params
*
* @return boolean
*/
private function createItem($params)
{
$model = BaseDatabaseModel::getInstance('Item', 'GSDModel');
$item = $model->validate(null, $params);
return $model->save($item);
}
/**
* The "appview" column was introduced in 5.1.0 and due to
* the fact that we did not include it in the main "gsd" table
* SQL file right away, some users may be missing it.
*
* We check whether this column exists and if not, add it, otherwise, abort.
*
* @return void
*/
private function checkAndAddAppviewColumn()
{
$db = Factory::getDBO();
$query = "SHOW COLUMNS FROM `#__gsd` LIKE 'appview'";
$db->setQuery($query);
// Column exists
if ($res = $db->loadResult())
{
return;
}
// Add column
$sql = "ALTER TABLE `#__gsd` ADD `appview` VARCHAR(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '*' AFTER `plugin`";
$db->setQuery($sql);
$db->execute();
}
} PluginBase.php 0000644 00000035120 15237362756 0007327 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD;
defined('_JEXEC') or die('Restricted access');
use GSD\Json;
use GSD\Helper;
use GSD\MappingOptions;
use NRFramework\Cache;
use NRFramework\Assignments;
use Joomla\Registry\Registry;
use Joomla\String\StringHelper;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Multilanguage;
/**
* Google Structured Data helper class
*/
class PluginBase extends CMSPlugin
{
/**
* Auto load the plugin language file
*
* @var boolean
*/
protected $autoloadLanguage = true;
/**
* Joomla Application Object
*
* @var object
*/
protected $app;
/**
* The selected app view.
*
* @var string
*/
protected $appview;
/**
* Joomla Database Object
*
* @var object
*/
protected $db;
/**
* Holds all available snippets for the current active page.
*
* @var array
*/
protected $snippets;
/**
* Indicates the query string parameter name that is used by the front-end component
*
* @var string
*/
protected $thingRequestIDName = 'id';
/**
* Indicates the request variable name used by plugin's assosiated component
*
* @var string
*/
protected $thingRequestViewVar = 'view';
/**
* Plugin constructor
*
* @param mixed &$subject
* @param array $config
*/
public function __construct(&$subject, $config = [])
{
// Load main language file
Factory::getLanguage()->load('plg_system_gsd', JPATH_PLUGINS . '/system/gsd');
// execute parent constructor
parent::__construct($subject, $config);
}
/**
* Return a list of all supported views.
*
* While in most Apps we support 1 view, in Apps like the J-Business Directory where we support 3 views. The App View dropdown helps us tell what
* snippets should be rendered per view without the need for Conditions.
*
* The App View information helps us improve performance on the front-end and UX on the back-end. In detail using the App View we can:
*
* 1. [Front-end] Fetch only the snippets based on the active view.
* 2. [Back-end] Filter the Mapping Dropdown options. (Eg: When marking up a Product page we don't need mapping options related to an Event page.)
* 3. [Back-end] Filter displayed Conditions per view. (Eg: When marking up a Product page, we don't need Conditions related to Event pages.)
*
* @return array
*/
public function advertiseSupportedViews()
{
$methods = get_class_methods($this);
$supportedViews = [];
foreach ($methods as $method)
{
if (strpos($method, 'view') !== 0)
{
continue;
}
$viewName = strtolower(str_replace('view', '', $method));
$supportedViews[$viewName] = Text::_('PLG_GSD_' . strtoupper($this->_name) . '_VIEW_' . strtoupper($viewName));
}
return $supportedViews;
}
/**
* Event triggered to gather all available plugins.
* Mostly used by the dropdowns in the backend.
*
* @param boolean $mustBeInstalled If enabled, the assosiated component must be installed
*
* @return array
*/
public function onGSDGetType($mustBeInstalled = true)
{
if ($mustBeInstalled && !\NRFramework\Extension::isInstalled($this->_name))
{
return;
}
return [
'name' => Text::_('PLG_GSD_' . strtoupper($this->_name) . '_ALIAS'),
'alias' => $this->_name
];
}
/**
* Prepare form.
*
* @param Form $form The form to be altered.
* @param mixed $data The associated data for the form.
*
* @return boolean
*/
public function onContentPrepareForm($form, $data)
{
// Make sure we are on the right context
if ($this->app->isClient('site') || $form->getName() != 'com_gsd.item')
{
return;
}
// When item is not saved yet, the $data variable is type of Array.
$tempData = (object) $data;
if (!isset($tempData->plugin) || is_null($tempData->plugin) || $tempData->plugin != $this->_name)
{
return;
}
$view = isset($tempData->appview) ? $tempData->appview : '';
$this->appview = $view;
$viewXMLName = !empty($view) && $view !== '*' ? $view : 'assignments';
// The assignments XML file base
$assignmentsXMLFileBase = JPATH_PLUGINS . '/gsd/' . $this->_name . '/form/';
$assignmentsXML = $assignmentsXMLFileBase . $viewXMLName . '.xml';
/**
* The XML file can be found in the following files:
*
* - {VIEW}.xml
* Used individually for each view to provide different assignments.
* - assignments.xml
* Used by single-view integrations or multi-view integrations that offer
* the same assignments per view (i.e. J2Store).
*/
// Check view-based XML
if (!is_file($assignmentsXML))
{
$assignmentsXML = $assignmentsXMLFileBase . 'assignments.xml';
// Check generic XML
if (!is_file($assignmentsXML))
{
return;
}
}
$form->loadFile($assignmentsXML, false);
}
/**
* The event triggered before the JSON markup be appended to the document.
*
* @param array &$data The JSON snippets to be appended to the document
*
* @return void
*/
public function onGSDBeforeRender(&$data)
{
// Quick filtering on component check
if (!$this->passContext())
{
return;
}
// Let's check if the plugin supports the current component's view.
if (!$payload = $this->getPayload())
{
return;
}
// Now, let's see if we have valid snippets for the active page. If not abort.
if (!$this->snippets = $this->getSnippets())
{
$this->log('No valid items found');
return;
}
// Prepare snippets
foreach ($this->snippets as $snippet)
{
// Here, the payload must be merged with the snippet data
$jsonData = $this->preparePayload($snippet, $payload);
// Create JSON
$jsonClass = new Json($jsonData);
$json = $jsonClass->generate();
// Add json back to main data object
$data[] = $json;
}
}
/**
* Validate context to decide whether the plugin should run or not.
*
* @return bool
*/
protected function passContext()
{
return Helper::getComponentAlias() == $this->_name;
}
/**
* Get Item's ID
*
* @return string
*/
protected function getThingID()
{
return $this->app->input->getInt($this->thingRequestIDName);
}
/**
* Get component's items and validate conditions
*
* @return Mixed Null if no items found, The valid items array on success
*/
protected function getSnippets()
{
BaseDatabaseModel::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_gsd/models');
$model = BaseDatabaseModel::getInstance('Items', 'GSDModel', ['ignore_request' => true]);
$model->setState('filter.plugin', $this->_name);
// Since we did not code any migration script, pass asterisk to match old rows as well.
$model->setState('filter.appview', [$this->getView(), '*']);
$model->setState('filter.state', 1);
if (Multilanguage::isEnabled())
{
$model->setState('filter.language', [Factory::getLanguage()->getTag(), '*']);
}
if (!$rows = $model->getItems())
{
return;
}
// Check publishing assignments for each item
foreach ($rows as $key => $row)
{
if (!isset($row->assignments) || !is_object($row->assignments))
{
continue;
}
// Prepare assignments
$assignmentsFound = [];
foreach ($row->assignments as $alias => $assignment)
{
if ($assignment->assignment_state == '0')
{
continue;
}
// Remove unwanted assignments added by Free Pro code blocks
if (strpos($alias, '@'))
{
continue;
}
// If user hasn't made any selection, skip the assignment.
if (!isset($assignment->selection))
{
continue;
}
// Comply with the new conditions requirements
$condition = (object) [
'alias' => $alias,
'value' => $assignment->selection,
'params' => isset($assignment->params) ? $assignment->params : [],
'assignment_state' => $assignment->assignment_state
];
// Pass with 'AND' matching method. Hence the assignment to first [0] cell.
$assignmentsFound[0][] = $condition;
}
// Validate assignments
if (!$pass = (new Assignments())->passAll($assignmentsFound))
{
$this->log('Item #' . $row->id . ' does not pass the conditions check');
unset($rows[$key]);
}
}
$items = array_map(function($row)
{
$contentType = $row->contenttype;
// After we have selected an Integration and a Content Type and we hit Save,
// the item needs to be re-saved in order to access the Content Type options.
//
// We need to find a way to auto-populate the Content Type with default data during 1st save.
//
// A possible approach would be: Upon clicking on the New button, we display a popup modal where
// the user can choose a Content Type, an Integration and a Title for the structured data item.
// Then they will be redirected to the item editing page with these data prefilled.
//
// UPDATE 04/05/2022: Since we have changed the way the structured data item is saved using the Joomla Loader (state) this may be no longer an issue. It needs a check.
$contentTypeData = property_exists($row, $contentType) ? $row->{$contentType} : [];
$s = new Registry($contentTypeData);
$s->set('contentType', $contentType);
$s->set('snippet_id', $row->id);
// Help troubleshooting by logging item ID.
$this->log('ID: ' . $row->id);
return $s;
}, $rows);
return $items;
}
/**
* Asks for data from the child plugin based on the active view name
*
* @return Registry The payload Registry
*/
protected function getPayload()
{
$view = $this->getView();
$method = 'view' . ucfirst($view);
if (!$view || !method_exists($this, $method))
{
$this->log('View ' . $view . ' is not supported');
return;
}
// Yeah. Let's call the method.
$payload = $this->$method();
// We need a valid array
if (!is_array($payload))
{
$this->log('Invalid Payload Array');
return;
}
// If the payload contains any objects, convert them to an associative array
$payload = json_decode(json_encode($payload), true);
// Convert payload to Registry object and return it
return new Registry($payload);
}
/**
* Prepares the payload to be used in the JSON class
*
* @return string
*/
private function preparePayload($snippet, $payload)
{
$schema = \GSD\Schemas\Helper::getInstance($snippet['contentType']);
$schema->onPayloadPrepare($payload); // Temporary workaround. See comments in the Custom_Code class.
MappingOptions::prepare($snippet);
// Create a new combined object by merging the snippet data into the payload
// Note: In order to produce a valid merged object, payload's array keys should match the field names
// as declared in the form's XML file.
$p = clone $payload;
$s = $p->merge($snippet, false);
// Replace Smart Tags - This can be implemented with a Plugin
$s = MappingOptions::replace($s, $payload);
$prepareContent = Helper::getParams()->get('preparecontent', false);
// Content Preparation
if ($prepareContent)
{
$s['headline'] = $this->prepareText($s['headline']);
$s['description'] = $this->prepareText($s['description']);
}
return $schema->setData($s)->get();
}
/**
* Get View Name
*
* @return string Return the current executed view in the front-end
*/
protected function getView()
{
return $this->app->input->get($this->thingRequestViewVar);
}
/**
* Prepare given text with Content and Field plugins
*
* @param string $text
*
* @return string
*/
private function prepareText($text)
{
if (!$text)
{
return;
}
return \Joomla\CMS\HTML\HTMLHelper::_('content.prepare', $text);
}
/**
* Log messages
*
* @param string $message The message to log
*
* @return void
*/
protected function log($message)
{
Helper::log(Text::_('PLG_GSD_' . $this->_name . '_ALIAS') . ' - ' . $message);
}
} PluginBaseArticle.php 0000644 00000002113 15237362756 0010627 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD;
defined('_JEXEC') or die('Restricted access');
/**
* Google Structured Data Product Plugin Base
*/
class PluginBaseArticle extends \GSD\PluginBase
{
/**
* Listening to the onAfterRender Joomla event
*
* @return void
*/
public function onAfterRender()
{
// Make sure we are on the right context
if ($this->app->isClient('administrator') || !$this->passContext() || !$this->params->get('remove_default_schema', true))
{
return;
}
// Remove the most common article-based schemas
$schemas = [
'BlogPosting',
'Article',
'NewsArticle',
'Blog',
'AggregateRating',
'Person'
];
\GSD\SchemaCleaner::remove($schemas, false);
}
} PluginBaseEvent.php 0000644 00000005467 15237362756 0010344 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD;
defined('_JEXEC') or die('Restricted access');
use GSD\PluginBase;
use GSD\MappingOptions;
/**
* Google Structured Data Product Plugin Base
*/
class PluginBaseEvent extends PluginBase
{
/**
* The MapOptions Backend Event. Triggered by the mappingoptions fields to help each integration add its own map options.
*
* @param string $plugin
* @param array $options
*
* @return void
*/
public function onMapOptions($plugin, &$options)
{
if ($plugin != $this->_name)
{
return;
}
$remove_options = [
'modified',
'created',
'ratingValue',
'reviewCount'
];
// Remove unsupported mapping options
foreach ($remove_options as $key => $option)
{
unset($options['GSD_INTEGRATION']['gsd.item.' . $option]);
}
// Add Event based options
$new_options = [
'startdate' => 'GSD_EVENT_START_DATE',
'enddate' => 'GSD_EVENT_END_DATE',
'offerprice' => 'GSD_EVENT_OFFER_PRICE',
'locationname' => 'GSD_EVENT_LOCATION_NAME',
'locationaddress' => 'GSD_EVENT_STREET_ADDRESS',
'addressCountry' => 'GSD_BUSINESSLISTING_ADDRESS_COUNTRY',
'addressLocality' => 'GSD_BUSINESSLISTING_ADDRESS_LOCALITY',
'addressRegion' => 'GSD_BUSINESSLISTING_ADDRESS_REGION',
'postalCode' => 'GSD_BUSINESSLISTING_POSTAL_CODE',
'offercurrency' => 'GSD_PRODUCT_OFFER_CURRENCY',
'offerinventorylevel' => 'GSD_EVENT_INVENTORY_LEVEL',
'offerstartdate' => 'GSD_EVENT_AVAILABILITY_START_DATE',
'organizerType' => 'GSD_EVENT_ORGANIZER_TYPE',
'organizerName' => 'GSD_EVENT_ORGANIZER_NAME',
'organizerURL' => 'GSD_EVENT_ORGANIZER_URL',
'performerType' => 'GSD_EVENT_PERFORMER_TYPE',
'performerName' => 'GSD_EVENT_PERFORMER_NAME',
'performerURL' => 'GSD_EVENT_PERFORMER_URL'
];
MappingOptions::add($options, $new_options, 'GSD_INTEGRATION', 'gsd.item.');
}
/**
* Remove 3rd party structured data
*
* @return void
*/
public function onAfterRender()
{
// Make sure we are on the right context
if ($this->app->isClient('Administrator') || !$this->passContext() || !$this->params->get('remove_default_schema', true))
{
return;
}
// Remove the most common event-based schemas
$schemas = [
'Event',
'Place',
'PostalAddress',
'GeoCoordinates',
];
\GSD\SchemaCleaner::remove($schemas);
}
} PluginBaseProduct.php 0000644 00000003606 15237362756 0010674 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD;
defined('_JEXEC') or die('Restricted access');
use GSD\PluginBase;
use GSD\MappingOptions;
/**
* Google Structured Data Product Plugin Base
*/
class PluginBaseProduct extends PluginBase
{
/**
* The MapOptions Backend Event. Triggered by the mappingoptions fields to help each integration add its own map options.
*
* @param string $plugin
* @param array $options
*
* @return void
*/
public function onMapOptions($plugin, &$options)
{
if ($plugin != $this->_name)
{
return;
}
$new_options = [
'sku' => 'SKU',
'mpn' => 'MPN',
'brand' => 'GSD_PRODUCT_BRAND_NAME',
'offerprice' => 'GSD_PRODUCT_OFFER_PRICE',
'currency' => 'GSD_PRODUCT_OFFER_CURRENCY',
'offerAvailability' => 'GSD_PRODUCT_AVAILABILITY'
];
MappingOptions::add($options, $new_options, 'GSD_INTEGRATION', 'gsd.item.');
}
/**
* Asks for data from the child plugin based on the active view name
*
* @return Registry The payload Registry
*/
protected function getPayload()
{
if (!$payload = parent::getPayload())
{
return;
}
$schema_prefix = 'https://schema.org/';
// Add offerAvailability property
if (!$payload->offsetExists('offerAvailability'))
{
$available = method_exists($this, 'productIsAvailable') ? $this->productIsAvailable() : true;
$payload->set('offerAvailability', $schema_prefix . ($available ? 'InStock' : 'OutOfStock'));
}
return $payload;
}
} SchemaCleaner.php 0000644 00000013410 15237362756 0007766 0 ustar 00 <?php
/**
* @package Google Structured Data
* @version 5.6.5 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace GSD;
defined('_JEXEC') or die('Restricted Access');
use Joomla\String\StringHelper;
use Joomla\CMS\Factory;
class SchemaCleaner
{
/**
* Remove JSON-LD Structured Data injected by the integration or 3rd party extensions.
*
* Note: This method should be called either in the onAfterRender or in the onAfterInitialize Events when the document's buffer is available.
*
* @param mixed $schema_type Search and remove only specific schema types
* @param mixed $remove_json If true, the JSON-based markup will be removed from the page
* @param mixed $remove_microdata If true, the microdata-based will be removed from the page
*
* @return void
*/
public static function remove($schema_type, $remove_json = true, $remove_microdata = true)
{
if (empty($schema_type))
{
return;
}
$app = Factory::getApplication();
// Get document buffer
$body = $app->getBody();
$replacements_count = 0;
$schema_types = (array) $schema_type;
// Search and remove JSON-LD scripts
if ($remove_json)
{
foreach ($schema_types as $schema_type)
{
$replacements_count += self::removeJSONSchema($body, strtolower($schema_type));
}
}
// Search and remove microdata
if ($remove_microdata)
{
foreach ($schema_types as $schema_type)
{
$replacements_count += self::removeMicrodata($body, strtolower($schema_type));
}
}
// If no replacements made, exit.
if ($replacements_count == 0)
{
return;
}
// Set the new document body back.
$app->setBody($body);
}
/**
* Remove microdata from a string
*
* @param string $text The text to search for
* @param mixed $schema_type Search and remove only specific schema types
*
* @return integer
*/
private static function removeMicrodata(&$text, $schema_type = null)
{
// Simple check to decide whether the plugin should procceed or not.
if (StringHelper::strpos($text, 'itemtype') === false)
{
return;
}
// Base replacement pattern
// We do not include itemprop property here as some components renders the element
// like itemscope itemtype="http://schema.org/" or itemscope="" itemtype="http://schema.org/"
$patterns = ['/(itemscope)? itemtype=(\'|")?http(s?):\/\/(www.)?schema.org\/' . $schema_type . '(\'|")?/msi'];
if ($schema_type == 'all')
{
$patterns = [
'/(itemscope)? itemtype=(\'|")?http(s?):\/\/(.*?)schema.org\/(.*?(\'|"))(\'|")?/msi',
'/<meta(.*?)(itemscope|itemprop)(.*?)\/?>/',
'/itemprop=("|\')(.*?)("|\')/'
];
}
// Extra rules for the Event type
if ($schema_type == 'event')
{
$extra_patterns = [
'/<meta itemprop="(url|startDate|addressRegion|postalCode|latitude|longitude|streetAddress|addressLocality)"[^>]+>/'
];
$patterns = array_merge($patterns, $extra_patterns);
}
// Extra rules for the Article type
if ($schema_type == 'article')
{
$extra_patterns = [
'/itemprop="(url|name|author|headline|image|keywords|articleBody|datePublished)"/',
'/<meta itemprop="(inLanguage|datePublished)"[^>]+>/'
];
$patterns = array_merge($patterns, $extra_patterns);
}
// Extra rules for the Product type
if ($schema_type == 'product')
{
$extra_patterns = [
'/<meta itemprop="(price|priceCurrency)"[^>]+>/',
'/itemprop="(sku|description|offers|name)"/',
'/<link itemprop="availability" href="http(s?):\/\/schema.org\/InStock" \/>/'
];
$patterns = array_merge($patterns, $extra_patterns);
}
// Extra rules for the Breadcrumbs type
if ($schema_type == 'breadcrumblist')
{
$extra_patterns = [
'/itemprop="(itemListElement|position|item)"/',
'/itemscope itemtype="http(s?):\/\/schema.org\/ListItem"/'
];
$patterns = array_merge($patterns, $extra_patterns);
}
// Extra rules for the AggregateRating type
if ($schema_type == 'aggregaterating')
{
$extra_patterns = [
'/itemprop="(aggregateRating|ratingValue|bestRating)"/',
'/<meta itemprop="(ratingCount|bestRating|worstRating)"[^>]+>/',
];
$patterns = array_merge($patterns, $extra_patterns);
}
// Do the replacements and return the number of replacements
$text = preg_replace($patterns, '', $text, -1, $count);
return $count;
}
/**
* Remove JSON-LD scripts from a string
*
* @param string $text The text to search for
* @param mixed $schema_type Search and remove only specific schema types
*
* @return integer
*/
private static function removeJSONSchema(&$text, $schema_type = null)
{
// Simple check to decide whether we should procceed or not.
if (StringHelper::strpos($text, '//schema.org/') === false)
{
return;
}
$re = '/<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/msi';
preg_match_all($re, $text, $matches, PREG_SET_ORDER, 0);
if (!$matches)
{
return;
}
$replacements_count = 0;
foreach ($matches as $match)
{
// Ignore our scripts
if (strpos($match[0], 'data-type="gsd"') !== false)
{
continue;
}
// If we are searching for a specific schema type, make sure it exists.
if (!is_null($schema_type) && !preg_match('/"@type"\s*:\s*"' . $schema_type . '"/si', $match[1]))
{
continue;
}
$text = str_replace($match[0], '', $text);
$replacements_count++;
}
return $replacements_count;
}
}