Compare commits

..

No commits in common. "2" and "1.0.7" have entirely different histories.
2 ... 1.0.7

62 changed files with 1055 additions and 1812 deletions

View File

@ -10,5 +10,8 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.{yml,js,scss,css,json}]
[{*.yml,package.json}]
indent_size = 2
# The indent size used in the package.json file cannot be changed:
# https://github.com/npm/npm/pull/3180#issuecomment-16336516

1
.gitattributes vendored
View File

@ -4,4 +4,3 @@
/.gitignore export-ignore
/.travis.yml export-ignore
/.scrutinizer.yml export-ignore
/codecov.yml export-ignore

View File

@ -1,11 +0,0 @@
name: CI
on:
push:
pull_request:
workflow_dispatch:
jobs:
ci:
name: CI
uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1

View File

@ -1,16 +0,0 @@
name: Dispatch CI
on:
# At 2:00 PM UTC, only on Wednesday and Thursday
schedule:
- cron: '0 14 * * 3,4'
jobs:
dispatch-ci:
name: Dispatch CI
# Only run cron on the silverstripe account
if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule')
runs-on: ubuntu-latest
steps:
- name: Dispatch CI
uses: silverstripe/gha-dispatch-ci@v1

View File

@ -1,17 +0,0 @@
name: Keepalive
on:
workflow_dispatch:
# The 4th of every month at 10:50am UTC
schedule:
- cron: '50 10 4 * *'
jobs:
keepalive:
name: Keepalive
# Only run cron on the silverstripe account
if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule')
runs-on: ubuntu-latest
steps:
- name: Keepalive
uses: silverstripe/gha-keepalive@v1

69
.scrutinizer.yml Normal file
View File

