diff options
author | Matthieu Aubry <matt@piwik.org> | 2015-02-12 23:44:44 +0300 |
---|---|---|
committer | Matthieu Aubry <matt@piwik.org> | 2015-02-12 23:44:44 +0300 |
commit | c9770a9faa6c76e3d6c06584f3757bc43b140857 (patch) | |
tree | 6e87297ea77970bff426a233d62cc2bc7d941a8c /plugins | |
parent | 620c6f3da261995f3ea1062691c212f65137ee0a (diff) | |
parent | 7eb49e23c93c6e37ec73bee6f3ee48cfcdeea74c (diff) |
Merge pull request #7174 from piwik/seo-metrics
Refactored the SEO plugin to make it extensible and error-proof
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/SEO/API.php | 117 | ||||
-rw-r--r-- | plugins/SEO/Metric/Aggregator.php | 63 | ||||
-rw-r--r-- | plugins/SEO/Metric/Alexa.php | 51 | ||||
-rw-r--r-- | plugins/SEO/Metric/Bing.php | 54 | ||||
-rw-r--r-- | plugins/SEO/Metric/Dmoz.php | 61 | ||||
-rw-r--r-- | plugins/SEO/Metric/DomainAge.php | 141 | ||||
-rw-r--r-- | plugins/SEO/Metric/Google.php | 170 | ||||
-rw-r--r-- | plugins/SEO/Metric/Majestic.php (renamed from plugins/SEO/MajesticClient.php) | 48 | ||||
-rw-r--r-- | plugins/SEO/Metric/Metric.php | 145 | ||||
-rw-r--r-- | plugins/SEO/Metric/MetricsProvider.php | 21 | ||||
-rw-r--r-- | plugins/SEO/Metric/ProviderCache.php | 48 | ||||
-rw-r--r-- | plugins/SEO/RankChecker.php | 378 | ||||
-rw-r--r-- | plugins/SEO/Widgets.php | 3 | ||||
-rw-r--r-- | plugins/SEO/templates/getRank.twig | 15 |
14 files changed, 835 insertions, 480 deletions
diff --git a/plugins/SEO/API.php b/plugins/SEO/API.php index c57aea2156..fdadbf518f 100644 --- a/plugins/SEO/API.php +++ b/plugins/SEO/API.php @@ -4,25 +4,28 @@ * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * */ + namespace Piwik\Plugins\SEO; -use Piwik\Cache; use Piwik\DataTable; use Piwik\Piwik; +use Piwik\Plugins\SEO\Metric\Aggregator; +use Piwik\Plugins\SEO\Metric\Metric; +use Piwik\Plugins\SEO\Metric\ProviderCache; +use Piwik\Url; /** * @see plugins/Referrers/functions.php - * @method static \Piwik\Plugins\SEO\API getInstance() + * @method static API getInstance() */ require_once PIWIK_INCLUDE_PATH . '/plugins/Referrers/functions.php'; /** - * The SEO API lets you access a list of SEO metrics for the specified URL: Google Pagerank, Goolge/Bing indexed pages + * The SEO API lets you access a list of SEO metrics for the specified URL: Google PageRank, Google/Bing indexed pages * Alexa Rank, age of the Domain name and count of DMOZ entries. * - * @method static \Piwik\Plugins\SEO\API getInstance() + * @method static API getInstance() */ class API extends \Piwik\Plugin\API { @@ -36,93 +39,37 @@ class API extends \Piwik\Plugin\API { Piwik::checkUserHasSomeViewAccess(); - $data = $this->getCachedRanks($url); - - $translated = array(); - if (!empty($data)) { - foreach ($data as $title => $rank) { - $translated[Piwik::translate($title)] = $rank; - } - } + $metricProvider = new ProviderCache(new Aggregator()); + $domain = Url::getHostFromUrl($url); + $metrics = $metricProvider->getMetrics($domain); - return DataTable::makeFromIndexedArray($translated); + return $this->toDataTable($metrics); } - private function getCachedRanks($url) - { - $cacheId = 'SEO_getRank_' . md5($url); - - $cache = Cache::getLazyCache(); - $data = $cache->fetch($cacheId); - - if (empty($data)) { - $data = $this->getRanks($url); - - $cache->save($cacheId, $data, 60 * 60 * 6); - } - - return $data; - } - - private function getRanks($url) + /** + * @param Metric[] $metrics + * @return DataTable + */ + private function toDataTable(array $metrics) { - $rank = new RankChecker($url); - - $linkToMajestic = MajesticClient::getLinkForUrl($url); - - $data = array( - 'Google PageRank' => array( - 'rank' => $rank->getPageRank(), - 'logo' => \Piwik\Plugins\Referrers\getSearchEngineLogoFromUrl('http://google.com'), - 'id' => 'pagerank' - ), - 'SEO_Google_IndexedPages' => array( - 'rank' => $rank->getIndexedPagesGoogle(), - 'logo' => \Piwik\Plugins\Referrers\getSearchEngineLogoFromUrl('http://google.com'), - 'id' => 'google-index', - ), - 'SEO_Bing_IndexedPages' => array( - 'rank' => $rank->getIndexedPagesBing(), - 'logo' => \Piwik\Plugins\Referrers\getSearchEngineLogoFromUrl('http://bing.com'), - 'id' => 'bing-index', - ), - 'SEO_AlexaRank' => array( - 'rank' => $rank->getAlexaRank() . '', - 'logo' => \Piwik\Plugins\Referrers\getSearchEngineLogoFromUrl('http://alexa.com'), - 'id' => 'alexa', - ), - 'SEO_DomainAge' => array( - 'rank' => $rank->getAge(), - 'logo' => 'plugins/SEO/images/whois.png', - 'id' => 'domain-age', - ), - 'SEO_ExternalBacklinks' => array( - 'rank' => $rank->getExternalBacklinkCount(), - 'logo' => 'plugins/SEO/images/majesticseo.png', - 'logo_link' => $linkToMajestic, - 'logo_tooltip' => Piwik::translate('SEO_ViewBacklinksOnMajesticSEO'), - 'id' => 'external-backlinks', - ), - 'SEO_ReferrerDomains' => array( - 'rank' => $rank->getReferrerDomainCount(), - 'logo' => 'plugins/SEO/images/majesticseo.png', - 'logo_link' => $linkToMajestic, - 'logo_tooltip' => Piwik::translate('SEO_ViewBacklinksOnMajesticSEO'), - 'id' => 'referrer-domains', - ), - ); + $translated = array(); - // Add DMOZ only if > 0 entries found - $dmozRank = array( - 'rank' => $rank->getDmoz(), - 'logo' => \Piwik\Plugins\Referrers\getSearchEngineLogoFromUrl('http://dmoz.org'), - 'id' => 'dmoz', - ); + foreach ($metrics as $metric) { + if (!$metric instanceof Metric) { + continue; + } - if ($dmozRank['rank'] > 0) { - $data['SEO_Dmoz'] = $dmozRank; + $label = Piwik::translate($metric->getName()); + $translated[$label] = array( + 'id' => $metric->getId(), + 'rank' => $metric->getValue(), + 'logo' => $metric->getLogo(), + 'logo_link' => $metric->getLogoLink(), + 'logo_tooltip' => Piwik::translate($metric->getLogoTooltip()), + 'rank_suffix' => Piwik::translate($metric->getValueSuffix()), + ); } - return $data; + return DataTable::makeFromIndexedArray($translated); } } diff --git a/plugins/SEO/Metric/Aggregator.php b/plugins/SEO/Metric/Aggregator.php new file mode 100644 index 0000000000..69f9139d1e --- /dev/null +++ b/plugins/SEO/Metric/Aggregator.php @@ -0,0 +1,63 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\SEO\Metric; + +use Piwik\Container\StaticContainer; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; + +/** + * Aggregates metrics from several providers. + */ +class Aggregator implements MetricsProvider +{ + /** + * @var MetricsProvider[] + */ + private $providers; + + public function __construct() + { + $this->providers = $this->getProviders(); + } + + public function getMetrics($domain) + { + $metrics = array(); + + foreach ($this->providers as $provider) { + $metrics = array_merge($metrics, $provider->getMetrics($domain)); + } + + return $metrics; + } + + private function getProviders() + { + $container = StaticContainer::getContainer(); + + $providers = array( + $container->get('Piwik\Plugins\SEO\Metric\Google'), + $container->get('Piwik\Plugins\SEO\Metric\Bing'), + $container->get('Piwik\Plugins\SEO\Metric\Alexa'), + $container->get('Piwik\Plugins\SEO\Metric\DomainAge'), + $container->get('Piwik\Plugins\SEO\Metric\Majestic'), + $container->get('Piwik\Plugins\SEO\Metric\Dmoz'), + ); + + /** + * Use this event to register new SEO metrics providers. + * + * @param array $providers Contains an array of Piwik\Plugins\SEO\Metric\MetricsProvider instances. + */ + Piwik::postEvent('SEO.getMetricsProviders', array(&$providers)); + + return $providers; + } +} diff --git a/plugins/SEO/Metric/Alexa.php b/plugins/SEO/Metric/Alexa.php new file mode 100644 index 0000000000..d9ec020954 --- /dev/null +++ b/plugins/SEO/Metric/Alexa.php @@ -0,0 +1,51 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\SEO\Metric; + +use Piwik\Http; +use Psr\Log\LoggerInterface; + +/** + * Retrieves the Alexa rank. + */ +class Alexa implements MetricsProvider +{ + const URL = 'http://data.alexa.com/data?cli=10&url='; + const LINK = 'http://www.alexa.com/siteinfo/'; + + /** + * @var LoggerInterface + */ + private $logger; + + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + public function getMetrics($domain) + { + try { + $response = Http::sendHttpRequest(self::URL . urlencode($domain), $timeout = 10, @$_SERVER['HTTP_USER_AGENT']); + + $xml = @simplexml_load_string($response); + $value = $xml ? (string)$xml->SD->POPULARITY['TEXT'] : null; + } catch (\Exception $e) { + $this->logger->warning('Error while getting Alexa SEO stats: {message}', array('message' => $e->getMessage())); + $value = null; + } + + $logo = \Piwik\Plugins\Referrers\getSearchEngineLogoFromUrl('http://alexa.com'); + $link = self::LINK . urlencode($domain); + + return array( + new Metric('alexa', 'SEO_AlexaRank', $value, $logo, $link) + ); + } +} diff --git a/plugins/SEO/Metric/Bing.php b/plugins/SEO/Metric/Bing.php new file mode 100644 index 0000000000..c01b23d576 --- /dev/null +++ b/plugins/SEO/Metric/Bing.php @@ -0,0 +1,54 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\SEO\Metric; + +use Piwik\Http; +use Psr\Log\LoggerInterface; + +/** + * Fetches the number of pages indexed in Bing. + */ +class Bing implements MetricsProvider +{ + const URL = 'http://www.bing.com/search?mkt=en-US&q=site%3A'; + + /** + * @var LoggerInterface + */ + private $logger; + + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + public function getMetrics($domain) + { + $url = self::URL . urlencode($domain); + + try { + $response = str_replace(' ', ' ', Http::sendHttpRequest($url, $timeout = 10, @$_SERVER['HTTP_USER_AGENT'])); + + if (preg_match('#([0-9\,]+) results#i', $response, $p)) { + $pageCount = (int)str_replace(',', '', $p[1]); + } else { + $pageCount = 0; + } + } catch (\Exception $e) { + $this->logger->warning('Error while getting Bing SEO stats: {message}', array('message' => $e->getMessage())); + $pageCount = null; + } + + $logo = \Piwik\Plugins\Referrers\getSearchEngineLogoFromUrl('http://bing.com'); + + return array( + new Metric('bing-index', 'SEO_Bing_IndexedPages', $pageCount, $logo, null, null, 'General_Pages') + ); + } +} diff --git a/plugins/SEO/Metric/Dmoz.php b/plugins/SEO/Metric/Dmoz.php new file mode 100644 index 0000000000..82ffd248ba --- /dev/null +++ b/plugins/SEO/Metric/Dmoz.php @@ -0,0 +1,61 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\SEO\Metric; + +use Piwik\Http; +use Psr\Log\LoggerInterface; + +/** + * Retrieves the number of Dmoz.org entries. + */ +class Dmoz implements MetricsProvider +{ + const URL = 'http://www.dmoz.org/search?q='; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param LoggerInterface $logger + */ + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + public function getMetrics($domain) + { + try { + $response = Http::sendHttpRequest(self::URL . urlencode($domain), $timeout = 10, @$_SERVER['HTTP_USER_AGENT']); + + preg_match('#Open Directory Sites[^\(]+\([0-9]-[0-9]+ of ([0-9]+)\)#', $response, $p); + if (!empty($p[1])) { + $value = (int)$p[1]; + } else { + $value = 0; + } + + // Add DMOZ only if > 0 entries found + if ($value == 0) { + return array(); + } + } catch (\Exception $e) { + $this->logger->warning('Error while getting Dmoz SEO stats: {message}', array('message' => $e->getMessage())); + $value = null; + } + + $logo = \Piwik\Plugins\Referrers\getSearchEngineLogoFromUrl('http://dmoz.org'); + + return array( + new Metric('dmoz', 'SEO_Dmoz', $value, $logo) + ); + } +} diff --git a/plugins/SEO/Metric/DomainAge.php b/plugins/SEO/Metric/DomainAge.php new file mode 100644 index 0000000000..a059b49572 --- /dev/null +++ b/plugins/SEO/Metric/DomainAge.php @@ -0,0 +1,141 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\SEO\Metric; + +use Piwik\Http; +use Piwik\Metrics\Formatter; +use Psr\Log\LoggerInterface; + +/** + * Fetches the domain age using archive.org, who.is and whois.com. + */ +class DomainAge implements MetricsProvider +{ + /** + * @var Formatter + */ + private $formatter; + + /** + * @var LoggerInterface + */ + private $logger; + + public function __construct(Formatter $formatter, LoggerInterface $logger) + { + $this->formatter = $formatter; + $this->logger = $logger; + } + + public function getMetrics($domain) + { + $ages = array(); + + $age = $this->getAgeArchiveOrg($domain); + if ($age > 0) { + $ages[] = $age; + } + + $age = $this->getAgeWhoIs($domain); + if ($age > 0) { + $ages[] = $age; + } + + $age = $this->getAgeWhoisCom($domain); + if ($age > 0) { + $ages[] = $age; + } + + if (count($ages) > 0) { + $value = min($ages); + $value = $this->formatter->getPrettyTimeFromSeconds(time() - $value, true); + } else { + $value = null; + } + + return array( + new Metric('domain-age', 'SEO_DomainAge', $value, 'plugins/SEO/images/whois.png') + ); + } + + /** + * Returns the domain age archive.org lists for the current url + * + * @param string $domain + * @return int + */ + private function getAgeArchiveOrg($domain) + { + $url = str_replace('www.', '', $domain); + $data = $this->getUrl('http://wayback.archive.org/web/*/' . urlencode($url)); + preg_match('#<a href=\"([^>]*)' . preg_quote($url) . '/\">([^<]*)<\/a>#', $data, $p); + if (!empty($p[2])) { + $value = strtotime($p[2]); + if ($value === false) { + return 0; + } + return $value; + } + return 0; + } + + /** + * Returns the domain age who.is lists for the current url + * + * @param string $domain + * @return int + */ + private function getAgeWhoIs($domain) + { + $url = preg_replace('/^www\./', '', $domain); + $url = 'http://www.who.is/whois/' . urlencode($url); + $data = $this->getUrl($url); + preg_match('#(?:Creation Date|Created On|created|Registered on)\.*:\s*([ \ta-z0-9\/\-:\.]+)#si', $data, $p); + if (!empty($p[1])) { + $value = strtotime(trim($p[1])); + if ($value === false) { + return 0; + } + return $value; + } + return 0; + } + + /** + * Returns the domain age whois.com lists for the current url + * + * @param string $domain + * @return int + */ + private function getAgeWhoisCom($domain) + { + $url = preg_replace('/^www\./', '', $domain); + $url = 'http://www.whois.com/whois/' . urlencode($url); + $data = $this->getUrl($url); + preg_match('#(?:Creation Date|Created On|created):\s*([ \ta-z0-9\/\-:\.]+)#si', $data, $p); + if (!empty($p[1])) { + $value = strtotime(trim($p[1])); + if ($value === false) { + return 0; + } + return $value; + } + return 0; + } + + private function getUrl($url) + { + try { + return str_replace(' ', ' ', Http::sendHttpRequest($url, $timeout = 10, @$_SERVER['HTTP_USER_AGENT'])); + } catch (\Exception $e) { + $this->logger->warning('Error while getting SEO stats (domain age): {message}', array('message' => $e->getMessage())); + return ''; + } + } +} diff --git a/plugins/SEO/Metric/Google.php b/plugins/SEO/Metric/Google.php new file mode 100644 index 0000000000..84e021c2ea --- /dev/null +++ b/plugins/SEO/Metric/Google.php @@ -0,0 +1,170 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\SEO\Metric; + +use Piwik\Http; +use Psr\Log\LoggerInterface; + +/** + * Retrieves Google PageRank. + */ +class Google implements MetricsProvider +{ + const SEARCH_URL = 'http://www.google.com/search?hl=en&q=site%3A'; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param LoggerInterface $logger + */ + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + public function getMetrics($domain) + { + $pageCount = $this->fetchIndexedPagesCount($domain); + $pageRank = $this->fetchPageRank($domain); + + $logo = \Piwik\Plugins\Referrers\getSearchEngineLogoFromUrl('http://google.com'); + + return array( + new Metric('google-index', 'SEO_Google_IndexedPages', $pageCount, $logo, null, null, 'General_Pages'), + new Metric('pagerank', 'Google PageRank', $pageRank, $logo, null, null, '/10'), + ); + } + + public function fetchIndexedPagesCount($domain) + { + $url = self::SEARCH_URL . urlencode($domain); + + try { + $response = str_replace(' ', ' ', Http::sendHttpRequest($url, $timeout = 10, @$_SERVER['HTTP_USER_AGENT'])); + + if (preg_match('#([0-9\,]+) results#i', $response, $p)) { + return (int)str_replace(',', '', $p[1]); + } else { + return 0; + } + } catch (\Exception $e) { + $this->logger->warning('Error while getting Google search SEO stats: {message}', array('message' => $e->getMessage())); + return null; + } + } + + public function fetchPageRank($domain) + { + $chwrite = $this->checkHash($this->hashURL($domain)); + + $url = "http://toolbarqueries.google.com/tbr?client=navclient-auto&ch=" . $chwrite . "&features=Rank&q=info:" . $domain . "&num=100&filter=0"; + + try { + $response = Http::sendHttpRequest($url, $timeout = 10, @$_SERVER['HTTP_USER_AGENT']); + + preg_match('#Rank_[0-9]:[0-9]:([0-9]+){1,}#si', $response, $p); + + return isset($p[1]) ? $p[1] : null; + } catch (\Exception $e) { + $this->logger->warning('Error while getting Google PageRank for SEO stats: {message}', array('message' => $e->getMessage())); + return null; + } + } + + /** + * Generate a hash for a url + * + * @param string $string + * @return int + */ + private function hashURL($string) + { + $Check1 = $this->strToNum($string, 0x1505, 0x21); + $Check2 = $this->strToNum($string, 0, 0x1003F); + + $Check1 >>= 2; + $Check1 = (($Check1 >> 4) & 0x3FFFFC0) | ($Check1 & 0x3F); + $Check1 = (($Check1 >> 4) & 0x3FFC00) | ($Check1 & 0x3FF); + $Check1 = (($Check1 >> 4) & 0x3C000) | ($Check1 & 0x3FFF); + + $T1 = (((($Check1 & 0x3C0) << 4) | ($Check1 & 0x3C)) << 2) | ($Check2 & 0xF0F); + $T2 = (((($Check1 & 0xFFFFC000) << 4) | ($Check1 & 0x3C00)) << 0xA) | ($Check2 & 0xF0F0000); + + return ($T1 | $T2); + } + + /** + * Generate a checksum for the hash string + * + * @param int $hashnum + * @return string + */ + private function checkHash($hashnum) + { + $CheckByte = 0; + $Flag = 0; + + $HashStr = sprintf('%u', $hashnum); + $length = strlen($HashStr); + + for ($i = $length - 1; $i >= 0; $i--) { + $Re = $HashStr{$i}; + if (1 === ($Flag % 2)) { + $Re += $Re; + $Re = (int)($Re / 10) + ($Re % 10); + } + $CheckByte += $Re; + $Flag++; + } + + $CheckByte %= 10; + if (0 !== $CheckByte) { + $CheckByte = 10 - $CheckByte; + if (1 === ($Flag % 2)) { + if (1 === ($CheckByte % 2)) { + $CheckByte += 9; + } + $CheckByte >>= 1; + } + } + + return '7' . $CheckByte . $HashStr; + } + + /** + * Convert numeric string to int + * + * @param string $Str + * @param int $Check + * @param int $Magic + * @return int + */ + private function strToNum($Str, $Check, $Magic) + { + $Int32Unit = 4294967296; // 2^32 + + $length = strlen($Str); + for ($i = 0; $i < $length; $i++) { + $Check *= $Magic; + // If the float is beyond the boundaries of integer (usually +/- 2.15e+9 = 2^31), + // the result of converting to integer is undefined + // refer to http://www.php.net/manual/en/language.types.integer.php + if ($Check >= $Int32Unit) { + $Check = ($Check - $Int32Unit * (int)($Check / $Int32Unit)); + //if the check less than -2^31 + $Check = ($Check < -2147483648) ? ($Check + $Int32Unit) : $Check; + } + $Check += ord($Str{$i}); + } + return $Check; + } +} diff --git a/plugins/SEO/MajesticClient.php b/plugins/SEO/Metric/Majestic.php index b7a7eeddc8..f276232945 100644 --- a/plugins/SEO/MajesticClient.php +++ b/plugins/SEO/Metric/Majestic.php @@ -4,22 +4,54 @@ * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * */ -namespace Piwik\Plugins\SEO; -use Piwik\Common; +namespace Piwik\Plugins\SEO\Metric; + use Piwik\Http; +use Psr\Log\LoggerInterface; /** * Client for Majestic SEO's HTTP API. - * - * Hides the HTTP request sending logic. */ -class MajesticClient +class Majestic implements MetricsProvider { const API_BASE = 'http://simpleapi.majesticseo.com/sapi/'; const API_KEY = 'ETHPYY'; // please only use this key within Piwik + const LOGO = 'plugins/SEO/images/majesticseo.png'; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param LoggerInterface $logger + */ + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + public function getMetrics($domain) + { + try { + $stats = $this->getBacklinkStats($domain); + $backlinks = $stats['backlink_count']; + $referrers = $stats['referrer_domains_count']; + } catch (\Exception $e) { + $this->logger->warning('Error while getting Majestic SEO stats: {message}', array('message' => $e->getMessage())); + $backlinks = null; + $referrers = null; + } + + $link = $this->getLinkForUrl($domain); + + return array( + new Metric('external-backlinks', 'SEO_ExternalBacklinks', $backlinks, self::LOGO, $link, 'SEO_ViewBacklinksOnMajesticSEO'), + new Metric('referrer-domains', 'SEO_ReferrerDomains', $referrers, self::LOGO, $link, 'SEO_ViewBacklinksOnMajesticSEO'), + ); + } /** * Returns a URL that can be used to view all SEO data for a particular website. @@ -28,7 +60,7 @@ class MajesticClient * accessible for. * @return string */ - public static function getLinkForUrl($targetSiteUrl) + private function getLinkForUrl($targetSiteUrl) { $domain = @parse_url($targetSiteUrl, PHP_URL_HOST); return "http://www.majesticseo.com/reports/site-explorer/summary/$domain?IndexDataSource=F"; @@ -52,7 +84,7 @@ class MajesticClient * If either stat is false, either the API returned an * error, or the IP was blocked for this request. */ - public function getBacklinkStats($siteDomain, $timeout = 300) + private function getBacklinkStats($siteDomain, $timeout = 300) { $apiUrl = $this->getApiUrl($method = 'GetBacklinkStats', $args = array( 'items' => '1', diff --git a/plugins/SEO/Metric/Metric.php b/plugins/SEO/Metric/Metric.php new file mode 100644 index 0000000000..ec28b55138 --- /dev/null +++ b/plugins/SEO/Metric/Metric.php @@ -0,0 +1,145 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\SEO\Metric; + +/** + * Describes a SEO metric. + */ +class Metric +{ + /** + * @var string + */ + private $id; + + /** + * @var string + */ + private $name; + + /** + * @var string + */ + private $value; + + /** + * @var string + */ + private $logo; + + /** + * @var string|null + */ + private $logoLink; + + /** + * @var string|null + */ + private $logoTooltip; + + /** + * @var string|null + */ + private $valueSuffix; + + /** + * @param string $id + * @param string $name Can be a string or a translation ID. + * @param string $value Rank value. + * @param string $logo URL to a logo. + * @param string|null $logoLink + * @param string|null $logoTooltip + * @param string|null $valueSuffix + */ + public function __construct($id, $name, $value, $logo, $logoLink = null, $logoTooltip = null, $valueSuffix = null) + { + $this->id = $id; + $this->name = $name; + $this->value = $value; + $this->logo = $logo; + $this->logoLink = $logoLink; + $this->logoTooltip = $logoTooltip; + $this->valueSuffix = $valueSuffix; + } + + /** + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * @return string + */ + public function getLogo() + { + return $this->logo; + } + + /** + * @return string|null + */ + public function getLogoLink() + { + return $this->logoLink; + } + + /** + * @return string|null + */ + public function getLogoTooltip() + { + return $this->logoTooltip; + } + + /** + * @return null|string + */ + public function getValueSuffix() + { + return $this->valueSuffix; + } + + /** + * Allows the class to be serialized with var_export (in the cache). + * + * @param array $array + * @return Metric + */ + public static function __set_state($array) + { + return new self( + $array['id'], + $array['name'], + $array['value'], + $array['logo'], + $array['logoLink'], + $array['logoTooltip'], + $array['valueSuffix'] + ); + } +} diff --git a/plugins/SEO/Metric/MetricsProvider.php b/plugins/SEO/Metric/MetricsProvider.php new file mode 100644 index 0000000000..c737efb928 --- /dev/null +++ b/plugins/SEO/Metric/MetricsProvider.php @@ -0,0 +1,21 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\SEO\Metric; + +/** + * Provides SEO metrics for a domain. + */ +interface MetricsProvider +{ + /** + * @param string $domain + * @return Metric[] + */ + public function getMetrics($domain); +} diff --git a/plugins/SEO/Metric/ProviderCache.php b/plugins/SEO/Metric/ProviderCache.php new file mode 100644 index 0000000000..cb03063281 --- /dev/null +++ b/plugins/SEO/Metric/ProviderCache.php @@ -0,0 +1,48 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\SEO\Metric; + +use Piwik\Cache; + +/** + * Caches another provider. + */ +class ProviderCache implements MetricsProvider +{ + /** + * @var MetricsProvider + */ + private $provider; + + /** + * @var Cache\Lazy + */ + private $cache; + + public function __construct(MetricsProvider $provider) + { + $this->provider = $provider; + $this->cache = Cache::getLazyCache(); + } + + public function getMetrics($domain) + { + $cacheId = 'SEO_getRank_' . md5($domain); + + $metrics = $this->cache->fetch($cacheId); + + if (! is_array($metrics)) { + $metrics = $this->provider->getMetrics($domain); + + $this->cache->save($cacheId, $metrics, 60 * 60 * 6); + } + + return $metrics; + } +} diff --git a/plugins/SEO/RankChecker.php b/plugins/SEO/RankChecker.php deleted file mode 100644 index 655c434fc9..0000000000 --- a/plugins/SEO/RankChecker.php +++ /dev/null @@ -1,378 +0,0 @@ -<?php -/** - * Piwik - free/libre analytics platform - * - * @link http://piwik.org - * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * - */ -namespace Piwik\Plugins\SEO; - -use Exception; -use Piwik\Http; -use Piwik\Log; -use Piwik\Metrics\Formatter; - -/** - * The functions below are derived/adapted from GetRank.org's - * Free PageRank Script v2.0, released under GPL. - * - * @copyright Copyright (C) 2007 - 2010 GetRank.Org All rights reserved. - * @link http://www.getrank.org/free-pagerank-script/ - * @license GPL - */ -class RankChecker -{ - private $url; - private $majesticInfo = null; - private $formatter = null; - - public function __construct($url) - { - $this->url = self::extractDomainFromUrl($url); - $this->formatter = new Formatter(); - } - - /** - * Extract domain from URL as the web services generally - * expect only a domain name (i.e., no protocol, port, path, query, etc). - * - * @param string $url - * @return string - */ - public static function extractDomainFromUrl($url) - { - return preg_replace( - array( - '~^https?\://~si', // strip protocol - '~[/:#?;%&].*~', // strip port, path, query, anchor, etc - '~\.$~', // trailing period - ), - '', $url); - } - - /** - * Web service proxy that retrieves the content at the specified URL - * - * @param string $url - * @return string - */ - private function getPage($url) - { - try { - return str_replace(' ', ' ', Http::sendHttpRequest($url, $timeout = 10, @$_SERVER['HTTP_USER_AGENT'])); - } catch (Exception $e) { - return ''; - } - } - - /** - * Returns the google page rank for the current url - * - * @return int - */ - public function getPageRank() - { - $chwrite = $this->CheckHash($this->HashURL($this->url)); - - $url = "http://toolbarqueries.google.com/tbr?client=navclient-auto&ch=" . $chwrite . "&features=Rank&q=info:" . $this->url . "&num=100&filter=0"; - $data = $this->getPage($url); - preg_match('#Rank_[0-9]:[0-9]:([0-9]+){1,}#si', $data, $p); - $value = isset($p[1]) ? $p[1] : 0; - - return $value; - } - - /** - * Returns the alexa traffic rank for the current url - * - * @return int - */ - public function getAlexaRank() - { - $xml = @simplexml_load_string($this->getPage('http://data.alexa.com/data?cli=10&url=' . urlencode($this->url))); - return $xml ? $xml->SD->POPULARITY['TEXT'] : ''; - } - - /** - * Returns the number of Dmoz.org entries for the current url - * - * @return int - */ - public function getDmoz() - { - $url = 'http://www.dmoz.org/search?q=' . urlencode($this->url); - $data = $this->getPage($url); - preg_match('#Open Directory Sites[^\(]+\([0-9]-[0-9]+ of ([0-9]+)\)#', $data, $p); - if (!empty($p[1])) { - return (int)$p[1]; - } - return 0; - } - - /** - * Returns the number of pages google holds in it's index for the current url - * - * @return int - */ - public function getIndexedPagesGoogle() - { - $url = 'http://www.google.com/search?hl=en&q=site%3A' . urlencode($this->url); - $data = $this->getPage($url); - if (preg_match('#([0-9\,]+) results#i', $data, $p)) { - $indexedPages = (int)str_replace(',', '', $p[1]); - return $indexedPages; - } - return 0; - } - - /** - * Returns the number of pages bing holds in it's index for the current url - * - * @return int - */ - public function getIndexedPagesBing() - { - $url = 'http://www.bing.com/search?mkt=en-US&q=site%3A' . urlencode($this->url); - $data = $this->getPage($url); - if (preg_match('#([0-9\,]+) results#i', $data, $p)) { - return (int)str_replace(',', '', $p[1]); - } - return 0; - } - - /** - * Returns the domain age for the current url - * - * @return int - */ - public function getAge() - { - $ageArchiveOrg = $this->_getAgeArchiveOrg(); - $ageWhoIs = $this->_getAgeWhoIs(); - $ageWhoisCom = $this->_getAgeWhoisCom(); - - $ages = array(); - - if ($ageArchiveOrg > 0) { - $ages[] = $ageArchiveOrg; - } - - if ($ageWhoIs > 0) { - $ages[] = $ageWhoIs; - } - - if ($ageWhoisCom > 0) { - $ages[] = $ageWhoisCom; - } - - if (count($ages) > 1) { - $maxAge = min($ages); - } else { - $maxAge = array_shift($ages); - } - - if ($maxAge) { - return $this->formatter->getPrettyTimeFromSeconds(time() - $maxAge, true); - } - return false; - } - - /** - * Returns the number backlinks that link to the current site. - * - * @return int - */ - public function getExternalBacklinkCount() - { - try { - $majesticInfo = $this->getMajesticInfo(); - return $majesticInfo['backlink_count']; - } catch (Exception $e) { - Log::info($e); - return 0; - } - } - - /** - * Returns the number of referrer domains that link to the current site. - * - * @return int - */ - public function getReferrerDomainCount() - { - try { - $majesticInfo = $this->getMajesticInfo(); - return $majesticInfo['referrer_domains_count']; - } catch (Exception $e) { - Log::info($e); - return 0; - } - } - - /** - * Returns the domain age archive.org lists for the current url - * - * @return int - */ - protected function _getAgeArchiveOrg() - { - $url = str_replace('www.', '', $this->url); - $data = @$this->getPage('http://wayback.archive.org/web/*/' . urlencode($url)); - preg_match('#<a href=\"([^>]*)' . preg_quote($url) . '/\">([^<]*)<\/a>#', $data, $p); - if (!empty($p[2])) { - $value = strtotime($p[2]); - if ($value === false) { - return 0; - } - return $value; - } - return 0; - } - - /** - * Returns the domain age who.is lists for the current url - * - * @return int - */ - protected function _getAgeWhoIs() - { - $url = preg_replace('/^www\./', '', $this->url); - $url = 'http://www.who.is/whois/' . urlencode($url); - $data = $this->getPage($url); - preg_match('#(?:Creation Date|Created On|created|Registered on)\.*:\s*([ \ta-z0-9\/\-:\.]+)#si', $data, $p); - if (!empty($p[1])) { - $value = strtotime(trim($p[1])); - if ($value === false) { - return 0; - } - return $value; - } - return 0; - } - - /** - * Returns the domain age whois.com lists for the current url - * - * @return int - */ - protected function _getAgeWhoisCom() - { - $url = preg_replace('/^www\./', '', $this->url); - $url = 'http://www.whois.com/whois/' . urlencode($url); - $data = $this->getPage($url); - preg_match('#(?:Creation Date|Created On|created):\s*([ \ta-z0-9\/\-:\.]+)#si', $data, $p); - if (!empty($p[1])) { - $value = strtotime(trim($p[1])); - if ($value === false) { - return 0; - } - return $value; - } - return 0; - } - - /** - * Convert numeric string to int - * - * @see getPageRank() - * - * @param string $Str - * @param int $Check - * @param int $Magic - * @return int - */ - private function StrToNum($Str, $Check, $Magic) - { - $Int32Unit = 4294967296; // 2^32 - - $length = strlen($Str); - for ($i = 0; $i < $length; $i++) { - $Check *= $Magic; - // If the float is beyond the boundaries of integer (usually +/- 2.15e+9 = 2^31), - // the result of converting to integer is undefined - // refer to http://www.php.net/manual/en/language.types.integer.php - if ($Check >= $Int32Unit) { - $Check = ($Check - $Int32Unit * (int)($Check / $Int32Unit)); - //if the check less than -2^31 - $Check = ($Check < -2147483648) ? ($Check + $Int32Unit) : $Check; - } - $Check += ord($Str{$i}); - } - return $Check; - } - - /** - * Generate a hash for a url - * - * @see getPageRank() - * - * @param string $String - * @return int - */ - private function HashURL($String) - { - $Check1 = $this->StrToNum($String, 0x1505, 0x21); - $Check2 = $this->StrToNum($String, 0, 0x1003F); - - $Check1 >>= 2; - $Check1 = (($Check1 >> 4) & 0x3FFFFC0) | ($Check1 & 0x3F); - $Check1 = (($Check1 >> 4) & 0x3FFC00) | ($Check1 & 0x3FF); - $Check1 = (($Check1 >> 4) & 0x3C000) | ($Check1 & 0x3FFF); - - $T1 = (((($Check1 & 0x3C0) << 4) | ($Check1 & 0x3C)) << 2) | ($Check2 & 0xF0F); - $T2 = (((($Check1 & 0xFFFFC000) << 4) | ($Check1 & 0x3C00)) << 0xA) | ($Check2 & 0xF0F0000); - - return ($T1 | $T2); - } - - /** - * Generate a checksum for the hash string - * - * @see getPageRank() - * - * @param int $Hashnum - * @return string - */ - private function CheckHash($Hashnum) - { - $CheckByte = 0; - $Flag = 0; - - $HashStr = sprintf('%u', $Hashnum); - $length = strlen($HashStr); - - for ($i = $length - 1; $i >= 0; $i--) { - $Re = $HashStr{$i}; - if (1 === ($Flag % 2)) { - $Re += $Re; - $Re = (int)($Re / 10) + ($Re % 10); - } - $CheckByte += $Re; - $Flag++; - } - - $CheckByte %= 10; - if (0 !== $CheckByte) { - $CheckByte = 10 - $CheckByte; - if (1 === ($Flag % 2)) { - if (1 === ($CheckByte % 2)) { - $CheckByte += 9; - } - $CheckByte >>= 1; - } - } - - return '7' . $CheckByte . $HashStr; - } - - private function getMajesticInfo() - { - if ($this->majesticInfo === null) { - $client = new MajesticClient(); - $this->majesticInfo = $client->getBacklinkStats($this->url); - } - - return $this->majesticInfo; - } -} diff --git a/plugins/SEO/Widgets.php b/plugins/SEO/Widgets.php index bf2e95e0a7..1f06c2ec8b 100644 --- a/plugins/SEO/Widgets.php +++ b/plugins/SEO/Widgets.php @@ -11,6 +11,7 @@ namespace Piwik\Plugins\SEO; use Piwik\Common; use Piwik\DataTable\Renderer; use Piwik\Site; +use Piwik\Url; use Piwik\UrlHelper; use Piwik\View; @@ -41,7 +42,7 @@ class Widgets extends \Piwik\Plugin\Widgets $dataTable = API::getInstance()->getRank($url); $view = new View('@SEO/getRank'); - $view->urlToRank = RankChecker::extractDomainFromUrl($url); + $view->urlToRank = Url::getHostFromUrl($url); /** @var \Piwik\DataTable\Renderer\Php $renderer */ $renderer = Renderer::factory('php'); diff --git a/plugins/SEO/templates/getRank.twig b/plugins/SEO/templates/getRank.twig index 5233fc82ea..80bf40ef7b 100644 --- a/plugins/SEO/templates/getRank.twig +++ b/plugins/SEO/templates/getRank.twig @@ -22,25 +22,24 @@ {% endset %} {{ 'SEO_SEORankingsFor'|translate(cleanUrl)|raw }} <table cellspacing="2" style="margin:auto;line-height:1.5em;padding-top:10px;"> + {% for rank in ranks %} <tr> -{% set seoLink %}{% if rank.logo_link is defined %}<a class="linkContent" href="?module=Proxy&action=redirect&url={{ rank.logo_link|url_encode }}" +{% set seoLink %}{% if rank.logo_link is not empty %}<a class="linkContent" href="?module=Proxy&action=redirect&url={{ rank.logo_link|url_encode }}" target="_blank" {% if rank.logo_tooltip is not empty %}title="{{ rank.logo_tooltip }}"{% endif %}>{% endif %}{% endset %} {% set majesticLink %}{{ seoLink }}Majestic</a>{% endset %} - <td>{% if rank.logo_link is defined %}{{ seoLink|raw }}{% endif %}<img + <td>{% if rank.logo_link is not empty %}{{ seoLink|raw }}{% endif %}<img style='vertical-align:middle;margin-right:6px;' src='{{ rank.logo }}' border='0' - alt="{{ rank.label }}">{% if rank.logo_link is defined %}</a>{% endif %} {{ rank.label|replace({"Majestic": + alt="{{ rank.label }}">{% if rank.logo_link is not empty %}</a>{% endif %} {{ rank.label|replace({"Majestic": majesticLink})|raw }} </td> <td> <div style="margin-left:15px;"> - {% if rank.logo_link is defined %}{{ seoLink|raw }}{% endif %} + {% if rank.logo_link is not empty %}{{ seoLink|raw }}{% endif %} {% if rank.rank %}{{ rank.rank|raw }}{% else %}-{% endif %} - {% if rank.id=='pagerank' %} /10 - {% elseif rank.id=='google-index' or rank.id=='bing-index' %} {{ 'General_Pages'|translate }} - {% endif %} - {% if rank.logo_link is defined %}</a>{% endif %} + {{ rank.rank_suffix }} + {% if rank.logo_link is not empty %}</a>{% endif %} </div> </td> </tr> |