mirror of
https://github.com/silverstripe/silverstripe-externallinks.git
synced 2024-09-29 20:59:31 +02:00
Compare commits
133 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2ee2bfbc8a | ||
|
b12e6868bc | ||
|
e12e2aaece | ||
|
8797f1f41e | ||
|
a01c16b829 | ||
|
20c2231bff | ||
|
bea1716d94 | ||
|
062319682e | ||
|
2560888df0 | ||
|
ae9dbddfb0 | ||
|
4f5309163b | ||
|
44b575db9b | ||
|
4d67a327fe | ||
|
418d69813f | ||
|
8b27314490 | ||
|
6ea69fabb2 | ||
|
7698371ebf | ||
|
cbd278685b | ||
|
2595fff257 | ||
|
a055868bfa | ||
|
bef19ec35c | ||
|
1b66be22cd | ||
|
9381f33959 | ||
|
93b0303ce0 | ||
|
0009a4bf13 | ||
|
a70a9d1e82 | ||
|
b47db91c2a | ||
|
2f72f9873a | ||
|
813cd2e4e7 | ||
|
d1cc3f80ab | ||
|
ec36ece776 | ||
|
d2c1aedf2e | ||
|
11a4a6d6e1 | ||
|
ae9e7d0c96 | ||
|
4df31ad641 | ||
|
e743b90607 | ||
|
d4871e7d41 | ||
|
6d288c54f2 | ||
|
3721dab425 | ||
|
164032af76 | ||
|
1bfaf12909 | ||
|
3fc084a1f0 | ||
|
b1cd65d538 | ||
|
4176da66c7 | ||
|
b4ccd03025 | ||
|
c4e41d58ee | ||
|
aed847dc13 | ||
|
75585f983a | ||
|
fb9c340edb | ||
|
3da3567a7a | ||
|
40c96ce23f | ||
|
b4c210f211 | ||
|
478d6613f8 | ||
|
7b68fe0eaa | ||
|
75a4988f48 | ||
|
504f742aa7 | ||
|
703ae9b2ca | ||
|
9282ed2e2d | ||
|
9cb88c96b2 | ||
|
80ddb80123 | ||
|
05ded71c02 | ||
|
839cbb7185 | ||
|
3746cb1368 | ||
|
a166257690 | ||
|
b39a81b60b | ||
|
7a335726c4 | ||
|
1ce8c3bbdf | ||
|
a180db21b9 | ||
|
a1b7d3979e | ||
|
5091bf91e5 | ||
|
e41ee18b7c | ||
|
4f0df463df | ||
|
60893a87b1 | ||
|
a3aed13ed0 | ||
|
1cfb249384 | ||
|
17604a5b42 | ||
|
08fc708521 | ||
|
4311de28fd | ||
|
61a63f36d9 | ||
|
28ce2df1b7 | ||
|
8002624da9 | ||
|
4723e8742c | ||
|
3ff01bf9e9 | ||
|
39044de8ad | ||
|
65a9f11708 | ||
|
2bbf756f3e | ||
|
e33f65ecc0 | ||
|
18c6ca73dd | ||
|
8e60c0b396 | ||
|
bedef230e7 | ||
|
5746cb1d63 | ||
|
ceba6c1406 | ||
|
717b126dda | ||
|
2279b2c9d8 | ||
|
99d08dadc7 | ||
|
63c07de2e1 | ||
|
e83da88305 | ||
|
7a34198567 | ||
|
9e9946ef64 | ||
|
6cf7a466fb | ||
|
e21c0ad470 | ||
|
c9f5ca6c72 | ||
|
44fbc027f3 | ||
|
d81f8dc52e | ||
|
a119add32e | ||
|
6739163212 | ||
|
e58190a9b3 | ||
|
78fbc68797 | ||
|
06996e0ae7 | ||
|
789ce91e1c | ||
|
4a8fb14465 | ||
|
4675a539d5 | ||
|
a26a6e14c5 | ||
|
12df3947c2 | ||
|
9ea5067467 | ||
|
7d340eade7 | ||
|
bb7a1c3eda | ||
|
a52cb9209b | ||
|
98dac9b314 | ||
|
ed7d91becb | ||
|
0374d66b32 | ||
|
77eaa62efc | ||
|
7bce7dcb2f | ||
|
a88e8be403 | ||
|
1ed9a23a71 | ||
|
e6ddf0fe47 | ||
|
4b59fdba02 | ||
|
3c69b3fa77 | ||
|
3547869393 | ||
|
ded3b7cd82 | ||
|
76fd1169db | ||
|
99d4a6ee1a | ||
|
0354be75d8 |
@ -10,8 +10,5 @@ indent_style = space
|
|||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[{*.yml,package.json}]
|
[*.{yml,js,scss,css,json}]
|
||||||
indent_size = 2
|
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
1
.gitattributes
vendored
@ -4,3 +4,4 @@
|
|||||||
/.gitignore export-ignore
|
/.gitignore export-ignore
|
||||||
/.travis.yml export-ignore
|
/.travis.yml export-ignore
|
||||||
/.scrutinizer.yml export-ignore
|
/.scrutinizer.yml export-ignore
|
||||||
|
/codecov.yml export-ignore
|
||||||
|
11
.github/workflows/ci.yml
vendored
Normal file
11
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
name: CI
|
||||||
|
uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1
|
16
.github/workflows/dispatch-ci.yml
vendored
Normal file
16
.github/workflows/dispatch-ci.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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
|
17
.github/workflows/keepalive.yml
vendored
Normal file
17
.github/workflows/keepalive.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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
|
@ -1,69 +0,0 @@
|
|||||||
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/*]
|
|
34
.travis.yml
34
.travis.yml
@ -1,34 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
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
|
|
||||||
- php: 7.1
|
|
||||||
env: DB=MYSQL CORE_RELEASE=3.6
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
script:
|
|
||||||
- vendor/bin/phpunit externallinks/tests
|
|
@ -1,8 +1,9 @@
|
|||||||
[main]
|
[main]
|
||||||
host = https://www.transifex.com
|
host = https://www.transifex.com
|
||||||
|
|
||||||
[silverstripe-externallinks.master]
|
[o:silverstripe:p:silverstripe-externallinks:r:master]
|
||||||
file_filter = lang/<lang>.yml
|
file_filter = lang/<lang>.yml
|
||||||
source_file = lang/en.yml
|
source_file = lang/en.yml
|
||||||
source_lang = en
|
source_lang = en
|
||||||
type = YML
|
type = YML
|
||||||
|
|
||||||
|
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
|
94
README.md
94
README.md
@ -1,6 +1,7 @@
|
|||||||
# External links
|
# External links
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/silverstripe-labs/silverstripe-externallinks.svg?branch=master)](https://travis-ci.org/silverstripe-labs/silverstripe-externallinks)
|
[![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/)
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
@ -12,25 +13,23 @@ The external links module is a task and ModelAdmin to track and to report on bro
|
|||||||
|
|
||||||
## Requirements
|
## 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
|
## Features
|
||||||
|
|
||||||
* Add external links to broken links reports
|
* Add external links to broken links reports
|
||||||
* Add a task to track external broken links
|
* Add a task to track external broken links
|
||||||
|
|
||||||
See the [changelog](CHANGELOG.md) for version history.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. If you have composer you can use `composer require silverstripe/externallinks:*`. Otherwise,
|
1. Require the module via composer: `composer require silverstripe/externallinks`
|
||||||
download the module from GitHub and extract to the 'externallinks' folder. Place this directory
|
2. Run `/dev/build` in your browser to rebuild the database.
|
||||||
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
|
3. Run the following task *http://path.to.silverstripe/dev/tasks/CheckExternalLinks* to check for
|
||||||
broken external links
|
broken external links
|
||||||
|
|
||||||
## Report ##
|
## Report
|
||||||
|
|
||||||
A new report is added called 'External Broken links report'. When viewing this report, a user may press
|
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.
|
the "Create new report" button which will trigger an ajax request to initiate a report run.
|
||||||
@ -56,27 +55,72 @@ 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.
|
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/CheckExternalLinks* to check your site for external
|
Run the following task *http://path.to.silverstripe/dev/tasks/CheckExternalLinksTask* to check your site for external
|
||||||
broken links.
|
broken links.
|
||||||
|
|
||||||
## Queued job ##
|
## Queued job
|
||||||
|
|
||||||
If you have the queuedjobs module installed you can set the task to be run every so ofter
|
If you have the queuedjobs module installed you can set the task to be run every so often.
|
||||||
Add the following yml config to config.yml in mysite/_config have the the task run once every day (86400 seconds)
|
|
||||||
|
|
||||||
CheckExternalLinks:
|
## Whitelisting codes
|
||||||
Delay: 86400
|
|
||||||
|
|
||||||
## Whitelisting codes ##
|
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`:
|
||||||
|
|
||||||
If you want to ignore or whitelist certain http codes this can be setup via IgnoreCodes in the config.yml
|
```yml
|
||||||
file in mysite/_config
|
SilverStripe\ExternalLinks\Tasks\CheckExternalLinksTask:
|
||||||
|
ignore_codes:
|
||||||
|
- 401
|
||||||
|
- 403
|
||||||
|
- 501
|
||||||
|
```
|
||||||
|
|
||||||
CheckExternalLinks:
|
## Upgrading from 1.x to 2.x
|
||||||
Delay: 60
|
|
||||||
IgnoreCodes:
|
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:
|
||||||
- 401
|
|
||||||
- 403
|
* Configuration property `CheckExternalLinksTask.IgnoreCodes` renamed to `CheckExternalLinksTask.ignore_codes`
|
||||||
- 501
|
* 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'
|
||||||
|
...
|
||||||
|
```
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
---
|
---
|
||||||
Name: externallinksdependencies
|
Name: externallinksdependencies
|
||||||
---
|
---
|
||||||
Injector:
|
SilverStripe\Core\Injector\Injector:
|
||||||
LinkChecker: CurlLinkChecker
|
SilverStripe\ExternalLinks\Tasks\LinkChecker: SilverStripe\ExternalLinks\Tasks\CurlLinkChecker
|
||||||
|
Psr\SimpleCache\CacheInterface.CurlLinkChecker:
|
||||||
|
factory: SilverStripe\Core\Cache\CacheFactory
|
||||||
|
constructor:
|
||||||
|
namespace: 'curllinkchecker'
|
||||||
|
5
_config/legacy.yml
Normal file
5
_config/legacy.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
SilverStripe\ORM\DatabaseAdmin:
|
||||||
|
classname_value_remapping:
|
||||||
|
BrokenExternalLink: SilverStripe\ExternalLinks\Model\BrokenExternalLink
|
||||||
|
BrokenExternalPageTrack: SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack
|
||||||
|
BrokenExternalPageTrackStatus: SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus
|
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
Name: externallink
|
Name: externallinkroutes
|
||||||
After: framework/routes
|
Before: '#adminroutes'
|
||||||
---
|
---
|
||||||
Director:
|
SilverStripe\Control\Director:
|
||||||
rules:
|
rules:
|
||||||
'admin/externallinks//$Action': 'CMSExternalLinks_Controller'
|
'admin/externallinks//$Action': SilverStripe\ExternalLinks\Controllers\CMSExternalLinksController
|
||||||
|
29
behat.yml
Normal file
29
behat.yml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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
|
4
client/css/BrokenExternalLinksReport.css
Normal file
4
client/css/BrokenExternalLinksReport.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.external-links-report__create-report,
|
||||||
|
.external-links-report__report-progress {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
140
client/javascript/BrokenExternalLinksReport.js
Normal file
140
client/javascript/BrokenExternalLinksReport.js
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
(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));
|
@ -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,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "silverstripe/externallinks",
|
"name": "silverstripe/externallinks",
|
||||||
"description": "Adds tracking of external broken links to the SilverStripe CMS",
|
"description": "Adds tracking of broken external links to the SilverStripe CMS",
|
||||||
"type": "silverstripe-module",
|
"type": "silverstripe-vendormodule",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"silverstripe",
|
"silverstripe",
|
||||||
"broken",
|
"broken",
|
||||||
@ -16,15 +16,30 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"silverstripe/framework": "~3.1",
|
"php": "^7.4 || ^8.0",
|
||||||
"silverstripe/cms": "~3.1"
|
"silverstripe/cms": "^4.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"hafriedlander/silverstripe-phockito": "*",
|
"phpunit/phpunit": "^9.5",
|
||||||
"phpunit/PHPUnit": "~3.7@stable"
|
"squizlabs/php_codesniffer": "^3.0",
|
||||||
|
"symbiote/silverstripe-queuedjobs": "^4.9"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"silverstripe/queuedjobs": "Speeds up running the job for Content Editors fropm the report"
|
"symbiote/silverstripe-queuedjobs": "Provides a more efficient method of generating/updating the report"
|
||||||
},
|
},
|
||||||
"extra": []
|
"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
|
||||||
}
|
}
|
@ -1,69 +0,0 @@
|
|||||||
(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));
|
|
10
lang/de.yml
10
lang/de.yml
@ -1,8 +1,14 @@
|
|||||||
de:
|
de:
|
||||||
BrokenExternalLink:
|
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
|
||||||
|
TITLE: 'Überprüfe defekte externe Links'
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
|
||||||
NOTAVAILABLE: 'Server nicht verfügbar'
|
NOTAVAILABLE: 'Server nicht verfügbar'
|
||||||
PLURALNAME: 'Defekte externe Links'
|
PLURALNAME: 'Defekte externe Links'
|
||||||
|
PLURALS:
|
||||||
|
one: 'Ein defekter externer Link'
|
||||||
|
other: '{count} defekte externe Links'
|
||||||
SINGULARNAME: 'Defekter externer Link'
|
SINGULARNAME: 'Defekter externer Link'
|
||||||
UNKNOWNRESPONSE: 'Unbekannter Antwortcode'
|
UNKNOWNRESPONSE: 'Unbekannter Antwortcode'
|
||||||
ExternalBrokenLinksReport:
|
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
|
||||||
|
EXTERNALBROKENLINKS: 'Defekte externe Links Report'
|
||||||
RUNREPORT: 'Generiere neuen Bericht'
|
RUNREPORT: 'Generiere neuen Bericht'
|
||||||
|
35
lang/en.yml
35
lang/en.yml
@ -1,17 +1,38 @@
|
|||||||
en:
|
en:
|
||||||
BrokenExternalLink:
|
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
|
||||||
|
TITLE: 'Checking for external broken links'
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
|
||||||
NOTAVAILABLE: 'Server Not Available'
|
NOTAVAILABLE: 'Server Not Available'
|
||||||
PLURALNAME: 'Broken External Links'
|
PLURALNAME: 'Broken External Links'
|
||||||
|
PLURALS:
|
||||||
|
one: 'A Broken External Link'
|
||||||
|
other: '{count} Broken External Links'
|
||||||
SINGULARNAME: 'Broken External Link'
|
SINGULARNAME: 'Broken External Link'
|
||||||
UNKNOWNRESPONSE: 'Unknown Response Code'
|
UNKNOWNRESPONSE: 'Unknown Response Code'
|
||||||
BrokenExternalPageTrack:
|
db_HTTPCode: 'HTTP code'
|
||||||
|
db_Link: Link
|
||||||
|
has_one_Status: Status
|
||||||
|
has_one_Track: Track
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
|
||||||
PLURALNAME: 'Broken External Page Tracks'
|
PLURALNAME: 'Broken External Page Tracks'
|
||||||
|
PLURALS:
|
||||||
|
one: 'A Broken External Page Track'
|
||||||
|
other: '{count} Broken External Page Tracks'
|
||||||
SINGULARNAME: 'Broken External Page Track'
|
SINGULARNAME: 'Broken External Page Track'
|
||||||
BrokenExternalPageTrackStatus:
|
db_Processed: Processed
|
||||||
PLURALNAME: 'Broken External Page Track Statuss'
|
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'
|
||||||
SINGULARNAME: 'Broken External Page Track Status'
|
SINGULARNAME: 'Broken External Page Track Status'
|
||||||
CheckExternalLiksJob:
|
db_JobInfo: 'Job info'
|
||||||
TITLE: 'Checking for external broken links'
|
db_Status: Status
|
||||||
ExternalBrokenLinksReport:
|
has_many_BrokenLinks: 'Broken links'
|
||||||
|
has_many_TrackedPages: 'Tracked pages'
|
||||||
|
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
|
||||||
EXTERNALBROKENLINKS: 'External broken links report'
|
EXTERNALBROKENLINKS: 'External broken links report'
|
||||||
RUNREPORT: 'Create new report'
|
RUNREPORT: 'Create new report'
|
||||||
|
33
lang/eo.yml
33
lang/eo.yml
@ -1,17 +1,38 @@
|
|||||||
eo:
|
eo:
|
||||||
BrokenExternalLink:
|
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
|
||||||
|
TITLE: 'Kontrolas por eksteraj rompitaj ligiloj'
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
|
||||||
NOTAVAILABLE: 'Servilo estas neatingenbla'
|
NOTAVAILABLE: 'Servilo estas neatingenbla'
|
||||||
PLURALNAME: 'Rompitaj eksteraj ligiloj'
|
PLURALNAME: 'Rompitaj eksteraj ligiloj'
|
||||||
|
PLURALS:
|
||||||
|
one: 'Unu rompita ekstera ligilo'
|
||||||
|
other: '{count} rompitaj eksteraj ligiloj'
|
||||||
SINGULARNAME: 'Rompita ekstera ligilo'
|
SINGULARNAME: 'Rompita ekstera ligilo'
|
||||||
UNKNOWNRESPONSE: 'Nekonata respondokodo'
|
UNKNOWNRESPONSE: 'Nekonata respondokodo'
|
||||||
BrokenExternalPageTrack:
|
db_HTTPCode: HTTP-kodo
|
||||||
|
db_Link: Ligilo
|
||||||
|
has_one_Status: Stato
|
||||||
|
has_one_Track: Spuri
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
|
||||||
PLURALNAME: 'Rompitaj eksteraj paĝaj trakoj'
|
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'
|
SINGULARNAME: 'Rompita ekstera paĝa trako'
|
||||||
BrokenExternalPageTrackStatus:
|
db_Processed: Traktita
|
||||||
|
has_many_BrokenLinks: 'Rompitaj ligiloj'
|
||||||
|
has_one_Page: Paĝo
|
||||||
|
has_one_Status: Stato
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
|
||||||
PLURALNAME: 'Stato de rompitaj eksteraj paĝaj trakoj'
|
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'
|
SINGULARNAME: 'Stato de rompita ekstera paĝa trako'
|
||||||
CheckExternalLiksJob:
|
db_JobInfo: 'Taska informo'
|
||||||
TITLE: 'Kontrolas por eksteraj rompitaj ligiloj'
|
db_Status: Stato
|
||||||
ExternalBrokenLinksReport:
|
has_many_BrokenLinks: 'Rompitaj ligiloj'
|
||||||
|
has_many_TrackedPages: 'Spuritaj paĝoj'
|
||||||
|
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
|
||||||
EXTERNALBROKENLINKS: 'Raporto pri eksteraj rompitaj ligiloj'
|
EXTERNALBROKENLINKS: 'Raporto pri eksteraj rompitaj ligiloj'
|
||||||
RUNREPORT: 'Krei novan raporton'
|
RUNREPORT: 'Krei novan raporton'
|
||||||
|
16
lang/fi.yml
Normal file
16
lang/fi.yml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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'
|
26
lang/fi_FI.yml
Normal file
26
lang/fi_FI.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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'
|
29
lang/it.yml
Normal file
29
lang/it.yml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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'
|
26
lang/nl.yml
Normal file
26
lang/nl.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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'
|
12
lang/pl.yml
12
lang/pl.yml
@ -1,17 +1,17 @@
|
|||||||
pl:
|
pl:
|
||||||
BrokenExternalLink:
|
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
|
||||||
|
TITLE: 'Wyszukiwanie uszkodzonych linków zewnętrznych'
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
|
||||||
NOTAVAILABLE: 'Serwer niedostępny'
|
NOTAVAILABLE: 'Serwer niedostępny'
|
||||||
PLURALNAME: 'Uszkodzone linki zewnętrzne'
|
PLURALNAME: 'Uszkodzone linki zewnętrzne'
|
||||||
SINGULARNAME: 'Uszkodzony link zewnętrzny'
|
SINGULARNAME: 'Uszkodzony link zewnętrzny'
|
||||||
UNKNOWNRESPONSE: 'Nieznany kod odpowiedzi'
|
UNKNOWNRESPONSE: 'Nieznany kod odpowiedzi'
|
||||||
BrokenExternalPageTrack:
|
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
|
||||||
PLURALNAME: 'Wykrywania wadliwych stron zewnętrznych'
|
PLURALNAME: 'Wykrywania wadliwych stron zewnętrznych'
|
||||||
SINGULARNAME: 'Wykrywanie wadliwych stron zewnętrznych'
|
SINGULARNAME: 'Wykrywanie wadliwych stron zewnętrznych'
|
||||||
BrokenExternalPageTrackStatus:
|
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
|
||||||
PLURALNAME: 'Statusy wykrywania wadliwych stron zewnętrznych'
|
PLURALNAME: 'Statusy wykrywania wadliwych stron zewnętrznych'
|
||||||
SINGULARNAME: 'Status wykrywania wadliwych stron zewnętrznych'
|
SINGULARNAME: 'Status wykrywania wadliwych stron zewnętrznych'
|
||||||
CheckExternalLiksJob:
|
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
|
||||||
TITLE: 'Wyszukiwanie uszkodzonych linków zewnętrznych'
|
|
||||||
ExternalBrokenLinksReport:
|
|
||||||
EXTERNALBROKENLINKS: 'Raport uszkodzonych linków zewnętrznych'
|
EXTERNALBROKENLINKS: 'Raport uszkodzonych linków zewnętrznych'
|
||||||
RUNREPORT: 'Stwórz nowy raport'
|
RUNREPORT: 'Stwórz nowy raport'
|
||||||
|
13
lang/ru.yml
13
lang/ru.yml
@ -1,17 +1,16 @@
|
|||||||
ru:
|
ru:
|
||||||
BrokenExternalLink:
|
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
|
||||||
|
TITLE: 'Проверяю внешние ссылки'
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
|
||||||
NOTAVAILABLE: 'Сервер не доступен'
|
NOTAVAILABLE: 'Сервер не доступен'
|
||||||
PLURALNAME: 'Недоступные внешние ссылки'
|
PLURALNAME: 'Недоступные внешние ссылки'
|
||||||
SINGULARNAME: 'Недоступная внешняя ссылка'
|
SINGULARNAME: 'Недоступная внешняя ссылка'
|
||||||
UNKNOWNRESPONSE: 'Неизвестный ответ сервера'
|
UNKNOWNRESPONSE: 'Неизвестный ответ сервера'
|
||||||
BrokenExternalPageTrack:
|
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
|
||||||
PLURALNAME: 'Внешнее отслеживание страниц нарушено'
|
PLURALNAME: 'Внешнее отслеживание страниц нарушено'
|
||||||
SINGULARNAME: 'Внешнее отслеживание страниц нарушено'
|
SINGULARNAME: 'Внешнее отслеживание страниц нарушено'
|
||||||
BrokenExternalPageTrackStatus:
|
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
|
||||||
PLURALNAME: 'Внешнее отслеживание страниц нарушено'
|
|
||||||
SINGULARNAME: 'Внешнее отслеживание страниц нарушено'
|
SINGULARNAME: 'Внешнее отслеживание страниц нарушено'
|
||||||
CheckExternalLiksJob:
|
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
|
||||||
TITLE: 'Проверяю внешние ссылки'
|
|
||||||
ExternalBrokenLinksReport:
|
|
||||||
EXTERNALBROKENLINKS: 'Отчёт о неработающих внешних ссылках'
|
EXTERNALBROKENLINKS: 'Отчёт о неработающих внешних ссылках'
|
||||||
RUNREPORT: 'Создать новый отчёт'
|
RUNREPORT: 'Создать новый отчёт'
|
||||||
|
44
lang/sk.yml
Normal file
44
lang/sk.yml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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'
|
32
lang/sl.yml
Normal file
32
lang/sl.yml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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'
|
@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2016, SilverStripe Limited
|
Copyright (c) 2017, SilverStripe Limited
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
13
phpcs.xml.dist
Normal file
13
phpcs.xml.dist
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?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>
|
16
phpunit.xml.dist
Normal file
16
phpunit.xml.dist
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?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>
|
79
src/Controllers/CMSExternalLinksController.php
Normal file
79
src/Controllers/CMSExternalLinksController.php
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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';
|
||||||
|
}
|
||||||
|
}
|
86
src/Model/BrokenExternalLink.php
Normal file
86
src/Model/BrokenExternalLink.php
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
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);
|
||||||
|
}
|
||||||
|
}
|
169
src/Model/BrokenExternalPageTrackStatus.php
Normal file
169
src/Model/BrokenExternalPageTrackStatus.php
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
98
src/Reports/BrokenExternalLinksReport.php
Normal file
98
src/Reports/BrokenExternalLinksReport.php
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
246
src/Tasks/CheckExternalLinksTask.php
Normal file
246
src/Tasks/CheckExternalLinksTask.php
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
101
src/Tasks/CurlLinkChecker.php
Normal file
101
src/Tasks/CurlLinkChecker.php
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
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,157 +0,0 @@
|
|||||||
<?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'
|
|
||||||
);
|
|
||||||
}
|
|
0
tests/behat/_manifest_exclude
Normal file
0
tests/behat/_manifest_exclude
Normal file
34
tests/behat/features/external-links.feature
Normal file
34
tests/behat/features/external-links.feature
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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 " 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"
|
0
tests/behat/files/blank.txt
Normal file
0
tests/behat/files/blank.txt
Normal file
9
tests/behat/src/FeatureContext.php
Normal file
9
tests/behat/src/FeatureContext.php
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ExternalLinks\Tests\Behat\Context;
|
||||||
|
|
||||||
|
use SilverStripe\BehatExtension\Context\SilverStripeContext;
|
||||||
|
|
||||||
|
class FeatureContext extends SilverStripeContext
|
||||||
|
{
|
||||||
|
}
|
9
tests/behat/src/FixtureContext.php
Normal file
9
tests/behat/src/FixtureContext.php
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ExternalLinks\Tests\Behat\Context;
|
||||||
|
|
||||||
|
use SilverStripe\BehatExtension\Context\FixtureContext as BaseFixtureContext;
|
||||||
|
|
||||||
|
class FixtureContext extends BaseFixtureContext
|
||||||
|
{
|
||||||
|
}
|
153
tests/php/ExternalLinksTest.php
Normal file
153
tests/php/ExternalLinksTest.php
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<?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());
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
ExternalLinksTest_Page:
|
SilverStripe\ExternalLinks\Tests\Stubs\ExternalLinksTestPage:
|
||||||
# Tests mix of broken and working external links
|
# Tests mix of broken and working external links
|
||||||
page1:
|
page1:
|
||||||
Title: 'Page 1'
|
Title: 'Page 1'
|
56
tests/php/Model/BrokenExternalLinkTest.php
Normal file
56
tests/php/Model/BrokenExternalLinkTest.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
tests/php/Stubs/ExternalLinksTestPage.php
Normal file
15
tests/php/Stubs/ExternalLinksTestPage.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?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'
|
||||||
|
);
|
||||||
|
}
|
29
tests/php/Stubs/PretendLinkChecker.php
Normal file
29
tests/php/Stubs/PretendLinkChecker.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user