@ -0,0 +1,69 @@
inherit: true
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
filter:
paths: [code/*, tests/*]

36
.travis.yml Normal file
View File

@ -0,0 +1,36 @@
# See https://github.com/silverstripe-labs/silverstripe-travis-support for setup details
sudo: false
language: php
php:
- 5.3
- 5.4
- 5.5
- 5.6
- 7.0
env:
- DB=MYSQL CORE_RELEASE=3.2
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
allow_failures:
- php: 7.0
before_script:
- composer self-update || true
- git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
- cd ~/builds/ss
- composer install
script:
- vendor/bin/phpunit externallinks/tests

View File

@ -1,9 +1,8 @@
[main]
host = https://www.transifex.com
[o:silverstripe:p:silverstripe-externallinks:r:master]
[silverstripe-externallinks.master]
file_filter = lang/<lang>.yml
source_file = lang/en.yml
source_lang = en
type = YML
type = YML

View File

@ -1,10 +0,0 @@
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

View File

@ -1,7 +1,6 @@
# External links
[![CI](https://github.com/silverstripe/silverstripe-externallinks/actions/workflows/ci.yml/badge.svg)](https://github.com/silverstripe/silverstripe-externallinks/actions/workflows/ci.yml)
[![Silverstripe supported module](https://img.shields.io/badge/silverstripe-supported-0071C4.svg)](https://www.silverstripe.org/software/addons/silverstripe-commercially-supported-module-list/)
[![Build Status](https://travis-ci.org/silverstripe-labs/silverstripe-externallinks.svg?branch=master)](https://travis-ci.org/silverstripe-labs/silverstripe-externallinks)
## Introduction
@ -13,23 +12,25 @@ The external links module is a task and ModelAdmin to track and to report on bro
## Requirements
* 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).
* SilverStripe 3.1 +
## 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. Require the module via composer: `composer require silverstripe/externallinks`
2. Run `/dev/build` in your browser to rebuild the database.
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.
3. Run the following task *http://path.to.silverstripe/dev/tasks/CheckExternalLinks* to check for
broken external links
## Report
## Report ##
A new report is added called 'External Broken links report'. When viewing this report, a user may press
the "Create new report" button which will trigger an ajax request to initiate a report run.
@ -55,72 +56,27 @@ with the status. The user may leave this page and return to it later to view the
Any subsequent report may not be generated until a prior report has completed.
## Dev task
## Dev task ##
Run the following task *http://path.to.silverstripe/dev/tasks/CheckExternalLinksTask* to check your site for external
Run the following task *http://path.to.silverstripe/dev/tasks/CheckExternalLinks* to check your site for external
broken links.
## Queued job
## Queued job ##
If you have the queuedjobs module installed you can set the task to be run every so often.
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)
## Whitelisting codes
CheckExternalLinks:
Delay: 86400
If you want to ignore or whitelist certain HTTP codes this can be setup via `ignore_codes` in the config.yml
file in `mysite/_config`:
## Whitelisting codes ##
```yml
SilverStripe\ExternalLinks\Tasks\CheckExternalLinksTask:
ignore_codes:
- 401
- 403
- 501
```
If you want to ignore or whitelist certain http codes this can be setup via IgnoreCodes in the config.yml
file in mysite/_config
## Upgrading from 1.x to 2.x
When upgrading from 1.x to 2.x (Silverstripe 3.x to 4.x) you will need to be aware of the following API changes:
* Configuration property `CheckExternalLinksTask.IgnoreCodes` renamed to `CheckExternalLinksTask.ignore_codes`
* Configuration property `CheckExternalLinksTask.FollowLocation` and `BypassCache` renamed to `follow_location` and `bypass_cache`
## Follow 301 redirects
You may want to follow a redirected URL a example of this would be redirecting from http to https
can give you a false poitive as the http code of 301 will be returned which will be classed
as a working link.
To allow redirects to be followed setup the following config in your config.yml
```yaml
# Follow 301 redirects
SilverStripe\ExternalLinks\Tasks\CurlLinkChecker:
follow_location: 1
```
## Bypass cache
By default the task will attempt to cache any results the cache can be bypassed with the
following config in config.yml.
```yaml
# Bypass SS_Cache
SilverStripe\ExternalLinks\Tasks\CurlLinkChecker::
bypass_cache: 1
```
## Headers
You may want to set headers to be sent with the CURL request (eg: user-agent) to avoid website rejecting the request thinking it is a bot.
You can set them with the following config in config.yml.
```yaml
# Headers
SilverStripe\ExternalLinks\Tasks\CurlLinkChecker:
headers:
- 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:53.0) Gecko/20100101 Firefox/53.0'
- 'accept-encoding: gzip, deflate, br'
- 'referer: https://www.domain.com/'
- 'sec-fetch-mode: navigate'
...
```
CheckExternalLinks:
Delay: 60
IgnoreCodes:
- 401
- 403
- 501

View File

@ -1,9 +1,5 @@
---
Name: externallinksdependencies
---
SilverStripe\Core\Injector\Injector:
SilverStripe\ExternalLinks\Tasks\LinkChecker: SilverStripe\ExternalLinks\Tasks\CurlLinkChecker
Psr\SimpleCache\CacheInterface.CurlLinkChecker:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: 'curllinkchecker'
Injector:
LinkChecker: CurlLinkChecker

View File

@ -1,5 +0,0 @@
SilverStripe\ORM\DatabaseAdmin:
classname_value_remapping:
BrokenExternalLink: SilverStripe\ExternalLinks\Model\BrokenExternalLink
BrokenExternalPageTrack: SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack
BrokenExternalPageTrackStatus: SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus

View File

@ -1,7 +1,7 @@
---
Name: externallinkroutes
Before: '#adminroutes'
Name: externallink
After: framework/routes
---
SilverStripe\Control\Director:
Director:
rules:
'admin/externallinks//$Action': SilverStripe\ExternalLinks\Controllers\CMSExternalLinksController
'admin/externallinks//$Action': 'CMSExternalLinks_Controller'

View File

@ -1,29 +0,0 @@
default:
suites:
externallinks:
paths:
- "%paths.modules.externallinks%/tests/behat/features"
contexts:
- SilverStripe\Admin\Tests\Behat\Context\AdminContext
- SilverStripe\BehatExtension\Context\BasicContext
- SilverStripe\BehatExtension\Context\EmailContext
- SilverStripe\BehatExtension\Context\LoginContext
- SilverStripe\Framework\Tests\Behaviour\CmsFormsContext
- SilverStripe\Framework\Tests\Behaviour\CmsUiContext
- SilverStripe\ExternalLinks\Tests\Behat\Context\FeatureContext
- SilverStripe\ExternalLinks\Tests\Behat\Context\FixtureContext
-
SilverStripe\ExternalLinks\Tests\Behat\Context\FixtureContext:
- "%paths.modules.externallinks%/tests/behat/files/"
extensions:
SilverStripe\BehatExtension\MinkExtension:
default_session: facebook_web_driver
javascript_session: facebook_web_driver
facebook_web_driver:
browser: chrome
wd_host: "http://127.0.0.1:9515"
SilverStripe\BehatExtension\Extension:
screenshot_path: "%paths.base%/artifacts/screenshots"
bootstrap_file: vendor/silverstripe/framework/tests/behat/serve-bootstrap.php

View File

@ -1,4 +0,0 @@
.external-links-report__create-report,
.external-links-report__report-progress {
margin-top: 20px;
}

View File

@ -1,140 +0,0 @@
(function($) {
$.entwine('ss', function($) {
$('.external-links-report__create-report').entwine({
PollTimeout: null,
ButtonIsLoading: false,
onclick: function(e) {
e.preventDefault();
this.buttonLoading();
this.start();
},
onmatch: function() {
// poll the current job and update the front end status
this.poll();
},
start: function() {
var self = this;
// initiate a new job
$('.external-links-report__report-progress')
.empty()
.text('Running report 0%');
$.ajax({
url: "admin/externallinks/start",
async: true,
timeout: 3000,
success: function() {
self.poll();
},
error: function() {
self.buttonReset();
}
});
},
/**
* Get the "create report" button selector
*
* @return {Object}
*/
getButton: function() {
return $('.external-links-report__create-report');
},
/**
* Sets the button into a loading state. See LeftAndMain.js.
*/
buttonLoading: function() {
if (this.getButtonIsLoading()) {
return;
}
this.setButtonIsLoading(true);
var $button = this.getButton();
// set button to "submitting" state
$button.addClass('btn--loading loading');
$button.attr('disabled', true);
if ($button.is('button')) {
$button.append($(
'<div class="btn__loading-icon">'+
'<span class="btn__circle btn__circle--1" />'+
'<span class="btn__circle btn__circle--2" />'+
'<span class="btn__circle btn__circle--3" />'+
'</div>'));
$button.css($button.outerWidth() + 'px');
}
},
/**
* Reset the button back to its original state after loading. See LeftAndMain.js.
*/
buttonReset: function() {
this.setButtonIsLoading(false);
var $button = this.getButton();
$button.removeClass('btn--loading loading');
$button.attr('disabled', false);
$button.find('.btn__loading-icon').remove();
$button.css('width', 'auto');
},
poll: function() {
var self = this;
this.buttonLoading();
$.ajax({
url: "admin/externallinks/getJobStatus",
async: true,
success: function(data) {
// No report, so let user create one
if (!data) {
self.buttonReset();
return;
}
// Parse data
var completed = data.Completed ? data.Completed : 0;
var total = data.Total ? data.Total : 0;
// If complete status
if (data.Status === 'Completed') {
$('.external-links-report__report-progress')
.text('Report finished ' + completed + '/' + total);
self.buttonReset();
return;
}
// If incomplete update status
if (completed < total) {
var percent = (completed / total) * 100;
$('.external-links-report__report-progress')
.text('Running report ' + completed + '/' + total + ' (' + percent.toFixed(2) + '%)');
}
// Ensure the regular poll method is run
// kill any existing timeout
if (self.getPollTimeout() !== null) {
clearTimeout(self.getPollTimeout());
}
self.setPollTimeout(setTimeout(function() {
$('.external-links-report__create-report').poll();
}, 1000));
},
error: function() {
self.buttonReset();
}
});
}
});
});
}(jQuery));

