Compare commits

...

126 Commits
1.1.0 ... 2

Author SHA1 Message Date
github-actions 2ee2bfbc8a Merge branch '2.4' into 2 2024-02-11 14:04:18 +00:00
Guy Sartorelli b12e6868bc
TLN Update translations (#120) 2024-02-07 16:05:28 +13:00
Steve Boyd e12e2aaece Merge branch '2.4' into 2 2023-11-13 17:24:09 +13:00
Sabina Talipova 8797f1f41e
ENH Restrict access to getJobStatus execution (#113) 2023-11-09 10:06:58 +13:00
github-actions a01c16b829 Merge branch '2.4' into 2 2023-08-27 14:03:52 +00:00
Guy Sartorelli 20c2231bff
ENH Update translations (#106) 2023-08-21 12:24:21 +12:00
Steve Boyd bea1716d94 Merge branch '2.4' into 2 2023-06-16 12:01:45 +12:00
Guy Sartorelli 062319682e
Merge pull request #102 from creative-commoners/pulls/2.4/tx-1686724684
ENH Update translations
2023-06-15 10:06:36 +12:00
Steve Boyd 2560888df0 ENH Update translations 2023-06-14 18:38:04 +12:00
Steve Boyd ae9dbddfb0 Merge branch '2.4' into 2 2023-03-29 09:58:29 +13:00
Guy Sartorelli 4f5309163b
MNT Revert erroneous dependency changes (#98) 2023-03-28 17:08:50 +13:00
Guy Sartorelli 44b575db9b
Merge pull request #96 from creative-commoners/pulls/2.4/fix-create-report-button
FIX Create button is disabled if server returns error
2023-03-27 11:42:20 +13:00
Sabina Talipova 4d67a327fe FIX Create button is disabled if server returns error 2023-03-24 12:59:39 +13:00
Maxime Rainville 418d69813f
Merge pull request #95 from creative-commoners/pulls/2/dispatch-ci
MNT Use gha-dispatch-ci
2023-03-23 14:10:53 +13:00
Steve Boyd 8b27314490 MNT Use gha-dispatch-ci 2023-03-21 12:25:06 +13:00
Guy Sartorelli 6ea69fabb2
MNT Update development dependencies 2023-03-10 16:34:29 +13:00
Guy Sartorelli 7698371ebf
MNT Update release dependencies 2023-03-10 16:34:25 +13:00
Guy Sartorelli cbd278685b
MNT Update development dependencies 2023-03-10 12:21:30 +13:00
Guy Sartorelli 2595fff257
Merge pull request #92 from creative-commoners/pulls/2/tx-1678079850
ENH Update translations
2023-03-08 10:25:03 +13:00
Steve Boyd a055868bfa ENH Update translations 2023-03-06 18:17:30 +13:00
Sabina Talipova bef19ec35c
Merge pull request #86 from creative-commoners/pulls/2/stop-using-depr
API Stop using deprecated API
2022-12-05 16:43:44 +13:00
Steve Boyd 1b66be22cd API Stop using deprecated API 2022-11-28 17:40:24 +13:00
Steve Boyd 9381f33959 Merge branch '2.3' into 2 2022-08-02 18:49:41 +12:00
Steve Boyd 93b0303ce0 Merge branch '2.2' into 2.3 2022-08-02 18:49:38 +12:00
Guy Sartorelli 0009a4bf13
Merge pull request #83 from creative-commoners/pulls/2.2/standardise-modules
MNT Standardise modules
2022-08-02 15:10:52 +12:00
Steve Boyd a70a9d1e82 MNT Standardise modules 2022-08-01 16:21:58 +12:00
Steve Boyd b47db91c2a Merge branch '2.3' into 2 2022-07-25 11:26:02 +12:00
Steve Boyd 2f72f9873a Merge branch '2.2' into 2.3 2022-07-25 11:25:44 +12:00
Guy Sartorelli 813cd2e4e7
Merge pull request #82 from creative-commoners/pulls/2.2/behat
FIX Move files to client directory
2022-07-20 09:41:32 +12:00
Steve Boyd d1cc3f80ab FIX Move files to client directory 2022-07-19 18:05:43 +12:00
Guy Sartorelli ec36ece776
Merge pull request #81 from creative-commoners/pulls/2.2/module-standards
MNT Use GitHub Actions CI
2022-07-15 16:58:23 +12:00
Steve Boyd d2c1aedf2e MNT Use GitHub Actions CI 2022-07-07 15:41:58 +12:00
Guy Sartorelli 11a4a6d6e1
Merge pull request #80 from ssmarco/patch-1
MNT - Composer.json - Fix wrong spelling of symbiote in suggestion
2022-05-13 11:40:39 +12:00
Marco Hermo ae9e7d0c96
MNT - Composer.json - Fix wrong spelling of symbiote in suggestion 2022-05-11 00:15:44 +12:00
Guy Sartorelli 4df31ad641 Merge branch '2.3' into 2 2022-05-06 09:56:59 +12:00
Guy Sartorelli e743b90607 Merge branch '2.2' into 2.3 2022-05-06 09:53:57 +12:00
Guy Sartorelli d4871e7d41
Merge pull request #78 from creative-commoners/pulls/2.2/fix-permission-cmsmain
FIX: CMSMain user able to see external broken links
2022-05-06 09:29:39 +12:00
Sabina Talipova 6d288c54f2 Add new permission group 2022-05-06 08:54:12 +12:00
Steve Boyd 3721dab425 Update translations 2022-05-04 13:29:41 +12:00
Guy Sartorelli 164032af76
Merge pull request #77 from creative-commoners/pulls/2/php81
ENH PHP 8.1 compatibility
2022-04-26 17:58:36 +12:00
Steve Boyd 1bfaf12909 ENH PHP 8.1 compatibility 2022-04-13 10:29:57 +12:00
Maxime Rainville 3fc084a1f0
Merge pull request #76 from creative-commoners/pulls/2/php74
DEP Set PHP 7.4 as the minimum version
2022-02-18 21:48:06 +13:00
Steve Boyd b1cd65d538 DEP Set PHP 7.4 as the minimum version 2022-02-10 16:18:31 +13:00
Steve Boyd 4176da66c7 Merge branch '2.2' into 2 2021-12-22 10:28:09 +13:00
Steve Boyd b4ccd03025 MNT Remove obsolete branch-alias 2021-11-30 13:55:20 +13:00
Steve Boyd c4e41d58ee Merge branch '2.1' into 2 2021-11-18 17:20:11 +13:00
Maxime Rainville aed847dc13
Merge pull request #74 from creative-commoners/pulls/2.1/behat
MNT Add behat tests
2021-11-10 16:51:36 +13:00
Maxime Rainville 75585f983a
Merge pull request #75 from creative-commoners/pulls/2/sapphire-test-nine
API phpunit 9 support
2021-11-01 21:26:14 +13:00
Steve Boyd fb9c340edb API phpunit 9 support 2021-10-27 18:06:53 +13:00
Steve Boyd 3da3567a7a MNT Add behat tests 2021-10-01 19:55:27 +13:00
Steve Boyd 40c96ce23f Merge branch '2.1' into 2 2021-02-04 09:47:18 +13:00
Garion Herman b4c210f211
FIX Exclude links attached to archived Pages from report (#72) 2021-02-04 09:45:39 +13:00
Steve Boyd 478d6613f8
Update build status badge 2021-01-21 16:34:25 +13:00
Steve Boyd 7b68fe0eaa Merge branch '2.0' into 2 2020-11-11 17:13:31 +13:00
Serge Latyntsev 75a4988f48
Merge pull request #70 from creative-commoners/pulls/2.0/shared-config
MNT Use shared travis config, use sminnee/phpunit
2020-11-10 17:00:31 +13:00
Steve Boyd 504f742aa7 MNT Use shared travis config, use sminnee/phpunit 2020-11-10 12:55:10 +13:00
Maxime Rainville 703ae9b2ca Merge branch '2.0' into 2 2020-10-22 22:07:03 +13:00
Alex Saelens 9282ed2e2d
FIX Allow to configure CurlLinkChecker request headers (#64)
* Allow to configure CurlLinkChecker request headers
* Make adding headers more explicit via array
* Remove extra character in comment
* Fix lint
* Align headers array with php docs
2020-07-07 10:25:25 +12:00
Robbie Averill 9cb88c96b2
Merge pull request #67 from creative-commoners/pulls/2.0/travis
Travis 2.0
2020-06-23 10:14:42 -07:00
Steve Boyd 80ddb80123 Travis 2.0 2020-06-23 12:58:22 +12:00
Maxime Rainville 05ded71c02 Merge branch '2.0' into 2 2020-06-12 14:04:09 +12:00
Guy Marriott 839cbb7185
Update translations 2019-08-20 16:37:59 +12:00
Robbie Averill 3746cb1368 Merge branch '2.0' 2019-08-15 10:00:55 +12:00
Dylan Wagstaff a166257690
Merge pull request #62 from creative-commoners/pulls/2.0/handle-http-errors-gracefully
FIX Add missing namespace import - unknown HTTP status codes are now handled
2019-07-29 10:17:31 +12:00
Robbie Averill b39a81b60b Use trusty in Travis builds 2019-07-26 11:22:53 +02:00
Robbie Averill 7a335726c4 Remove SilverStripe 4.0-4.2 from Travis builds 2019-07-26 11:22:37 +02:00
Robbie Averill 1ce8c3bbdf FIX Add missing namespace import - unknown HTTP status codes are now handled 2019-07-26 11:16:31 +02:00
Robbie Averill a180db21b9
Merge pull request #60 from lhalaa/pulls/add-legacy-file
Add legacy.yml for SS3 to SS4 upgrades
2019-05-15 16:51:48 +12:00
Sheila Bañez a1b7d3979e Add legacy.yml for SS3 to SS4 upgrades 2019-05-15 15:26:48 +12:00
Robbie Averill 5091bf91e5 Merge branch '2.0' 2019-03-11 15:39:46 +13:00
Robbie Averill e41ee18b7c
Merge pull request #59 from creative-commoners/pulls/2.0/fix-initial-button-state
FIX Reference alias for this inside javascript closure - report now works on initial load
2019-03-11 15:39:27 +13:00
Robbie Averill 4f0df463df FIX Reference alias for this inside javascript closure - report now works on initial load 2019-03-11 15:27:31 +13:00
Dylan Wagstaff 60893a87b1
Merge pull request #58 from creative-commoners/pulls/2.0/disable-button
FIX External links report generate button is now disabled while it is loading
2019-03-08 10:51:07 +13:00
Robbie Averill a3aed13ed0 FIX External links report generate button is now disabled while it is loading 2019-03-08 10:06:50 +13:00
Guy Marriott 1cfb249384
Merge pull request #56 from creative-commoners/pulls/2.0/remove-json-methods
FIX Replace Convert JSON methods with json_* methods, deprecated from SilverStripe 4.4
2018-10-29 11:30:10 +13:00
Robbie Averill 17604a5b42 FIX Replace Convert JSON methods with json_* methods, deprecated from SilverStripe 4.4 2018-10-28 21:36:50 +00:00
Robbie Averill 08fc708521 Merge branch '2.0' 2018-08-27 09:24:51 +12:00
Robbie Averill 4311de28fd
Merge pull request #55 from creative-commoners/pulls/2.0/fix-them-plurals
FIX Update plural name of BrokenExternalPageTrackStatus
2018-08-27 09:23:50 +12:00
Raissa North 61a63f36d9 FIX Update plural name of BrokenExternalPageTrackStatus 2018-08-24 17:13:31 +12:00
Robbie Averill 28ce2df1b7
Merge pull request #52 from creative-commoners/pulls/master/add-supported-module-badge
Add supported module badge to readme
2018-06-18 10:47:43 +12:00
Dylan Wagstaff 8002624da9 Add supported module badge to readme 2018-06-15 17:35:22 +12:00
Robbie Averill 4723e8742c Merge branch '2.0' 2018-05-08 15:38:30 +12:00
Dylan Wagstaff 3ff01bf9e9
Merge pull request #48 from creative-commoners/pulls/2.0/fix-cache-api
FIX Use correct CacheInterface API methods and remove doubled up logic
2018-04-06 11:57:18 +12:00
Robbie Averill 39044de8ad FIX Use correct CacheInterface API methods and remove doubled up logic 2018-04-06 10:59:31 +12:00
Dylan Wagstaff 65a9f11708
Merge pull request #47 from creative-commoners/pulls/2.0/travis-updates
Add various recipe versions to Travis build matrix
2018-04-06 10:58:11 +12:00
Robbie Averill 2bbf756f3e Add various recipe versions to Travis build matrix 2018-04-06 10:48:15 +12:00
Robbie Averill e33f65ecc0 Merge branch '2.0' 2018-04-06 10:45:42 +12:00
Robbie Averill 18c6ca73dd Remove obsolete branch alias 2018-04-06 10:44:45 +12:00
Dylan Wagstaff 8e60c0b396
Merge pull request #45 from silverstripe/raissanorth-patch-1
Update README.md
2018-03-26 11:32:52 +13:00
Raissa North bedef230e7
Update README.md
Rename CheckExternalLinksTask in the installation instructions
2018-03-26 11:28:02 +13:00
Robbie Averill 5746cb1d63
Merge pull request #43 from creative-commoners/pulls/2.0/fixup-reqs
FIX Update requirements & travis config to be consistent with acutal …
2018-02-26 14:23:09 +13:00
Dylan Wagstaff ceba6c1406 FIX Update requirements & travis config to be consistent with acutal requirements 2018-02-26 12:50:26 +13:00
Robbie Averill 717b126dda FIX Update linting in CurlLinkChecker for PSR-2 2018-02-22 10:58:16 +13:00
Robbie Averill 2279b2c9d8 Merge branch '1' 2018-02-22 10:51:59 +13:00
Robbie Averill 99d08dadc7 Merge branch '1.1' into 1 2018-02-22 10:43:11 +13:00
Robbie Averill 63c07de2e1 Update branch alias for 1.2.x-dev 2018-02-22 10:26:23 +13:00
Dylan Wagstaff 6cf7a466fb
Merge pull request #39 from creative-commoners/pulls/2.0/handle-obsolete-class-names
FIX Handle thrown exceptions because of obsolete class names
2017-11-29 14:57:44 +13:00
Dylan Wagstaff e21c0ad470
Merge pull request #38 from creative-commoners/pulls/2.0/ux-improvement
NEW Add loading animation to Create Report button, fix bug in CurlLinkChecker
2017-11-29 14:56:39 +13:00
Robbie Averill c9f5ca6c72 FIX Handle thrown exceptions because of obsolete class names 2017-11-29 14:23:28 +13:00
Robbie Averill 44fbc027f3 NEW Add loading animation to Create Report button, fix bug in CurlLinkChecker 2017-11-29 12:14:39 +13:00
Dylan Wagstaff a119add32e
Merge pull request #32 from creative-commoners/pulls/2.0/rename-config-props
API Rename IgnoreCodes configuration property to ignore_codes
2017-11-28 10:06:29 +13:00
Dylan Wagstaff 6739163212
Merge pull request #31 from creative-commoners/pulls/2.0/juggle-stubs
FIX Implement TestOnly in PretendLinkChecker, move Page stub and sort class imports
2017-11-28 09:53:07 +13:00
Robbie Averill e58190a9b3 API Rename IgnoreCodes configuration property to ignore_codes 2017-11-28 09:45:40 +13:00
Robbie Averill 78fbc68797 FIX Implement TestOnly in PretendLinkChecker, move Page stub and sort class imports 2017-11-28 09:35:31 +13:00
Robbie Averill 789ce91e1c
Merge pull request #27 from creative-commoners/pulls/2.0/update-for-four 2017-11-27 19:41:26 +13:00
Robbie Averill 4a8fb14465 Update branch alias for 2.x-dev 2017-11-27 19:30:16 +13:00
Dylan Wagstaff 4675a539d5 remove phockito from composer dev reqs 2017-11-27 18:02:26 +13:00
Dylan Wagstaff a26a6e14c5 Pull in new Russian translation from upstream/master
A language has been added since forking for the upgrade work, so:
Merge remote-tracking branch 'upstream/master' into pulls/2.0/update-for-four
2017-11-27 17:51:30 +13:00
Dylan Wagstaff 12df3947c2 Update readme in light of ss4 compatibility enhancements 2017-11-27 17:41:17 +13:00
Robbie Averill 9ea5067467 Merge branch '1.0' 2017-11-27 17:03:35 +13:00
Dylan Wagstaff bb7a1c3eda Update language files to contain new namespaced classnames 2017-11-27 16:42:36 +13:00
Dylan Wagstaff a52cb9209b Update UPDATE query to use true table name, not assumed one 2017-11-27 15:30:34 +13:00
Dylan Wagstaff 98dac9b314 Update unit tests to be rid of Phockito
This odd dependency is causing inclusion issues to do with namespacing and
the ability to work well with the framework (according to the dependency's
readme). The usage of the tool in this case adds no value, as it's
performing purely stub type work, rather than mock work. So instead we can
use an actual stub (simply return test values depending on input) and have
everything work well.

After removing the Phockito dependency it was found that the tests that
would skip when it isn't present were actually in a failing state. So they
had to be repaired too, mostly through the use of fixing the poor code
that was causing them to fail (as opposed to being bad tests). This is
both configuration and proceedural work.
2017-11-27 15:14:16 +13:00
Dylan Wagstaff ed7d91becb re-add transifex config as it was removed from the wrong branch. 2017-11-27 11:23:36 +13:00
Dylan Wagstaff 0374d66b32 Convert to new cache layer, clean up other overlooked points in the ss4 upgrade 2017-11-27 11:19:58 +13:00
Dylan Wagstaff 77eaa62efc update readme to reflect v4 upgrade work 2017-11-23 15:36:00 +13:00
Dylan Wagstaff 7bce7dcb2f Update tests & left over fixes from upgrader tool 2017-11-23 14:54:27 +13:00
Dylan Wagstaff a88e8be403 FIX Linting errors for code cleanliness 2017-11-23 13:19:00 +13:00
Dylan Wagstaff 1ed9a23a71 Run the upgrader & linting tools
An initial (untested) run at a proper upgrade. Progress commit.
2017-11-23 12:56:44 +13:00
Dylan Wagstaff e6ddf0fe47 Add namespaces via `upgrade-code add-namespace` tool
Running this command for each directory in the `src` folder we generate a
valid `.upgrade.yml` which can be used by other modules that may use this
one as a dependency to upgrade themselves without issue.
2017-11-22 14:26:15 +13:00
Dylan Wagstaff 4b59fdba02 Update supporting items for SilverStripe 4 conventions
Update versions and configurations for tests and code checkers, such as
phpunit and the SilverStripe CI tools. Altered the layout of the
repository to be more in line with other SilverStripe 4 modules (including
core ones).
2017-11-22 14:01:40 +13:00
Dylan Wagstaff 3c69b3fa77 begin SilverStripe v4 compatibility update 2017-11-22 11:46:40 +13:00
Damian Mooyman 3547869393 Merge pull request #24 from robbieaverill/pulls/fix-readme-badge
Update Travis badge in readme
2017-05-12 21:31:56 +12:00
Damian Mooyman ded3b7cd82 Merge pull request #23 from robbieaverill/pulls/translatable-conflict-in-test
FIX Ensure that translatable cannot break tests. Separate page stub class. Remove injector nesting.
2017-05-12 11:50:43 +12:00
Robbie Averill 76fd1169db Update Travis badge in readme 2017-05-12 11:41:14 +12:00
Robbie Averill 99d4a6ee1a FIX Ensure that translatable cannot break tests. Separate page stub class. Remove injector nesting. 2017-05-12 11:38:49 +12:00
62 changed files with 1802 additions and 1087 deletions

View File

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

1
.gitattributes vendored
View File

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

11
.github/workflows/ci.yml vendored Normal file
View 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
View 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
View 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

View File

@ -1,69 +0,0 @@
inherit: true
checks:
php:
verify_property_names: true
verify_argument_usable_as_reference: true
verify_access_scope_valid: true
useless_calls: true
use_statement_alias_conflict: true
variable_existence: true
unused_variables: true
unused_properties: true
unused_parameters: true
unused_methods: true
unreachable_code: true
too_many_arguments: true
sql_injection_vulnerabilities: true
simplify_boolean_return: true
side_effects_or_types: true
security_vulnerabilities: true
return_doc_comments: true
return_doc_comment_if_not_inferrable: true
require_scope_for_properties: true
require_scope_for_methods: true
require_php_tag_first: true
psr2_switch_declaration: true
psr2_class_declaration: true
property_assignments: true
prefer_while_loop_over_for_loop: true
precedence_mistakes: true
precedence_in_conditions: true
phpunit_assertions: true
php5_style_constructor: true
parse_doc_comments: true
parameter_non_unique: true
parameter_doc_comments: true
param_doc_comment_if_not_inferrable: true
optional_parameters_at_the_end: true
one_class_per_file: true
no_unnecessary_if: true
no_trailing_whitespace: true
no_property_on_interface: true
no_non_implemented_abstract_methods: true
no_error_suppression: true
no_duplicate_arguments: true
no_commented_out_code: true
newline_at_end_of_file: true
missing_arguments: true
method_calls_on_non_object: true
instanceof_class_exists: true
foreach_traversable: true
fix_line_ending: true
fix_doc_comments: true
duplication: true
deprecated_code_usage: true
deadlock_detection_in_loops: true
code_rating: true
closure_use_not_conflicting: true
catch_class_exists: true
blank_line_after_namespace_declaration: false
avoid_multiple_statements_on_same_line: true
avoid_duplicate_types: true
avoid_conflicting_incrementers: true
avoid_closing_tag: true
assignment_of_null_return: true
argument_type_checks: true
filter:
paths: [code/*, tests/*]

View File

@ -1,33 +0,0 @@
# See https://github.com/silverstripe/silverstripe-travis-support for setup details
sudo: false
language: php
php:
- 5.4
- 5.5
env:
- DB=MYSQL CORE_RELEASE=3.5
matrix:
include:
- php: 5.6
env: DB=MYSQL CORE_RELEASE=3
- php: 5.6
env: DB=MYSQL CORE_RELEASE=3.1
- php: 5.6
env: DB=PGSQL CORE_RELEASE=3.2
- php: 7.1
env: DB=MYSQL CORE_RELEASE=3.6
before_script:
- composer self-update || true
- git clone git://github.com/silverstripe/silverstripe-travis-support.git ~/travis-support
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
- cd ~/builds/ss
- composer install
script:
- vendor/bin/phpunit externallinks/tests

9
.tx/config Normal file
View 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
View 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

View File

@ -1,6 +1,7 @@
# 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
@ -12,25 +13,23 @@ The external links module is a task and ModelAdmin to track and to report on bro
## Requirements
* SilverStripe 3.1 +
* Silverstripe ^4.0
**Note:** For a Silverstripe 3.x compatible version, please use [the 1.x release line](https://github.com/silverstripe/silverstripe-externallinks/tree/1.0).
## Features
* Add external links to broken links reports
* Add a task to track external broken links
See the [changelog](CHANGELOG.md) for version history.
## Installation
1. If you have composer you can use `composer require silverstripe/externallinks:*`. Otherwise,
download the module from GitHub and extract to the 'externallinks' folder. Place this directory
in your sites root directory. This is the one with framework and cms in it.
2. Run in your browser - `/dev/build` to rebuild the database.
1. Require the module via composer: `composer require silverstripe/externallinks`
2. Run `/dev/build` in your browser to rebuild the database.
3. Run the following task *http://path.to.silverstripe/dev/tasks/CheckExternalLinks* to check for
broken external links
## Report ##
## Report
A new report is added called 'External Broken links report'. When viewing this report, a user may press
the "Create new report" button which will trigger an ajax request to initiate a report run.
@ -44,7 +43,7 @@ In this initial ajax request this module will do one of two things, depending on
In either case, the background task will loop over every page in the system, inspecting all external urls and
checking the status code returned by requesting each one. If a URL returns a response code that is considered
"broken" (defined as < 200 or > 302) then the `ss-broken` css class will be assigned to that url, and
"broken" (defined as < 200 or > 302) then the `ss-broken` css class will be assigned to that url, and
a line item will be added to the report. If a previously broken link has been corrected or fixed, then
this class is removed.
@ -56,32 +55,36 @@ with the status. The user may leave this page and return to it later to view the
Any subsequent report may not be generated until a prior report has completed.
## Dev task ##
## Dev task
Run the following task *http://path.to.silverstripe/dev/tasks/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.
## Queued job ##
## Queued job
If you have the queuedjobs module installed you can set the task to be run every so ofter
Add the following yml config to config.yml in mysite/_config have the the task run once every day (86400 seconds)
If you have the queuedjobs module installed you can set the task to be run every so often.
CheckExternalLinks:
Delay: 86400
## Whitelisting codes
## 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
file in mysite/_config
```yml
SilverStripe\ExternalLinks\Tasks\CheckExternalLinksTask:
ignore_codes:
- 401
- 403
- 501
```
CheckExternalLinks:
Delay: 60
IgnoreCodes:
- 401
- 403
- 501
## Upgrading from 1.x to 2.x
## Follow 301 redirects ##
When upgrading from 1.x to 2.x (Silverstripe 3.x to 4.x) you will need to be aware of the following API changes:
* Configuration property `CheckExternalLinksTask.IgnoreCodes` renamed to `CheckExternalLinksTask.ignore_codes`
* Configuration property `CheckExternalLinksTask.FollowLocation` and `BypassCache` renamed to `follow_location` and `bypass_cache`
## Follow 301 redirects
You may want to follow a redirected URL a example of this would be redirecting from http to https
can give you a false poitive as the http code of 301 will be returned which will be classed
@ -89,15 +92,35 @@ as a working link.
To allow redirects to be followed setup the following config in your config.yml
# Follow 301 redirects
CurlLinkChecker:
FollowLocation: 1
```yaml
# Follow 301 redirects
SilverStripe\ExternalLinks\Tasks\CurlLinkChecker:
follow_location: 1
```
## Bypass cache ##
## Bypass cache
By default the task will attempt to cache any results the cache can be bypassed with the
following config in config.yml.
# Bypass SS_Cache
CurlLinkChecker:
BypassCache: 1
```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'
...
```

View File

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

5
_config/legacy.yml Normal file
View 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

View File

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

29
behat.yml Normal file
View 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

View File

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

View 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));

View File

@ -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();
}
}
}

View File

@ -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';
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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');
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -1,75 +0,0 @@
<?php
/**
* Check links using curl
*/
class CurlLinkChecker implements LinkChecker {
/**
* If we want to follow redirects a 301 http code for example
* Set via YAML file
*
* @config
* @var boolean
*/
private static $FollowLocation = false;
/**
* If we want to bypass the cache
* Set via YAML file
*
* @config
* @var boolean
*/
private static $BypassCache = false;
/**
* 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;
if (!Config::inst()->get('CurlLinkChecker', 'BypassCache')) {
// 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);
// do we want to follow any redirect locations eg http to https
if (Config::inst()->get('CurlLinkChecker', 'FollowLocation')) {
curl_setopt($handle, CURLOPT_FOLLOWLOCATION, 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);
if (!Config::inst()->get('CurlLinkChecker', 'BypassCache')) {
// Cache result
$this->getCache()->save($httpCode, $cacheKey);
}
return $httpCode;
}
}

View File

@ -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
View File

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

View File

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

View File

@ -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));

View File

@ -1,8 +1,14 @@
de:
BrokenExternalLink:
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'
ExternalBrokenLinksReport:
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
EXTERNALBROKENLINKS: 'Defekte externe Links Report'
RUNREPORT: 'Generiere neuen Bericht'

View File

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

View File

@ -1,17 +1,38 @@
eo:
BrokenExternalLink:
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'
BrokenExternalPageTrack:
db_HTTPCode: HTTP-kodo
db_Link: Ligilo
has_one_Status: Stato
has_one_Track: Spuri
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
PLURALNAME: 'Rompitaj eksteraj paĝaj trakoj'
PLURALS:
one: 'Unu rompita ekstera paĝa trako'
other: '{count} rompitaj eksteraj paĝaj trakoj'
SINGULARNAME: 'Rompita ekstera paĝa trako'
BrokenExternalPageTrackStatus:
db_Processed: Traktita
has_many_BrokenLinks: 'Rompitaj ligiloj'
has_one_Page: Paĝo
has_one_Status: Stato
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
PLURALNAME: 'Stato de rompitaj eksteraj paĝaj trakoj'
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'
CheckExternalLiksJob:
TITLE: 'Kontrolas por eksteraj rompitaj ligiloj'
ExternalBrokenLinksReport:
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
View 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
View 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
View 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
View 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'

View File

@ -1,17 +1,17 @@
pl:
BrokenExternalLink:
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'
BrokenExternalPageTrack:
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrack:
PLURALNAME: 'Wykrywania wadliwych stron zewnętrznych'
SINGULARNAME: 'Wykrywanie wadliwych stron zewnętrznych'
BrokenExternalPageTrackStatus:
SilverStripe\ExternalLinks\Model\BrokenExternalPageTrackStatus:
PLURALNAME: 'Statusy wykrywania wadliwych stron zewnętrznych'
SINGULARNAME: 'Status wykrywania wadliwych stron zewnętrznych'
CheckExternalLiksJob:
TITLE: 'Wyszukiwanie uszkodzonych linków zewnętrznych'
ExternalBrokenLinksReport:
SilverStripe\ExternalLinks\Reports\BrokenExternalLinksReport:
EXTERNALBROKENLINKS: 'Raport uszkodzonych linków zewnętrznych'
RUNREPORT: 'Stwórz nowy raport'

View File

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

44
lang/sk.yml Normal file
View 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
View 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'

View File

@ -1,4 +1,4 @@
Copyright (c) 2016, SilverStripe Limited
Copyright (c) 2017, SilverStripe Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

13
phpcs.xml.dist Normal file
View 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
View 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>

View 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();
}
}
}

View 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';
}
}

View 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);
}
}

View 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);
}
}

View 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');
}
}
}

View 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;
}
}

View 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();
}
}
}

View 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
View 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);
}

View File

@ -1,157 +0,0 @@
<?php
class ExternalLinksTest extends SapphireTest {
protected static $fixture_file = 'ExternalLinksTest.yml';
protected $extraDataObjects = array(
'ExternalLinksTest_Page'
);
public function setUpOnce() {
if (class_exists('Phockito')) {
Phockito::include_hamcrest(false);
}
parent::setUpOnce();
}
public function setUp() {
parent::setUp();
Injector::nest();
// Check dependencies
if (!class_exists('Phockito')) {
$this->skipTest = true;
return $this->markTestSkipped("These tests need the Phockito module installed to run");
}
// Mock link checker
$checker = Phockito::mock('LinkChecker');
Phockito::when($checker)
->checkLink('http://www.working.com')
->return(200);
Phockito::when($checker)
->checkLink('http://www.broken.com/url/thing') // 404 on working site
->return(404);
Phockito::when($checker)
->checkLink('http://www.broken.com') // 403 on working site
->return(403);
Phockito::when($checker)
->checkLink('http://www.nodomain.com') // no ping
->return(0);
Phockito::when($checker)
->checkLink('/internal/link')
->return(null);
Phockito::when($checker)
->checkLink('[sitetree_link,id=9999]')
->return(null);
Phockito::when($checker)
->checkLink('home')
->return(null);
Phockito::when($checker)
->checkLink('broken-internal')
->return(null);
Phockito::when($checker)
->checkLink('[sitetree_link,id=1]')
->return(null);
Phockito::when($checker)
->checkLink(Hamcrest_Matchers::anything()) // anything else is 404
->return(404);
Injector::inst()->registerService($checker, 'LinkChecker');
}
public function tearDown() {
Injector::unnest();
parent::tearDown();
}
public function testLinks() {
// Run link checker
$task = CheckExternalLinksTask::create();
$task->setSilent(true); // Be quiet during the test!
$task->runLinksCheck();
// Get all links checked
$status = BrokenExternalPageTrackStatus::get_latest();
$this->assertEquals('Completed', $status->Status);
$this->assertEquals(5, $status->TotalPages);
$this->assertEquals(5, $status->CompletedPages);
// Check all pages have had the correct HTML adjusted
for($i = 1; $i <= 5; $i++) {
$page = $this->objFromFixture('ExternalLinksTest_Page', 'page'.$i);
$this->assertNotEmpty($page->Content);
$this->assertEquals(
$page->ExpectedContent,
$page->Content,
"Assert that the content of page{$i} has been updated"
);
}
// Check that the correct report of broken links is generated
$links = $status
->BrokenLinks()
->sort('Link');
$this->assertEquals(4, $links->count());
$this->assertEquals(
array(
'http://www.broken.com',
'http://www.broken.com/url/thing',
'http://www.broken.com/url/thing',
'http://www.nodomain.com'
),
array_values($links->map('ID', 'Link')->toArray())
);
// Check response codes are correct
$expected = array(
'http://www.broken.com' => 403,
'http://www.broken.com/url/thing' => 404,
'http://www.nodomain.com' => 0
);
$actual = $links->map('Link', 'HTTPCode')->toArray();
$this->assertEquals($expected, $actual);
// Check response descriptions are correct
i18n::set_locale('en_NZ');
$expected = array(
'http://www.broken.com' => '403 (Forbidden)',
'http://www.broken.com/url/thing' => '404 (Not Found)',
'http://www.nodomain.com' => '0 (Server Not Available)'
);
$actual = $links->map('Link', 'HTTPCodeDescription')->toArray();
$this->assertEquals($expected, $actual);
}
/**
* Test that broken links appears in the reports list
*/
public function testReportExists() {
$reports = SS_Report::get_reports();
$reportNames = array();
foreach($reports as $report) {
$reportNames[] = $report->class;
}
$this->assertContains('BrokenExternalLinksReport',$reportNames,
'BrokenExternalLinksReport is in reports list');
}
}
class ExternalLinksTest_Page extends Page implements TestOnly {
private static $db = array(
'ExpectedContent' => 'HTMLText'
);
}

View File

View 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 &quot; and the hyperlink will be wrong
# When the page is published it should be converted by tinymce to double quotes
Given a "page" "My page" has the "Content" "<p>My <a href='http://fsdjoifidsohfiohfsoifhiodshfhdosi.com'>link</a> content</p>"
Scenario: Operate the external links report
Given I am logged in with "ADMIN" permissions
# Publish page
When I go to "/admin/pages"
And I follow "My page"
And I press the "Publish" button
# Run report
When I go to "/admin/reports"
And I follow "External broken links"
And I press the "Create new report" button
# Run queuedjob, new job will be the first row
When I go to "/admin/queuedjobs"
When I click on the ".gridfield-button-jobexecute" element
And I wait for 15 seconds
# Assert report
When I go to "/admin/reports"
And I follow "External broken links"
Then I should see "http://fsdjoifidsohfiohfsoifhiodshfhdosi.com"
And I should see "My page"

View File

View File

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

View File

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

View 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());
}
}

View File

@ -1,4 +1,4 @@
ExternalLinksTest_Page:
SilverStripe\ExternalLinks\Tests\Stubs\ExternalLinksTestPage:
# Tests mix of broken and working external links
page1:
Title: 'Page 1'
@ -18,7 +18,7 @@ ExternalLinksTest_Page:
Content: >
<p><a href="http://www.broken.com/url/thing" class="ss-broken">Still Broken</a></p>
ExpectedContent: >
<p><a href="http://www.broken.com/url/thing" class="ss-broken">Still Broken</a></p>
<p><a href="http://www.broken.com/url/thing" class="ss-broken">Still Broken</a></p>
# Tests internal broken links not marking a page as broken
page3:
Title: 'Page 3'

View 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());
}
}
}

View 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'
);
}

View 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;
}
}
}