diff --git a/composer.json b/composer.json index bd4ce0a..86eecba 100644 --- a/composer.json +++ b/composer.json @@ -10,10 +10,10 @@ "email": "nivanka@silverstripers.com" }], "require": { - "silverstripe/framework": "4.* | 5.*", - "silverstripe/reports": "4.* | 5.*", - "silverstripe/assets": "1.* | 2.*", - "silverstripe/asset-admin": "1.* | 2.*", + "silverstripe/framework": "^6", + "silverstripe/reports": "^6", + "silverstripe/assets": "^3", + "silverstripe/asset-admin": "^3", "spatie/schema-org": "3.13.0" }, "extra": { diff --git a/src/Control/RobotsTXTController.php b/src/Control/RobotsTXTController.php index 840418f..4efb819 100644 --- a/src/Control/RobotsTXTController.php +++ b/src/Control/RobotsTXTController.php @@ -24,6 +24,7 @@ public function index() if($siteConfig->RobotsTXT) { return $siteConfig->RobotsTXT; } + if ($siteConfig->RobotsPublishedPagesOnly) { $links = []; $excludeClasses = [ @@ -32,12 +33,14 @@ public function index() foreach (SiteTree::get()->exclude('ClassName', $excludeClasses) as $page) { $links[] = 'Allow: ' . $page->Link(); } + $links = implode("\n", $links); return <<HeadScripts && strpos($body, '') !== false) { - $head = strpos($body, ''); - $before = substr($body, 0, $head); - $after = substr($body, $head + strlen('')); + if($config->HeadScripts && str_contains((string) $body, '')) { + $head = strpos((string) $body, ''); + $before = substr((string) $body, 0, $head); + $after = substr((string) $body, $head + strlen('')); $body = $before . "\n" . $config->HeadScripts . "\n" . '' . "\n" . $after; } // end of body - if($config->BodyStartScripts && strpos($body, '/", $body, $matches); - if (!$matches) { - preg_match("//", $body, $matches); + if($config->BodyStartScripts && str_contains((string) $body, '/", (string) $body, $matches); + if ($matches === []) { + preg_match("//", (string) $body, $matches); } - if($matches) { + + if($matches !== []) { $bodyTag = $matches[0]; - $start = strpos($body, $bodyTag); - $before = substr($body, 0, $start); - $after = substr($body, $start + strlen($bodyTag)); + $start = strpos((string) $body, $bodyTag); + $before = substr((string) $body, 0, $start); + $after = substr((string) $body, $start + strlen($bodyTag)); $body = $before . "\n" . $bodyTag . "\n" . $config->BodyStartScripts . "\n" . $after; } } // end of body - if (strpos($body, '') !== false) { + if (str_contains((string) $body, '')) { /* @var $record SEODataExtension */ $help = false; if (($request->requestVar('structureddata_help') == 1) && ($record = SEODataExtension::get_seo_record())) { $help = $record->getStructuredDataHelpTips(); } + if ($config->BodyEndScripts || $help) { - $bodyEnd = strpos($body, ''); - $before = substr($body, 0, $bodyEnd); - $after = substr($body, $bodyEnd + strlen('')); + $bodyEnd = strpos((string) $body, ''); + $before = substr((string) $body, 0, $bodyEnd); + $after = substr((string) $body, $bodyEnd + strlen('')); $content = $help . "\n" . $config->BodyEndScripts; $body = $before . "\n" . $content . "\n" . '' . "\n" . $after; } } + return $body; } @@ -79,13 +82,13 @@ public function process(HTTPRequest $request, callable $delegate) * @var $response HTTPResponse */ $response = $delegate($request); - $headers = $response->getHeaders(); if($response && ($body = $response->getbody()) && $this->canAddSEOScripts($request, $response)) { $body = $this->processInputs($body, $request); $response->setBody($body); } + return $response; } @@ -95,23 +98,23 @@ private function canAddSEOScripts(HTTPRequest $request, HTTPResponse $response) $headers = $response->getHeaders(); $rules = self::config()->get('exclude_rules'); - if (count($rules)) { + if (count($rules) > 0) { foreach ($rules as $rule) { - if (substr($rule, -1) == '*') { - if (strpos($url, substr($rule, 0, -1)) === 0) { + if (str_ends_with((string) $rule, '*')) { + if (str_starts_with($url, substr((string) $rule, 0, -1))) { return false; } - } elseif (substr($rule, 0, 1) == '*') { - if (substr($url, -1 * strlen(substr($rule, 0, 1))) == substr($rule, 0, 1)) { + } elseif (str_starts_with((string) $rule, '*')) { + if (substr($url, -1) === substr((string) $rule, 0, 1)) { return false; } - } elseif (ltrim($url) == ltrim($rule)) { + } elseif (ltrim($url) === ltrim((string) $rule)) { return false; } } } return isset($headers['content-type']) - && strpos($headers['content-type'], 'text/html;') !== false; + && str_contains($headers['content-type'], 'text/html;'); } } diff --git a/src/Extension/SEODataExtension.php b/src/Extension/SEODataExtension.php index 8785dda..0836a92 100644 --- a/src/Extension/SEODataExtension.php +++ b/src/Extension/SEODataExtension.php @@ -10,9 +10,11 @@ namespace SilverStripers\SEO\Extension; -use JsonLd\Context; -use JsonLd\ContextTypes\AbstractContext; -use JsonLd\ContextTypes\Product; +use SilverStripe\Core\Extension; +use SilverStripe\Forms\FormField; +use SilverStripe\Model\ModelData; +use SilverStripe\Core\Validation\ValidationResult; +use Exception; use SilverStripe\Assets\File; use SilverStripe\Assets\Image; use SilverStripe\CMS\Model\SiteTree; @@ -21,23 +23,17 @@ use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Configurable; -use SilverStripe\Core\Injector\Injector; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\TextareaField; use SilverStripe\Forms\ToggleCompositeField; use SilverStripe\i18n\i18n; -use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; -use SilverStripe\ORM\FieldType\DBDate; -use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBField; -use SilverStripe\ORM\ValidationResult; use SilverStripe\Security\Permission; use SilverStripe\SiteConfig\SiteConfig; use SilverStripe\View\HTML; use SilverStripe\View\Parsers\HTMLValue; -use SilverStripe\View\ViewableData; use SilverStripers\SEO\Fields\SEOEditor; use SilverStripers\SEO\Model\MetaTitleTemplate; use SilverStripers\SEO\Model\Variable; @@ -47,17 +43,34 @@ * Class SEODataExtension * @package SilverStripers\SEO\Extension * - * @property DataObject $owner - * @method MetaTitleTemplate MetaTitleTemplate + * @method DataObject|SEODataExtension getOwner + * @property string $FocusKeyword + * @property string $MetaKeywords + * @property string $MetaTitle + * @property string $MetaDescription + * @property string $FacebookTitle + * @property string $FacebookDescription + * @property string $TwitterTitle + * @property string $TwitterDescription + * @property string $MetaRobotsFollow + * @property string $MetaRobotsIndex + * @property string $CanonicalURL + * @property string $TrackingCodes + * @property int $FacebookImageID + * @property int $TwitterImageID + * @property int $MetaTitleTemplateID + * @method Image FacebookImage + * @method Image TwitterImage + * @method MetaTitleTemplate MetaTitleTemplate */ -class SEODataExtension extends DataExtension +class SEODataExtension extends Extension { use Configurable; - private static $override_seo = null; + private static $override_seo; - private static $seo_record = null; + private static $seo_record; private static $add_self_canoniacal = true; @@ -100,13 +113,13 @@ public function updateCMSFields(FieldList $fields) )); $fields->removeByName($scaffoldFields); - if (!$fields->fieldByName('Root.Main.SEOFields_Container')) { + if (!$fields->fieldByName('Root.Main.SEOFields_Container') instanceof FormField) { $fields->addFieldToTab('Root.Main', ToggleCompositeField::create( 'SEOFields_Container', 'Meta Data & SEO', [ - SEOEditor::create('SEOFields')->setRecord($this->owner) + SEOEditor::create('SEOFields')->setRecord($this->getOwner()) ] ) ); @@ -120,7 +133,7 @@ public function updateSettingsFields(FieldList $fields) ); } - public static function override_seo_from(ViewableData $record) + public static function override_seo_from(ModelData $record) { self::$override_seo = $record; } @@ -149,22 +162,22 @@ public function SEOData() { return [ 'HostName' => Director::host(), - 'FocusKeyword' => $this->owner->FocusKeyword, - 'MetaTitle' => $this->owner->MetaTitle, - 'MetaKeywords' => $this->owner->MetaKeywords, - 'MetaDescription' => $this->owner->MetaDescription, - 'FacebookTitle' => $this->owner->FacebookTitle, - 'FacebookDescription' => $this->owner->FacebookDescription, - 'TwitterTitle' => $this->owner->TwitterTitle, - 'TwitterDescription' => $this->owner->TwitterDescription, - 'MetaRobotsFollow' => $this->owner->MetaRobotsFollow, - 'MetaRobotsIndex' => $this->owner->MetaRobotsIndex, - 'CanonicalURL' => $this->owner->CanonicalURL, - 'FacebookImageID' => $this->owner->FacebookImageID, - 'FacebookImageURL' => $this->owner->FacebookImageID ? $this->owner->FacebookImage()->Link() : null, - 'TwitterImageID' => $this->owner->TwitterImageID, - 'TwitterImageURL' => $this->owner->TwitterImageID ? $this->owner->TwitterImage()->Link() : null, - 'MetaTitleTemplateID' => $this->owner->MetaTitleTemplateID + 'FocusKeyword' => $this->getOwner()->FocusKeyword, + 'MetaTitle' => $this->getOwner()->MetaTitle, + 'MetaKeywords' => $this->getOwner()->MetaKeywords, + 'MetaDescription' => $this->getOwner()->MetaDescription, + 'FacebookTitle' => $this->getOwner()->FacebookTitle, + 'FacebookDescription' => $this->getOwner()->FacebookDescription, + 'TwitterTitle' => $this->getOwner()->TwitterTitle, + 'TwitterDescription' => $this->getOwner()->TwitterDescription, + 'MetaRobotsFollow' => $this->getOwner()->MetaRobotsFollow, + 'MetaRobotsIndex' => $this->getOwner()->MetaRobotsIndex, + 'CanonicalURL' => $this->getOwner()->CanonicalURL, + 'FacebookImageID' => $this->getOwner()->FacebookImageID, + 'FacebookImageURL' => $this->getOwner()->FacebookImageID ? $this->getOwner()->FacebookImage()->Link() : null, + 'TwitterImageID' => $this->getOwner()->TwitterImageID, + 'TwitterImageURL' => $this->getOwner()->TwitterImageID ? $this->getOwner()->TwitterImage()->Link() : null, + 'MetaTitleTemplateID' => $this->getOwner()->MetaTitleTemplateID ]; } @@ -172,34 +185,34 @@ public function MetaTagCollection($raw = false) { $tags = []; $siteConfig = SiteConfig::current_site_config(); - $record = SEODataExtension::get_override() ? : $this->owner; + $record = SEODataExtension::get_override() ? : $this->getOwner(); self::set_seo_record($record); if($metaTitle = $record->ComputeMetaTitle()) { $tags['title'] = $raw ? $metaTitle : HTML::createTag('title', [], $metaTitle); - $tags['meta_title'] = $raw ? $metaTitle : HTML::createTag('meta', array( + $tags['meta_title'] = $raw ? $metaTitle : HTML::createTag('meta', [ 'name' => 'title', 'content' => $metaTitle, - )); + ]); } if ($record->obj('MetaKeywords')->getValue()) { - $tags['keywords'] = $raw ? $record->obj('MetaKeywords')->getValue() : HTML::createTag('meta', array( + $tags['keywords'] = $raw ? $record->obj('MetaKeywords')->getValue() : HTML::createTag('meta', [ 'name' => 'keywords', 'content' => $record->obj('MetaKeywords'), - )); + ]); } if ($metaDescription = $record->ComputeMetaDescription()) { - $tags['meta_description'] = $raw ? $metaDescription : HTML::createTag('meta', array( + $tags['meta_description'] = $raw ? $metaDescription : HTML::createTag('meta', [ 'name' => 'description', 'content' => $metaDescription, - )); + ]); } - if (ClassInfo::exists('SilverStripe\CMS\Model\SiteTree')) { - $generator = trim(Config::inst()->get(SiteTree::class, 'meta_generator')); - if (!empty($generator)) { + if (ClassInfo::exists(SiteTree::class)) { + $generator = trim((string) Config::inst()->get(SiteTree::class, 'meta_generator')); + if ($generator !== '' && $generator !== '0') { $tags['generator'] = $raw ? $generator : HTML::createTag('meta', [ 'name' => 'generator', 'content' => $generator, @@ -214,17 +227,17 @@ public function MetaTagCollection($raw = false) ]); $robots = []; - if(SiteConfig::current_site_config()->DisableSearchEngineVisibility) { - $robots[] = 'noindex'; - } - else if($record->MetaRobotsIndex) { - $robots[] = $record->MetaRobotsIndex; - } + if (SiteConfig::current_site_config()->DisableSearchEngineVisibility) { + $robots[] = 'noindex'; + } elseif ($record->MetaRobotsIndex) { + $robots[] = $record->MetaRobotsIndex; + } + if($record->MetaRobotsFollow) { $robots[] = $record->MetaRobotsFollow; } - if(!empty($robots)) { + if($robots !== []) { $tags['robots'] = $raw ? implode(',', $robots) : HTML::createTag('meta', [ 'name' => 'robots', 'content' => implode(',', $robots) @@ -273,6 +286,7 @@ public function MetaTagCollection($raw = false) } elseif (!$raw) { $facebookTitle = MetaTitleTemplate::parse_meta_title($record, $facebookTitle); } + if ($facebookTitle) { $tags['og:title'] = $raw ? $facebookTitle : HTML::createTag('meta', [ 'property' => 'og:title', @@ -284,6 +298,7 @@ public function MetaTagCollection($raw = false) if (!$fbDescription) { $fbDescription = $metaDescription; } + if ($fbDescription) { $tags['og:description'] = $raw ? $fbDescription : HTML::createTag('meta', [ 'property' => 'og:description', @@ -308,13 +323,13 @@ public function MetaTagCollection($raw = false) if (!$fbImage->exists()) { $fbImage = $record->getDefaultImage(); } - if($fbImage && $fbImage->exists()) { - $tags['og:image'] = $raw ? $fbImage->AbsoluteLink() : HTML::createTag('meta', [ + + if ($fbImage && $fbImage->exists()) { + $tags['og:image'] = $raw ? $fbImage->AbsoluteLink() : HTML::createTag('meta', [ 'property' => 'og:image', 'content' => $fbImage->AbsoluteLink() ]); - } - else if ($siteConfig->GlobalSocialSharingImage()->exists()) { + } elseif ($siteConfig->GlobalSocialSharingImage()->exists()) { $tags['og:image'] = $raw ? $siteConfig->GlobalSocialSharingImage()->AbsoluteLink() : HTML::createTag('meta', [ 'property' => 'og:image', 'content' => $siteConfig->GlobalSocialSharingImage()->AbsoluteLink() @@ -334,6 +349,7 @@ public function MetaTagCollection($raw = false) } elseif (!$raw) { $twTitle = MetaTitleTemplate::parse_meta_title($record, $twTitle); } + if ($twTitle) { $tags['twitter:title'] = $raw ? $twTitle : HTML::createTag('meta', [ 'name' => 'twitter:title', @@ -346,6 +362,7 @@ public function MetaTagCollection($raw = false) if (!$twDescription) { $twDescription = $metaDescription; } + if($twDescription) { $tags['twitter:description'] = $raw ? $twDescription : HTML::createTag('meta', [ 'name' => 'twitter:description', @@ -357,6 +374,7 @@ public function MetaTagCollection($raw = false) if (!$twImage->exists()) { $twImage = $record->getDefaultImage(); } + if ($twImage && $twImage->exists()) { $tags['twitter:image'] = $raw ? $twImage->AbsoluteLink() : HTML::createTag('meta', [ 'name' => 'twitter:image', @@ -383,23 +401,26 @@ public function GenerateMetaTags() if (is_array($tags)) { $tags = implode("\n", $tags); } + $tags = Variable::process_varialbes($tags); if ($structuredData = $this->StructuredData()) { $tags .= "\n" . $structuredData . "\n"; } - $this->owner->extend('MetaTags', $tags); + + $this->getOwner()->extend('MetaTags', $tags); return $tags; } public function getDefaultImage() { - $relation = $this->owner->config()->get('default_seo_image'); + $relation = $this->getOwner()->config()->get('default_seo_image'); if ($relation) { - $image = $this->owner->getComponent($relation); + $image = $this->getOwner()->getComponent($relation); if ($image && $image->exists()) { return $image; } } + return null; } @@ -419,7 +440,7 @@ public function updateStatusFlags(&$flags) } foreach ($scores as $type => $score) { - if ($score) { + if ($score !== 0) { $flags['seo' . $type] = [ 'text' => $score > 9 ? '9+' : $score, 'title' => $score . ' ' . $type . 's' @@ -433,9 +454,10 @@ public function updateStatusFlags(&$flags) public function getOGPostType() { - if(method_exists($this->owner, 'getOGPostType')) { - return $this->owner->getOGPostType(); + if(method_exists($this->getOwner(), 'getOGPostType')) { + return $this->getOwner()->getOGPostType(); } + return 'article'; } @@ -445,13 +467,9 @@ public static function get_duplicates_list(DataList $list) $items = []; foreach ($list as $duplicate) { $link = method_exists($duplicate, 'Link') ? $duplicate->Link() : null; - if($link) { - $items[] = '' . $duplicate->getTitle() . ''; - } - else { - $items[] = $duplicate->getTitle(); - } + $items[] = $link ? '' . $duplicate->getTitle() . '' : $duplicate->getTitle(); } + return implode(",\n ", $items); } @@ -460,26 +478,27 @@ public static function get_duplicates_list(DataList $list) // public function validateKeyword(ValidationResult $result) { - $record = $this->owner; + $record = $this->getOwner(); $keyword = $record->FocusKeyword; if(empty($keyword)) { $result->addFieldError('FocusKeyword', - _t(__CLASS__.'.FocusKeywordEmpty', + _t(self::class.'.FocusKeywordEmpty', 'No focus keyword was set for this page. If you do not set a focus keyword, no score can be calculated.'), ValidationResult::TYPE_ERROR); } else { - $duplicates = DataList::create(get_class($this->owner)) + $duplicates = DataList::create($this->getOwner()::class) ->exclude('ID', $record->ID) ->filter('FocusKeyword', $keyword); if ($duplicates->count()) { $items = self::get_duplicates_list($duplicates); - $result->addFieldError('FocusKeyword', sprintf(_t(__CLASS__.'.FocusKeywordIsNotUnique', - 'This keyword is not unique. It is also used by \'%s\''), $items), + $result->addFieldError('FocusKeyword', sprintf(_t(self::class.'.FocusKeywordIsNotUnique', + "This keyword is not unique. It is also used by '%s'"), $items), ValidationResult::TYPE_ERROR, null, ValidationResult::CAST_HTML); } + if ($result->isValid()) { - $result->addFieldMessage('FocusKeyword', _t(__CLASS__.'.FocusKeywordPassed', + $result->addFieldMessage('FocusKeyword', _t(self::class.'.FocusKeywordPassed', 'This keyword is not used by any other pages on this site'), ValidationResult::TYPE_GOOD); } } @@ -487,110 +506,115 @@ public function validateKeyword(ValidationResult $result) public function validateMetaTitle(ValidationResult $result) { - $record = $this->owner; + $record = $this->getOwner(); if(empty($record->MetaTitle)) { $result->addFieldError('MetaTitle', - sprintf(_t(__CLASS__.'.MetaTitleEmpty', + sprintf(_t(self::class.'.MetaTitleEmpty', 'You have not set a meta title. The title will default to "%s"'), $record->getTitle()), ValidationResult::TYPE_WARNING); } else { $metaTitle = $this->ComputeMetaTitle(); - if ($record->FocusKeyword && stripos($metaTitle, $record->FocusKeyword) === false) { + if ($record->FocusKeyword && stripos((string) $metaTitle, (string) $record->FocusKeyword) === false) { $result->addFieldError('MetaTitle', - sprintf(_t(__CLASS__.'.MetaTitleNoKeyword', + sprintf(_t(self::class.'.MetaTitleNoKeyword', 'The focus keyword "%s" does not appear in the SEO title.'), $record->FocusKeyword), ValidationResult::TYPE_ERROR); } - if (strlen($metaTitle) < 45) { - $result->addFieldError('MetaTitle', - _t(__CLASS__.'.MetaTitleTooShort', + + if (strlen((string) $metaTitle) < 45) { + $result->addFieldError('MetaTitle', + _t(self::class.'.MetaTitleTooShort', 'The SEO title is too short. Use the space to add keyword variations or create compelling call-to-action copy.'), ValidationResult::TYPE_WARNING); - } else if (strlen($metaTitle) > 70) { - $result->addFieldError('MetaTitle', - _t(__CLASS__.'.MetaTitleTooLong', 'The SEO title is over 70 characters and may be truncated on search + } elseif (strlen((string) $metaTitle) > 70) { + $result->addFieldError('MetaTitle', + _t(self::class.'.MetaTitleTooLong', 'The SEO title is over 70 characters and may be truncated on search results pages'), ValidationResult::TYPE_WARNING); - } else { + } else { $result->addFieldMessage('MetaTitle', - _t(__CLASS__.'.MetaTitleLengthGood', 'The SEO title has a nice length.') , ValidationResult::TYPE_GOOD); + _t(self::class.'.MetaTitleLengthGood', 'The SEO title has a nice length.') , ValidationResult::TYPE_GOOD); } - $duplicates = DataList::create(get_class($record)) + $duplicates = DataList::create($record::class) ->exclude('ID', $record->ID) ->filter('MetaTitle', $record->MetaTitle); if ($duplicates->count()) { $items = self::get_duplicates_list($duplicates); $result->addFieldError('MetaTitle', - sprintf(_t(__CLASS__.'.MetaTitleDuplicated', + sprintf(_t(self::class.'.MetaTitleDuplicated', 'This title is not unique. It is also used by %s'), $items), ValidationResult::TYPE_ERROR, null, ValidationResult::CAST_HTML); } else { $result->addFieldMessage('MetaTitle', - _t(__CLASS__.'.MetaTitleUnique', + _t(self::class.'.MetaTitleUnique', 'This title is not used by any other pages on this site'), ValidationResult::TYPE_GOOD); } } + return $result; } public function validateMetaDescription(ValidationResult $result) { - $record = $this->owner; + $record = $this->getOwner(); $desc = $record->MetaDescription; if(empty($desc)) { - $result->addFieldError('MetaDescription', _t(__CLASS__.'.MetaDescriptionEmpty', + $result->addFieldError('MetaDescription', _t(self::class.'.MetaDescriptionEmpty', 'No meta description has been specified.'), ValidationResult::TYPE_ERROR); } else { $desc = $this->ComputeMetaDescription(); - if ($record->FocusKeyword && stripos($desc, $record->FocusKeyword) === false) { + if ($record->FocusKeyword && stripos((string) $desc, (string) $record->FocusKeyword) === false) { $result->addFieldError('MetaDescription', - _t(__CLASS__.'.MetaDescriptionNoKeyword','The meta description does not contain the focus keyword.'), + _t(self::class.'.MetaDescriptionNoKeyword','The meta description does not contain the focus keyword.'), ValidationResult::TYPE_ERROR); } - if (strlen($desc) < 120) { - $result->addFieldError('MetaDescription', - _t(__CLASS__.'.MetaDescriptionTooShort', + + if (strlen((string) $desc) < 120) { + $result->addFieldError('MetaDescription', + _t(self::class.'.MetaDescriptionTooShort', 'The meta description is under 120 characters long. However, up to 156 characters are available.'), ValidationResult::TYPE_WARNING); - } else if (strlen($desc) > 156) { - $result->addFieldError('MetaDescription', - _t(__CLASS__.'.MetaDescriptionTooLong', + } elseif (strlen((string) $desc) > 156) { + $result->addFieldError('MetaDescription', + _t(self::class.'.MetaDescriptionTooLong', 'The meta description is over 156 characters. Reducing the length will ensure the entire description will be visible.'), ValidationResult::TYPE_WARNING); - } else { + } else { $result->addFieldMessage('MetaDescription', - _t(__CLASS__.'.MetaDescriptionGoodLength', 'The length of the meta description is sufficient.'), + _t(self::class.'.MetaDescriptionGoodLength', 'The length of the meta description is sufficient.'), ValidationResult::TYPE_GOOD); } - $duplicates = DataList::create(get_class($record)) + + $duplicates = DataList::create($record::class) ->exclude('ID', $record->ID) ->filter('MetaDescription', $record->MetaDescription); if ($duplicates->count()) { $items = self::get_duplicates_list($duplicates); $result->addFieldError('MetaDescription', - sprintf(_t(__CLASS__.'.MetaDescriptionGoodLength', 'This description is not unique. It is also used by %s'), $items), + sprintf(_t(self::class.'.MetaDescriptionGoodLength', 'This description is not unique. It is also used by %s'), $items), ValidationResult::TYPE_ERROR, null, ValidationResult::CAST_HTML); } else { $result->addFieldMessage('MetaDescription', - _t(__CLASS__.'.MetaDescriptionUnique', 'This description is unique to this page'), + _t(self::class.'.MetaDescriptionUnique', 'This description is unique to this page'), ValidationResult::TYPE_GOOD); } } + return $result; } /** - * @return ValidationResult - */ - public function validateSEO() + * @return ValidationResult + */ + public function validateSEO() { - $results = new ValidationResult(); + $results = ValidationResult::create(); $this->validateKeyword($results); $this->validateMetaTitle($results); $this->validateMetaDescription($results); @@ -607,10 +631,12 @@ public function getSEOComments($type = null) $errors[] = $comment['message']; } } + $htmlText = HTMLValue::create(); $htmlText->setContent(implode(', ', $errors)); return $htmlText; } + return null; } @@ -621,19 +647,20 @@ public function getSEOErrors() public function TrackingCodesHTML() { - return DBField::create_field('HTMLText', $this->owner->TrackingCodes); + return DBField::create_field('HTMLText', $this->getOwner()->TrackingCodes); } public function StructuredData() { - if ($context = $this->getStructuredDataContext()) { + if (($context = $this->getStructuredDataContext()) instanceof BaseType) { return $context->toScript(); } + return null; } public function getSchemeType() { - $owner = $this->owner; + $owner = $this->getOwner(); $shemaType = $owner->config()->get('schema_type'); return $shemaType ?? 'Article'; } @@ -643,11 +670,12 @@ public function getStructuredDataTypeObject($shemaType = null) : ?BaseType if (!$shemaType) { $shemaType = 'Thing'; } + $className = 'Spatie\\SchemaOrg\\' . $shemaType; if (class_exists($className)) { return new $className(); } else { - throw new \Exception( + throw new Exception( sprintf('Type %s is not found within the Schema.org types', $shemaType) ); } @@ -655,7 +683,7 @@ public function getStructuredDataTypeObject($shemaType = null) : ?BaseType private function getStructuredDataProperties() { - return ($type = $this->getStructuredDataContext()) + return (($type = $this->getStructuredDataContext()) instanceof BaseType) ? $type->getProperties() : null; } @@ -663,20 +691,22 @@ private function getStructuredDataProperties() private function parseSchemaDataField($mapping, $record = null) { if (!$record) { - $record = $this->owner; + $record = $this->getOwner(); } - if (substr($mapping, 0, 1) == '`' && substr($mapping, -1) == '`') { // is a value - $val = trim($mapping, '`'); + + if (str_starts_with((string) $mapping, '`') && str_ends_with((string) $mapping, '`')) { // is a value + $val = trim((string) $mapping, '`'); } elseif ($mapping == 'NULL') { // use NULL as a keyword $val = null; - } elseif (strpos($mapping, '.')) { // dot functions - $partials = explode('.', $mapping); + } elseif (strpos((string) $mapping, '.')) { // dot functions + $partials = explode('.', (string) $mapping); $currentRecord = $record; foreach ($partials as $partialMapping) { if (is_object($currentRecord)) { $currentRecord = $this->parseSchemaDataField($partialMapping, $currentRecord); } } + $val = $currentRecord; } elseif (ClassInfo::hasMethod($record, $mapping)) { $val = call_user_func_array([ @@ -690,6 +720,7 @@ private function parseSchemaDataField($mapping, $record = null) if ($val && is_a($val, File::class)) { $val = $val->AbsoluteLink(); } + return $val; } @@ -705,23 +736,20 @@ private function processSchemaFields(BaseType $schema, $data) ] ); } else { - call_user_func_array( - [$schema, 'setProperty'], - [ - $property, - $this->parseSchemaDataField($value) - ] - ); + $schema->setProperty(...[ + $property, + $this->parseSchemaDataField($value) + ]); } - } else if (is_array($value)) { // this is a reference type + } elseif (is_array($value)) { + // this is a reference type if (empty($value['@type'])) { - throw new \Exception( + throw new Exception( sprintf('No type provided for the reference values use @type for "%s"', $property) ); } $referenceSchema = $this->getStructuredDataTypeObject($value['@type']); $this->processSchemaFields($referenceSchema, $value['schema']); - if (method_exists($schema, $property)) { call_user_func_array( [$schema, $property], @@ -737,7 +765,6 @@ private function processSchemaFields(BaseType $schema, $data) ] ); } - } } } @@ -748,44 +775,49 @@ private function processSchemaFields(BaseType $schema, $data) */ private function getStructuredDataContext() : ?BaseType { - /* @var $owner ViewableData */ - $owner = $this->owner; + $owner = $this->getOwner(); if ($shemaType = $this->getSchemeType()) { $map = $owner->config()->get('schema', Config::UNINHERITED); if (!$map) { $map = $owner->config()->get('schema'); } + if (!$map) { $map = []; } + $schema = $this->getStructuredDataTypeObject($shemaType); $this->processSchemaFields($schema, $map); return $schema; } + return null; } public function ComputeMetaTitle() { - $record = $this->owner; + $record = $this->getOwner(); $metaTitle = $record->obj('MetaTitle')->forTemplate(); if (!$metaTitle) { $metaTitle = $record->obj('Title')->forTemplate(); } + $record->invokeWithExtensions('updateMetaTitle', $metaTitle); if($metaTitle) { - $metaTitle = MetaTitleTemplate::parse_meta_title($this->owner, $metaTitle); + $metaTitle = MetaTitleTemplate::parse_meta_title($this->getOwner(), $metaTitle); } + return $metaTitle; } public function ComputeMetaDescription() { - $record = $this->owner; + $record = $this->getOwner(); $metaDescription = $record->obj('MetaDescription')->getValue(); if (!$metaDescription && ($fallbackField = $record->config()->get('fallback_meta_description')) && $record->obj($fallbackField)) { $metaDescription = $record->dbObject($fallbackField)->forTemplate(); } + $record->invokeWithExtensions('updateMetaDescription', $metaDescription); return Variable::process_varialbes($metaDescription); } @@ -797,29 +829,28 @@ public function getStructuredDataHelpTips() unset($types['@context']); unset($types['@type']); - $owner = $this->owner; + $owner = $this->getOwner(); $map = $owner->config()->get('schema'); $schemaType = $owner->config()->get('schema_type'); - $class = get_class($owner); + $class = $owner::class; $settings = ''; foreach ($types as $type => $val) { - $val = !empty($map[$type]) ? $map[$type] : ''; + $val = empty($map[$type]) ? '' : $map[$type]; $settings .= " {$type}: " . $val . "\n"; } - $html = <<
-$owner:
-    schema_type: '$schemaType'
+{$owner}:
+    schema_type: '{$schemaType}'
     schema:
 {$settings}
 
HTML; - - return $html; } + return null; } } diff --git a/src/Extension/SiteConfigExtension.php b/src/Extension/SiteConfigExtension.php index a97fdf1..9712b5d 100644 --- a/src/Extension/SiteConfigExtension.php +++ b/src/Extension/SiteConfigExtension.php @@ -9,21 +9,35 @@ namespace SilverStripers\SEO\Extension; +use SilverStripe\Core\Extension; use SilverStripe\AssetAdmin\Forms\UploadField; use SilverStripe\Assets\Image; use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor; -use SilverStripe\Forms\HeaderField; use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\TextareaField; use SilverStripe\Forms\TextField; -use SilverStripe\ORM\DataExtension; +use SilverStripe\SiteConfig\SiteConfig; use SilverStripers\SEO\Model\MetaTitleTemplate; use SilverStripers\SEO\Model\Variable; -class SiteConfigExtension extends DataExtension +/** + * @property SiteConfig $owner + * @property bool $DisableSearchEngineVisibility + * @property string $TwitterUsername + * @property string $FacebookAdmin + * @property string $FacebookAppID + * @property string $HeadScripts + * @property string $BodyStartScripts + * @property string $BodyEndScripts + * @property string $RobotsTXT + * @property bool $RobotsPublishedPagesOnly + * @property string $DefaultMetaTitle + * @method Image GlobalSocialSharingImage + */ +class SiteConfigExtension extends Extension { private static $db = [ @@ -97,6 +111,6 @@ public function updateCMSFields(FieldList $fields) ->setConfig(GridFieldConfig_RecordEditor::create()) ]); - $this->owner->invokeWithExtensions('updateSEOFields', $fields); + $this->getOwner()->invokeWithExtensions('updateSEOFields', $fields); } } diff --git a/src/Fields/SEOEditor.php b/src/Fields/SEOEditor.php index 3412b25..2240a5e 100644 --- a/src/Fields/SEOEditor.php +++ b/src/Fields/SEOEditor.php @@ -40,12 +40,17 @@ class SEOEditor extends FormField 'duplicatecheck' ]; - private $record = null; + private $record; + private $enableSettings = true; + private $enableSEOImages = true; - private $fallBackImage = null; - private $singular_name = null; - private $plural_name = null; + + private $fallBackImage; + + private $singular_name; + + private $plural_name; public function __construct($name, $title = null, $value = null, $record = null) { @@ -117,6 +122,7 @@ public function getSingularName() if ($this->singular_name) { return $this->singular_name; } + return $this->record->i18n_singular_name(); } @@ -125,6 +131,7 @@ public function getPluralName() if ($this->plural_name) { return $this->plural_name; } + return $this->record->i18n_plural_name(); } @@ -138,6 +145,7 @@ public function getSEOJSON() } } } + return json_encode($data); } @@ -157,10 +165,10 @@ public function getVariableMetaTitlesJSONAttr() return Convert::raw2att(json_encode(MetaTitleTemplate::meta_titles())); } - public function Field($properties = array()) + public function Field($properties = []) { Requirements::javascript('silverstripers/seo:client/dist/js/bundle.js'); - Requirements::add_i18n_javascript('silverstripers/seo:client/lang', false, true); + Requirements::add_i18n_javascript('silverstripers/seo:client/lang', false); return parent::Field($properties); } @@ -169,14 +177,14 @@ public function getRecordLink() if($this->record && method_exists($this->record, 'AbsoluteLink')) { return $this->record->AbsoluteLink(); } + return Director::absoluteBaseURL(); } public function getFields() { $seoData = $this->record->SEOData(); - $fields = array_keys($seoData); - return $fields; + return array_keys($seoData); } public function isSavable() @@ -186,15 +194,16 @@ public function isSavable() return true; } } + return false; } - public function saveInto(DataObjectInterface $record) + public function saveInto(DataObjectInterface $record) { if($this->isSavable()) { foreach($this->getFields() as $fieldName) { if (isset($this->value[$fieldName])) { - $record->setCastedField($fieldName, !empty($this->value[$fieldName]) ? $this->value[$fieldName] : null); + $record->setCastedField($fieldName, empty($this->value[$fieldName]) ? null : $this->value[$fieldName]); } } } @@ -220,7 +229,7 @@ public function duplicatecheck() ]; if($this->record && $this->request->requestVar('Field') && $this->request->requestVar('Needle')) { $result['checked'] = 1; - $list = DataList::create(get_class($this->record)) + $list = DataList::create($this->record::class) ->filter($this->request->requestVar('Field') . ':PartialMatch', $this->request->requestVar('Needle')) ->exclude('ID', $this->record->ID); if($list->count()) { @@ -228,6 +237,7 @@ public function duplicatecheck() $result['duplicates'] = SEODataExtension::get_duplicates_list($list); } } + return json_encode($result); } diff --git a/src/Model/MetaTitleTemplate.php b/src/Model/MetaTitleTemplate.php index c633bee..ce605f2 100644 --- a/src/Model/MetaTitleTemplate.php +++ b/src/Model/MetaTitleTemplate.php @@ -43,6 +43,7 @@ public function getCMSFields() ], array_keys(Variable::get_sort_variables())); $valueField->setDescription('

Options to choose from:
{' . implode('}
{', $vars) . '}

'); } + return $fields; } @@ -62,9 +63,11 @@ public static function parse_meta_title($record, $title) if ($template && $template->exists()) { $titleTemplate = $template->Value; } + if (!$titleTemplate) { $titleTemplate = self::get_default_title(); } + return Variable::process_varialbes($titleTemplate, [ 'MetaTitle' => $title ]); @@ -84,6 +87,7 @@ public static function meta_titles() 'value' => $var->Value ]; } + return $ret; } } diff --git a/src/Model/Variable.php b/src/Model/Variable.php index 4afad3f..7a9e23e 100644 --- a/src/Model/Variable.php +++ b/src/Model/Variable.php @@ -29,7 +29,7 @@ class Variable extends DataObject 'Value' ]; - private static $vars = null; + private static $vars; public static function get_sort_variables() { @@ -41,8 +41,10 @@ public static function get_sort_variables() foreach (self::get() as $var) { $ret[$var->Name] = $var->Value; } + self::$vars = $ret; } + return self::$vars; } @@ -51,10 +53,12 @@ public static function process_varialbes($text, $options = []) if (is_null($text)) { $text = ''; } + $vars = array_merge($options, self::get_sort_variables()); foreach ($vars as $name => $val) { $text = str_replace('{' . $name . '}', $val, $text); } + return $text; } } diff --git a/src/reporting/SEOReport.php b/src/Reporting/SEOReport.php similarity index 71% rename from src/reporting/SEOReport.php rename to src/Reporting/SEOReport.php index 38aa295..f77e400 100644 --- a/src/reporting/SEOReport.php +++ b/src/Reporting/SEOReport.php @@ -10,12 +10,12 @@ namespace SilverStripers\SEO\Reporting; +use SilverStripe\Model\List\ArrayList; use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Core\ClassInfo; use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\TextField; -use SilverStripe\ORM\ArrayList; use SilverStripe\Reports\Report; use SilverStripe\Versioned\Versioned; @@ -23,30 +23,22 @@ class SEOReport extends Report { - public function title() + public function title() { - return _t(__CLASS__.'.SEOReport', 'SEO report'); + return _t(self::class.'.SEOReport', 'SEO report'); } public function group() { - return _t(__CLASS__.'.SEOReportGroup', 'SEO reports'); + return _t(self::class.'.SEOReportGroup', 'SEO reports'); } public function getParameterFields() { - return new FieldList( - TextField::create('Title'), - TextField::create('MetaTitle'), - TextField::create('MetaDescription'), - CheckboxField::create('DuplicatedOnes', 'Show only duplicate titles'), - CheckboxField::create('EmptyMetaTitles', 'Show only empty meta titles'), - CheckboxField::create('EmptyMetaDescriptions', 'Show only empty meta descriptions'), - CheckboxField::create('OnLive', 'Check live site') - ); + return FieldList::create(TextField::create('Title'), TextField::create('MetaTitle'), TextField::create('MetaDescription'), CheckboxField::create('DuplicatedOnes', 'Show only duplicate titles'), CheckboxField::create('EmptyMetaTitles', 'Show only empty meta titles'), CheckboxField::create('EmptyMetaDescriptions', 'Show only empty meta descriptions'), CheckboxField::create('OnLive', 'Check live site')); } - public function columns() + public function columns() { return [ 'Title' => [ @@ -71,7 +63,7 @@ public function columns() public function sourceRecords($params = null) { - if (ClassInfo::exists('SilverStripe\\CMS\\Model\\SiteTree')) { + if (ClassInfo::exists(SiteTree::class)) { $stage = isset($params['OnLive']) ? Versioned::LIVE : Versioned::DRAFT; $list = Versioned::get_by_stage(SiteTree::class, $stage); @@ -93,6 +85,7 @@ public function sourceRecords($params = null) return $list; } + return ArrayList::create(); }