View File

@ -0,0 +1,54 @@
<?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();
}
}
}

View File

@ -0,0 +1,34 @@
<?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';
}
}

View File

@ -0,0 +1,71 @@
<?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);
}
}

View File

@ -0,0 +1,28 @@
<?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);
}
}

View File

@ -0,0 +1,128 @@
<?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');
}
}
}

View File

@ -0,0 +1,83 @@
<?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;
}
}

View File

@ -0,0 +1,188 @@
<?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();
}
}
}

View File

@ -0,0 +1,49 @@
<?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;
}
}

View File

@ -0,0 +1,15 @@
<?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);
}

View File

@ -1,2 +0,0 @@
comment: false

View File

@ -1,7 +1,7 @@
{
"name": "silverstripe/externallinks",
"description": "Adds tracking of broken external links to the SilverStripe CMS",
"type": "silverstripe-vendormodule",
"description": "Adds tracking of external broken links to the SilverStripe CMS",
"type": "silverstripe-module",
"keywords": [
"silverstripe",
"broken",
@ -16,30 +16,15 @@
}
],
"require": {
"php": "^7.4 || ^8.0",
"silverstripe/cms": "^4.0"
"silverstripe/framework": "~3.1",
"silverstripe/cms": "~3.1"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "^3.0",
"symbiote/silverstripe-queuedjobs": "^4.9"
"hafriedlander/silverstripe-phockito": "*",
"phpunit/PHPUnit": "~3.7@stable"
},
"suggest": {
"symbiote/silverstripe-queuedjobs": "Provides a more efficient method of generating/updating the report"
"silverstripe/queuedjobs": "Speeds up running the job for Content Editors fropm the report"
},
"autoload": {
"psr-4": {
"SilverStripe\\ExternalLinks\\": "src/",
"SilverStripe\\ExternalLinks\\Tests\\": "tests/",
"SilverStripe\\ExternalLinks\\Tests\\Behat\\Context\\": "tests/behat/src/"
}
},
"extra": {
"expose": [
"client/css",
"client/javascript"
]
},
"minimum-stability": "dev",
"prefer-stable": true
"extra": []
}

View File

@ -0,0 +1,69 @@
(function($) {
$.entwine('ss', function($) {
$('#externalLinksReport').entwine({
PollTimeout: null,
onclick: function() {
this.start();
},
onmatch: function() {
// poll the current job and update the front end status
$('#externalLinksReport').hide();
this.poll();
},
start: function() {
// initiate a new job
$('#ReportHolder').empty();
$('#ReportHolder').text('Running report 0%');
$('#ReportHolder').append('<span class="ss-ui-loading-icon"></span>');
$('#externalLinksReport').hide();
$.ajax({url: "admin/externallinks/start", async: false, timeout: 3000 });
this.poll();
},
poll: function() {
var self = this;
$.ajax({
url: "admin/externallinks/getJobStatus",
async: true,
success: function(data) {
// No report, so let user create one
if (!data) {
$('#externalLinksReport').show();
return;
}
// Parse data
var completed = data.Completed ? data.Completed : 0;
var total = data.Total ? data.Total : 0;
// If complete status
if (data.Status === 'Completed') {
$('#ReportHolder').text('Report Finished ' + completed + '/' + total);
$('#externalLinksReport').show();
return;
}
// If incomplete update status
if (completed < total) {
var percent = (completed / total) * 100;
$('#ReportHolder')
.text('Running report ' + completed + '/' + total + ' (' + percent.toFixed(2) + '%)')
.append('<span class="ss-ui-loading-icon"></span>');
}
// Ensure the regular poll method is run
// kill any existing timeout
if(self.getPollTimeout() !== null) {
clearTimeout(self.getPollTimeout());
}
self.setPollTimeout(setTimeout(function() { $('#externalLinksReport').poll(); }, 1000));
},
error: function(e) {
if(typeof console !== 'undefined') console.log(e);
}
});
}
});
});
}(jQuery));

View File

@ -1,14 +1,8 @@
de:
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
TITLE: 'Überprüfe defekte externe Links'
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
BrokenExternalLink:
NOTAVAILABLE: 'Server nicht verfügbar'
PLURALNAME: 'Defekte externe Links'
PLURALS:
one: 'Ein defekter externer Link'
other: '{count} defekte externe Links'
SINGULARNAME: 'Defekter externer Link'
UNKNOWNRESPONSE: 'Unbekannter Antwortcode'
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
EXTERNALBROKENLINKS: 'Defekte externe Links Report'
ExternalBrokenLinksReport:
RUNREPORT: 'Generiere neuen Bericht'

View File

