mirror of
https://github.com/silverstripe/silverstripe-externallinks.git
synced 2024-10-22 17:05:44 +02:00
Merge pull request #27 from creative-commoners/pulls/2.0/update-for-four
This commit is contained in:
commit
789ce91e1c
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -4,3 +4,4 @@
|
||||
/.gitignore export-ignore
|
||||
/.travis.yml export-ignore
|
||||
/.scrutinizer.yml export-ignore
|
||||
/codecov.yml export-ignore
|
||||
|
@ -1,69 +1,15 @@
|
||||
inherit: true
|
||||
|
||||
build:
|
||||
nodes:
|
||||
analysis:
|
||||
tests:
|
||||
override: [php-scrutinizer-run]
|
||||
|
||||
checks:
|
||||
php:
|
||||
verify_property_names: true
|
||||
verify_argument_usable_as_reference: true
|
||||
verify_access_scope_valid: true
|
||||
useless_calls: true
|
||||
use_statement_alias_conflict: true
|
||||
variable_existence: true
|
||||
unused_variables: true
|
||||
unused_properties: true
|
||||
unused_parameters: true
|
||||
unused_methods: true
|
||||
unreachable_code: true
|
||||
too_many_arguments: true
|
||||
sql_injection_vulnerabilities: true
|
||||
simplify_boolean_return: true
|
||||
side_effects_or_types: true
|
||||
security_vulnerabilities: true
|
||||
return_doc_comments: true
|
||||
return_doc_comment_if_not_inferrable: true
|
||||
require_scope_for_properties: true
|
||||
require_scope_for_methods: true
|
||||
require_php_tag_first: true
|
||||
psr2_switch_declaration: true
|
||||
psr2_class_declaration: true
|
||||
property_assignments: true
|
||||
prefer_while_loop_over_for_loop: true
|
||||
precedence_mistakes: true
|
||||
precedence_in_conditions: true
|
||||
phpunit_assertions: true
|
||||
php5_style_constructor: true
|
||||
parse_doc_comments: true
|
||||
parameter_non_unique: true
|
||||
parameter_doc_comments: true
|
||||
param_doc_comment_if_not_inferrable: true
|
||||
optional_parameters_at_the_end: true
|
||||
one_class_per_file: true
|
||||
no_unnecessary_if: true
|
||||
no_trailing_whitespace: true
|
||||
no_property_on_interface: true
|
||||
no_non_implemented_abstract_methods: true
|
||||
no_error_suppression: true
|
||||
no_duplicate_arguments: true
|
||||
no_commented_out_code: true
|
||||
newline_at_end_of_file: true
|
||||
missing_arguments: true
|
||||
method_calls_on_non_object: true
|
||||
instanceof_class_exists: true
|
||||
foreach_traversable: true
|
||||
fix_line_ending: true
|
||||
fix_doc_comments: true
|
||||
duplication: true
|
||||
deprecated_code_usage: true
|
||||
deadlock_detection_in_loops: true
|
||||
code_rating: true
|
||||
closure_use_not_conflicting: true
|
||||
catch_class_exists: true
|
||||
blank_line_after_namespace_declaration: false
|
||||
avoid_multiple_statements_on_same_line: true
|
||||
avoid_duplicate_types: true
|
||||
avoid_conflicting_incrementers: true
|
||||
avoid_closing_tag: true
|
||||
assignment_of_null_return: true
|
||||
argument_type_checks: true
|
||||
duplication: true
|
||||
|
||||
filter:
|
||||
paths: [code/*, tests/*]
|
||||
paths: [src/*, tests/*]
|
||||
|
45
.travis.yml
45
.travis.yml
@ -1,34 +1,35 @@
|
||||
# See https://github.com/silverstripe/silverstripe-travis-support for setup details
|
||||
|
||||
sudo: false
|
||||
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 5.3
|
||||
- 5.4
|
||||
- 5.5
|
||||
|
||||
env:
|
||||
- DB=MYSQL CORE_RELEASE=3.5
|
||||
global:
|
||||
- COMPOSER_ROOT_VERSION=2.0.x-dev
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- php: 5.6
|
||||
env: DB=MYSQL CORE_RELEASE=3
|
||||
- php: 5.6
|
||||
env: DB=MYSQL CORE_RELEASE=3.1
|
||||
- php: 5.6
|
||||
env: DB=PGSQL CORE_RELEASE=3.2
|
||||
env: DB=MYSQL PHPCS_TEST=1 PHPUNIT_TEST=1
|
||||
- php: 7.0
|
||||
env: DB=MYSQL PHPUNIT_TEST=1
|
||||
- php: 7.1
|
||||
env: DB=MYSQL CORE_RELEASE=3.6
|
||||
env: DB=PGSQL PHPUNIT_COVERAGE_TEST=1
|
||||
- php: 7.2
|
||||
env: DB=MYSQL PHPUNIT_TEST=1
|
||||
|
||||
before_script:
|
||||
- composer self-update || true
|
||||
- git clone git://github.com/silverstripe/silverstripe-travis-support.git ~/travis-support
|
||||
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
|
||||
- cd ~/builds/ss
|
||||
- composer install
|
||||
# Init PHP
|
||||
- phpenv rehash
|
||||
- phpenv config-rm xdebug.ini
|
||||
|
||||
# Install composer dependencies
|
||||
- composer validate
|
||||
- composer require --no-update silverstripe/installer 4.0.x-dev
|
||||
- if [[ $DB == PGSQL ]]; then composer require --no-update silverstripe/postgresql 2.0.x-dev; fi
|
||||
- composer install --prefer-dist --no-interaction --no-progress --no-suggest --optimize-autoloader --verbose --profile
|
||||
|
||||
script:
|
||||
- vendor/bin/phpunit externallinks/tests
|
||||
- if [[ $PHPUNIT_TEST ]]; then vendor/bin/phpunit; fi
|
||||
- if [[ $PHPUNIT_COVERAGE_TEST ]]; then phpdbg -qrr vendor/bin/phpunit --coverage-clover=coverage.xml; fi
|
||||
- if [[ $PHPCS_TEST ]]; then vendor/bin/phpcs src/ tests/ *.php; fi
|
||||
|
||||
after_success:
|
||||
- if [[ $PHPUNIT_COVERAGE_TEST ]]; then bash <(curl -s https://codecov.io/bash) -f coverage.xml; fi
|
||||
|
10
.upgrade.yml
Normal file
10
.upgrade.yml
Normal file
@ -0,0 +1,10 @@
|
||||
mappings:
|
||||
CMSExternalLinks_Controller: SilverStripe\ExternalLinks\Controllers\CMSExternalLinksController
|
||||
CheckExternalLinksJob: SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob
|
||||
BrokenExternalLink: SilverStripe\ExternalLinks\Model\BrokenExternalLink
|
||||
BrokenExternalPageTrack: SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack
|
||||
BrokenExternalPageTrackStatus: SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus
|
||||
BrokenExternalLinksReport: SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport
|
||||
CheckExternalLinksTask: SilverStripe\ExternalLinks\Tasks\CheckExternalLinksTask
|
||||
CurlLinkChecker: SilverStripe\ExternalLinks\Tasks\CurlLinkChecker
|
||||
LinkChecker: SilverStripe\ExternalLinks\Tasks\LinkChecker
|
29
README.md
29
README.md
@ -1,6 +1,8 @@
|
||||
# External links
|
||||
|
||||
[![Build Status](https://travis-ci.org/silverstripe/silverstripe-externallinks.svg?branch=master)](https://travis-ci.org/silverstripe/silverstripe-externallinks)
|
||||
[![Build Status](http://img.shields.io/travis/silverstripe/silverstripe-externallinks.svg?style=flat)](https://travis-ci.org/silverstripe/silverstripe-externallinks)
|
||||
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/silverstripe/silverstripe-externallinks/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/silverstripe/silverstripe-externallinks/?branch=master)
|
||||
[![codecov](https://codecov.io/gh/silverstripe/silverstripe-externallinks/branch/master/graph/badge.svg)](https://codecov.io/gh/silverstripe/silverstripe-externallinks)
|
||||
|
||||
## Introduction
|
||||
|
||||
@ -12,21 +14,19 @@ The external links module is a task and ModelAdmin to track and to report on bro
|
||||
|
||||
## Requirements
|
||||
|
||||
* SilverStripe 3.1 +
|
||||
* SilverStripe ^4.0
|
||||
|
||||
**Note:** For a SilverStripe 3.x compatible version, please use [the 1.x release line](https://github.com/silverstripe/silverstripe-externallinks/tree/1.0).
|
||||
|
||||
## Features
|
||||
|
||||
* Add external links to broken links reports
|
||||
* Add a task to track external broken links
|
||||
|
||||
See the [changelog](CHANGELOG.md) for version history.
|
||||
|
||||
## Installation
|
||||
|
||||
1. If you have composer you can use `composer require silverstripe/externallinks:*`. Otherwise,
|
||||
download the module from GitHub and extract to the 'externallinks' folder. Place this directory
|
||||
in your sites root directory. This is the one with framework and cms in it.
|
||||
2. Run in your browser - `/dev/build` to rebuild the database.
|
||||
1. Require the module via composer: `composer require silverstripe/externallinks`
|
||||
2. Run `/dev/build` in your browser to rebuild the database.
|
||||
3. Run the following task *http://path.to.silverstripe/dev/tasks/CheckExternalLinks* to check for
|
||||
broken external links
|
||||
|
||||
@ -63,20 +63,17 @@ broken links.
|
||||
|
||||
## Queued job ##
|
||||
|
||||
If you have the queuedjobs module installed you can set the task to be run every so ofter
|
||||
Add the following yml config to config.yml in mysite/_config have the the task run once every day (86400 seconds)
|
||||
|
||||
CheckExternalLinks:
|
||||
Delay: 86400
|
||||
If you have the queuedjobs module installed you can set the task to be run every so often.
|
||||
|
||||
## Whitelisting codes ##
|
||||
|
||||
If you want to ignore or whitelist certain http codes this can be setup via IgnoreCodes in the config.yml
|
||||
file in mysite/_config
|
||||
file in `mysite/_config`
|
||||
|
||||
CheckExternalLinks:
|
||||
Delay: 60
|
||||
```yml
|
||||
SilverStripe\ExternalLinks\Tasks\CheckExternalLinksTask:
|
||||
IgnoreCodes:
|
||||
- 401
|
||||
- 403
|
||||
- 501
|
||||
```
|
||||
|
@ -1,5 +1,9 @@
|
||||
---
|
||||
Name: externallinksdependencies
|
||||
---
|
||||
Injector:
|
||||
LinkChecker: CurlLinkChecker
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
SilverStripe\ExternalLinks\Tasks\LinkChecker: SilverStripe\ExternalLinks\Tasks\CurlLinkChecker
|
||||
Psr\SimpleCache\CacheInterface.CurlLinkChecker:
|
||||
factory: SilverStripe\Core\Cache\CacheFactory
|
||||
constructor:
|
||||
namespace: 'curllinkchecker'
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
Name: externallink
|
||||
After: framework/routes
|
||||
Name: externallinkroutes
|
||||
Before: '#adminroutes'
|
||||
---
|
||||
Director:
|
||||
SilverStripe\Control\Director:
|
||||
rules:
|
||||
'admin/externallinks//$Action': 'CMSExternalLinks_Controller'
|
||||
'admin/externallinks//$Action': SilverStripe\ExternalLinks\Controllers\CMSExternalLinksController
|
||||
|
@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
class CMSExternalLinks_Controller extends Controller {
|
||||
|
||||
private static $allowed_actions = array('getJobStatus', 'start');
|
||||
|
||||
/*
|
||||
* Respond to Ajax requests for info on a running job
|
||||
*
|
||||
* @return string JSON string detailing status of the job
|
||||
*/
|
||||
public function getJobStatus() {
|
||||
// Set headers
|
||||
HTTP::set_cache_age(0);
|
||||
HTTP::add_cache_headers($this->response);
|
||||
$this->response
|
||||
->addHeader('Content-Type', 'application/json')
|
||||
->addHeader('Content-Encoding', 'UTF-8')
|
||||
->addHeader('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
// Format status
|
||||
$track = BrokenExternalPageTrackStatus::get_latest();
|
||||
if($track) return json_encode(array(
|
||||
'TrackID' => $track->ID,
|
||||
'Status' => $track->Status,
|
||||
'Completed' => $track->getCompletedPages(),
|
||||
'Total' => $track->getTotalPages()
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Starts a broken external link check
|
||||
*/
|
||||
public function start() {
|
||||
// return if the a job is already running
|
||||
$status = BrokenExternalPageTrackStatus::get_latest();
|
||||
if ($status && $status->Status == 'Running') return;
|
||||
|
||||
// Create a new job
|
||||
if (class_exists('QueuedJobService')) {
|
||||
// Force the creation of a new run
|
||||
BrokenExternalPageTrackStatus::create_status();
|
||||
$checkLinks = new CheckExternalLinksJob();
|
||||
singleton('QueuedJobService')->queueJob($checkLinks);
|
||||
} else {
|
||||
//TODO this hangs as it waits for the connection to be released
|
||||
// should return back and continue processing
|
||||
// http://us3.php.net/manual/en/features.connection-handling.php
|
||||
$task = CheckExternalLinksTask::create();
|
||||
$task->runLinksCheck();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
if(!class_exists('AbstractQueuedJob')) return;
|
||||
|
||||
/**
|
||||
* A Job for running a external link check for published pages
|
||||
*
|
||||
*/
|
||||
class CheckExternalLinksJob extends AbstractQueuedJob implements QueuedJob {
|
||||
|
||||
public function getTitle() {
|
||||
return _t('CheckExternalLiksJob.TITLE', 'Checking for external broken links');
|
||||
}
|
||||
|
||||
public function getJobType() {
|
||||
return QueuedJob::QUEUED;
|
||||
}
|
||||
|
||||
public function getSignature() {
|
||||
return md5(get_class($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check an individual page
|
||||
*/
|
||||
public function process() {
|
||||
$task = CheckExternalLinksTask::create();
|
||||
$track = $task->runLinksCheck(1);
|
||||
$this->currentStep = $track->CompletedPages;
|
||||
$this->totalSteps = $track->TotalPages;
|
||||
$this->isComplete = $track->Status === 'Completed';
|
||||
}
|
||||
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents a single link checked for a single run that is broken
|
||||
*
|
||||
* @method BrokenExternalPageTrack Track()
|
||||
* @method BrokenExternalPageTrackStatus Status()
|
||||
*/
|
||||
class BrokenExternalLink extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Link' => 'Varchar(2083)', // 2083 is the maximum length of a URL in Internet Explorer.
|
||||
'HTTPCode' =>'Int'
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'Track' => 'BrokenExternalPageTrack',
|
||||
'Status' => 'BrokenExternalPageTrackStatus'
|
||||
);
|
||||
|
||||
private static $summary_fields = array(
|
||||
'Created' => 'Checked',
|
||||
'Link' => 'External Link',
|
||||
'HTTPCodeDescription' => 'HTTP Error Code',
|
||||
'Page.Title' => 'Page link is on'
|
||||
);
|
||||
|
||||
private static $searchable_fields = array(
|
||||
'HTTPCode' => array('title' => 'HTTP Code')
|
||||
);
|
||||
|
||||
/**
|
||||
* @return SiteTree
|
||||
*/
|
||||
public function Page() {
|
||||
return $this->Track()->Page();
|
||||
}
|
||||
|
||||
public function canEdit($member = false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function canView($member = false) {
|
||||
$member = $member ? $member : Member::currentUser();
|
||||
$codes = array('content-authors', 'administrators');
|
||||
return Permission::checkMember($member, $codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a human readable description of a response code
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHTTPCodeDescription() {
|
||||
$code = $this->HTTPCode;
|
||||
if(empty($code)) {
|
||||
// Assume that $code = 0 means there was no response
|
||||
$description = _t('BrokenExternalLink.NOTAVAILABLE', 'Server Not Available');
|
||||
} elseif(
|
||||
($descriptions = Config::inst()->get('SS_HTTPResponse', 'status_codes'))
|
||||
&& isset($descriptions[$code])
|
||||
) {
|
||||
$description = $descriptions[$code];
|
||||
} else {
|
||||
$description = _t('BrokenExternalLink.UNKNOWNRESPONSE', 'Unknown Response Code');
|
||||
}
|
||||
return sprintf("%d (%s)", $code, $description);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents a track for a single page
|
||||
*/
|
||||
class BrokenExternalPageTrack extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Processed' => 'Boolean'
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'Page' => 'SiteTree',
|
||||
'Status' => 'BrokenExternalPageTrackStatus'
|
||||
);
|
||||
|
||||
private static $has_many = array(
|
||||
'BrokenLinks' => 'BrokenExternalLink'
|
||||
);
|
||||
|
||||
/**
|
||||
* @return SiteTree
|
||||
*/
|
||||
public function Page() {
|
||||
return Versioned::get_by_stage('SiteTree', 'Stage')
|
||||
->byID($this->PageID);
|
||||
}
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents the status of a track run
|
||||
*
|
||||
* @method DataList TrackedPages()
|
||||
* @method DataList BrokenLinks()
|
||||
* @property int $TotalPages Get total pages count
|
||||
* @property int $CompletedPages Get completed pages count
|
||||
*/
|
||||
class BrokenExternalPageTrackStatus extends DataObject {
|
||||
|
||||
private static $db = array(
|
||||
'Status' => 'Enum("Completed, Running", "Running")',
|
||||
'JobInfo' => 'Varchar(255)'
|
||||
);
|
||||
|
||||
private static $has_many = array(
|
||||
'TrackedPages' => 'BrokenExternalPageTrack',
|
||||
'BrokenLinks' => 'BrokenExternalLink'
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the latest track status
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function get_latest() {
|
||||
return self::get()
|
||||
->sort('ID', 'DESC')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of Pages yet to be checked
|
||||
*
|
||||
* @return DataList
|
||||
*/
|
||||
public function getIncompletePageList() {
|
||||
$pageIDs = $this
|
||||
->getIncompleteTracks()
|
||||
->column('PageID');
|
||||
if($pageIDs) return Versioned::get_by_stage('SiteTree', 'Stage')
|
||||
->byIDs($pageIDs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of incomplete BrokenExternalPageTrack
|
||||
*
|
||||
* @return DataList
|
||||
*/
|
||||
public function getIncompleteTracks() {
|
||||
return $this
|
||||
->TrackedPages()
|
||||
->filter('Processed', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total pages count
|
||||
*/
|
||||
public function getTotalPages() {
|
||||
return $this->TrackedPages()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get completed pages count
|
||||
*/
|
||||
public function getCompletedPages() {
|
||||
return $this
|
||||
->TrackedPages()
|
||||
->filter('Processed', 1)
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest run, or otherwise creates a new one
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function get_or_create() {
|
||||
// Check the current status
|
||||
$status = self::get_latest();
|
||||
if ($status && $status->Status == 'Running') {
|
||||
$status->updateStatus();
|
||||
return $status;
|
||||
}
|
||||
|
||||
return self::create_status();
|
||||
}
|
||||
|
||||
/*
|
||||
* Create and prepare a new status
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function create_status() {
|
||||
// If the script is to be started create a new status
|
||||
$status = self::create();
|
||||
$status->updateJobInfo('Creating new tracking object');
|
||||
|
||||
// Setup all pages to test
|
||||
$pageIDs = Versioned::get_by_stage('SiteTree', 'Stage')
|
||||
->column('ID');
|
||||
foreach ($pageIDs as $pageID) {
|
||||
$trackPage = BrokenExternalPageTrack::create();
|
||||
$trackPage->PageID = $pageID;
|
||||
$trackPage->StatusID = $status->ID;
|
||||
$trackPage->write();
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
public function updateJobInfo($message) {
|
||||
$this->JobInfo = $message;
|
||||
$this->write();
|
||||
}
|
||||
|
||||
/**
|
||||
* Self check status
|
||||
*/
|
||||
public function updateStatus() {
|
||||
if ($this->CompletedPages == $this->TotalPages) {
|
||||
$this->Status = 'Completed';
|
||||
$this->updateJobInfo('Setting to completed');
|
||||
}
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Content side-report listing pages with external broken links
|
||||
* @package externallinks
|
||||
* @subpackage content
|
||||
*/
|
||||
|
||||
class BrokenExternalLinksReport extends SS_Report {
|
||||
|
||||
/**
|
||||
* Returns the report title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function title() {
|
||||
return _t('ExternalBrokenLinksReport.EXTERNALBROKENLINKS', "External broken links report");
|
||||
}
|
||||
|
||||
public function columns() {
|
||||
return array(
|
||||
"Created" => "Checked",
|
||||
'Link' => array(
|
||||
'title' => 'External Link',
|
||||
'formatting' => function($value, $item) {
|
||||
return sprintf(
|
||||
'<a target="_blank" href="%s">%s</a>',
|
||||
Convert::raw2att($item->Link),
|
||||
Convert::raw2xml($item->Link)
|
||||
);
|
||||
}
|
||||
),
|
||||
'HTTPCodeDescription' => 'HTTP Error Code',
|
||||
"Title" => array(
|
||||
"title" => 'Page link is on',
|
||||
'formatting' => function($value, $item) {
|
||||
$page = $item->Page();
|
||||
return sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
Convert::raw2att($page->CMSEditLink()),
|
||||
Convert::raw2xml($page->Title)
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of columns(), to support the export to csv action
|
||||
* in {@link GridFieldExportButton} generateExportFileData method.
|
||||
* @return array
|
||||
*/
|
||||
public function getColumns() {
|
||||
return $this->columns();
|
||||
}
|
||||
|
||||
public function sourceRecords() {
|
||||
$track = BrokenExternalPageTrackStatus::get_latest();
|
||||
if ($track) return $track->BrokenLinks();
|
||||
return new ArrayList();
|
||||
}
|
||||
|
||||
public function getCMSFields() {
|
||||
Requirements::javascript('externallinks/javascript/BrokenExternalLinksReport.js');
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$reportResultSpan = '</ br></ br><h3 id="ReportHolder"></h3>';
|
||||
$reportResult = new LiteralField('ResultTitle', $reportResultSpan);
|
||||
$fields->push($reportResult);
|
||||
|
||||
$button = '<button id="externalLinksReport" type="button">%s</button>';
|
||||
$runReportButton = new LiteralField(
|
||||
'runReport',
|
||||
sprintf(
|
||||
$button,
|
||||
_t('ExternalBrokenLinksReport.RUNREPORT', 'Create new report')
|
||||
)
|
||||
);
|
||||
$fields->push($runReportButton);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
<?php
|
||||
|
||||
class CheckExternalLinksTask extends BuildTask {
|
||||
|
||||
private static $dependencies = array(
|
||||
'LinkChecker' => '%$LinkChecker'
|
||||
);
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $silent = false;
|
||||
|
||||
/**
|
||||
* @var LinkChecker
|
||||
*/
|
||||
protected $linkChecker;
|
||||
|
||||
protected $title = 'Checking broken External links in the SiteTree';
|
||||
|
||||
protected $description = 'A task that records external broken links in the SiteTree';
|
||||
|
||||
protected $enabled = true;
|
||||
|
||||
/**
|
||||
* Log a message
|
||||
*
|
||||
* @param string $message
|
||||
*/
|
||||
protected function log($message) {
|
||||
if(!$this->silent) Debug::message($message);
|
||||
}
|
||||
|
||||
public function run($request) {
|
||||
$this->runLinksCheck();
|
||||
}
|
||||
/**
|
||||
* Turn on or off message output
|
||||
*
|
||||
* @param bool $silent
|
||||
*/
|
||||
public function setSilent($silent) {
|
||||
$this->silent = $silent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LinkChecker $linkChecker
|
||||
*/
|
||||
public function setLinkChecker(LinkChecker $linkChecker) {
|
||||
$this->linkChecker = $linkChecker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LinkChecker
|
||||
*/
|
||||
public function getLinkChecker() {
|
||||
return $this->linkChecker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the status of a single link on a page
|
||||
*
|
||||
* @param BrokenExternalPageTrack $pageTrack
|
||||
* @param DOMNode $link
|
||||
*/
|
||||
protected function checkPageLink(BrokenExternalPageTrack $pageTrack, DOMNode $link) {
|
||||
$class = $link->getAttribute('class');
|
||||
$href = $link->getAttribute('href');
|
||||
$markedBroken = preg_match('/\b(ss-broken)\b/', $class);
|
||||
|
||||
// Check link
|
||||
$httpCode = $this->linkChecker->checkLink($href);
|
||||
if($httpCode === null) return; // Null link means uncheckable, such as an internal link
|
||||
|
||||
// If this code is broken then mark as such
|
||||
if($foundBroken = $this->isCodeBroken($httpCode)) {
|
||||
// Create broken record
|
||||
$brokenLink = new BrokenExternalLink();
|
||||
$brokenLink->Link = $href;
|
||||
$brokenLink->HTTPCode = $httpCode;
|
||||
$brokenLink->TrackID = $pageTrack->ID;
|
||||
$brokenLink->StatusID = $pageTrack->StatusID; // Slight denormalisation here for performance reasons
|
||||
$brokenLink->write();
|
||||
}
|
||||
|
||||
// Check if we need to update CSS class, otherwise return
|
||||
if($markedBroken == $foundBroken) return;
|
||||
if($foundBroken) {
|
||||
$class .= ' ss-broken';
|
||||
} else {
|
||||
$class = preg_replace('/\s*\b(ss-broken)\b\s*/', ' ', $class);
|
||||
}
|
||||
$link->setAttribute('class', trim($class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given HTTP code is "broken"
|
||||
*
|
||||
* @param int $httpCode
|
||||
* @return bool True if this is a broken code
|
||||
*/
|
||||
protected function isCodeBroken($httpCode) {
|
||||
// Null represents no request attempted
|
||||
if($httpCode === null) return false;
|
||||
|
||||
// do we have any whitelisted codes
|
||||
$ignoreCodes = Config::inst()->get('CheckExternalLinks', 'IgnoreCodes');
|
||||
if(is_array($ignoreCodes) && in_array($httpCode, $ignoreCodes)) return false;
|
||||
|
||||
// Check if code is outside valid range
|
||||
return $httpCode < 200 || $httpCode > 302;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the links checker and returns the track used
|
||||
*
|
||||
* @param int $limit Limit to number of pages to run, or null to run all
|
||||
* @return BrokenExternalPageTrackStatus
|
||||
*/
|
||||
public function runLinksCheck($limit = null) {
|
||||
// Check the current status
|
||||
$status = BrokenExternalPageTrackStatus::get_or_create();
|
||||
|
||||
// Calculate pages to run
|
||||
$pageTracks = $status->getIncompleteTracks();
|
||||
if($limit) $pageTracks = $pageTracks->limit($limit);
|
||||
|
||||
// Check each page
|
||||
foreach ($pageTracks as $pageTrack) {
|
||||
// Flag as complete
|
||||
$pageTrack->Processed = 1;
|
||||
$pageTrack->write();
|
||||
|
||||
// Check value of html area
|
||||
$page = $pageTrack->Page();
|
||||
$this->log("Checking {$page->Title}");
|
||||
$htmlValue = Injector::inst()->create('HTMLValue', $page->Content);
|
||||
if (!$htmlValue->isValid()) continue;
|
||||
|
||||
// Check each link
|
||||
$links = $htmlValue->getElementsByTagName('a');
|
||||
foreach($links as $link) {
|
||||
$this->checkPageLink($pageTrack, $link);
|
||||
}
|
||||
|
||||
// Update content of page based on link fixes / breakages
|
||||
$htmlValue->saveHTML();
|
||||
$page->Content = $htmlValue->getContent();
|
||||
$page->write();
|
||||
|
||||
// Once all links have been created for this page update HasBrokenLinks
|
||||
$count = $pageTrack->BrokenLinks()->count();
|
||||
$this->log("Found {$count} broken links");
|
||||
if($count) {
|
||||
// Bypass the ORM as syncLinkTracking does not allow you to update HasBrokenLink to true
|
||||
DB::query(sprintf(
|
||||
'UPDATE "SiteTree" SET "HasBrokenLink" = 1 WHERE "ID" = \'%d\'',
|
||||
intval($pageTrack->ID)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$status->updateJobInfo('Updating completed pages');
|
||||
$status->updateStatus();
|
||||
return $status;
|
||||
}
|
||||
|
||||
private function updateCompletedPages($trackID = 0) {
|
||||
$noPages = BrokenExternalPageTrack::get()
|
||||
->filter(array(
|
||||
'TrackID' => $trackID,
|
||||
'Processed' => 1
|
||||
))
|
||||
->count();
|
||||
$track = BrokenExternalPageTrackStatus::get_latest();
|
||||
$track->CompletedPages = $noPages;
|
||||
$track->write();
|
||||
return $noPages;
|
||||
}
|
||||
|
||||
private function updateJobInfo($message) {
|
||||
$track = BrokenExternalPageTrackStatus::get_latest();
|
||||
if($track) {
|
||||
$track->JobInfo = $message;
|
||||
$track->write();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Check links using curl
|
||||
*/
|
||||
class CurlLinkChecker implements LinkChecker {
|
||||
|
||||
/**
|
||||
* Return cache
|
||||
*
|
||||
* @return Zend_Cache_Frontend
|
||||
*/
|
||||
protected function getCache() {
|
||||
return SS_Cache::factory(
|
||||
__CLASS__,
|
||||
'Output',
|
||||
array('automatic_serialization' => true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the http status code for a given link
|
||||
*
|
||||
* @param string $href URL to check
|
||||
* @return int HTTP status code, or null if not checkable (not a link)
|
||||
*/
|
||||
public function checkLink($href) {
|
||||
// Skip non-external links
|
||||
if(!preg_match('/^https?[^:]*:\/\//', $href)) return null;
|
||||
|
||||
// Check if we have a cached result
|
||||
$cacheKey = md5($href);
|
||||
$result = $this->getCache()->load($cacheKey);
|
||||
if($result !== false) return $result;
|
||||
|
||||
// No cached result so just request
|
||||
$handle = curl_init($href);
|
||||
curl_setopt($handle, CURLOPT_RETURNTRANSFER, TRUE);
|
||||
curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
curl_setopt($handle, CURLOPT_TIMEOUT, 10);
|
||||
curl_exec($handle);
|
||||
$httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
|
||||
curl_close($handle);
|
||||
|
||||
// Cache result
|
||||
$this->getCache()->save($httpCode, $cacheKey);
|
||||
return $httpCode;
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Provides an interface for checking that a link is valid
|
||||
*/
|
||||
interface LinkChecker {
|
||||
|
||||
/**
|
||||
* Determine the http status code for a given link
|
||||
*
|
||||
* @param string $href URL to check
|
||||
* @return int HTTP status code, or null if not checkable (not a link)
|
||||
*/
|
||||
public function checkLink($href);
|
||||
}
|
2
codecov.yml
Normal file
2
codecov.yml
Normal file
@ -0,0 +1,2 @@
|
||||
comment: false
|
||||
|
@ -1,13 +1,9 @@
|
||||
{
|
||||
"name": "silverstripe/externallinks",
|
||||
"description": "Adds tracking of external broken links to the SilverStripe CMS",
|
||||
"type": "silverstripe-module",
|
||||
"keywords": [
|
||||
"silverstripe",
|
||||
"broken",
|
||||
"links",
|
||||
"href"
|
||||
],
|
||||
"description":
|
||||
"Adds tracking of broken external links to the SilverStripe CMS",
|
||||
"type": "silverstripe-vendormodule",
|
||||
"keywords": ["silverstripe", "broken", "links", "href"],
|
||||
"license": "BSD-3-Clause",
|
||||
"authors": [
|
||||
{
|
||||
@ -16,19 +12,28 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"silverstripe/framework": "~3.1",
|
||||
"silverstripe/cms": "~3.1"
|
||||
"silverstripe/recipe-cms": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"hafriedlander/silverstripe-phockito": "*",
|
||||
"phpunit/PHPUnit": "~3.7@stable"
|
||||
"phpunit/PHPUnit": "^5.7",
|
||||
"squizlabs/php_codesniffer": "^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"silverstripe/queuedjobs": "Speeds up running the job for Content Editors fropm the report"
|
||||
"silverstripe/queuedjobs":
|
||||
"Provides a more efficient method of generating/updating the report"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"SilverStripe\\ExternalLinks\\": "src/",
|
||||
"SilverStripe\\ExternalLinks\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"expose": ["javascript"],
|
||||
"branch-alias": {
|
||||
"dev-master": "2.x-dev"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
de:
|
||||
BrokenExternalLink:
|
||||
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
|
||||
NOTAVAILABLE: 'Server nicht verfügbar'
|
||||
PLURALNAME: 'Defekte externe Links'
|
||||
SINGULARNAME: 'Defekter externer Link'
|
||||
UNKNOWNRESPONSE: 'Unbekannter Antwortcode'
|
||||
ExternalBrokenLinksReport:
|
||||
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
|
||||
RUNREPORT: 'Generiere neuen Bericht'
|
||||
|
10
lang/en.yml
10
lang/en.yml
@ -1,17 +1,17 @@
|
||||
en:
|
||||
BrokenExternalLink:
|
||||
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
|
||||
NOTAVAILABLE: 'Server Not Available'
|
||||
PLURALNAME: 'Broken External Links'
|
||||
SINGULARNAME: 'Broken External Link'
|
||||
UNKNOWNRESPONSE: 'Unknown Response Code'
|
||||
BrokenExternalPageTrack:
|
||||
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
|
||||
PLURALNAME: 'Broken External Page Tracks'
|
||||
SINGULARNAME: 'Broken External Page Track'
|
||||
BrokenExternalPageTrackStatus:
|
||||
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
|
||||
PLURALNAME: 'Broken External Page Track Statuss'
|
||||
SINGULARNAME: 'Broken External Page Track Status'
|
||||
CheckExternalLiksJob:
|
||||
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
|
||||
TITLE: 'Checking for external broken links'
|
||||
ExternalBrokenLinksReport:
|
||||
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
|
||||
EXTERNALBROKENLINKS: 'External broken links report'
|
||||
RUNREPORT: 'Create new report'
|
||||
|
10
lang/eo.yml
10
lang/eo.yml
@ -1,17 +1,17 @@
|
||||
eo:
|
||||
BrokenExternalLink:
|
||||
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
|
||||
NOTAVAILABLE: 'Servilo estas neatingenbla'
|
||||
PLURALNAME: 'Rompitaj eksteraj ligiloj'
|
||||
SINGULARNAME: 'Rompita ekstera ligilo'
|
||||
UNKNOWNRESPONSE: 'Nekonata respondokodo'
|
||||
BrokenExternalPageTrack:
|
||||
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
|
||||
PLURALNAME: 'Rompitaj eksteraj paĝaj trakoj'
|
||||
SINGULARNAME: 'Rompita ekstera paĝa trako'
|
||||
BrokenExternalPageTrackStatus:
|
||||
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
|
||||
PLURALNAME: 'Stato de rompitaj eksteraj paĝaj trakoj'
|
||||
SINGULARNAME: 'Stato de rompita ekstera paĝa trako'
|
||||
CheckExternalLiksJob:
|
||||
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
|
||||
TITLE: 'Kontrolas por eksteraj rompitaj ligiloj'
|
||||
ExternalBrokenLinksReport:
|
||||
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
|
||||
EXTERNALBROKENLINKS: 'Raporto pri eksteraj rompitaj ligiloj'
|
||||
RUNREPORT: 'Krei novan raporton'
|
||||
|
10
lang/pl.yml
10
lang/pl.yml
@ -1,17 +1,17 @@
|
||||
pl:
|
||||
BrokenExternalLink:
|
||||
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
|
||||
NOTAVAILABLE: 'Serwer niedostępny'
|
||||
PLURALNAME: 'Uszkodzone linki zewnętrzne'
|
||||
SINGULARNAME: 'Uszkodzony link zewnętrzny'
|
||||
UNKNOWNRESPONSE: 'Nieznany kod odpowiedzi'
|
||||
BrokenExternalPageTrack:
|
||||
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
|
||||
PLURALNAME: 'Wykrywania wadliwych stron zewnętrznych'
|
||||
SINGULARNAME: 'Wykrywanie wadliwych stron zewnętrznych'
|
||||
BrokenExternalPageTrackStatus:
|
||||
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
|
||||
PLURALNAME: 'Statusy wykrywania wadliwych stron zewnętrznych'
|
||||
SINGULARNAME: 'Status wykrywania wadliwych stron zewnętrznych'
|
||||
CheckExternalLiksJob:
|
||||
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
|
||||
TITLE: 'Wyszukiwanie uszkodzonych linków zewnętrznych'
|
||||
ExternalBrokenLinksReport:
|
||||
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
|
||||
EXTERNALBROKENLINKS: 'Raport uszkodzonych linków zewnętrznych'
|
||||
RUNREPORT: 'Stwórz nowy raport'
|
||||
|
17
lang/ru.yml
17
lang/ru.yml
@ -1,17 +0,0 @@
|
||||
ru:
|
||||
BrokenExternalLink:
|
||||
NOTAVAILABLE: 'Сервер не доступен'
|
||||
PLURALNAME: 'Недоступные внешние ссылки'
|
||||
SINGULARNAME: 'Недоступная внешняя ссылка'
|
||||
UNKNOWNRESPONSE: 'Неизвестный ответ сервера'
|
||||
BrokenExternalPageTrack:
|
||||
PLURALNAME: 'Внешнее отслеживание страниц нарушено'
|
||||
SINGULARNAME: 'Внешнее отслеживание страниц нарушено'
|
||||
BrokenExternalPageTrackStatus:
|
||||
PLURALNAME: 'Внешнее отслеживание страниц нарушено'
|
||||
SINGULARNAME: 'Внешнее отслеживание страниц нарушено'
|
||||
CheckExternalLiksJob:
|
||||
TITLE: 'Проверяю внешние ссылки'
|
||||
ExternalBrokenLinksReport:
|
||||
EXTERNALBROKENLINKS: 'Отчёт о неработающих внешних ссылках'
|
||||
RUNREPORT: 'Создать новый отчёт'
|
@ -1,4 +1,4 @@
|
||||
Copyright (c) 2016, SilverStripe Limited
|
||||
Copyright (c) 2017, SilverStripe Limited
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
10
phpcs.xml.dist
Normal file
10
phpcs.xml.dist
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ruleset name="SilverStripe">
|
||||
<description>CodeSniffer ruleset for SilverStripe coding conventions.</description>
|
||||
|
||||
<rule ref="PSR2" >
|
||||
<!-- Current exclusions -->
|
||||
<exclude name="PSR1.Methods.CamelCapsMethodName" />
|
||||
<exclude name="PSR1.Files.SideEffects.FoundWithSymbols" />
|
||||
</rule>
|
||||
</ruleset>
|
13
phpunit.xml.dist
Normal file
13
phpunit.xml.dist
Normal file
@ -0,0 +1,13 @@
|
||||
<phpunit bootstrap="vendor/silverstripe/cms/tests/bootstrap.php" colors="true">
|
||||
<testsuite name="Default">
|
||||
<directory>tests/</directory>
|
||||
</testsuite>
|
||||
<filter>
|
||||
<whitelist addUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">src/</directory>
|
||||
<exclude>
|
||||
<directory suffix=".php">tests/</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
73
src/Controllers/CMSExternalLinksController.php
Normal file
73
src/Controllers/CMSExternalLinksController.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ExternalLinks\Controllers;
|
||||
|
||||
use SilverStripe\Control\HTTP;
|
||||
use SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus;
|
||||
use SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob;
|
||||
use SilverStripe\ExternalLinks\Tasks\CheckExternalLinksTask;
|
||||
use SilverStripe\Control\Controller;
|
||||
use Symbiote\QueuedJobs\Services\QueuedJobService;
|
||||
|
||||
class CMSExternalLinksController extends Controller
|
||||
{
|
||||
|
||||
private static $allowed_actions = [
|
||||
'getJobStatus',
|
||||
'start'
|
||||
];
|
||||
|
||||
/**
|
||||
* Respond to Ajax requests for info on a running job
|
||||
*
|
||||
* @return string JSON string detailing status of the job
|
||||
*/
|
||||
public function getJobStatus()
|
||||
{
|
||||
// Set headers
|
||||
HTTP::set_cache_age(0);
|
||||
HTTP::add_cache_headers($this->response);
|
||||
$this->response
|
||||
->addHeader('Content-Type', 'application/json')
|
||||
->addHeader('Content-Encoding', 'UTF-8')
|
||||
->addHeader('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
// Format status
|
||||
$track = BrokenExternalPageTrackStatus::get_latest();
|
||||
if ($track) {
|
||||
return json_encode([
|
||||
'TrackID' => $track->ID,
|
||||
'Status' => $track->Status,
|
||||
'Completed' => $track->getCompletedPages(),
|
||||
'Total' => $track->getTotalPages()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts a broken external link check
|
||||
*/
|
||||
public function start()
|
||||
{
|
||||
// return if the a job is already running
|
||||
$status = BrokenExternalPageTrackStatus::get_latest();
|
||||
if ($status && $status->Status == 'Running') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new job
|
||||
if (class_exists(QueuedJobService::class)) {
|
||||
// Force the creation of a new run
|
||||
BrokenExternalPageTrackStatus::create_status();
|
||||
$checkLinks = new CheckExternalLinksJob();
|
||||
singleton(QueuedJobService::class)->queueJob($checkLinks);
|
||||
} else {
|
||||
//TODO this hangs as it waits for the connection to be released
|
||||
// should return back and continue processing
|
||||
// http://us3.php.net/manual/en/features.connection-handling.php
|
||||
$task = CheckExternalLinksTask::create();
|
||||
$task->runLinksCheck();
|
||||
}
|
||||
}
|
||||
}
|
46
src/Jobs/CheckExternalLinksJob.php
Normal file
46
src/Jobs/CheckExternalLinksJob.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ExternalLinks\Jobs;
|
||||
|
||||
use Symbiote\QueuedJobs\Services\AbstractQueuedJob;
|
||||
use Symbiote\QueuedJobs\Services\QueuedJob;
|
||||
use SilverStripe\ExternalLinks\Tasks\CheckExternalLinksTask;
|
||||
|
||||
if (!class_exists(AbstractQueuedJob::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Job for running a external link check for published pages
|
||||
*
|
||||
*/
|
||||
class CheckExternalLinksJob extends AbstractQueuedJob implements QueuedJob
|
||||
{
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return _t(__CLASS__ . '.TITLE', 'Checking for external broken links');
|
||||
}
|
||||
|
||||
public function getJobType()
|
||||
{
|
||||
return QueuedJob::QUEUED;
|
||||
}
|
||||
|
||||
public function getSignature()
|
||||
{
|
||||
return md5(get_class($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check an individual page
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
$task = CheckExternalLinksTask::create();
|
||||
$track = $task->runLinksCheck(1);
|
||||
$this->currentStep = $track->CompletedPages;
|
||||
$this->totalSteps = $track->TotalPages;
|
||||
$this->isComplete = $track->Status === 'Completed';
|
||||
}
|
||||
}
|
85
src/Model/BrokenExternalLink.php
Normal file
85
src/Model/BrokenExternalLink.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ExternalLinks\Model;
|
||||
|
||||
use SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack;
|
||||
use SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
/**
|
||||
* Represents a single link checked for a single run that is broken
|
||||
*
|
||||
* @method BrokenExternalPageTrack Track()
|
||||
* @method BrokenExternalPageTrackStatus Status()
|
||||
*/
|
||||
class BrokenExternalLink extends DataObject
|
||||
{
|
||||
private static $table_name = 'BrokenExternalLink';
|
||||
|
||||
private static $db = array(
|
||||
'Link' => 'Varchar(2083)', // 2083 is the maximum length of a URL in Internet Explorer.
|
||||
'HTTPCode' =>'Int'
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'Track' => BrokenExternalPageTrack::class,
|
||||
'Status' => BrokenExternalPageTrackStatus::class
|
||||
);
|
||||
|
||||
private static $summary_fields = array(
|
||||
'Created' => 'Checked',
|
||||
'Link' => 'External Link',
|
||||
'HTTPCodeDescription' => 'HTTP Error Code',
|
||||
'Page.Title' => 'Page link is on'
|
||||
);
|
||||
|
||||
private static $searchable_fields = array(
|
||||
'HTTPCode' => array('title' => 'HTTP Code')
|
||||
);
|
||||
|
||||
/**
|
||||
* @return SiteTree
|
||||
*/
|
||||
public function Page()
|
||||
{
|
||||
return $this->Track()->Page();
|
||||
}
|
||||
|
||||
public function canEdit($member = false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function canView($member = false)
|
||||
{
|
||||
$member = $member ? $member : Member::currentUser();
|
||||
$codes = array('content-authors', 'administrators');
|
||||
return Permission::checkMember($member, $codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a human readable description of a response code
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHTTPCodeDescription()
|
||||
{
|
||||
$code = $this->HTTPCode;
|
||||
|
||||
try {
|
||||
$response = HTTPResponse::create('', $code);
|
||||
// Assume that $code = 0 means there was no response
|
||||
$description = $code ?
|
||||
$response->getStatusDescription() :
|
||||
_t(__CLASS__ . '.NOTAVAILABLE', 'Server Not Available');
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$description = _t(__CLASS__ . '.UNKNOWNRESPONSE', 'Unknown Response Code');
|
||||
}
|
||||
|
||||
return sprintf("%d (%s)", $code, $description);
|
||||
}
|
||||
}
|
39
src/Model/BrokenExternalPageTrack.php
Normal file
39
src/Model/BrokenExternalPageTrack.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ExternalLinks\Model;
|
||||
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
use SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus;
|
||||
use SilverStripe\ExternalLinks\Model\BrokenExternalLink;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
/**
|
||||
* Represents a track for a single page
|
||||
*/
|
||||
class BrokenExternalPageTrack extends DataObject
|
||||
{
|
||||
private static $table_name = 'BrokenExternalPageTrack';
|
||||
|
||||
private static $db = array(
|
||||
'Processed' => 'Boolean'
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'Page' => SiteTree::class,
|
||||
'Status' => BrokenExternalPageTrackStatus::class
|
||||
);
|
||||
|
||||
private static $has_many = array(
|
||||
'BrokenLinks' => BrokenExternalLink::class
|
||||
);
|
||||
|
||||
/**
|
||||
* @return SiteTree
|
||||
*/
|
||||
public function Page()
|
||||
{
|
||||
return Versioned::get_by_stage(SiteTree::class, 'Stage')
|
||||
->byID($this->PageID);
|
||||
}
|
||||
}
|
153
src/Model/BrokenExternalPageTrackStatus.php
Normal file
153
src/Model/BrokenExternalPageTrackStatus.php
Normal file
@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ExternalLinks\Model;
|
||||
|
||||
use SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack;
|
||||
use SilverStripe\ExternalLinks\Model\BrokenExternalLink;
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
/**
|
||||
* Represents the status of a track run
|
||||
*
|
||||
* @method DataList TrackedPages()
|
||||
* @method DataList BrokenLinks()
|
||||
* @property int $TotalPages Get total pages count
|
||||
* @property int $CompletedPages Get completed pages count
|
||||
*/
|
||||
class BrokenExternalPageTrackStatus extends DataObject
|
||||
{
|
||||
private static $table_name = 'BrokenExternalPageTrackStatus';
|
||||
|
||||
private static $db = array(
|
||||
'Status' => 'Enum("Completed, Running", "Running")',
|
||||
'JobInfo' => 'Varchar(255)'
|
||||
);
|
||||
|
||||
private static $has_many = array(
|
||||
'TrackedPages' => BrokenExternalPageTrack::class,
|
||||
'BrokenLinks' => BrokenExternalLink::class
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the latest track status
|
||||
*
|
||||
* @return BrokenExternalPageTrackStatus
|
||||
*/
|
||||
public static function get_latest()
|
||||
{
|
||||
return self::get()
|
||||
->sort('ID', 'DESC')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of Pages yet to be checked
|
||||
*
|
||||
* @return DataList
|
||||
*/
|
||||
public function getIncompletePageList()
|
||||
{
|
||||
$pageIDs = $this
|
||||
->getIncompleteTracks()
|
||||
->column('PageID');
|
||||
if ($pageIDs) {
|
||||
return Versioned::get_by_stage(SiteTree::class, 'Stage')
|
||||
->byIDs($pageIDs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of incomplete BrokenExternalPageTrack
|
||||
*
|
||||
* @return DataList
|
||||
*/
|
||||
public function getIncompleteTracks()
|
||||
{
|
||||
return $this
|
||||
->TrackedPages()
|
||||
->filter('Processed', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total pages count
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getTotalPages()
|
||||
{
|
||||
return $this->TrackedPages()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get completed pages count
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCompletedPages()
|
||||
{
|
||||
return $this
|
||||
->TrackedPages()
|
||||
->filter('Processed', 1)
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest run, or otherwise creates a new one
|
||||
*
|
||||
* @return BrokenExternalPageTrackStatus
|
||||
*/
|
||||
public static function get_or_create()
|
||||
{
|
||||
// Check the current status
|
||||
$status = self::get_latest();
|
||||
if ($status && $status->Status == 'Running') {
|
||||
$status->updateStatus();
|
||||
return $status;
|
||||
}
|
||||
|
||||
return self::create_status();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and prepare a new status
|
||||
*
|
||||
* @return BrokenExternalPageTrackStatus
|
||||
*/
|
||||
public static function create_status()
|
||||
{
|
||||
// If the script is to be started create a new status
|
||||
$status = self::create();
|
||||
$status->updateJobInfo('Creating new tracking object');
|
||||
|
||||
// Setup all pages to test
|
||||
$pageIDs = Versioned::get_by_stage(SiteTree::class, 'Stage')
|
||||
->column('ID');
|
||||
foreach ($pageIDs as $pageID) {
|
||||
$trackPage = BrokenExternalPageTrack::create();
|
||||
$trackPage->PageID = $pageID;
|
||||
$trackPage->StatusID = $status->ID;
|
||||
$trackPage->write();
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
public function updateJobInfo($message)
|
||||
{
|
||||
$this->JobInfo = $message;
|
||||
$this->write();
|
||||
}
|
||||
|
||||
/**
|
||||
* Self check status
|
||||
*/
|
||||
public function updateStatus()
|
||||
{
|
||||
if ($this->CompletedPages == $this->TotalPages) {
|
||||
$this->Status = 'Completed';
|
||||
$this->updateJobInfo('Setting to completed');
|
||||
}
|
||||
}
|
||||
}
|
102
src/Reports/BrokenExternalLinksReport.php
Normal file
102
src/Reports/BrokenExternalLinksReport.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ExternalLinks\Reports;
|
||||
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\View\HTML;
|
||||
use SilverStripe\Forms\LiteralField;
|
||||
use SilverStripe\Reports\Report;
|
||||
use SilverStripe\View\Requirements;
|
||||
|
||||
/**
|
||||
* Content side-report listing pages with external broken links
|
||||
* @package externallinks
|
||||
*/
|
||||
|
||||
class BrokenExternalLinksReport extends Report
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns the report title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function title()
|
||||
{
|
||||
return _t(__CLASS__ . '.EXTERNALBROKENLINKS', "External broken links report");
|
||||
}
|
||||
|
||||
public function columns()
|
||||
{
|
||||
return array(
|
||||
"Created" => "Checked",
|
||||
'Link' => array(
|
||||
'title' => 'External Link',
|
||||
'formatting' => function ($value, $item) {
|
||||
return sprintf(
|
||||
'<a target="_blank" href="%s">%s</a>',
|
||||
Convert::raw2att($item->Link),
|
||||
Convert::raw2xml($item->Link)
|
||||
);
|
||||
}
|
||||
),
|
||||
'HTTPCodeDescription' => 'HTTP Error Code',
|
||||
"Title" => array(
|
||||
"title" => 'Page link is on',
|
||||
'formatting' => function ($value, $item) {
|
||||
$page = $item->Page();
|
||||
return sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
Convert::raw2att($page->CMSEditLink()),
|
||||
Convert::raw2xml($page->Title)
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of columns(), to support the export to csv action
|
||||
* in {@link GridFieldExportButton} generateExportFileData method.
|
||||
* @return array
|
||||
*/
|
||||
public function getColumns()
|
||||
{
|
||||
return $this->columns();
|
||||
}
|
||||
|
||||
public function sourceRecords()
|
||||
{
|
||||
$track = BrokenExternalPageTrackStatus::get_latest();
|
||||
if ($track) {
|
||||
return $track->BrokenLinks();
|
||||
}
|
||||
return ArrayList::create();
|
||||
}
|
||||
|
||||
public function getCMSFields()
|
||||
{
|
||||
Requirements::javascript('silverstripe/externallinks: javascript/BrokenExternalLinksReport.js');
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$reportResultSpan = '</ br></ br><h3 id="ReportHolder"></h3>';
|
||||
$reportResult = LiteralField::create('ResultTitle', $reportResultSpan);
|
||||
$fields->push($reportResult);
|
||||
|
||||
$button = HTML::createTag(
|
||||
'button',
|
||||
[
|
||||
'id' => 'externalLinksReport',
|
||||
'type' => 'button',
|
||||
'class' => 'btn btn-primary'
|
||||
],
|
||||
_t(__CLASS__ . '.RUNREPORT', 'Create new report')
|
||||
);
|
||||
$runReportButton = LiteralField::create('runReport', $button);
|
||||
$fields->push($runReportButton);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
230
src/Tasks/CheckExternalLinksTask.php
Normal file
230
src/Tasks/CheckExternalLinksTask.php
Normal file
@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ExternalLinks\Tasks;
|
||||
|
||||
use SilverStripe\ExternalLinks\Model\BrokenExternalLink;
|
||||
use SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack;
|
||||
use SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus;
|
||||
use SilverStripe\Dev\BuildTask;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\Dev\Debug;
|
||||
use DOMNode;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\ExternalLinks\Tasks\LinkChecker;
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
|
||||
class CheckExternalLinksTask extends BuildTask
|
||||
{
|
||||
|
||||
private static $dependencies = [
|
||||
'LinkChecker' => '%$' . LinkChecker::class
|
||||
];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $silent = false;
|
||||
|
||||
/**
|
||||
* @var LinkChecker
|
||||
*/
|
||||
protected $linkChecker;
|
||||
|
||||
protected $title = 'Checking broken External links in the SiteTree';
|
||||
|
||||
protected $description = 'A task that records external broken links in the SiteTree';
|
||||
|
||||
protected $enabled = true;
|
||||
|
||||
/**
|
||||
* Log a message
|
||||
*
|
||||
* @param string $message
|
||||
*/
|
||||
protected function log($message)
|
||||
{
|
||||
if (!$this->silent) {
|
||||
Debug::message($message);
|
||||
}
|
||||
}
|
||||
|
||||
public function run($request)
|
||||
{
|
||||
$this->runLinksCheck();
|
||||
}
|
||||
/**
|
||||
* Turn on or off message output
|
||||
*
|
||||
* @param bool $silent
|
||||
*/
|
||||
public function setSilent($silent)
|
||||
{
|
||||
$this->silent = $silent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LinkChecker $linkChecker
|
||||
*/
|
||||
public function setLinkChecker(LinkChecker $linkChecker)
|
||||
{
|
||||
$this->linkChecker = $linkChecker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LinkChecker
|
||||
*/
|
||||
public function getLinkChecker()
|
||||
{
|
||||
return $this->linkChecker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the status of a single link on a page
|
||||
*
|
||||
* @param BrokenExternalPageTrack $pageTrack
|
||||
* @param DOMNode $link
|
||||
*/
|
||||
protected function checkPageLink(BrokenExternalPageTrack $pageTrack, DOMNode $link)
|
||||
{
|
||||
$class = $link->getAttribute('class');
|
||||
$href = $link->getAttribute('href');
|
||||
$markedBroken = preg_match('/\b(ss-broken)\b/', $class);
|
||||
|
||||
// Check link
|
||||
$httpCode = $this->linkChecker->checkLink($href);
|
||||
if ($httpCode === null) {
|
||||
return; // Null link means uncheckable, such as an internal link
|
||||
}
|
||||
|
||||
// If this code is broken then mark as such
|
||||
if ($foundBroken = $this->isCodeBroken($httpCode)) {
|
||||
// Create broken record
|
||||
$brokenLink = new BrokenExternalLink();
|
||||
$brokenLink->Link = $href;
|
||||
$brokenLink->HTTPCode = $httpCode;
|
||||
$brokenLink->TrackID = $pageTrack->ID;
|
||||
$brokenLink->StatusID = $pageTrack->StatusID; // Slight denormalisation here for performance reasons
|
||||
$brokenLink->write();
|
||||
}
|
||||
|
||||
// Check if we need to update CSS class, otherwise return
|
||||
if ($markedBroken == $foundBroken) {
|
||||
return;
|
||||
}
|
||||
if ($foundBroken) {
|
||||
$class .= ' ss-broken';
|
||||
} else {
|
||||
$class = preg_replace('/\s*\b(ss-broken)\b\s*/', ' ', $class);
|
||||
}
|
||||
$link->setAttribute('class', trim($class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given HTTP code is "broken"
|
||||
*
|
||||
* @param int $httpCode
|
||||
* @return bool True if this is a broken code
|
||||
*/
|
||||
protected function isCodeBroken($httpCode)
|
||||
{
|
||||
// Null represents no request attempted
|
||||
if ($httpCode === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// do we have any whitelisted codes
|
||||
$ignoreCodes = $this->config()->get('IgnoreCodes');
|
||||
if (is_array($ignoreCodes) && in_array($httpCode, $ignoreCodes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if code is outside valid range
|
||||
return $httpCode < 200 || $httpCode > 302;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the links checker and returns the track used
|
||||
*
|
||||
* @param int $limit Limit to number of pages to run, or null to run all
|
||||
* @return BrokenExternalPageTrackStatus
|
||||
*/
|
||||
public function runLinksCheck($limit = null)
|
||||
{
|
||||
// Check the current status
|
||||
$status = BrokenExternalPageTrackStatus::get_or_create();
|
||||
|
||||
// Calculate pages to run
|
||||
$pageTracks = $status->getIncompleteTracks();
|
||||
if ($limit) {
|
||||
$pageTracks = $pageTracks->limit($limit);
|
||||
}
|
||||
|
||||
// Check each page
|
||||
foreach ($pageTracks as $pageTrack) {
|
||||
// Flag as complete
|
||||
$pageTrack->Processed = 1;
|
||||
$pageTrack->write();
|
||||
|
||||
// Check value of html area
|
||||
$page = $pageTrack->Page();
|
||||
$this->log("Checking {$page->Title}");
|
||||
$htmlValue = Injector::inst()->create('HTMLValue', $page->Content);
|
||||
if (!$htmlValue->isValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check each link
|
||||
$links = $htmlValue->getElementsByTagName('a');
|
||||
foreach ($links as $link) {
|
||||
$this->checkPageLink($pageTrack, $link);
|
||||
}
|
||||
|
||||
// Update content of page based on link fixes / breakages
|
||||
$htmlValue->saveHTML();
|
||||
$page->Content = $htmlValue->getContent();
|
||||
$page->write();
|
||||
|
||||
// Once all links have been created for this page update HasBrokenLinks
|
||||
$count = $pageTrack->BrokenLinks()->count();
|
||||
$this->log("Found {$count} broken links");
|
||||
if ($count) {
|
||||
$siteTreeTable = DataObject::getSchema()->tableName(SiteTree::class);
|
||||
// Bypass the ORM as syncLinkTracking does not allow you to update HasBrokenLink to true
|
||||
DB::query(sprintf(
|
||||
'UPDATE "%s" SET "HasBrokenLink" = 1 WHERE "ID" = \'%d\'',
|
||||
$siteTreeTable,
|
||||
intval($pageTrack->ID)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$status->updateJobInfo('Updating completed pages');
|
||||
$status->updateStatus();
|
||||
return $status;
|
||||
}
|
||||
|
||||
private function updateCompletedPages($trackID = 0)
|
||||
{
|
||||
$noPages = BrokenExternalPageTrack::get()
|
||||
->filter(array(
|
||||
'TrackID' => $trackID,
|
||||
'Processed' => 1
|
||||
))
|
||||
->count();
|
||||
$track = BrokenExternalPageTrackStatus::get_latest();
|
||||
$track->CompletedPages = $noPages;
|
||||
$track->write();
|
||||
return $noPages;
|
||||
}
|
||||
|
||||
private function updateJobInfo($message)
|
||||
{
|
||||
$track = BrokenExternalPageTrackStatus::get_latest();
|
||||
if ($track) {
|
||||
$track->JobInfo = $message;
|
||||
$track->write();
|
||||
}
|
||||
}
|
||||
}
|
57
src/Tasks/CurlLinkChecker.php
Normal file
57
src/Tasks/CurlLinkChecker.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ExternalLinks\Tasks;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
|
||||
/**
|
||||
* Check links using curl
|
||||
*/
|
||||
class CurlLinkChecker implements LinkChecker
|
||||
{
|
||||
|
||||
/**
|
||||
* Return cache
|
||||
*
|
||||
* @return Zend_Cache_Frontend
|
||||
*/
|
||||
protected function getCache()
|
||||
{
|
||||
return Injector::inst()->get(CacheInterface::class . '.CurlLinkChecker');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the http status code for a given link
|
||||
*
|
||||
* @param string $href URL to check
|
||||
* @return int HTTP status code, or null if not checkable (not a link)
|
||||
*/
|
||||
public function checkLink($href)
|
||||
{
|
||||
// Skip non-external links
|
||||
if (!preg_match('/^https?[^:]*:\/\//', $href)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if we have a cached result
|
||||
$cacheKey = md5($href);
|
||||
$result = $this->getCache()->get($cacheKey);
|
||||
if ($result !== false) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// No cached result so just request
|
||||
$handle = curl_init($href);
|
||||
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
curl_setopt($handle, CURLOPT_TIMEOUT, 10);
|
||||
curl_exec($handle);
|
||||
$httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
|
||||
curl_close($handle);
|
||||
|
||||
// Cache result
|
||||
$this->getCache()->set($httpCode, $cacheKey);
|
||||
return $httpCode;
|
||||
}
|
||||
}
|
18
src/Tasks/LinkChecker.php
Normal file
18
src/Tasks/LinkChecker.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ExternalLinks\Tasks;
|
||||
|
||||
/**
|
||||
* Provides an interface for checking that a link is valid
|
||||
*/
|
||||
interface LinkChecker
|
||||
{
|
||||
|
||||
/**
|
||||
* Determine the http status code for a given link
|
||||
*
|
||||
* @param string $href URL to check
|
||||
* @return int HTTP status code, or null if not checkable (not a link)
|
||||
*/
|
||||
public function checkLink($href);
|
||||
}
|
@ -1,148 +1,110 @@
|
||||
<?php
|
||||
|
||||
class ExternalLinksTest extends SapphireTest {
|
||||
namespace SilverStripe\ExternalLinks\Tests;
|
||||
|
||||
protected static $fixture_file = 'ExternalLinksTest.yml';
|
||||
use SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport;
|
||||
use SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus;
|
||||
use SilverStripe\ExternalLinks\Tasks\CheckExternalLinksTask;
|
||||
use SilverStripe\ExternalLinks\Tests\ExternalLinksTestPage;
|
||||
use SilverStripe\i18n\i18n;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\ExternalLinks\Tasks\LinkChecker;
|
||||
use SilverStripe\ExternalLinks\Tests\Stubs\PretendLinkChecker;
|
||||
use SilverStripe\Reports\Report;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
|
||||
protected $extraDataObjects = array(
|
||||
'ExternalLinksTestPage'
|
||||
);
|
||||
class ExternalLinksTest extends SapphireTest
|
||||
{
|
||||
|
||||
protected $illegalExtensions = array(
|
||||
'SiteTree' => array('Translatable')
|
||||
protected static $fixture_file = 'ExternalLinksTest.yml';
|
||||
|
||||
protected static $extra_dataobjects = array(
|
||||
ExternalLinksTestPage::class
|
||||
);
|
||||
|
||||
public function setUpOnce() {
|
||||
if (class_exists('Phockito')) {
|
||||
Phockito::include_hamcrest(false);
|
||||
}
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
parent::setUpOnce();
|
||||
}
|
||||
// Stub link checker
|
||||
$checker = new PretendLinkChecker;
|
||||
Injector::inst()->registerService($checker, LinkChecker::class);
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
public function testLinks()
|
||||
{
|
||||
// Run link checker
|
||||
$task = CheckExternalLinksTask::create();
|
||||
$task->setSilent(true); // Be quiet during the test!
|
||||
$task->runLinksCheck();
|
||||
|
||||
// Check dependencies
|
||||
if (!class_exists('Phockito')) {
|
||||
$this->skipTest = true;
|
||||
return $this->markTestSkipped("These tests need the Phockito module installed to run");
|
||||
}
|
||||
// Get all links checked
|
||||
$status = BrokenExternalPageTrackStatus::get_latest();
|
||||
$this->assertEquals('Completed', $status->Status);
|
||||
$this->assertEquals(5, $status->TotalPages);
|
||||
$this->assertEquals(5, $status->CompletedPages);
|
||||
|
||||
// Mock link checker
|
||||
$checker = Phockito::mock('LinkChecker');
|
||||
Phockito::when($checker)
|
||||
->checkLink('http://www.working.com')
|
||||
->return(200);
|
||||
// Check all pages have had the correct HTML adjusted
|
||||
for ($i = 1; $i <= 5; $i++) {
|
||||
$page = $this->objFromFixture(ExternalLinksTestPage::class, 'page'.$i);
|
||||
$this->assertNotEmpty($page->Content);
|
||||
$this->assertEquals(
|
||||
$page->ExpectedContent,
|
||||
$page->Content,
|
||||
"Assert that the content of page{$i} has been updated"
|
||||
);
|
||||
}
|
||||
|
||||
Phockito::when($checker)
|
||||
->checkLink('http://www.broken.com/url/thing') // 404 on working site
|
||||
->return(404);
|
||||
// Check that the correct report of broken links is generated
|
||||
$links = $status
|
||||
->BrokenLinks()
|
||||
->sort('Link');
|
||||
|
||||
Phockito::when($checker)
|
||||
->checkLink('http://www.broken.com') // 403 on working site
|
||||
->return(403);
|
||||
$this->assertEquals(4, $links->count());
|
||||
$this->assertEquals(
|
||||
array(
|
||||
'http://www.broken.com',
|
||||
'http://www.broken.com/url/thing',
|
||||
'http://www.broken.com/url/thing',
|
||||
'http://www.nodomain.com'
|
||||
),
|
||||
array_values($links->map('ID', 'Link')->toArray())
|
||||
);
|
||||
|
||||
Phockito::when($checker)
|
||||
->checkLink('http://www.nodomain.com') // no ping
|
||||
->return(0);
|
||||
// Check response codes are correct
|
||||
$expected = array(
|
||||
'http://www.broken.com' => 403,
|
||||
'http://www.broken.com/url/thing' => 404,
|
||||
'http://www.nodomain.com' => 0
|
||||
);
|
||||
$actual = $links->map('Link', 'HTTPCode')->toArray();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
Phockito::when($checker)
|
||||
->checkLink('/internal/link')
|
||||
->return(null);
|
||||
// Check response descriptions are correct
|
||||
i18n::set_locale('en_NZ');
|
||||
$expected = array(
|
||||
'http://www.broken.com' => '403 (Forbidden)',
|
||||
'http://www.broken.com/url/thing' => '404 (Not Found)',
|
||||
'http://www.nodomain.com' => '0 (Server Not Available)'
|
||||
);
|
||||
$actual = $links->map('Link', 'HTTPCodeDescription')->toArray();
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
Phockito::when($checker)
|
||||
->checkLink('[sitetree_link,id=9999]')
|
||||
->return(null);
|
||||
|
||||
Phockito::when($checker)
|
||||
->checkLink('home')
|
||||
->return(null);
|
||||
|
||||
Phockito::when($checker)
|
||||
->checkLink('broken-internal')
|
||||
->return(null);
|
||||
|
||||
Phockito::when($checker)
|
||||
->checkLink('[sitetree_link,id=1]')
|
||||
->return(null);
|
||||
|
||||
Phockito::when($checker)
|
||||
->checkLink(Hamcrest_Matchers::anything()) // anything else is 404
|
||||
->return(404);
|
||||
|
||||
Injector::inst()->registerService($checker, 'LinkChecker');
|
||||
}
|
||||
|
||||
public function testLinks() {
|
||||
// Run link checker
|
||||
$task = CheckExternalLinksTask::create();
|
||||
$task->setSilent(true); // Be quiet during the test!
|
||||
$task->runLinksCheck();
|
||||
|
||||
// Get all links checked
|
||||
$status = BrokenExternalPageTrackStatus::get_latest();
|
||||
$this->assertEquals('Completed', $status->Status);
|
||||
$this->assertEquals(5, $status->TotalPages);
|
||||
$this->assertEquals(5, $status->CompletedPages);
|
||||
|
||||
// Check all pages have had the correct HTML adjusted
|
||||
for($i = 1; $i <= 5; $i++) {
|
||||
$page = $this->objFromFixture('ExternalLinksTestPage', 'page'.$i);
|
||||
$this->assertNotEmpty($page->Content);
|
||||
$this->assertEquals(
|
||||
$page->ExpectedContent,
|
||||
$page->Content,
|
||||
"Assert that the content of page{$i} has been updated"
|
||||
);
|
||||
}
|
||||
|
||||
// Check that the correct report of broken links is generated
|
||||
$links = $status
|
||||
->BrokenLinks()
|
||||
->sort('Link');
|
||||
|
||||
$this->assertEquals(4, $links->count());
|
||||
$this->assertEquals(
|
||||
array(
|
||||
'http://www.broken.com',
|
||||
'http://www.broken.com/url/thing',
|
||||
'http://www.broken.com/url/thing',
|
||||
'http://www.nodomain.com'
|
||||
),
|
||||
array_values($links->map('ID', 'Link')->toArray())
|
||||
);
|
||||
|
||||
// Check response codes are correct
|
||||
$expected = array(
|
||||
'http://www.broken.com' => 403,
|
||||
'http://www.broken.com/url/thing' => 404,
|
||||
'http://www.nodomain.com' => 0
|
||||
);
|
||||
$actual = $links->map('Link', 'HTTPCode')->toArray();
|
||||
$this->assertEquals($expected, $actual);
|
||||
|
||||
// Check response descriptions are correct
|
||||
i18n::set_locale('en_NZ');
|
||||
$expected = array(
|
||||
'http://www.broken.com' => '403 (Forbidden)',
|
||||
'http://www.broken.com/url/thing' => '404 (Not Found)',
|
||||
'http://www.nodomain.com' => '0 (Server Not Available)'
|
||||
);
|
||||
$actual = $links->map('Link', 'HTTPCodeDescription')->toArray();
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that broken links appears in the reports list
|
||||
*/
|
||||
public function testReportExists() {
|
||||
$reports = SS_Report::get_reports();
|
||||
$reportNames = array();
|
||||
foreach($reports as $report) {
|
||||
$reportNames[] = $report->class;
|
||||
}
|
||||
$this->assertContains('BrokenExternalLinksReport',$reportNames,
|
||||
'BrokenExternalLinksReport is in reports list');
|
||||
}
|
||||
/**
|
||||
* Test that broken links appears in the reports list
|
||||
*/
|
||||
public function testReportExists()
|
||||
{
|
||||
$reports = Report::get_reports();
|
||||
$reportNames = array();
|
||||
foreach ($reports as $report) {
|
||||
$reportNames[] = get_class($report);
|
||||
}
|
||||
$this->assertContains(
|
||||
BrokenExternalLinksReport::class,
|
||||
$reportNames,
|
||||
'BrokenExternalLinksReport is in reports list'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
ExternalLinksTestPage:
|
||||
SilverStripe\ExternalLinks\Tests\ExternalLinksTestPage:
|
||||
# Tests mix of broken and working external links
|
||||
page1:
|
||||
Title: 'Page 1'
|
||||
|
@ -1,7 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ExternalLinks\Tests;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use Page;
|
||||
|
||||
class ExternalLinksTestPage extends Page implements TestOnly
|
||||
{
|
||||
private static $table_name = 'ExternalLinksTestPage';
|
||||
|
||||
private static $db = array(
|
||||
'ExpectedContent' => 'HTMLText'
|
||||
);
|
||||
|
28
tests/Stubs/PretendLinkChecker.php
Normal file
28
tests/Stubs/PretendLinkChecker.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace SilverStripe\ExternalLinks\Tests\Stubs;
|
||||
|
||||
use SilverStripe\ExternalLinks\Tasks\LinkChecker;
|
||||
|
||||
class PretendLinkChecker implements LinkChecker
|
||||
{
|
||||
public function checkLink($href)
|
||||
{
|
||||
switch ($href) {
|
||||
case 'http://www.working.com':
|
||||
return 200;
|
||||
case 'http://www.broken.com':
|
||||
return 403;
|
||||
case 'http://www.nodomain.com':
|
||||
return 0;
|
||||
case '/internal/link':
|
||||
case '[sitetree_link,id=9999]':
|
||||
case 'home':
|
||||
case 'broken-internal':
|
||||
case '[sitetree_link,id=1]':
|
||||
return null;
|
||||
case 'http://www.broken.com/url/thing':
|
||||
default:
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user