mirror of
https://github.com/silverstripe/silverstripe-externallinks.git
synced 2024-09-29 12:49:24 +02:00
Compare commits
169 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 | ||
|
98ce40f759 | ||
|
04551ccf4d | ||
|
93435932f6 | ||
|
3547869393 | ||
|
ded3b7cd82 | ||
|
76fd1169db | ||
|
99d4a6ee1a | ||
|
0354be75d8 | ||
|
b235367b20 | ||
|
2eb0ab1872 | ||
|
738ca1733e | ||
|
ecd7014cd5 | ||
|
248b93f8d7 | ||
|
eab4080934 | ||
|
c5e9a30235 | ||
|
32812adba3 | ||
|
1e9cec9c70 | ||
|
c3ba71fa10 | ||
|
f1b12c8d4c | ||
|
dafe6af910 | ||
|
cb32766c88 | ||
|
9d14238eaf | ||
|
fe8d93cdbd | ||
|
14ccb2aa8d | ||
|
79a4e691ee | ||
|
638844b34a | ||
|
a5b162cdc5 | ||
|
2479280fbb | ||
|
8d54709675 | ||
|
939b19c31f | ||
|
97c3623acf | ||
|
929bf30ea5 | ||
|
0531d699e8 | ||
|
821d86ae76 | ||
|
c37a13b3e2 | ||
|
f0134c98f1 | ||
|
928f6049a0 | ||
|
c949b125ea | ||
|
4411d2e85d | ||
|
b219ebb52e | ||
|
38659bac98 |
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# For more information about the properties used in this file,
|
||||||
|
# please see the EditorConfig documentation:
|
||||||
|
# http://editorconfig.org
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{yml,js,scss,css,json}]
|
||||||
|
indent_size = 2
|
7
.gitattributes
vendored
Normal file
7
.gitattributes
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/tests export-ignore
|
||||||
|
/docs export-ignore
|
||||||
|
/.gitattributes export-ignore
|
||||||
|
/.gitignore export-ignore
|
||||||
|
/.travis.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
|
23
.travis.yml
23
.travis.yml
@ -1,23 +0,0 @@
|
|||||||
language: php
|
|
||||||
|
|
||||||
php:
|
|
||||||
- 5.3
|
|
||||||
- 5.4
|
|
||||||
|
|
||||||
env:
|
|
||||||
- DB=MYSQL CORE_RELEASE=3.1
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- php: 5.4
|
|
||||||
env: DB=PGSQL CORE_RELEASE=3.1
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- composer self-update
|
|
||||||
- phpenv rehash
|
|
||||||
- git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support
|
|
||||||
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
|
|
||||||
- cd ~/builds/ss
|
|
||||||
|
|
||||||
script:
|
|
||||||
- vendor/bin/phpunit externallinks/tests
|
|
9
.tx/config
Normal file
9
.tx/config
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[main]
|
||||||
|
host = https://www.transifex.com
|
||||||
|
|
||||||
|
[o:silverstripe:p:silverstripe-externallinks:r:master]
|
||||||
|
file_filter = lang/<lang>.yml
|
||||||
|
source_file = lang/en.yml
|
||||||
|
source_lang = en
|
||||||
|
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
|
24
LICENSE
24
LICENSE
@ -1,24 +0,0 @@
|
|||||||
* Copyright (c) 2014, Silverstripe Ltd.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are met:
|
|
||||||
* * Redistributions of source code must retain the above copyright
|
|
||||||
* notice, this list of conditions and the following disclaimer.
|
|
||||||
* * Redistributions in binary form must reproduce the above copyright
|
|
||||||
* notice, this list of conditions and the following disclaimer in the
|
|
||||||
* documentation and/or other materials provided with the distribution.
|
|
||||||
* * Neither the name of the <organization> nor the
|
|
||||||
* names of its contributors may be used to endorse or promote products
|
|
||||||
* derived from this software without specific prior written permission.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY Silverstripe Ltd. ``AS IS'' AND ANY
|
|
||||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
* DISCLAIMED. IN NO EVENT SHALL Silverstripe Ltd. BE LIABLE FOR ANY
|
|
||||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
||||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
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
|
||||||
|
|
||||||
@ -8,11 +9,13 @@ The external links module is a task and ModelAdmin to track and to report on bro
|
|||||||
|
|
||||||
## Maintainer Contact
|
## Maintainer Contact
|
||||||
|
|
||||||
* Kirk Mayo kirk (at) silverstripe (dot) com
|
* Damian Mooyman (@tractorcow) <damian@silverstripe.com>
|
||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
@ -21,14 +24,12 @@ The external links module is a task and ModelAdmin to track and to report on bro
|
|||||||
|
|
||||||
## 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.
|
||||||
@ -54,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
|
19
changelog.md
Normal file
19
changelog.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## [1.0.5]
|
||||||
|
|
||||||
|
* Update translations.
|
||||||
|
|
||||||
|
## [1.0.4]
|
||||||
|
|
||||||
|
* Update license and editorconfig.
|
||||||
|
* Add code of conduct.
|
||||||
|
|
||||||
|
## [1.0.3]
|
||||||
|
|
||||||
|
* Changelog added.
|
||||||
|
* Update translations.
|
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
code-of-conduct.md
Normal file
1
code-of-conduct.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
When having discussions about this module in issues or pull request please adhere to the [SilverStripe Community Code of Conduct](https://docs.silverstripe.org/en/contributing/code_of_conduct).
|
@ -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,24 +1,45 @@
|
|||||||
{
|
{
|
||||||
"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": ["silverstripe", "broken", "links", "href"],
|
"keywords": [
|
||||||
"license": "BSD-3-Clause",
|
"silverstripe",
|
||||||
"authors": [
|
"broken",
|
||||||
{
|
"links",
|
||||||
"name": "Kirk Mayo",
|
"href"
|
||||||
"email": "kirk@silverstripe.com"
|
],
|
||||||
}
|
"license": "BSD-3-Clause",
|
||||||
],
|
"authors": [
|
||||||
"require": {
|
{
|
||||||
"silverstripe/framework": "~3.1",
|
"name": "Kirk Mayo",
|
||||||
"silverstripe/cms": "~3.1"
|
"email": "kirk@silverstripe.com"
|
||||||
},
|
}
|
||||||
"require-dev": {
|
],
|
||||||
"hafriedlander/silverstripe-phockito": "*",
|
"require": {
|
||||||
"phpunit/PHPUnit": "~3.7@stable"
|
"php": "^7.4 || ^8.0",
|
||||||
},
|
"silverstripe/cms": "^4.0"
|
||||||
"suggest": {
|
},
|
||||||
"silverstripe/queuedjobs": "Speeds up running the job for Content Editors fropm the report"
|
"require-dev": {
|
||||||
}
|
"phpunit/phpunit": "^9.5",
|
||||||
|
"squizlabs/php_codesniffer": "^3.0",
|
||||||
|
"symbiote/silverstripe-queuedjobs": "^4.9"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"symbiote/silverstripe-queuedjobs": "Provides a more efficient method of generating/updating the report"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"SilverStripe\\ExternalLinks\\": "src/",
|
||||||
|
"SilverStripe\\ExternalLinks\\Tests\\": "tests/",
|
||||||
|
"SilverStripe\\ExternalLinks\\Tests\\Behat\\Context\\": "tests/behat/src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"expose": [
|
||||||
|
"client/css",
|
||||||
|
"client/javascript"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"prefer-stable": true
|
||||||
}
|
}
|
@ -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));
|
|
14
lang/de.yml
Normal file
14
lang/de.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
de:
|
||||||
|
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
|
||||||
|
TITLE: 'Überprüfe defekte externe Links'
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
|
||||||
|
NOTAVAILABLE: 'Server nicht verfügbar'
|
||||||
|
PLURALNAME: 'Defekte externe Links'
|
||||||
|
PLURALS:
|
||||||
|
one: 'Ein defekter externer Link'
|
||||||
|
other: '{count} defekte externe Links'
|
||||||
|
SINGULARNAME: 'Defekter externer Link'
|
||||||
|
UNKNOWNRESPONSE: 'Unbekannter Antwortcode'
|
||||||
|
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
|
||||||
|
EXTERNALBROKENLINKS: 'Defekte externe Links Report'
|
||||||
|
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'
|
||||||
|
38
lang/eo.yml
Normal file
38
lang/eo.yml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
eo:
|
||||||
|
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
|
||||||
|
TITLE: 'Kontrolas por eksteraj rompitaj ligiloj'
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
|
||||||
|
NOTAVAILABLE: 'Servilo estas neatingenbla'
|
||||||
|
PLURALNAME: 'Rompitaj eksteraj ligiloj'
|
||||||
|
PLURALS:
|
||||||
|
one: 'Unu rompita ekstera ligilo'
|
||||||
|
other: '{count} rompitaj eksteraj ligiloj'
|
||||||
|
SINGULARNAME: 'Rompita ekstera ligilo'
|
||||||
|
UNKNOWNRESPONSE: 'Nekonata respondokodo'
|
||||||
|
db_HTTPCode: HTTP-kodo
|
||||||
|
db_Link: Ligilo
|
||||||
|
has_one_Status: Stato
|
||||||
|
has_one_Track: Spuri
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
|
||||||
|
PLURALNAME: 'Rompitaj eksteraj paĝaj trakoj'
|
||||||
|
PLURALS:
|
||||||
|
one: 'Unu rompita ekstera paĝa trako'
|
||||||
|
other: '{count} rompitaj eksteraj paĝaj trakoj'
|
||||||
|
SINGULARNAME: 'Rompita ekstera paĝa trako'
|
||||||
|
db_Processed: Traktita
|
||||||
|
has_many_BrokenLinks: 'Rompitaj ligiloj'
|
||||||
|
has_one_Page: Paĝo
|
||||||
|
has_one_Status: Stato
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
|
||||||
|
PLURALNAME: 'Stato de rompitaj eksteraj paĝaj trakoj'
|
||||||
|
PLURALS:
|
||||||
|
one: 'Unu stato de rompita ekstera paĝa trako'
|
||||||
|
other: '{count} statoj de rompitaj eksteraj paĝaj trakoj'
|
||||||
|
SINGULARNAME: 'Stato de rompita ekstera paĝa trako'
|
||||||
|
db_JobInfo: 'Taska informo'
|
||||||
|
db_Status: Stato
|
||||||
|
has_many_BrokenLinks: 'Rompitaj ligiloj'
|
||||||
|
has_many_TrackedPages: 'Spuritaj paĝoj'
|
||||||
|
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
|
||||||
|
EXTERNALBROKENLINKS: 'Raporto pri eksteraj rompitaj ligiloj'
|
||||||
|
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'
|
17
lang/pl.yml
Normal file
17
lang/pl.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
pl:
|
||||||
|
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
|
||||||
|
TITLE: 'Wyszukiwanie uszkodzonych linków zewnętrznych'
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
|
||||||
|
NOTAVAILABLE: 'Serwer niedostępny'
|
||||||
|
PLURALNAME: 'Uszkodzone linki zewnętrzne'
|
||||||
|
SINGULARNAME: 'Uszkodzony link zewnętrzny'
|
||||||
|
UNKNOWNRESPONSE: 'Nieznany kod odpowiedzi'
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
|
||||||
|
PLURALNAME: 'Wykrywania wadliwych stron zewnętrznych'
|
||||||
|
SINGULARNAME: 'Wykrywanie wadliwych stron zewnętrznych'
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
|
||||||
|
PLURALNAME: 'Statusy wykrywania wadliwych stron zewnętrznych'
|
||||||
|
SINGULARNAME: 'Status wykrywania wadliwych stron zewnętrznych'
|
||||||
|
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
|
||||||
|
EXTERNALBROKENLINKS: 'Raport uszkodzonych linków zewnętrznych'
|
||||||
|
RUNREPORT: 'Stwórz nowy raport'
|
16
lang/ru.yml
Normal file
16
lang/ru.yml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
ru:
|
||||||
|
SilverStripe\ExternalLinks\Jobs\CheckExternalLinksJob:
|
||||||
|
TITLE: 'Проверяю внешние ссылки'
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalLink:
|
||||||
|
NOTAVAILABLE: 'Сервер не доступен'
|
||||||
|
PLURALNAME: 'Недоступные внешние ссылки'
|
||||||
|
SINGULARNAME: 'Недоступная внешняя ссылка'
|
||||||
|
UNKNOWNRESPONSE: 'Неизвестный ответ сервера'
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
|
||||||
|
PLURALNAME: 'Внешнее отслеживание страниц нарушено'
|
||||||
|
SINGULARNAME: 'Внешнее отслеживание страниц нарушено'
|
||||||
|
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
|
||||||
|
SINGULARNAME: 'Внешнее отслеживание страниц нарушено'
|
||||||
|
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
|
||||||
|
EXTERNALBROKENLINKS: 'Отчёт о неработающих внешних ссылках'
|
||||||
|
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'
|
12
license.md
Normal file
12
license.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Copyright (c) 2017, SilverStripe Limited
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
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,151 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
if (class_exists('Phockito')) Phockito::include_hamcrest();
|
|
||||||
|
|
||||||
class ExternalLinksTest extends SapphireTest {
|
|
||||||
|
|
||||||
protected static $fixture_file = 'ExternalLinksTest.yml';
|
|
||||||
|
|
||||||
protected $extraDataObjects = array(
|
|
||||||
'ExternalLinksTest_Page'
|
|
||||||
);
|
|
||||||
|
|
||||||
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(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