@ -1,38 +1,17 @@
en:
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
TITLE: 'Checking for external broken links'
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
BrokenExternalLink:
NOTAVAILABLE: 'Server Not Available'
PLURALNAME: 'Broken External Links'
PLURALS:
one: 'A Broken External Link'
other: '{count} Broken External Links'
SINGULARNAME: 'Broken External Link'
UNKNOWNRESPONSE: 'Unknown Response Code'
db_HTTPCode: 'HTTP code'
db_Link: Link
has_one_Status: Status
has_one_Track: Track
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
BrokenExternalPageTrack:
PLURALNAME: 'Broken External Page Tracks'
PLURALS:
one: 'A Broken External Page Track'
other: '{count} Broken External Page Tracks'
SINGULARNAME: 'Broken External Page Track'
db_Processed: Processed
has_many_BrokenLinks: 'Broken links'
has_one_Page: Page
has_one_Status: Status
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
PLURALNAME: 'Broken External Page Track Statuses'
PLURALS:
one: 'A Broken External Page Track Status'
other: '{count} Broken External Page Track Statuses'
BrokenExternalPageTrackStatus:
PLURALNAME: 'Broken External Page Track Statuss'
SINGULARNAME: 'Broken External Page Track Status'
db_JobInfo: 'Job info'
db_Status: Status
has_many_BrokenLinks: 'Broken links'
has_many_TrackedPages: 'Tracked pages'
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
CheckExternalLiksJob:
TITLE: 'Checking for external broken links'
ExternalBrokenLinksReport:
EXTERNALBROKENLINKS: 'External broken links report'
RUNREPORT: 'Create new report'

View File

@ -1,38 +1,17 @@
eo:
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
TITLE: 'Kontrolas por eksteraj rompitaj ligiloj'
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
BrokenExternalLink:
NOTAVAILABLE: 'Servilo estas neatingenbla'
PLURALNAME: 'Rompitaj eksteraj ligiloj'
PLURALS:
one: 'Unu rompita ekstera ligilo'
other: '{count} rompitaj eksteraj ligiloj'
SINGULARNAME: 'Rompita ekstera ligilo'
UNKNOWNRESPONSE: 'Nekonata respondokodo'
db_HTTPCode: HTTP-kodo
db_Link: Ligilo
has_one_Status: Stato
has_one_Track: Spuri
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
BrokenExternalPageTrack:
PLURALNAME: 'Rompitaj eksteraj paĝaj trakoj'
PLURALS:
one: 'Unu rompita ekstera paĝa trako'
other: '{count} rompitaj eksteraj paĝaj trakoj'
SINGULARNAME: 'Rompita ekstera paĝa trako'
db_Processed: Traktita
has_many_BrokenLinks: 'Rompitaj ligiloj'
has_one_Page: Paĝo
has_one_Status: Stato
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
BrokenExternalPageTrackStatus:
PLURALNAME: 'Stato de rompitaj eksteraj paĝaj trakoj'
PLURALS:
one: 'Unu stato de rompita ekstera paĝa trako'
other: '{count} statoj de rompitaj eksteraj paĝaj trakoj'
SINGULARNAME: 'Stato de rompita ekstera paĝa trako'
db_JobInfo: 'Taska informo'
db_Status: Stato
has_many_BrokenLinks: 'Rompitaj ligiloj'
has_many_TrackedPages: 'Spuritaj paĝoj'
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
CheckExternalLiksJob:
TITLE: 'Kontrolas por eksteraj rompitaj ligiloj'
ExternalBrokenLinksReport:
EXTERNALBROKENLINKS: 'Raporto pri eksteraj rompitaj ligiloj'
RUNREPORT: 'Krei novan raporton'

View File

@ -1,16 +0,0 @@
fi:
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
TITLE: 'Tarkastettaan rikkinäiset ulkoiset linkit'
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
NOTAVAILABLE: 'Palvelin ei saatavilla'
PLURALNAME: 'Rikkinäiset ulkoiset linkit'
SINGULARNAME: 'Rikkinäinen ulkoinen linkki'
UNKNOWNRESPONSE: 'Tuntematon vastauskoodi'
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
PLURALNAME: 'Rikkinäiset ulkoisen sivun reitit'
SINGULARNAME: 'Rikkinäinen ulkoisen sivun reitti'
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
SINGULARNAME: 'Rikkinäinen ulkoisen sivun reitin tila'
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
EXTERNALBROKENLINKS: 'Raportti ulkoisista rikkinäisistä linkeistä'
RUNREPORT: 'Luo uusi raportti'

View File

@ -1,26 +0,0 @@
fi_FI:
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
TITLE: 'Tarkastetaan ulkoiset rikkinäiset linkit'
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
NOTAVAILABLE: 'Palvelin ei saatavilla'
PLURALNAME: 'Rikkinäiset ulkoiset linkit'
PLURALS:
one: 'Rikkinäinen ulkoinen linkki'
other: '{count} rikkinäistä ulkoista linkkiä'
SINGULARNAME: 'Rikkinäinen ulkoinen linkki'
UNKNOWNRESPONSE: 'Tuntematon vastauskoodi'
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
PLURALNAME: 'Rikkinäiset ulkoiset sivut'
PLURALS:
one: 'Rikkinäinen ulkoinen sivu'
other: '{count} rikkinäistä ulkoista sivua'
SINGULARNAME: 'Rikkinäinen ulkoinen sivu'
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
PLURALNAME: 'Rikkinäisten ulkoisten sivujen tila'
PLURALS:
one: 'Rikkinäisen ulkoisen sivun tila'
other: '{count} rikkinäistä ulkoisten sivujen tilaa'
SINGULARNAME: 'Rikkinäisen ulkoisen sivun tila'
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
EXTERNALBROKENLINKS: 'Rikkinäisten ulkoisten linkkien raportti'
RUNREPORT: 'Luo uusi raportti'

View File

@ -1,29 +0,0 @@
it:
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
TITLE: 'Ricerca link esterni orfani'
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
NOTAVAILABLE: 'Server Non Disponibile'
PLURALNAME: 'Link Esterni Orfani'
PLURALS:
many: '{count} Link Esterni Orfani'
one: 'Un Link Esterno Orfano'
other: '{count} Link Esterni Orfani'
SINGULARNAME: 'Link Esterno Orfano'
UNKNOWNRESPONSE: 'Codice di Risposta Sconosciuto'
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
PLURALNAME: 'Tracce Pagine Esterne Orfane'
PLURALS:
many: '{count} Tracce Pagine Esterne Orfane'
one: 'Una Traccia Pagina Esterna Orfana'
other: '{count} Tracce Pagine Esterne Orfane'
SINGULARNAME: 'Traccia Pagina Esterna Orfana'
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
PLURALNAME: 'Stati Tracce Pagine Esterne Orfane'
PLURALS:
many: '{count} Stati Tracce Pagine Esterne Orfane'
one: 'Uno Stato Traccia Pagina Esterna Orfana'
other: '{count} Stati Tracce Pagine Esterne Orfane'
SINGULARNAME: 'Stato Traccia Pagina Esterna Orfana'
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
EXTERNALBROKENLINKS: 'Report link esterni orfani'
RUNREPORT: 'Creare nuovo report'

View File

@ -1,26 +0,0 @@
nl:
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
TITLE: 'Niet-werkende externe links worden gecontroleerd'
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
NOTAVAILABLE: 'Server niet beschikbaar'
PLURALNAME: 'Kapotte externe links'
PLURALS:
one: 'Kapotte externe link'
other: '{count} Kapotte externe links'
SINGULARNAME: 'Kapotte externe link'
UNKNOWNRESPONSE: 'Onbekende responsecode'
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
PLURALNAME: 'Kapotte externe Pagina Verwijzing'
PLURALS:
one: 'Kapotte externe Pagina Verwijzing'
other: '{count} Kapotte externe Pagina Verwijzing'
SINGULARNAME: 'Kapotte externe Pagina Verwijzing'
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
PLURALNAME: 'Kapotte externe Pagina Verwijzing Statusssen'
PLURALS:
one: 'Een kapotte externe Pagina Verwijzing Status'
other: '{count} Kapotte externe Pagina Verwijzing Statusssen'
SINGULARNAME: 'Kapotte externe Pagina Verwijzing Status'
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
EXTERNALBROKENLINKS: 'Externe kapotte links overzicht'
RUNREPORT: 'Maak nieuw overzicht'

View File

@ -1,17 +1,17 @@
pl:
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
TITLE: 'Wyszukiwanie uszkodzonych linków zewnętrznych'
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
BrokenExternalLink:
NOTAVAILABLE: 'Serwer niedostępny'
PLURALNAME: 'Uszkodzone linki zewnętrzne'
SINGULARNAME: 'Uszkodzony link zewnętrzny'
UNKNOWNRESPONSE: 'Nieznany kod odpowiedzi'
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
BrokenExternalPageTrack:
PLURALNAME: 'Wykrywania wadliwych stron zewnętrznych'
SINGULARNAME: 'Wykrywanie wadliwych stron zewnętrznych'
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
BrokenExternalPageTrackStatus:
PLURALNAME: 'Statusy wykrywania wadliwych stron zewnętrznych'
SINGULARNAME: 'Status wykrywania wadliwych stron zewnętrznych'
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
CheckExternalLiksJob:
TITLE: 'Wyszukiwanie uszkodzonych linków zewnętrznych'
ExternalBrokenLinksReport:
EXTERNALBROKENLINKS: 'Raport uszkodzonych linków zewnętrznych'
RUNREPORT: 'Stwórz nowy raport'

View File

@ -1,16 +1,11 @@
ru:
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
TITLE: 'Проверяю внешние ссылки'
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
BrokenExternalLink:
NOTAVAILABLE: 'Сервер не доступен'
PLURALNAME: 'Недоступные внешние ссылки'
SINGULARNAME: 'Недоступная внешняя ссылка'
UNKNOWNRESPONSE: 'Неизвестный ответ сервера'
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
PLURALNAME: 'Внешнее отслеживание страниц нарушено'
SINGULARNAME: 'Внешнее отслеживание страниц нарушено'
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
SINGULARNAME: 'Внешнее отслеживание страниц нарушено'
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
CheckExternalLiksJob:
TITLE: 'Проверяю внешние ссылки'
ExternalBrokenLinksReport:
EXTERNALBROKENLINKS: 'Отчёт о неработающих внешних ссылках'
RUNREPORT: 'Создать новый отчёт'

View File

@ -1,44 +0,0 @@
sk:
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
TITLE: 'Kontrola externých nefunkčných odkazov'
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
NOTAVAILABLE: 'Server nedostupný'
PLURALNAME: 'Nefunkčné externé odkazy'
PLURALS:
few: '{count} nefunkčné externé odkazy'
many: '{count} nefunkčných externých odkazov'
one: 'Nefunkčný externý odkaz'
other: '{count} nefunkčných externých odkazov'
SINGULARNAME: 'Nefunkčný externý odkaz'
UNKNOWNRESPONSE: 'Neznámy kód odpovede'
db_HTTPCode: 'HTTP kód'
db_Link: Odkaz
has_one_Status: Stav
has_one_Track: Stopa
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
PLURALNAME: 'Nefunkčné externé stopy stránok'
PLURALS:
few: '{count} nefunkčné externé stopy stránky'
many: '{count} nefunkčných externých stôp stránky'
one: 'Nefunkčná externá stopa stránky'
other: '{count} nefunkčných externých stôp stránky'
SINGULARNAME: 'Nefunkčná externá stopa stránky'
db_Processed: Spracované
has_many_BrokenLinks: 'Nefunkčné odkazy'
has_one_Page: Stránka
has_one_Status: Stav
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
PLURALNAME: 'Stavy nefunkčných externých stôp stránky'
PLURALS:
few: '{count} stavy nefunkčných externých stôp stránky'
many: '{count} stavov nefunkčných externých stôp stránky'
one: 'Stav nefunkčnej externej stopy stránky'
other: '{count} stavov nefunkčných externých stôp stránky'
SINGULARNAME: 'Stav nefunkčnej externej stopy stránky'
db_JobInfo: 'Informácie o úlohe'
db_Status: Stav
has_many_BrokenLinks: 'Nefunkčné odkazy'
has_many_TrackedPages: 'Sledované stránky'
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
EXTERNALBROKENLINKS: 'Správa o externých nefunkčných odkazoch'
RUNREPORT: 'Vytvoriť novú správu'

View File

@ -1,32 +0,0 @@
sl:
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
TITLE: 'Preverjamo, ali so zunanje povezave dostopne'
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
NOTAVAILABLE: 'Strežnik ni na voljo'
PLURALNAME: 'Nedostopne zunanje povezave'
PLURALS:
few: '{count} nedostopnih zunanjih povezav'
one: 'Nedostopna zunanja povezava'
other: '{count} nedostopnih zunanjih povezav'
two: '{count} nedostopni zunanji povezavi'
SINGULARNAME: 'Nedostopna zunanja povezava'
UNKNOWNRESPONSE: 'Neznana šifra odziva'
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
PLURALNAME: 'Preverjanja nedostopnih zunanjih povezav'
PLURALS:
few: '{count} preverjanj nedostopnih zunanjih povezav'
one: 'Preverjanje nedostopnih zunanjih povezav'
other: '{count} preverjanj nedostopnih zunanjih povezav'
two: '{count} preverjanji nedostopnih zunanjih povezav'
SINGULARNAME: 'Preverjanje nedostopnih zunanjih povezav'
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
PLURALNAME: 'Status preverjanj'
PLURALS:
few: '{count} statusov preverjanj'
one: 'Status preverjanja'
other: '{count} statusov preverjanj'
two: '{count} statusa preverjanj'
SINGULARNAME: 'Status preverjanja'
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
EXTERNALBROKENLINKS: 'Poročilo o nedostopnih zunanjih povezavah'
RUNREPORT: 'Pripravi novo poročilo'

View File

@ -1,4 +1,4 @@
Copyright (c) 2017, SilverStripe Limited
Copyright (c) 2016, 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:

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="SilverStripe">
<description>CodeSniffer ruleset for SilverStripe coding conventions.</description>
<file>src</file>
<file>tests</file>
<rule ref="PSR2" >
<!-- Current exclusions -->
<exclude name="PSR1.Methods.CamelCapsMethodName" />
<exclude name="PSR1.Files.SideEffects.FoundWithSymbols" />
</rule>
</ruleset>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/silverstripe/cms/tests/bootstrap.php" colors="true">
<testsuites>
<testsuite name="Default">
<directory>tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src/</directory>
<exclude>
<directory suffix=".php">tests/</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

@ -1,79 +0,0 @@
<?php
namespace SilverStripe\ExternalLinks\Controllers;
use SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus;
use SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob;
use SilverStripe\ExternalLinks\Tasks\CheckExternalLinksTask;
use SilverStripe\Control\Controller;
use Symbiote\QueuedJobs\Services\QueuedJobService;
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
use SilverStripe\Security\Permission;
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()
{
if (!Permission::check('CMS_ACCESS_CMSMain')) {
return $this->httpError(403, 'You do not have permission to access this resource');
}
// Set headers
HTTPCacheControlMiddleware::singleton()->setMaxAge(0);
$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()
{
if (!Permission::check('CMS_ACCESS_CMSMain')) {
return $this->httpError(403, 'You do not have permission to access this resource');
}
// 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();
}
}
}

View File

@ -1,46 +0,0 @@
<?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';
}
}

View File

@ -1,86 +0,0 @@
<?php
namespace SilverStripe\ExternalLinks\Model;
use InvalidArgumentException;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
/**
* Represents a single link checked for a single run that is broken
*
* @property string Link
* @property int HTTPCode
* @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 : Security::getCurrentUser();
$codes = ['CMS_ACCESS_CMSMain'];
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);
}
}

View File

@ -1,39 +0,0 @@
<?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);
}
}

View File

@ -1,169 +0,0 @@
<?php
namespace SilverStripe\ExternalLinks\Model;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\i18n\i18nEntityProvider;
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 implements i18nEntityProvider
{
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();
}
/**
* Returns the list of provided translations for this object
*
* @return array
*/
public function provideI18nEntities()
{
return [
__CLASS__ . '.SINGULARNAME' => 'Broken External Page Track Status',
__CLASS__ . '.PLURALNAME' => 'Broken External Page Track Statuses',
__CLASS__ . '.PLURALS' => [
'one' => 'A Broken External Page Track Status',
'other' => '{count} Broken External Page Track Statuses',
],
];
}
/**
* 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');
}
}
}

View File

@ -1,98 +0,0 @@
<?php
namespace SilverStripe\ExternalLinks\Reports;
use SilverStripe\Core\Convert;
use SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\FormAction;
use SilverStripe\ORM\ArrayList;
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) {
// Filter items that are attached to archived Pages
return $track->BrokenLinks()->exclude('Track.Page.ID', null);
}
return ArrayList::create();
}
public function getCMSFields()
{
Requirements::css('silverstripe/externallinks: client/css/BrokenExternalLinksReport.css');
Requirements::javascript('silverstripe/externallinks: client/javascript/BrokenExternalLinksReport.js');
$fields = parent::getCMSFields();
$runReportButton = FormAction::create('createReport', _t(__CLASS__ . '.RUNREPORT', 'Create new report'))
->addExtraClass('btn-primary external-links-report__create-report')
->setUseButtonTag(true);
$fields->push($runReportButton);
$reportResultSpan = '<p class="external-links-report__report-progress"></p>';
$reportResult = LiteralField::create('ResultTitle', $reportResultSpan);
$fields->push($reportResult);
return $fields;
}
}

View File

@ -1,246 +0,0 @@
<?php
namespace SilverStripe\ExternalLinks\Tasks;
use DOMNode;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\BuildTask;
use SilverStripe\Dev\Debug;
use SilverStripe\ExternalLinks\Model\BrokenExternalLink;
use SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack;
use SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus;
use SilverStripe\ExternalLinks\Tasks\LinkChecker;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\ValidationException;
class CheckExternalLinksTask extends BuildTask
{
private static $dependencies = [
'LinkChecker' => '%$' . LinkChecker::class
];
private static $segment = 'CheckExternalLinksTask';
/**
* Define a list of HTTP response codes that should not be treated as "broken", where they usually
* might be.
*
* @config
* @var array
*/
private static $ignore_codes = [];
/**
* @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('ignore_codes');
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();
try {
$page->write();
} catch (ValidationException $ex) {
$this->log("Exception caught for {$page->Title}, skipping. Message: " . $ex->getMessage());
continue;
}
// 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();
}
}
}

View File

@ -1,101 +0,0 @@
<?php
namespace SilverStripe\ExternalLinks\Tasks;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injector;
/**
* Check links using curl
*/
class CurlLinkChecker implements LinkChecker
{
use Configurable;
/**
* If we want to follow redirects a 301 http code for example
* Set via YAML file
*
* @config
* @var boolean
*/
private static $follow_location = false;
/**
* If we want to bypass the cache
* Set via YAML file
*
* @config
* @var boolean
*/
private static $bypass_cache = false;
/**
* Allow to pass custom header to be in CURL request
*
* @config
* @var array
*/
private static $headers = [];
/**
* Return cache
*
* @return CacheInterface
*/
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;
}
$cacheKey = md5($href ?? '');
if (!$this->config()->get('bypass_cache')) {
// Check if we have a cached result
$result = $this->getCache()->get($cacheKey, false);
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);
if ($this->config()->get('follow_location')) {
curl_setopt($handle, CURLOPT_FOLLOWLOCATION, true);
}
// Add headers
$headers = (array) $this->config()->get('headers');
if (!empty($headers)) {
curl_setopt($handle, CURLOPT_HTTPHEADER, $headers);
}
// Retrieve http code
curl_exec($handle);
$httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);
if (!$this->config()->get('bypass_cache')) {
// Cache result
$this->getCache()->set($cacheKey, $httpCode);
}
return $httpCode;
}
}

View File

@ -1,18 +0,0 @@
<?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);
}

157
tests/ExternalLinksTest.php Normal file
View File

@ -0,0 +1,157 @@
<?php
class ExternalLinksTest extends SapphireTest {
protected static $fixture_file = 'ExternalLinksTest.yml';
protected $extraDataObjects = array(
'ExternalLinksTest_Page'
);
public function setUpOnce() {
if (class_exists('Phockito')) {
Phockito::include_hamcrest(false);
}
parent::setUpOnce();
}
public function setUp() {
parent::setUp();
Injector::nest();
// Check dependencies
if (!class_exists('Phockito')) {
$this->skipTest = true;
return $this->markTestSkipped("These tests need the Phockito module installed to run");
}
// Mock link checker
$checker = Phockito::mock('LinkChecker');
Phockito::when($checker)
->checkLink('http://www.working.com')
->return(200);
Phockito::when($checker)
->checkLink('http://www.broken.com/url/thing') // 404 on working site
->return(404);
Phockito::when($checker)
->checkLink('http://www.broken.com') // 403 on working site
->return(403);
Phockito::when($checker)
->checkLink('http://www.nodomain.com') // no ping
->return(0);
Phockito::when($checker)
->checkLink('/internal/link')
->return(null);
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 tearDown() {
Injector::unnest();
parent::tearDown();
}
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('ExternalLinksTest_Page', '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');
}
}
class ExternalLinksTest_Page extends Page implements TestOnly {
private static $db = array(
'ExpectedContent' => 'HTMLText'
);
}

View File

@ -1,4 +1,4 @@
SilverStripe\ExternalLinks\Tests\Stubs\ExternalLinksTestPage:
ExternalLinksTest_Page:
# Tests mix of broken and working external links
page1:
Title: 'Page 1'

View File

@ -1,34 +0,0 @@
Feature: External links report
As a website user
I want to use the external links report
Background:
Given the "group" "EDITOR group" has permissions "CMS_ACCESS_LeftAndMain"
# Need to use single quotes rather than escaped double quotes when defining the fixture otherwise
# it'll end up saved as &quot; and the hyperlink will be wrong
# When the page is published it should be converted by tinymce to double quotes
Given a "page" "My page" has the "Content" "<p>My <a href='http://fsdjoifidsohfiohfsoifhiodshfhdosi.com'>link</a> content</p>"
Scenario: Operate the external links report
Given I am logged in with "ADMIN" permissions
# Publish page
When I go to "/admin/pages"
And I follow "My page"
And I press the "Publish" button
# Run report
When I go to "/admin/reports"
And I follow "External broken links"
And I press the "Create new report" button
# Run queuedjob, new job will be the first row
When I go to "/admin/queuedjobs"
When I click on the ".gridfield-button-jobexecute" element
And I wait for 15 seconds
# Assert report
When I go to "/admin/reports"
And I follow "External broken links"
Then I should see "http://fsdjoifidsohfiohfsoifhiodshfhdosi.com"
And I should see "My page"

View File

@ -1,9 +0,0 @@
<?php
namespace SilverStripe\ExternalLinks\Tests\Behat\Context;
use SilverStripe\BehatExtension\Context\SilverStripeContext;
class FeatureContext extends SilverStripeContext
{
}

View File

@ -1,9 +0,0 @@
<?php
namespace SilverStripe\ExternalLinks\Tests\Behat\Context;
use SilverStripe\BehatExtension\Context\FixtureContext as BaseFixtureContext;
class FixtureContext extends BaseFixtureContext
{
}

View File

@ -1,153 +0,0 @@
<?php
namespace SilverStripe\ExternalLinks\Tests;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus;
use SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport;
use SilverStripe\ExternalLinks\Tasks\CheckExternalLinksTask;
use SilverStripe\ExternalLinks\Tasks\LinkChecker;
use SilverStripe\ExternalLinks\Tests\Stubs\ExternalLinksTestPage;
use SilverStripe\ExternalLinks\Tests\Stubs\PretendLinkChecker;
use SilverStripe\i18n\i18n;
use SilverStripe\Reports\Report;
class ExternalLinksTest extends FunctionalTest
{
protected static $fixture_file = 'ExternalLinksTest.yml';
protected static $extra_dataobjects = array(
ExternalLinksTestPage::class
);
protected function setUp(): void
{
parent::setUp();
// Stub link checker
$checker = new PretendLinkChecker;
Injector::inst()->registerService($checker, LinkChecker::class);
}
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::class, '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 = Report::get_reports();
$reportNames = array();
foreach ($reports as $report) {
$reportNames[] = get_class($report);
}
$this->assertContains(
BrokenExternalLinksReport::class,
$reportNames,
'BrokenExternalLinksReport is in reports list'
);
}
public function testArchivedPagesAreHiddenFromReport()
{
// Run link checker
$task = CheckExternalLinksTask::create();
$task->setSilent(true); // Be quiet during the test!
$task->runLinksCheck();
// Ensure report lists all broken links
$this->assertEquals(4, BrokenExternalLinksReport::create()->sourceRecords()->count());
// Archive a page
$page = $this->objFromFixture(ExternalLinksTestPage::class, 'page1');
$page->doArchive();
// Ensure report does not list the link associated with an archived page
$this->assertEquals(3, BrokenExternalLinksReport::create()->sourceRecords()->count());
}
public function provideGetJobStatus(): array
{
return [
'ADMIN - valid permission' => ['ADMIN', 200],
'CMS_ACCESS_CMSMain - valid permission' => ['CMS_ACCESS_CMSMain', 200],
'VIEW_SITE - not enough permission' => ['VIEW_SITE', 403],
];
}
/**
* @dataProvider provideGetJobStatus
*/
public function testGetJobStatus(
string $permission,
int $expectedResponseCode
): void {
$this->logInWithPermission($permission);
$response = $this->get('admin/externallinks/start', null, ['Accept' => 'application/json']);
$this->assertEquals($expectedResponseCode, $response->getStatusCode());
$response = $this->get('admin/externallinks/getJobStatus', null, ['Accept' => 'application/json']);
$this->assertEquals($expectedResponseCode, $response->getStatusCode());
}
}

View File

@ -1,56 +0,0 @@
<?php
namespace SilverStripe\ExternalLinks\Tests\Model;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ExternalLinks\Model\BrokenExternalLink;
class BrokenExternalLinkTest extends SapphireTest
{
/**
* @dataProvider httpCodeProvider
*/
public function testGetHTTPCodeDescription(int $httpCode, string $expected)
{
$link = new BrokenExternalLink();
$link->HTTPCode = $httpCode;
$this->assertSame($expected, $link->getHTTPCodeDescription());
}
public function httpCodeProvider(): array
{
return [
[200, '200 (OK)'],
[302, '302 (Found)'],
[404, '404 (Not Found)'],
[500, '500 (Internal Server Error)'],
[789, '789 (Unknown Response Code)'],
];
}
public function permissionProvider(): array
{
return [
['admin', 'ADMIN'],
['content-author', 'CMS_ACCESS_CMSMain'],
['asset-admin', 'CMS_ACCESS_AssetAdmin'],
];
}
/**
* @dataProvider permissionProvider
*/
public function testCanViewReport(string $user, string $permission)
{
$this->logOut();
$this->logInWithPermission($permission);
$link = new BrokenExternalLink();
if ($user === 'asset-admin') {
$this->assertFalse($link->canView());
} else {
$this->assertTrue($link->canView());
}
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace SilverStripe\ExternalLinks\Tests\Stubs;
use Page;
use SilverStripe\Dev\TestOnly;
class ExternalLinksTestPage extends Page implements TestOnly
{
private static $table_name = 'ExternalLinksTestPage';
private static $db = array(
'ExpectedContent' => 'HTMLText'
);
}

View File

@ -1,29 +0,0 @@
<?php
namespace SilverStripe\ExternalLinks\Tests\Stubs;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ExternalLinks\Tasks\LinkChecker;
class PretendLinkChecker implements LinkChecker, TestOnly
{
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;
}
}
}