Compare commits

...

179 Commits
1.0.1-rc1 ... 2

Author SHA1 Message Date
Steve Boyd
55d18d8392 Merge branch '2.4' into 2 2024-07-11 16:17:47 +12:00
Guy Sartorelli
0b94f4b8a9
Merge pull request #115 from creative-commoners/pulls/2.4/composer
MNT Add dev version of framework
2024-07-11 10:00:07 +12:00
Steve Boyd
a2b6f45abd MNT Add dev version of framework 2024-07-10 12:45:43 +12:00
github-actions
b12eee33ed Merge branch '2.4' into 2 2024-03-03 22:44:42 +00:00
Guy Sartorelli
0a17d0c537
Merge pull request #101 from creative-commoners/pulls/2.4/exists
FIX Ensure page has extension before calling method
2024-03-04 11:43:49 +13:00
Steve Boyd
66da923502 FIX Ensure page has extension before calling method 2024-02-29 14:05:44 +13:00
github-actions
3c922398ca Merge branch '2.4' into 2 2024-02-08 11:14:51 +00:00
Guy Sartorelli
82872780eb
TLN Update translations (#99) 2024-02-07 16:14:36 +13:00
github-actions
900aae6a25 Merge branch '2.4' into 2 2023-08-24 11:15:01 +00:00
Guy Sartorelli
d082606ded
ENH Update translations (#88) 2023-08-21 13:18:55 +12:00
Steve Boyd
4129c23c0f Merge branch '2.4' into 2 2023-06-16 12:18:08 +12:00
Guy Sartorelli
84f1ed3914
Merge pull request #86 from creative-commoners/pulls/2.4/tx-1686724968
ENH Update translations
2023-06-15 10:06:18 +12:00
Steve Boyd
b11db96d7a ENH Update translations 2023-06-14 18:42:48 +12:00
Guy Sartorelli
1f015f119c
Merge branch '2.4' into 2 2023-04-26 12:47:28 +12:00
Guy Sartorelli
ebae2446a9
MNT Revert erroneous dependency changes (#83) 2023-03-28 17:12:13 +13:00
Maxime Rainville
633b85537e
Merge pull request #82 from creative-commoners/pulls/2/dispatch-ci
MNT Use gha-dispatch-ci
2023-03-23 12:04:15 +13:00
Steve Boyd
df5845176f MNT Use gha-dispatch-ci 2023-03-21 12:05:01 +13:00
Guy Sartorelli
4afd8ce074
MNT Update development dependencies 2023-03-10 16:35:17 +13:00
Guy Sartorelli
1e78dfc828
MNT Update release dependencies 2023-03-10 16:35:14 +13:00
Guy Sartorelli
b56ae94e87
MNT Update development dependencies 2023-03-10 12:21:30 +13:00
Guy Sartorelli
1ec4a141e3
Merge pull request #81 from creative-commoners/pulls/2/tx-1678080252
ENH Update translations
2023-03-08 10:32:29 +13:00
Steve Boyd
d44e506d00 ENH Update translations 2023-03-06 18:24:12 +13:00
Sabina Talipova
48220dad3a
Merge pull request #76 from creative-commoners/pulls/2/stop-using-depr
API Stop using deprecated API
2022-12-05 16:37:21 +13:00
Steve Boyd
6699b9074a API Stop using deprecated API 2022-11-24 13:10:46 +13:00
Guy Sartorelli
8041c15dc7
Merge pull request #75 from creative-commoners/pulls/2/depr-messages
API Update deprecations
2022-11-21 10:00:28 +13:00
Steve Boyd
9f6b6f9d8a API Update deprecations 2022-11-16 12:00:17 +13:00
Maxime Rainville
831c178514
Merge pull request #74 from creative-commoners/pulls/2/fix-userdoc-deploy
MNT Fix github action for deploying userdocs
2022-08-24 11:13:45 +12:00
Guy Sartorelli
2e1dc4a46e
MNT Fix github action for deploying userdocs 2022-08-23 14:42:19 +12:00
Guy Sartorelli
c5fdbcbd72
Merge pull request #73 from creative-commoners/pulls/2/userhelp-fix
DOC Correct title for userhelp
2022-08-22 11:00:33 +12:00
Maxime Rainville
2ba0438277 DOC Correct title for userhelp 2022-08-20 21:57:04 +12:00
Steve Boyd
656094763b Merge branch '2.3' into 2 2022-08-02 19:03:51 +12:00
Steve Boyd
d18a07213e Merge branch '2.2' into 2.3 2022-08-02 19:03:46 +12:00
Guy Sartorelli
e8da1e07bf
Merge pull request #72 from creative-commoners/pulls/2.2/standardise-modules
MNT Standardise modules
2022-08-02 16:01:03 +12:00
Steve Boyd
abb248242f MNT Standardise modules 2022-08-01 16:23:48 +12:00
Steve Boyd
75237f89db Merge branch '2.3' into 2 2022-07-25 11:49:37 +12:00
Steve Boyd
befc7f07d3 Merge branch '2.2' into 2.3 2022-07-25 11:49:33 +12:00
Guy Sartorelli
c5f4a5ccdb
Merge pull request #71 from creative-commoners/pulls/2.2/module-standards
MNT Use GitHub Actions CI
2022-07-15 12:00:16 +12:00
Steve Boyd
9433c0542a MNT Use GitHub Actions CI 2022-07-05 22:18:11 +12:00
Guy Sartorelli
f93f32d22b
Merge pull request #70 from creative-commoners/pulls/2/php81
ENH PHP 8.1 compatibility
2022-04-26 17:57:34 +12:00
Steve Boyd
600a4b2cde ENH PHP 8.1 compatibility 2022-04-13 13:53:29 +12:00
Maxime Rainville
7195e2a833
Merge pull request #69 from creative-commoners/pulls/2/php74
DEP Set PHP 7.4 as the minimum version
2022-02-18 22:10:56 +13:00
Steve Boyd
8171761d39 DEP Set PHP 7.4 as the minimum version 2022-02-10 17:45:12 +13:00
Steve Boyd
933c58128b Merge branch '2.1' into 2 2021-11-18 17:51:00 +13:00
Maxime Rainville
feb6218035
Merge pull request #67 from creative-commoners/pulls/2/sapphire-test-nine
API phpunit 9 support
2021-11-01 22:46:24 +13:00
Steve Boyd
2671406a86 API phpunit 9 support 2021-10-27 18:17:31 +13:00
Maxime Rainville
52f303d497 Update translations 2021-10-05 14:25:05 +13:00
Steve Boyd
baee472c9f Merge branch '2.1' into 2 2021-09-04 18:09:12 +12:00
Maxime Rainville
25ff52c1fc MNT Remove obsolete branch-alias 2021-08-27 12:06:28 +12:00
Maxime Rainville
b4ce6caead Update translations 2021-08-27 11:27:35 +12:00
Steve Boyd
42ea538f0f
Update build status badge 2021-01-21 16:45:17 +13:00
Steve Boyd
a7251dba0f Merge branch '2.0' into 2 2020-11-11 17:56:18 +13:00
Serge Latyntsev
c4ba8bf3a9
Merge pull request #66 from creative-commoners/pulls/2.0/shared-config
FIX Quote yml, use shared travis config, sminnee/phpunit
2020-11-10 16:18:37 +13:00
Steve Boyd
5cc58a1269 FIX Quote yml, use shared travis config, sminnee/phpunit 2020-11-09 17:14:48 +13:00
Robbie Averill
747cf4d68d
Merge pull request #65 from creative-commoners/pulls/2.0/travis
Update travis 2.0
2020-06-23 09:40:16 -07:00
Steve Boyd
f473126c9f Update travis 2020-06-23 16:41:21 +12:00
Garion Herman
952c7c3592
Merge pull request #64 from creative-commoners/pulls/2/merge-up
Merge up from 2.0
2020-06-12 14:58:46 +12:00
Maxime Rainville
1f832bcdc4 Merge branch '2.0' into 2 2020-06-12 14:47:54 +12:00
Serge Latyntsev
4d499524cd
Merge pull request #63 from creative-commoners/pulls/2.0/update-travis
Update travis matrix
2020-02-18 12:06:08 +13:00
Steve Boyd
ece9566bcb Update travis matrix 2020-02-18 11:59:52 +13:00
Aaron Carlino
8de3ec1dfc
META: Add github action to build docs 2019-12-19 13:55:11 +13:00
Garion Herman
5aef1bace1
Merge pull request #61 from creative-commoners/pulls/2/travis-ci
Travis config update
2019-11-26 14:46:42 +13:00
Serge Latyntcev
a56946b68b Travis config update 2019-11-26 14:38:45 +13:00
Robbie Averill
08539df0af Merge branch '2.0' 2019-05-10 10:16:35 +12:00
Robbie Averill
aae09ae51f Update translations 2019-05-10 10:16:23 +12:00
Robbie Averill
9260d8c744 Bump postgres version in Travis configuration to 2.1.x 2018-11-08 10:45:59 +02:00
Robbie Averill
831f03a2b8
Bump Postgres for Travis builds on SilverStripe 4.2+ 2018-08-17 23:45:25 +12:00
Robbie Averill
247039ff0e Merge branch '2.0' 2018-07-26 15:19:44 +12:00
Dylan Wagstaff
2986d7e0c3
Merge pull request #57 from creative-commoners/pulls/2.0/fix-cache-mode
FIX Separate tests, ensure versioned cache mode does not interfere
2018-06-19 10:51:28 +12:00
Robbie Averill
0be030efd1 Add various recipe versions to Travis matrix 2018-06-18 22:56:10 +12:00
Robbie Averill
bd59ce6200 FIX Separate tests, ensure versioned cache mode does not interfere 2018-06-18 22:54:33 +12:00
Robbie Averill
0356e1e0a2
Merge pull request #56 from creative-commoners/pulls/master/add-supported-module-badge
Add supported module badge to readme
2018-06-18 10:09:09 +12:00
Dylan Wagstaff
9065f82d79 Add supported module badge to readme 2018-06-15 17:52:36 +12:00
Robbie Averill
3b1c682a54 Merge branch '2.0' 2018-06-11 15:38:24 +12:00
Robbie Averill
d48fab6faa Remove obsolete branch alias 2018-06-11 15:38:06 +12:00
Dylan Wagstaff
5b38663322
Merge pull request #52 from silverstripe/docs/pulls/2.0/update-docs
Update docs to match rename of checkbox in CMS
2018-04-05 09:57:03 +12:00
Raissa North
205d077384 Update docs to match rename of checkbox in CMS 2018-04-05 09:47:06 +12:00
Dylan Wagstaff
3a6fe66e2f
Merge pull request #53 from silverstripe/pulls/2.0/update-developer-docs
DOCS Both version feeds are disabled by default
2018-03-27 21:01:21 +13:00
Raissa North
0c199bcae0
DOCS Both version feeds are disabled by default 2018-03-27 15:40:48 +13:00
Dylan Wagstaff
7b3d282802
Merge pull request #51 from creative-commoners/pulls/2.0/Content-with-Contents
FIX allow allchanges to handle removed Page types
2018-02-14 10:10:20 +13:00
Dylan Wagstaff
093af10c8a FIX allow allchanges to handle removed Page types
If a there exists at some point in the history a class that no longer
exists, Versioned will create it as a `DataObject` as opposed to some form
of the `SiteTree` superclass. This would break the absolute link
funcitonality, so we should make the version at least an instance of
SiteTree so we can generate the link accurately (without fatal errors).
2018-02-13 12:11:27 +13:00
Robbie Averill
29ef2a4920 FIX Update namespaces in Russian translation file 2017-12-18 18:09:39 +13:00
Robbie Averill
5c6b5f5e2c Merge branch '1' 2017-12-18 18:08:27 +13:00
Robbie Averill
f59ddb5578 Merge branch '1.2' into 1 2017-12-18 18:06:59 +13:00
Robbie Averill
ea21f4557f Exclude PHP 5.3 from Travis tests 2017-12-18 18:06:44 +13:00
Robbie Averill
a9a346a53d Merge branch '1.2' into 1 2017-12-18 18:06:14 +13:00
Robbie Averill
989911acb2 Remove Transifex configuration. Commit directly to lang files for SS3. 2017-12-18 18:03:51 +13:00
Robbie Averill
de70e59d80
Merge pull request #49 from creative-commoners/pulls/2.0/update-tests
FIX Use cached SiteTree object for history instead of creating a temporary object
2017-12-18 18:02:42 +13:00
Robbie Averill
5bde86198b FIX Do not use cached SiteTree object but ensure record ID is set before diff 2017-12-18 16:38:16 +13:00
Robbie Averill
6b6f4ec622 FIX Add warnings as descriptions to settings tabs, not literal fields. Remove duplicated translation 2017-12-18 15:59:11 +13:00
Robbie Averill
952b67a5cb Update documentation to include correct file paths 2017-12-18 15:45:07 +13:00
Robbie Averill
dd361929db FIX Use cached SiteTree object for history instead of creating a temporary object 2017-12-18 15:45:05 +13:00
Robbie Averill
921b7c7fb5
Merge pull request #48 from creative-commoners/pulls/2.0/new-version-feed-v4
FIX: update docs & language references
2017-12-13 16:11:36 +13:00
Robbie Averill
bfba519cc3 FIX Update translation class names and replace sprintf translations with parameters 2017-12-13 15:36:20 +13:00
Dylan Wagstaff
5096179825 FIX: update docs & language references 2017-12-12 16:55:08 +13:00
Robbie Averill
d536df0d32
Merge pull request #45 from creative-commoners/pulls/2.0/new-version-feed-v3
FIX: PSR-2 codebase. Formatting via phpcbf
2017-12-12 16:43:58 +13:00
Dylan Wagstaff
67e112fd12 FIX: Minor functional alterations and CI improvements
FIX: PSR-2 codebase. Formatting via phpcbf
FIX: rendering bug in allchanges
FIX: update .gitattributes to not export codecov's config file
FIX: Update SiteTree_versions to the ss4 equivalent SiteTree_Versions
2017-12-12 16:12:03 +13:00
Robbie Averill
17cf3d7487
Merge pull request #42 from creative-commoners/pulls/2.0/new-version-feed-v2
Pulls/2.0/new version feed v2
2017-12-12 10:37:41 +13:00
Dylan Wagstaff
23f2f45705 FIX Capitalisation of trait usage 2017-12-12 10:31:47 +13:00
Dylan Wagstaff
fe2b6597b3 FIX update CI setting files 2017-12-12 09:43:12 +13:00
Dylan Wagstaff
229463547d Edit updated things until the tests pass 2017-12-11 17:20:00 +13:00
Dylan Wagstaff
17699f2b8a Update from SS_Cache to Symfony/Cache 2017-12-11 12:50:45 +13:00
Dylan Wagstaff
9a7651b017 run the upgrader tool over the codebase to namespace the classes 2017-12-11 12:10:56 +13:00
Robbie Averill
f4c7a4d737
Merge pull request #41 from creative-commoners/pulls/2.0/new-version-feed
Make it installable with vendor folder usage
2017-12-11 11:54:57 +13:00
Dylan Wagstaff
bfb7422981 Make it installable with vendor folder usage 2017-12-11 10:52:01 +13:00
Franco Springveldt
3845294dfe Update translations 2017-08-28 16:35:54 +12:00
Robbie Averill
b25d7d728e Merge pull request #39 from silverstripe/sminnee-patch-1
FIX: Don't assume SS4 compatibility
2017-07-12 13:52:56 +12:00
Sam Minnée
24b2d06424 FIX: Don't assume SS4 compatibility
Major versions won't automatically work, and so I've amended the composer requirements
not to allow SS4.

This will also ensure that addons.silverstripe.org correctly reports which modules
work with SS4.
2017-07-12 13:36:11 +12:00
Daniel Hensby
6dffbdaee7 Merge pull request #38 from creative-commoners/pulls/travis-php7
Add PHP7 + SS3.6 build to Travis configuration
2017-06-15 16:59:17 +01:00
Robbie Averill
82600b37c5 Add PHP7 + SS3.6 build to Travis configuration 2017-06-15 11:46:40 +12:00
Damian Mooyman
fbb2baad15 Merge pull request #37 from silverstripe/robbieaverill-fix-readme-badge
Update Travis URL in readme badge
2017-05-15 22:29:31 +12:00
Robbie Averill
e7861750b1 Update branch alias for 1.3.x-dev 2017-05-12 14:32:03 +12:00
Robbie Averill
937eb88242 Merge remote-tracking branch 'origin/master' into 1.2 2017-05-12 14:29:35 +12:00
Damian Mooyman
bbc4457f97 Merge pull request #36 from robbieaverill/pulls/fix-feed-functional-test-config
FIX Ensure that version feeds are enabled by default in tests
2017-05-12 11:54:23 +12:00
Robbie Averill
b87cfaadf8 Update README.md 2017-05-12 10:23:51 +12:00
Robbie Averill
33cc3a13e8 FIX Ensure that version feeds are enabled by default in tests 2017-05-12 10:20:27 +12:00
Damian Mooyman
9a6c655f54 Remove obsolete branch-alias 2016-11-17 10:16:02 +13:00
Damian Mooyman
dc03856ca1 Update translations 2016-08-17 11:08:01 +12:00
Damian Mooyman
f53f353ba6 Add changelog for 1.2.2 release 2016-05-20 12:58:31 +12:00
Daniel Hensby
898c281ef5 Merge pull request #35 from chillu/pulls/VersionFeedFunctionalTest-sort-fix
Consistent sorting for getDiffList()
2016-05-20 01:13:03 +01:00
Ingo Schommer
c64e7ad2d7 Consistent sorting for getDiffList()
If changes were applied in the same second (e.g. through tests), the results will have an indeterminate order.
This lead to stability issues in VersionFeedFunctionalTest::testContainsChangesForPageOnly.
Sort by ID in addition to LastEdited.
2016-05-20 09:56:33 +12:00
Damian Mooyman
73a033f22d Updated changelog for 1.2.1 2016-02-04 18:13:10 +13:00
Damian Mooyman
843fb43979 Update translations 2016-02-04 18:11:13 +13:00
Damian Mooyman
c2e3f672dd Merge pull request #25 from helpfulrobot/add-standard-scrutinizer-config
Added standard Scrutinizer config
2016-02-04 18:10:49 +13:00
Damian Mooyman
7f7ecca151 Merge pull request #34 from helpfulrobot/update-license-year
Updated license year
2016-01-05 11:23:02 +13:00
helpfulrobot
a284681b39 Updated license year 2016-01-01 06:48:32 +13:00
Damian Mooyman
83947f03d2 Merge pull request #33 from mandrew/master
Moved user docs into userguide folder to display on userhelp site
2015-12-21 14:25:00 +13:00
Mike Andrewartha
0426a38412 updated link, moved user.md into userguide folder and renamed to index file plus updated test 2015-12-15 17:00:35 +13:00
Daniel Hensby
a71b15a437 Merge pull request #32 from helpfulrobot/add-standard-code-of-conduct
Added standard code of conduct
2015-11-21 12:29:17 +00:00
helpfulrobot
fc8ad92b29 Added standard code of conduct 2015-11-21 20:18:18 +13:00
helpfulrobot
6d96c59ebd Added standard Scrutinizer config 2015-11-21 19:33:52 +13:00
Daniel Hensby
06d70e4593 Merge pull request #27 from helpfulrobot/add-standard-travis-config
Added standard Travis config
2015-11-20 15:37:58 +00:00
Daniel Hensby
04cc2c3f07 Merge pull request #28 from helpfulrobot/add-standard-editor-config
Added standard editor config
2015-11-20 14:59:37 +00:00
Daniel Hensby
47de4e5112 Merge pull request #30 from helpfulrobot/add-standard-license
Added standard license
2015-11-19 12:04:19 +00:00
Daniel Hensby
6a748e0de1 Merge pull request #31 from helpfulrobot/add-standard-git-attributes
Added standard git attributes
2015-11-19 10:40:02 +00:00
helpfulrobot
e6f2a2f7a9 Added standard git attributes 2015-11-19 19:14:26 +13:00
helpfulrobot
015819e6e2 Added standard license 2015-11-19 18:33:06 +13:00
Damian Mooyman
84530d6678 Merge pull request #29 from scott1702/master
update changelog for release
2015-11-19 15:23:12 +13:00
scott1702
2cad57d442 update changelog for release 2015-11-19 15:13:41 +13:00
helpfulrobot
c297b7c229 Added standard Travis config 2015-11-19 14:21:24 +13:00
helpfulrobot
2913245cb5 Added standard editor config 2015-11-19 13:27:31 +13:00
Damian Mooyman
50661260c9 Merge pull request #22 from textagroup/textagroup-patch-1
Update alternate xml link tag to be W3 compliant
2015-11-16 09:27:01 +13:00
Michael Strong
7f4f7bb1b4 Merge pull request #24 from assertchris/add-scrutinizer-support
Added Scrutinizer support
2015-11-07 11:43:23 +13:00
Christopher Pitt
1fe90397b3 Added Scrutinizer support 2015-11-07 11:14:50 +13:00
Ingo Schommer
5b6dae480f Merge pull request #23 from tractorcow/pulls/tests
Tests for 3.2 and php 5.6
2015-11-03 08:37:53 +13:00
Damian Mooyman
4d278f1695 API Add cache_lifetime config
BUG Fix issue in test caching
Tests for 3.2 and php 5.6
2015-11-02 11:27:14 +13:00
textagroup
f0c92002b6 Update alternate xml link tag to be W3 compliant
The link tag being produced was not W3 compliant as it was using the nofollow value in the rel attribute on a link tag which is not permitted as per the W3 standards http://www.w3.org/TR/html5/links.html#linkTypes
2015-10-20 16:58:45 +13:00
Damian Mooyman
b6179c5eef Merge pull request #21 from silverstripe-labs/add-cwp-keyword
Added CWP keyword
2015-09-07 13:19:17 +12:00
Christopher Pitt
85c38311bc Added CWP keyword 2015-09-07 12:45:38 +12:00
Damian Mooyman
180a562b51 Merge pull request #20 from spekulatius/bug11-cache-tag-invalid
Checking if there is a version to avoid #11.
2015-08-20 17:25:37 +12:00
Peter Thaleikis
429b61036e Checking if there is a version to avoid #11. 2015-07-23 22:56:26 +12:00
Damian Mooyman
fc530d601b Merge pull request #19 from dhensby/patch-1
Move to new travis containerised infrastructure
2015-07-21 14:48:39 +12:00
Daniel Hensby
66dd4622e0 Move to new travis containerised infrastructure 2015-07-20 16:21:15 +01:00
Damian Mooyman
73951e2623 Update translations 2015-05-26 18:12:46 +12:00
Damian Mooyman
8de4e31fca Merge pull request #18 from mateusz/travis
Fix version dependencies, and fix what travis builds.
2015-03-30 14:42:38 +13:00
Mateusz Uzdowski
25c575fe16 Fix version dependencies, and fix what travis builds. 2015-03-30 14:36:03 +13:00
Damian Mooyman
d37785ff7b Merge pull request #17 from mateusz/optimise-and-limit
Optimise diff generation on small diferrences, and hard cap the list.
2015-03-30 12:57:10 +13:00
Mateusz Uzdowski
fd67399417 Optimise diff generation.
For 100 items, this change improves generation times on small diffs
(100s of bytes) 6-fold. The improvement deteriorates to about 4-fold on
diffs of 1kB.

This includes the 25% improvement from the removal of unnecessary
forTemplate calls - we pack the diffs into HTMLText and render them
later from a template anyway.
2015-03-30 11:29:33 +13:00
Mateusz Uzdowski
8a7d2ec9da NEW Refactor diff API and add managed limits.
Deprecate the getDiffedChanges method to get rid of confusing
fullHistory parameter. Add a getDiff method to fetch just a single diff
and a more versatile getDiffList method which allows specifying a limit.

We now also cap the amount of items coming from the diffing process
to a default value that we hope will result in generation times no
greater than a few seconds. This can be reconfigured by developers.
2015-03-30 11:28:42 +13:00
Damian Mooyman
2d6337dad9 Update translations 2014-11-19 14:23:02 +13:00
Damian Mooyman
797e5586c5 Fix testcase 2014-05-23 18:29:07 +12:00
Mateusz U
d718bf8758 Merge pull request #7 from tractorcow/pulls/disable-feed-config
API Ability to globally enable / disable RSS feeds
2014-05-22 11:43:01 +12:00
Damian Mooyman
094e3190df API Ability to globally enable / disable RSS feeds via config / siteconfig 2014-05-22 11:38:32 +12:00
Mateusz U
a3c4c52faf Merge pull request #10 from tractorcow/pulls/fix-tests-2
Disable caching in tests
2014-05-22 11:36:33 +12:00
Damian Mooyman
7f3adbf4d8 Disable caching in tests
Fixes #8
2014-05-22 09:53:49 +12:00
Damian Mooyman
d6fca1a5c4 Updated translations 2014-05-20 15:21:37 +12:00
Mateusz U
706ad85054 Merge pull request #5 from tractorcow/pulls/3.1-rate-limiting
API Better implementation of caching / rate limiting
2014-04-30 15:47:10 +12:00
Damian Mooyman
47991567b5 API Added lock cool down to rate limiting 2014-04-30 15:40:51 +12:00
Damian Mooyman
241f0604b0 Namespaced filters
Changed default settings (disable lock_bypage and set lock_timeout to 5)
Updated docs
2014-04-30 12:31:18 +12:00
Damian Mooyman
a243a3510a API Better implementation of caching / rate limiting 2014-04-29 16:51:23 +12:00
Sam Minnee
4946376793 FIX: Don't caused HTML tidying to make an unreliable test 2014-02-18 18:43:58 +13:00
Mateusz Uzdowski
55fe8eb050 Add new lang strings, convert to JS. 2014-01-24 14:37:02 +13:00
Ingo Schommer
27bd513938 Fixed static visibility 2013-10-31 10:38:49 +01:00
Ingo Schommer
01e1136311 Clear cache in tests (for repeat runs) 2013-10-31 10:21:46 +01:00
Ingo Schommer
aabb774d20 Merge pull request #4 from chillu/pulls/travis
Page specific cache key, unit and functional tests
2013-10-31 02:07:54 -07:00
Ingo Schommer
13bb2cf270 Travis support 2013-10-31 10:02:51 +01:00
Ingo Schommer
2d035a6e94 Page specific cache key, unit and functional tests
Added templates from CWP template to make the module
useful standalone outside of CWP.
2013-10-30 17:46:22 +01:00
Ingo Schommer
778ae7d42c Chinese/Arabic/Te Reo translations 2013-10-30 00:35:53 +01:00
Mateusz U
3ad4f401ee Merge pull request #3 from silverstripe-labs/pulls/transifex
Globalisation, transifex support
2013-10-29 13:47:19 -07:00
Ingo Schommer
8b4fdd31c3 Globalisation, transifex support 2013-10-24 23:33:20 +02:00
46 changed files with 1550 additions and 303 deletions

17
.editorconfig Normal file
View File

@ -0,0 +1,17 @@
# For more information about the properties used in this file,
# please see the EditorConfig documentation:
# http://editorconfig.org
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[{*.yml,package.json}]
indent_size = 2
# The indent size used in the package.json file cannot be changed:
# https://github.com/npm/npm/pull/3180#issuecomment-16336516

7
.gitattributes vendored Normal file
View File

@ -0,0 +1,7 @@
/tests export-ignore
/docs export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.travis.yml export-ignore
/.scrutinizer.yml export-ignore
/codecov.yml export-ignore

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

View File

@ -0,0 +1,16 @@
name: Deploy Userhelp Docs
on:
push:
branches:
- '3'
- '2'
- 'master'
paths:
- 'docs/en/userguide/**'
jobs:
deploy:
name: deploy-userhelp-docs
runs-on: ubuntu-latest
steps:
- name: Run build hook
run: curl -X POST -d {} https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_BUILD_HOOK }}

16
.github/workflows/dispatch-ci.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: Dispatch CI
on:
# At 11:10 AM UTC, only on Sunday and Monday
schedule:
- cron: '10 11 * * 0,1'
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 8th of every month at 11:50am UTC
schedule:
- cron: '50 11 8 * *'
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

9
.tx/config Normal file
View File

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

12
.upgrade.yml Normal file
View File

@ -0,0 +1,12 @@
mappings:
VersionFeed: SilverStripe\VersionFeed\VersionFeed
VersionFeed_Controller: SilverStripe\VersionFeed\VersionFeedController
VersionFeedSiteConfig: SilverStripe\VersionFeed\VersionFeedSiteConfig
CachedContentFilter: SilverStripe\VersionFeed\Filters\CachedContentFilter
ContentFilter: SilverStripe\VersionFeed\Filters\ContentFilter
RateLimitFilter: SilverStripe\VersionFeed\Filters\RateLimitFilter
\VersionFeed\Filters\CachedContentFilter: SilverStripe\VersionFeed\Filters\CachedContentFilter
\VersionFeed\Filters\ContentFilter: SilverStripe\VersionFeed\Filters\ContentFilter
\VersionFeed\Filters\RateLimitFilter: SilverStripe\VersionFeed\Filters\RateLimitFilter
VersionFeedFunctionalTest: SilverStripe\VersionFeed\Tests\VersionFeedFunctionalTest
VersionFeedTest: SilverStripe\VersionFeed\Tests\VersionFeedTest

17
LICENSE
View File

@ -1,17 +0,0 @@
Copyright (c) 2012-2013, SilverStripe Limited - www.silverstripe.com
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of SilverStripe nor the names of its contributors may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGE.

View File

@ -1,23 +1,28 @@
# Version Feed
[![CI](https://github.com/silverstripe/silverstripe-versionfeed/actions/workflows/ci.yml/badge.svg)](https://github.com/silverstripe/silverstripe-versionfeed/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/)
## Overview
The module creates an RSS feed on each page with their change history, as well as one for the entire site.
## Requirements
* SilverStripe 3.0+
* Silverstripe ^4
**Note:** For a Silverstripe 3.x compatible version, please use [the 1.x release line](https://github.com/silverstripe/silverstripe-versionfeed/tree/1.2).
## Installation
Install with composer by running:
composer require silverstripe/versionfeed:*
in the root of your SilverStripe project.
Or just clone/download the git repository into a subfolder (usually called "versionfeed") of your SilverStripe project.
Install with composer by running `composer require silverstripe/versionfeed` in the root of your Silverstripe project.
## Usage
For usage instructions see [user manual](docs/en/user.md).
For usage instructions see [user manual](docs/en/userguide/index.md).
## Translations
Translations of the natural language strings are managed through a third party translation interface, transifex.com. Newly added strings will be periodically uploaded there for translation, and any new translations will be merged back to the project source code.
Please use [https://www.transifex.com/projects/p/silverstripe-versionfeed](https://www.transifex.com/projects/p/silverstripe-versionfeed) to contribute translations, rather than sending pull requests with YAML files.

View File

@ -1,7 +0,0 @@
<?php
SiteTree::add_extension('VersionFeed');
ContentController::add_extension('VersionFeed_Controller');
// Set the cache lifetime to 5 mins.
SS_Cache::set_cache_lifetime('VersionFeed_Controller', 5*60);

25
_config/versionfeed.yml Normal file
View File

@ -0,0 +1,25 @@
---
Name: versionedfeedconfig
---
SilverStripe\Core\Injector\Injector:
RateLimitFilter: SilverStripe\VersionFeed\Filters\RateLimitFilter
ContentFilter:
class: SilverStripe\VersionFeed\Filters\CachedContentFilter
constructor:
- '%$RateLimitFilter'
Psr\SimpleCache\CacheInterface.VersionFeedController:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: 'VersionFeedController'
SilverStripe\CMS\Model\SiteTree:
extensions:
- SilverStripe\VersionFeed\VersionFeed
SilverStripe\SiteConfig\SiteConfig:
extensions:
- SilverStripe\VersionFeed\VersionFeedSiteConfig
SilverStripe\CMS\Controllers\ContentController:
extensions:
- SilverStripe\VersionFeed\VersionFeedController
SilverStripe\VersionFeed\VersionFeedController:
dependencies:
ContentFilter: '%$ContentFilter'

1
code-of-conduct.md Normal file
View File

@ -0,0 +1 @@
When having discussions about this module in issues or pull request please adhere to the [SilverStripe Community Code of Conduct](https://docs.silverstripe.org/en/contributing/code_of_conduct).

View File

@ -1,114 +0,0 @@
<?php
class VersionFeed extends SiteTreeExtension {
static $db = array(
'PublicHistory' => 'Boolean'
);
static $defaults = array(
'PublicHistory' => true
);
/**
* Compile a list of changes to the current page, excluding non-published and explicitly secured versions.
*
* @param int $highestVersion Top version number to consider.
* @param boolean $fullHistory Whether to get the full change history or just the previous version.
*
* @returns ArrayList List of cleaned records.
*/
public function getDiffedChanges($highestVersion = null, $fullHistory = true) {
// This can leak secured content if it was protected via inherited setting.
// For now the users will need to be aware about this shortcoming.
$offset = $highestVersion ? "AND \"SiteTree_versions\".\"Version\"<='".(int)$highestVersion."'" : '';
$limit = $fullHistory ? null : 2;
$versions = $this->owner->allVersions("\"WasPublished\"='1' AND \"CanViewType\" IN ('Anyone', 'Inherit') $offset", "\"LastEdited\" DESC", $limit);
// Process the list to add the comparisons.
$changeList = new ArrayList();
$previous = null;
$count = 0;
foreach ($versions as $version) {
$changed = false;
if (isset($previous)) {
// We have something to compare with.
$diff = $this->owner->compareVersions($version->Version, $previous->Version);
// Produce the diff fields for use in the template.
if ($version->Title != $previous->Title) {
$version->DiffTitle = new HTMLText();
$version->DiffTitle->setValue(
sprintf(
'<div><em>%s</em>' . $diff->Title . '</div>',
_t('RSSHistory.TITLECHANGED', 'Title has changed:')
)
);
$changed = true;
}
if ($version->Content != $previous->Content) {
$version->DiffContent = new HTMLText();
$version->DiffContent->setValue('<div>'.$diff->obj('Content')->forTemplate().'</div>');
$changed = true;
}
// Copy the link so it can be cached by SS_Cache.
$version->GeneratedLink = $version->AbsoluteLink();
}
// Omit the versions that haven't been visibly changed (only takes the above fields into consideration).
if ($changed) {
$changeList->push($version);
$count++;
}
// Store the last version for comparison.
$previous = $version;
}
// Push the first version on to the list - only if we're looking at the full history or if it's the first
// version in the version history.
if ($previous && ($fullHistory || $versions->count() == 1)) {
$first = clone($previous);
$first->DiffContent = new HTMLText();
$first->DiffContent->setValue('<div>' . $first->obj('Content')->forTemplate() . '</div>');
$changeList->push($first);
}
return $changeList;
}
public function updateSettingsFields(FieldList $fields) {
// Add public history field.
$fields->addFieldToTab('Root.Settings', $publicHistory = new FieldGroup(
new CheckboxField('PublicHistory', $this->owner->fieldLabel(_t(
'RSSHistory.LABEL',
'Make history public'))
)));
$publicHistory->setTitle($this->owner->fieldLabel('Public history'));
$warning =
"Publicising the history will also disclose the changes that have at the time been protected " .
"from the public view.";
$fields->addFieldToTab('Root.Settings', new LiteralField('PublicHistoryWarning', $warning), 'PublicHistory');
if ($this->owner->CanViewType!='Anyone') {
$warning =
"Changing access settings in such a way that this page or pages under it become publicly<br>" .
"accessible may result in publicising all historical changes on these pages too. Please review<br>" .
"this section's \"Public history\" settings to ascertain only intended information is disclosed.";
$fields->addFieldToTab('Root.Settings', new LiteralField('PublicHistoryWarning2', $warning), 'CanViewType');
}
}
public function getSiteRSSLink() {
// TODO: This link should be from the homepage, not this page.
return $this->owner->Link('allchanges');
}
public function getDefaultRSSLink() {
if ($this->owner->PublicHistory) return $this->owner->Link('changes');
}
}

View File

@ -1,109 +0,0 @@
<?php
class VersionFeed_Controller extends Extension {
static $allowed_actions = array(
'changes',
'allchanges'
);
function onAfterInit() {
// RSS feed for per-page changes.
if ($this->owner->PublicHistory) {
RSSFeed::linkToFeed($this->owner->Link() . 'changes',
sprintf(
_t('RSSHistory.SINGLEPAGEFEEDTITLE', 'Updates to %s page'),
$this->owner->Title
)
);
}
$this->linkToAllSiteRSSFeed();
return $this;
}
/**
* Get page-specific changes in a RSS feed.
*/
function changes() {
if(!$this->owner->PublicHistory) throw new SS_HTTPResponse_Exception('Page history not viewable', 404);;
// Cache the diffs to remove DOS possibility.
$cache = SS_Cache::factory('VersionFeed_Controller');
$cache->setOption('automatic_serialization', true);
$key = 'changes' . $this->owner->Version;
$entries = $cache->load($key);
if(!$entries || isset($_GET['flush'])) {
$entries = $this->owner->getDiffedChanges();
$cache->save($entries, $key);
}
// Generate the output.
$title = sprintf(_t('RSSHistory.SINGLEPAGEFEEDTITLE', 'Updates to %s page'), $this->owner->Title);
$rss = new RSSFeed($entries, $this->owner->request->getURL(), $title, '', 'Title', '', null);
$rss->setTemplate('Page_changes_rss');
return $rss->outputToBrowser();
}
/**
* Get all changes from the site in a RSS feed.
*/
function allchanges() {
$latestChanges = DB::query('SELECT * FROM "SiteTree_versions" WHERE "WasPublished"=\'1\' AND "CanViewType" IN (\'Anyone\', \'Inherit\') AND "ShowInSearch"=1 AND ("PublicHistory" IS NULL OR "PublicHistory" = \'1\') ORDER BY "LastEdited" DESC LIMIT 20');
$lastChange = $latestChanges->record();
$latestChanges->rewind();
if ($lastChange) {
// Cache the diffs to remove DOS possibility.
$member = Member::currentUser();
$cache = SS_Cache::factory('VersionFeed_Controller');
$cache->setOption('automatic_serialization', true);
$key = 'allchanges' . preg_replace('#[^a-zA-Z0-9_]#', '', $lastChange['LastEdited']) .
($member ? $member->ID : 'public');
$changeList = $cache->load($key);
if(!$changeList || isset($_GET['flush'])) {
$changeList = new ArrayList();
foreach ($latestChanges as $record) {
// Check if the page should be visible.
// WARNING: although we are providing historical details, we check the current configuration.
$page = SiteTree::get()->filter(array('ID'=>$record['RecordID']))->First();
if (!$page->canView(new Member())) continue;
// Get the diff to the previous version.
$version = new Versioned_Version($record);
$changes = $version->getDiffedChanges($version->Version, false);
if ($changes && $changes->Count()) $changeList->push($changes->First());
}
$cache->save($changeList, $key);
}
}
// Produce output
$rss = new RSSFeed($changeList, $this->owner->request->getURL(), $this->linkToAllSitesRSSFeedTitle(), '', 'Title', '', null);
$rss->setTemplate('Page_allchanges_rss');
return $rss->outputToBrowser();
}
function linkToAllSiteRSSFeed() {
// RSS feed to all-site changes.
$title = Convert::raw2xml($this->linkToAllSitesRSSFeedTitle());
$url = $this->owner->getSiteRSSLink();
Requirements::insertHeadTags(
'<link rel="alternate nofollow" type="application/rss+xml" title="' . $title .
'" href="' . $url . '" />');
}
function linkToAllSitesRSSFeedTitle() {
return sprintf(_t('RSSHistory.SITEFEEDTITLE', 'Updates to %s'), SiteConfig::current_site_config()->Title);
}
}

1
codecov.yml Normal file
View File

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

View File

@ -1,8 +1,13 @@
{
"name": "silverstripe/versionfeed",
"description": "Adds RSS feeds of content changes to SilverStripe",
"type": "silverstripe-module",
"keywords": ["silverstripe", "rss", "feed"],
"type": "silverstripe-vendormodule",
"keywords": [
"silverstripe",
"rss",
"feed",
"cwp"
],
"license": "BSD-3-Clause",
"authors": [
{
@ -10,9 +15,23 @@
"email": "robert@silverstripe.com"
}
],
"require":
{
"silverstripe/framework": "3.*",
"silverstripe/cms": "3.*"
"require": {
"php": "^7.4 || ^8.0",
"silverstripe/cms": "^4",
"silverstripe/versioned": "^1",
"silverstripe/siteconfig": "^4"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"silverstripe/framework": "^4.10",
"squizlabs/php_codesniffer": "^3.0"
},
"autoload": {
"psr-4": {
"SilverStripe\\VersionFeed\\": "src/",
"SilverStripe\\VersionFeed\\Tests\\": "tests/"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@ -7,17 +7,66 @@
Creating functions called `changes` or `allchanges` on any of your page types or controllers will cause confusion with
the extensions defined on the extension.
### Enabling / Disabling
By default the `allchanges` and `changes` feed are disabled.
The `allchanges` feed can be enabled by setting the `SilverStripe\VersionFeed\VersionFeed.allchanges_enabled` config to true. If this is true, then the allchanges feed can still be disabled by unchecking the "All page changes" checkbox in the "Settings" section in the CMS.
Likewise, the `changes` feed for each page can be globally enabled by setting the `SilverStripe\VersionFeed\VersionFeed.changes_enabled`
config to true. If this is true, then each page can still be individually disabled by unchecking the
'Make history public' checkbox in the CMS under page settings.
See [user documentation on enabling / disabling](userguide/index.md#enabling--disabling).
### Default RSS action
Templates can offer a "Subscribe" link with a link to the most relevant RSS feed. This will default to the changes feed
for the current page. You can override this behaviour by defining the `getDefaultRSSLink` function in your page type
and returning the URL of your desired RSS feed:
:::php
class MyPage extends Page {
function getDefaultRSSLink() {
```php
class MyPage extends Page
{
public function getDefaultRSSLink()
{
return $this->Link('myrssfeed');
}
}
```
This can be used in templates as `$DefaultRSSLink`.
### Rate limiting and caching
By default all content is filtered based on the rules specified in `vendor/silverstripe/versionfeed/_config/versionfeed.yml`.
Two filters are applied on top of one another:
* `SilverStripe\VersionFeed\Filters\CachedContentFilter` provides caching of versions based on an identifier built up of the record ID and the
most recently saved version number. There is no configuration required for this class.
* `SilverStripe\VersionFeed\Filters\RateLimitFilter` provides rate limiting to ensure that requests to uncached data does not overload the
server. This filter will only be applied if the `SilverStripe\VersionFeed\Filters\CachedContentFilter` does not have any cached record
for a request.
Either one of these can be replaced, added to, or removed, by adjusting the `SilverStripe\VersionFeed\VersionFeedController.dependencies`
config to point to a replacement (or no) filter.
For smaller servers where it's reasonable to apply a strict approach to rate limiting the default
settings should be sufficient. The `SilverStripe\VersionFeed\Filters\RateLimitFilter.lock_bypage` config defaults to false, meaning that a
single limit will be applied to all URLs. If set to true, then each URL will have its own rate limit,
and on smaller servers with lots of concurrent requests this can still overwhelm capacity. This will
also leave smaller servers vulnerable to DDoS attacks which target many URLs simultaneously.
This config will have no effect on the `allchanges` method.
`SilverStripe\VersionFeed\Filters\RateLimitFilter.lock_byuserip` can be set to true in order to prevent requests from different users
interfering with one another. However, this can provide an ineffective safeguard against malicious DDoS attacks
which use multiple IP addresses.
Another important variable is the `SilverStripe\VersionFeed\Filters\RateLimitFilter.lock_timeout` config, which is set to 5 seconds by default.
This should be increased on sites which may be slow to generate page versions, whether due to lower
server capacity or volume of content (number of page versions). Requests to this page after the timeout
will not trigger any rate limit safeguard, so you should be sure that this is set to an appropriate level.
You can set the `SilverStripe\VersionFeed\Filters\ContentFilter.cache_lifetime` config in order to control the maximum age of the cache.
This is an integer value in seconds, and defaults to 300 (five minutes). Set it to 0 or null to make this
cache unlimited.

View File

@ -1,26 +0,0 @@
# Version Feed
## Usage
### Accessing RSS feeds
There are two feeds that are automatically created for each page:
- Page changes: This feed will display all published versions of the page, highlighting any additions or deletions
with underscores or strikethroughs. It is accessible with the `changes` action - so `http://mysite.com/mypage/changes`
- Site changes: This will aggregate all the per-page change feeds into one feed and display the most recent 20. It is
accessible from any page with the `allchanges` action - so `http://mysite.com/home/allchanges`
### Enabling / disabling
You can enable or disable the feed on a per-page basis by checking or unchecking the *Public History* checkbox in the
Settings tab of each page. If a page has the Public History option, unchecked, it will not appear in the allchanges
feed.
#### Privacy
A page's history will be completely visible when it has public history enabled, even if some updates were made when it
was restricted to only being viewed by authenticated users. So if a page has ever had confidential data on it, it is
best to not enable this feature unless the data has entered the public domain.
There is a warning explaining this fact next to the *Public History* checkbox.

View File

@ -0,0 +1,34 @@
---
title: Content change RSS
summary: Adds page or site wide RSS feeds that display content changes
---
# Content change RSS
## In this section:
* Accessing RSS feeds
* Enabling and disabling via the CMS
## Before we begin
Make sure that your SilverStripe installation has the [versionfeed](http://addons.silverstripe.org/add-ons/silverstripe/versionfeed) module installed.
## Accessing RSS feeds
There are two feeds that are automatically created for each page:
* Page changes: This feed will display all published versions of the page, highlighting any additions or deletions with underscores or strikethroughs respectively. It is accessible with the `changes` action - so `http://mysite.com/mypage/changes`
* Site changes: This will aggregate all the per-page change feeds into one feed and display the most recent 20. It is accessible from any page with the `allchanges` action - so `http://mysite.com/home/allchanges`
## Enabling / disabling
You can enable or disable the feed on a per-page basis by checking or unchecking the *Make history public* checkbox (if available) in the Settings tab of each page. If a page has the Make history public option unchecked, it will not appear in the allchanges feed.
The allchanges feed can also be disabled by unchecking the "All page changes" checkbox in the "Settings" section in the cms.
### Privacy
A page's history will be completely visible when it has public history enabled, even if some updates were made when it was restricted to only being viewed by authenticated users. So if a page has ever had confidential data on it, it is best to not enable this feature unless the data has entered the public domain.
There is a warning explaining this fact next to the *Make history public* checkbox.

0
lang/_manifest_exclude Normal file
View File

8
lang/ar.yml Normal file
View File

@ -0,0 +1,8 @@
ar:
SilverStripe\VersionFeed\VersionFeed:
LABEL: 'اجعل التاريخ متاح للمشاهدة من قبل الجميع'
SINGLEPAGEFEEDTITLE: 'التحديثات و صلت ل s% من الصفحة'
SITEFEEDTITLE: 'التحديثات إلى s%'
TITLECHANGED: 'تم تغيير العنوان:'
Warning: 'إن نشر التاريخ سوف يظهر التغييرات التى تملكها و التى كنت تمنعها من العرض على العامة.'
Warning2: 'إن تغيير إعدادات الدخول بهذه الطريقة كى تكون هذه الصفحة أو الصفحات التى تحتها تصبح معلنة<br>قد يؤدي الوصول إليها في نشر جميع التغييرات التاريخية على هذه الصفحات أيضا. من فضلك قم بمراجعة<br>"التاريخ العام" لهذا القطاع لكى يتم التأكد من أن المعلومات المقصودة فقط هى التى يتم الإفصاح عنها.'

13
lang/en.yml Normal file
View File

@ -0,0 +1,13 @@
en:
SilverStripe\VersionFeed\VersionFeed:
LABEL: 'Make history public'
SINGLEPAGEFEEDTITLE: 'Updates to {title} page'
SITEFEEDTITLE: 'Updates to {title}'
TITLECHANGED: 'Title has changed:'
Warning: 'Publicising the history will also disclose the changes that have at the time been protected from the public view.'
Warning2: 'Changing access settings in such a way that this page or pages under it become publicly<br>accessible may result in publicising all historical changes on these pages too. Please review<br> this section''s "Public history" settings to ascertain only intended information is disclosed.'
db_PublicHistory: 'Public history'
SilverStripe\VersionFeed\VersionFeedSiteConfig:
ALLCHANGES: 'All page changes'
ALLCHANGESLABEL: 'Make global changes feed public'
db_AllChangesEnabled: 'All changes enabled'

13
lang/eo.yml Normal file
View File

@ -0,0 +1,13 @@
eo:
SilverStripe\VersionFeed\VersionFeed:
LABEL: 'Publikigu historion'
SINGLEPAGEFEEDTITLE: 'Ĝisdatigoj al paĝo {title}'
SITEFEEDTITLE: 'Ĝisdatigoj al {title}'
TITLECHANGED: 'Titolo estas ŝanĝita:'
Warning: 'Publikigi la historion ankaŭ malkaŝos la ŝanĝojn ĝis tiam protektitajn kontraŭ publika vido.'
Warning2: 'Ŝanĝi la alirajn agordojn tiel ke ĉi tiu paĝo, aŭ paĝoj sub ĝi, fariĝas publike alireblaj <br>eble rezultigos ke publikiĝos ĉiuj historiaj ŝanĝoj en tiuj paĝoj. Bonvole rekonsideru <br> la sekcion "Publika historio" de ĉi tiu sekcio, por certigi ke nur intencita informo publikiĝu.'
db_PublicHistory: 'Publika historio'
SilverStripe\VersionFeed\VersionFeedSiteConfig:
ALLCHANGES: 'Ĉiuj paĝaj ŝanĝoj'
ALLCHANGESLABEL: 'Ĉieaj ŝanĝoj fluu en publikan'
db_AllChangesEnabled: 'Ĉiuj ŝanĝoj enŝaltitaj'

11
lang/fi_FI.yml Normal file
View File

@ -0,0 +1,11 @@
fi_FI:
SilverStripe\VersionFeed\VersionFeed:
LABEL: 'Tee historiasta julkinen'
SINGLEPAGEFEEDTITLE: 'Päivityksiä {title} sivuun'
SITEFEEDTITLE: 'Päivityksiä: {title}'
TITLECHANGED: 'Otsikko on muuttunut:'
Warning: 'Historian julkaisu paljastaa myös muutokset, jotka ovat suojattu julkiselta tarkastelulta.'
Warning2: 'Muutettaessa tämä tai sen alasivut julkisiksi,<br>voi toimenpide aiheuttaa myös kaiken muutoshistorian muuttumisen julkiseksi kyseisillä sivuilla. Ole hyvä<br>ja tarkista "Julkinen historia"-asetuksista, että vain haluttu tieto on julkaistuna.'
SilverStripe\VersionFeed\VersionFeedSiteConfig:
ALLCHANGES: 'Kaikki sivumuokkaukset'
ALLCHANGESLABEL: 'Tee muutoksista julkisia'

11
lang/hr.yml Normal file
View File

@ -0,0 +1,11 @@
hr:
SilverStripe\VersionFeed\VersionFeed:
LABEL: 'Učini povijest dostupnu svima'
SINGLEPAGEFEEDTITLE: 'Ažuriranja za {title} stranicu'
SITEFEEDTITLE: 'Ažuriranja za {title}'
TITLECHANGED: 'Naziv se promijenio:'
Warning: 'Objavom povijesti će se također otkriti promjene koje su u to vrijeme bili zaštićeni od očiju javnosti.'
Warning2: 'Promjenom postavki pristupi na takav način će učiniti da ova stranica ili stranice ispod nje postanu javno<br>dostupne mogu rezultirati objavomm svih povijesnih promjena na tim stranicama također. Molimo pregledajte<br>podatke sekcije "Public history" da budete sigurni samo željene informacije da su prikazane.'
SilverStripe\VersionFeed\VersionFeedSiteConfig:
ALLCHANGES: 'Sve promjene stranice'
ALLCHANGESLABEL: 'Učini sveobuhvatne promjene feeda'

3
lang/id.yml Normal file
View File

@ -0,0 +1,3 @@
id:
SilverStripe\VersionFeed\VersionFeed:
TITLECHANGED: 'Judul telah diubah:'

11
lang/it.yml Normal file
View File

@ -0,0 +1,11 @@
it:
SilverStripe\VersionFeed\VersionFeed:
LABEL: 'Rendere pubblica la cronologia'
SINGLEPAGEFEEDTITLE: 'Aggiornare alla pagina {title}'
SITEFEEDTITLE: 'Aggiornare a {title}'
TITLECHANGED: 'Il titolo è cambiato:'
Warning: 'Pubblicare la cronologia divulgherà anche i cambiamenti che sono stati precedentemente protetti dalla vista pubblica.'
Warning2: 'Cambiare la modalità di accesso in modo che la pagina o le pagine sottostanti diventino pubbliche<br>può comportare la pubblicazione della cronologia dei cambiamenti di queste pagine. Si prega di rivedere<br>le impostazioni della sezione "Cronologia pubblica" per assicurarsi che siano divulgate solo le informazioni desiderate.'
SilverStripe\VersionFeed\VersionFeedSiteConfig:
ALLCHANGES: 'Tutti i cambiamenti pagina'
ALLCHANGESLABEL: 'Rendere pubblico il feed dei cambiamenti globali'

8
lang/mi.yml Normal file
View File

@ -0,0 +1,8 @@
mi:
SilverStripe\VersionFeed\VersionFeed:
LABEL: 'Meinga kia tūmatanui te hītori'
SINGLEPAGEFEEDTITLE: 'Ngā whakahou ki te whārangi {title}'
SITEFEEDTITLE: 'Ngā whakahou ki te {title}'
TITLECHANGED: 'Kua hurihia te taitara:'
Warning: 'Mā te whakarite kia tūmatanui te hītori ka whakaaturia hoki ngā huringa o mua tērā i hunaia i te tirohanga tūmatanui.'
Warning2: 'Mā te huri i ngā tautuhinga uru kia noho wātea <br>tūmatanui ai tēnei whārangi, ngā whārangi rānei i raro i taua whārangi, tērā pea ko te mutunga iho ko te wātea tūmatanui o ngā huringa hītori katoa i aua whārangi. Me arotake<br>ngā tautuhinga "Hītori tūmatanui" o tēnei wāhanga kia mōhio ai ka whakaaturia anake ngā mōhiohio ka hiahiatia.'

11
lang/ru.yml Normal file
View File

@ -0,0 +1,11 @@
ru:
SilverStripe\VersionFeed\VersionFeed:
LABEL: 'Сделать историю общедоступной'
SINGLEPAGEFEEDTITLE: 'Обновления для %s страниц'
SITEFEEDTITLE: 'Обновления для %s'
TITLECHANGED: 'Заголовок изменился:'
Warning: 'Делая историю общедоступной, вы также раскрываете все изменения, применённые в приватном режиме.'
Warning2: 'Такое изменение настроек страницы или страниц может привести к тому, что все изменения истории также станут общедоступными. Пожалуйста, проверьте раздел "Публичная история" данных настроек, чтобы удостовериться, что только необходимая информация будет раскрыта.'
SilverStripe\VersionFeed\VersionFeedSiteConfig:
ALLCHANGES: 'Все изменения страниц'
ALLCHANGESLABEL: 'Сделать канал глобальных изменений общедоступным'

13
lang/sk.yml Normal file
View File

@ -0,0 +1,13 @@
sk:
SilverStripe\VersionFeed\VersionFeed:
LABEL: 'Zverejniť históriu'
SINGLEPAGEFEEDTITLE: 'Aktualizácie stránky {title}'
SITEFEEDTITLE: 'Aktualizácie {title}'
TITLECHANGED: 'Názov sa zmenil:'
Warning: 'Zverejnením histórie sa zverejnia aj zmeny, ktoré boli v tom čase chránené pred zrakom verejnosti.'
Warning2: 'Zmena nastavení prístupu tak, aby sa táto stránka alebo stránky pod ňou stali verejne prístupné,<br>môže viesť k zverejneniu všetkých historických zmien aj na týchto stránkach.<br> Skontrolujte nastavenia v časti "Verejná história", aby ste sa uistili, že sú zverejnené iba zamýšľané informácie.'
db_PublicHistory: 'Verejná história'
SilverStripe\VersionFeed\VersionFeedSiteConfig:
ALLCHANGES: 'Všetky zmeny stránky'
ALLCHANGESLABEL: 'Zverejnite informačný kanál globálnych zmien'
db_AllChangesEnabled: 'Všetky zmeny povolené'

12
lang/sl.yml Normal file
View File

@ -0,0 +1,12 @@
sl:
SilverStripe\VersionFeed\VersionFeed:
LABEL: 'Javno objavi zgodovino'
SINGLEPAGEFEEDTITLE: 'Posodobitve strani ''{title}'' '
SITEFEEDTITLE: 'Posodobitve {title}'
TITLECHANGED: 'Spremenjen naslov:'
Warning: 'Objava zgodovine bo razkrila tudi spremembe, ki do sedaj niso bile javno objavljene.'
Warning2: 'Sprememba nastavitev dostopa tako, da so so ta stran ali njene podstrani dostopne javnosti, <br> lahko povzroči objavo tudi vseh sprememb na omenjenih straneh. Podrobno preverite <br> seznam sprememb in se prepričajte, da boste razkrili samo tiste informacije, ki jih želite.'
db_PublicHistory: 'Javna zgodovina'
SilverStripe\VersionFeed\VersionFeedSiteConfig:
ALLCHANGES: 'Vse spremembe'
ALLCHANGESLABEL: 'Javno objavi seznam s krovnimi spremembami'

8
lang/zh.yml Normal file
View File

@ -0,0 +1,8 @@
zh:
SilverStripe\VersionFeed\VersionFeed:
LABEL: 将历史记录公开
SINGLEPAGEFEEDTITLE: '更新至 {title} 页面'
SITEFEEDTITLE: '更新至 {title}'
TITLECHANGED: 标题已更改:
Warning: 发布历史记录还会在公开视图中显示受保护事件内进行的改动。
Warning2: 用这种方式更改访问设置会使得本页及下级页面变为公开的<br>可能还会使得这些页面的所有变动历史记录也变为公开的。请查阅<br>本节的“公开历史记录”设置,确保只将需要的信息披露出来。

12
license.md Normal file
View File

@ -0,0 +1,12 @@
Copyright (c) 2017, SilverStripe Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

12
phpcs.xml.dist Normal file
View File

@ -0,0 +1,12 @@
<?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" />
</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,40 @@
<?php
namespace SilverStripe\VersionFeed\Filters;
use SilverStripe\Core\Config\Config;
/**
* Caches results of a callback
*/
class CachedContentFilter extends ContentFilter
{
/**
* Enable caching
*
* @config
* @var boolean
*/
private static $cache_enabled = true;
public function getContent($key, $callback)
{
$cache = $this->getCache();
// Return cached value if available
$cacheEnabled = Config::inst()->get(get_class(), 'cache_enabled');
$result = (isset($_GET['flush']) || !$cacheEnabled)
? null
: $cache->get($key);
if ($result) {
return $result;
}
// Fallback to generate result
$result = parent::getContent($key, $callback);
$lifetime = Config::inst()->get(ContentFilter::class, 'cache_lifetime') ?: null;
$cache->set($key, $result, $lifetime);
return $result;
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace SilverStripe\VersionFeed\Filters;
use SilverStripe\VersionFeed\VersionFeedController;
use SilverStripe\Core\Config\Configurable;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Injector\Injector;
/**
* Conditionally executes a given callback, attempting to return the desired results
* of its execution.
*/
abstract class ContentFilter
{
use Configurable;
/**
* Nested content filter
*
* @var ContentFilter
*/
protected $nestedContentFilter;
/**
* Cache lifetime
*
* @config
* @var int
*/
private static $cache_lifetime = 300;
public function __construct($nestedContentFilter = null)
{
$this->nestedContentFilter = $nestedContentFilter;
}
/**
* Gets the cache to use
*
* @return CacheInterface
*/
protected function getCache()
{
return Injector::inst()->get(
CacheInterface::class . '.VersionFeedController'
);
}
/**
* Evaluates the result of the given callback
*
* @param string $key Unique key for this
* @param callable $callback Callback for evaluating the content
* @return mixed Result of $callback()
*/
public function getContent($key, $callback)
{
if ($this->nestedContentFilter) {
return $this->nestedContentFilter->getContent($key, $callback);
} else {
return call_user_func($callback);
}
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace SilverStripe\VersionFeed\Filters;
use SilverStripe\Core\Config\Config;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Versioned\Versioned;
/**
* Provides rate limiting of execution of a callback
*/
class RateLimitFilter extends ContentFilter
{
/**
* Time duration (in second) to allow for generation of cached results. Requests to
* pages that within this time period that do not hit the cache (and would otherwise trigger
* a version query) will be presented with a 429 (rate limit) HTTP error
*
* @config
* @var int
*/
private static $lock_timeout = 5;
/**
* Determine if the cache generation should be locked on a per-page basis. If true, concurrent page versions
* may be generated without rate interference.
*
* @config
* @var bool
*/
private static $lock_bypage = false;
/**
* Determine if rate limiting should be applied independently to each IP address. This method is not
* reliable, as most DDoS attacks use multiple IP addresses.
*
* @config
* @var bool
*/
private static $lock_byuserip = false;
/**
* Time duration (in sections) to deny further search requests after a successful search.
* Search requests within this time period while another query is in progress will be
* presented with a 429 (rate limit)
*
* @config
* @var int
*/
private static $lock_cooldown = 2;
/**
* Cache key prefix
*/
const CACHE_PREFIX = 'RateLimitBegin';
/**
* Determines the key to use for saving the current rate
*
* @param string $itemkey Input key
* @return string Result key
*/
protected function getCacheKey($itemkey)
{
$key = self::CACHE_PREFIX;
// Add global identifier
if ($this->config()->get('lock_bypage')) {
$key .= '_' . md5($itemkey ?? '');
}
// Add user-specific identifier
if ($this->config()->get('lock_byuserip') && Controller::has_curr()) {
$ip = Controller::curr()->getRequest()->getIP();
$key .= '_' . md5($ip ?? '');
}
return $key;
}
public function getContent($key, $callback)
{
// Bypass rate limiting if flushing, or timeout isn't set
$timeout = $this->config()->get('lock_timeout');
if (isset($_GET['flush']) || !$timeout) {
return parent::getContent($key, $callback);
}
// Generate result with rate limiting enabled
$limitKey = $this->getCacheKey($key);
$cache = $this->getCache();
if ($lockedUntil = $cache->get($limitKey)) {
if (time() < $lockedUntil) {
// Politely inform visitor of limit
$response = new HTTPResponse_Exception('Too Many Requests.', 429);
$response->getResponse()->addHeader('Retry-After', 1 + $lockedUntil - time());
throw $response;
}
}
$lifetime = Config::inst()->get(ContentFilter::class, 'cache_lifetime') ?: null;
// Apply rate limit
$cache->set($limitKey, time() + $timeout, $lifetime);
// Generate results
$result = parent::getContent($key, $callback);
// Reset rate limit with optional cooldown
if ($cooldown = $this->config()->get('lock_cooldown')) {
// Set cooldown on successful query execution
$cache->set($limitKey, time() + $cooldown, $lifetime);
} else {
// Without cooldown simply disable lock
$cache->delete($limitKey);
}
return $result;
}
}

241
src/VersionFeed.php Normal file
View File

@ -0,0 +1,241 @@
<?php
namespace SilverStripe\VersionFeed;
use SilverStripe\Dev\Deprecation;
use SilverStripe\CMS\Model\SiteTreeExtension;
use SilverStripe\Core\Config\Config;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldGroup;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\View\Parsers\Diff;
use SilverStripe\CMS\Model\SiteTree;
class VersionFeed extends SiteTreeExtension
{
private static $db = array(
'PublicHistory' => 'Boolean(true)'
);
private static $defaults = array(
'PublicHistory' => true
);
public function updateFieldLabels(&$labels)
{
$labels['PublicHistory'] = _t(__CLASS__ . '.LABEL', 'Make history public');
}
/**
* Enable the allchanges feed
*
* @config
* @var bool
*/
private static $allchanges_enabled = true;
/**
* Allchanges feed limit of items.
*
* @config
* @var int
*/
private static $allchanges_limit = 20;
/**
* Enables RSS feed for page-specific changes
*
* @config
* @var bool
*/
private static $changes_enabled = true;
/**
* Changes feed limit of items.
*
* @config
* @var int
*/
private static $changes_limit = 100;
/**
* Compile a list of changes to the current page, excluding non-published and explicitly secured versions.
*
* @param int $highestVersion Top version number to consider.
* @param int $limit Limit to the amount of items returned.
*
* @returns ArrayList List of cleaned records.
*/
public function getDiffList($highestVersion = null, $limit = 100)
{
// This can leak secured content if it was protected via inherited setting.
// For now the users will need to be aware about this shortcoming.
$offset = $highestVersion ? "AND \"SiteTree_Versions\".\"Version\"<='".(int)$highestVersion."'" : '';
// Get just enough elements for diffing. We need one more than desired to have something to compare to.
$qLimit = (int)$limit + 1;
$versions = $this->owner->Versions(
"\"WasPublished\"='1' AND \"CanViewType\" IN ('Anyone', 'Inherit') $offset",
"\"SiteTree\".\"LastEdited\" DESC, \"SiteTree\".\"ID\" DESC",
$qLimit
);
// Process the list to add the comparisons.
$changeList = new ArrayList();
$previous = null;
$count = 0;
foreach ($versions as $version) {
$changed = false;
// Check if we have something to compare with.
if (isset($previous)) {
// Produce the diff fields for use in the template.
if ($version->Title != $previous->Title) {
$diffTitle = Diff::compareHTML($version->Title, $previous->Title);
$version->DiffTitle = DBField::create_field('HTMLText', null);
$version->DiffTitle->setValue(
sprintf(
'<div><em>%s</em> ' . $diffTitle . '</div>',
_t(__CLASS__ . '.TITLECHANGED', 'Title has changed:')
)
);
$changed = true;
}
if ($version->Content != $previous->Content) {
$diffContent = Diff::compareHTML($version->Content, $previous->Content);
$version->DiffContent = DBField::create_field('HTMLText', null);
$version->DiffContent->setValue('<div>'.$diffContent.'</div>');
$changed = true;
}
// Copy the link so it can be cached.
$oldPage = $version->getField('object');
if (!$oldPage instanceof SiteTree) {
// We only need enough info to generate the link...
$oldPage = SiteTree::create([
'ID' => $oldPage->ID,
'URLSegment' => $oldPage->URLSegment,
'ParentID' => $oldPage->ParentID
]);
}
$version->GeneratedLink = $oldPage->AbsoluteLink();
}
// Omit the versions that haven't been visibly changed (only takes the above fields into consideration).
if ($changed) {
$changeList->push($version);
$count++;
}
// Store the last version for comparison.
$previous = $version;
}
// Make sure enough diff items have been generated to satisfy the $limit. If we ran out, add the final,
// non-diffed item (the initial version). This will also work for a single-diff request: if we are requesting
// a diff on the initial version we will just get that version, verbatim.
if ($previous && $versions->count()<$qLimit) {
$first = clone($previous);
$first->DiffContent = DBField::create_field('HTMLText', null);
$first->DiffContent->setValue('<div>' . $first->Content . '</div>');
// Copy the link so it can be cached.
$first->GeneratedLink = $first->AbsoluteLink();
$changeList->push($first);
}
return $changeList;
}
/**
* Return a single diff representing this version.
* Returns the initial version if there is nothing to compare to.
*
* @return DataObject|null Object with relevant fields diffed.
*/
public function getDiff()
{
$changes = $this->getDiffList($this->owner->Version, 1);
if ($changes && $changes->Count()) {
return $changes->First();
}
return null;
}
/**
* Compile a list of changes to the current page, excluding non-published and explicitly secured versions.
*
* @deprecated 2.0.0 Use VersionFeed::getDiffList() instead
*
* @param int $highestVersion Top version number to consider.
* @param boolean $fullHistory Set to true to get the full change history, set to false for a single diff.
* @param int $limit Limit to the amount of items returned.
*
* @returns ArrayList List of cleaned records.
*/
public function getDiffedChanges($highestVersion = null, $fullHistory = true, $limit = 100)
{
Deprecation::notice('2.0.0', 'Use VersionFeed::getDiffList() instead');
return $this->getDiffList(
$highestVersion,
$fullHistory ? $limit : 1
);
}
public function updateSettingsFields(FieldList $fields)
{
if (!$this->owner->config()->get('changes_enabled')) {
return;
}
// Add public history field.
$fields->addFieldToTab(
'Root.Settings',
$publicHistory = FieldGroup::create(
CheckboxField::create('PublicHistory', $this->owner->fieldLabel('PublicHistory'))
)
->setDescription(_t(
__CLASS__ . '.Warning',
"Publicising the history will also disclose the changes that have at the "
. "time been protected from the public view."
))
);
if ($this->owner->CanViewType != 'Anyone') {
$canViewType = $fields->fieldByName('Root.Settings.CanViewType');
if ($canViewType) {
$canViewType->setDescription(_t(
__CLASS__ . '.Warning2',
"Changing access settings in such a way that this page or pages under it become publicly<br>"
. "accessible may result in publicising all historical changes on these pages too. Please review"
. "<br> this section's \"Public history\" settings to ascertain only intended information is "
. "disclosed."
));
}
}
}
public function getSiteRSSLink()
{
// TODO: This link should be from the homepage, not this page.
if (Config::inst()->get(get_class(), 'allchanges_enabled')
&& SiteConfig::current_site_config()->AllChangesEnabled
) {
return $this->owner->Link('allchanges');
}
}
public function getDefaultRSSLink()
{
if (Config::inst()->get(get_class(), 'changes_enabled') && $this->owner->PublicHistory) {
return $this->owner->Link('changes');
}
}
}

View File

@ -0,0 +1,217 @@
<?php
namespace SilverStripe\VersionFeed;
use SilverStripe\Core\Config\Config;
use SilverStripe\Control\RSS\RSSFeed;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Security;
use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\ORM\DB;
use SilverStripe\Security\Member;
use SilverStripe\ORM\ArrayList;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Core\Convert;
use SilverStripe\View\Requirements;
use SilverStripe\Core\Extension;
use SilverStripe\VersionFeed\Filters\ContentFilter;
class VersionFeedController extends Extension
{
private static $allowed_actions = array(
'changes',
'allchanges'
);
/**
* Content handler
*
* @var ContentFilter
*/
protected $contentFilter;
/**
* Sets the content filter
*
* @param ContentFilter $contentFilter
*/
public function setContentFilter(ContentFilter $contentFilter)
{
$this->contentFilter = $contentFilter;
}
/**
* Evaluates the result of the given callback
*
* @param string $key Unique key for this
* @param callable $callback Callback for evaluating the content
* @return mixed Result of $callback()
*/
protected function filterContent($key, $callback)
{
if ($this->contentFilter) {
return $this->contentFilter->getContent($key, $callback);
} else {
return call_user_func($callback);
}
}
public function onAfterInit()
{
$this->linkToPageRSSFeed();
$this->linkToAllSiteRSSFeed();
}
/**
* Get page-specific changes in a RSS feed.
*/
public function changes()
{
// Check viewability of changes
if (!Config::inst()->get(VersionFeed::class, 'changes_enabled')
|| !$this->owner->PublicHistory
|| $this->owner->Version == ''
) {
return $this->owner->httpError(404, 'Page history not viewable');
}
// Cache the diffs to remove DOS possibility.
$target = $this->owner;
$key = implode('_', array('changes', $target->ID, $target->Version));
$entries = $this->filterContent($key, function () use ($target) {
return $target->getDiffList(null, Config::inst()->get(VersionFeed::class, 'changes_limit'));
});
// Generate the output.
$title = _t(
'SilverStripe\\VersionFeed\\VersionFeed.SINGLEPAGEFEEDTITLE',
'Updates to {title} page',
['title' => $this->owner->Title]
);
$rss = new RSSFeed($entries, $this->owner->request->getURL(), $title, '', 'Title', '', null);
$rss->setTemplate('Page_changes_rss');
return $rss->outputToBrowser();
}
/**
* Get all changes from the site in a RSS feed.
*/
public function allchanges()
{
// Check viewability of allchanges
if (!Config::inst()->get(VersionFeed::class, 'allchanges_enabled')
|| !SiteConfig::current_site_config()->AllChangesEnabled
) {
return $this->owner->httpError(404, 'Global history not viewable');
}
$limit = (int)Config::inst()->get(VersionFeed::class, 'allchanges_limit');
$latestChanges = DB::query('
SELECT * FROM "SiteTree_Versions"
WHERE "WasPublished" = \'1\'
AND "CanViewType" IN (\'Anyone\', \'Inherit\')
AND "ShowInSearch" = 1
AND ("PublicHistory" IS NULL OR "PublicHistory" = \'1\')
ORDER BY "LastEdited" DESC LIMIT ' . $limit);
$lastChange = $latestChanges->record();
$latestChanges->rewind();
if ($lastChange) {
// Cache the diffs to remove DOS possibility.
$key = 'allchanges'
. preg_replace('#[^a-zA-Z0-9_]#', '', $lastChange['LastEdited'] ?? '')
. (Security::getCurrentUser() ? Security::getCurrentUser()->ID : 'public');
$changeList = $this->filterContent($key, function () use ($latestChanges) {
$changeList = new ArrayList();
$canView = array();
foreach ($latestChanges as $record) {
// Check if the page should be visible.
// WARNING: although we are providing historical details, we check the current configuration.
$id = $record['RecordID'];
if (!isset($canView[$id])) {
$page = DataObject::get_by_id(SiteTree::class, $id);
$canView[$id] = $page && $page->canView(Security::getCurrentUser());
}
if (!$canView[$id]) {
continue;
}
// Get the diff to the previous version.
$record['ID'] = $record['RecordID'];
$version = SiteTree::create($record);
if ($diff = $version->getDiff()) {
$changeList->push($diff);
}
}
return $changeList;
});
} else {
$changeList = new ArrayList();
}
// Produce output
$url = $this->owner->getRequest()->getURL();
$rss = new RSSFeed(
$changeList,
$url,
$this->linkToAllSitesRSSFeedTitle(),
'',
'Title',
'',
null
);
$rss->setTemplate('Page_allchanges_rss');
return $rss->outputToBrowser();
}
/**
* Generates and embeds the RSS header link for the page-specific version rss feed
*/
public function linkToPageRSSFeed()
{
if (!Config::inst()->get(VersionFeed::class, 'changes_enabled') || !$this->owner->PublicHistory) {
return;
}
RSSFeed::linkToFeed(
$this->owner->Link('changes'),
_t(
'SilverStripe\\VersionFeed\\VersionFeed.SINGLEPAGEFEEDTITLE',
'Updates to {title} page',
['title' => $this->owner->Title]
)
);
}
/**
* Generates and embeds the RSS header link for the global version rss feed
*/
public function linkToAllSiteRSSFeed()
{
if (!Config::inst()->get(VersionFeed::class, 'allchanges_enabled')
|| !SiteConfig::current_site_config()->AllChangesEnabled
|| !method_exists($this->owner, 'getSiteRSSLink')
) {
return;
}
// RSS feed to all-site changes.
$title = Convert::raw2xml($this->linkToAllSitesRSSFeedTitle());
$url = $this->owner->getSiteRSSLink();
Requirements::insertHeadTags(
'<link rel="alternate" type="application/rss+xml" title="' . $title .
'" href="' . $url . '" />'
);
}
public function linkToAllSitesRSSFeedTitle()
{
return _t(
'SilverStripe\\VersionFeed\\VersionFeed.SITEFEEDTITLE',
'Updates to {title}',
['title' => SiteConfig::current_site_config()->Title]
);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace SilverStripe\VersionFeed;
use SilverStripe\Core\Config\Config;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldGroup;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\DataExtension;
/**
* Allows global configuration of all changes
*/
class VersionFeedSiteConfig extends DataExtension
{
private static $db = array(
'AllChangesEnabled' => 'Boolean(true)'
);
private static $defaults = array(
'AllChangesEnabled' => true
);
public function updateFieldLabels(&$labels)
{
$labels['AllChangesEnabled'] = _t(__CLASS__ . '.ALLCHANGESLABEL', 'Make global changes feed public');
}
public function updateCMSFields(FieldList $fields)
{
if (!Config::inst()->get(VersionFeed::class, 'allchanges_enabled')) {
return;
}
$fields->addFieldToTab(
'Root.Access',
FieldGroup::create(new CheckboxField('AllChangesEnabled', $this->owner->fieldLabel('AllChangesEnabled')))
->setTitle(_t(__CLASS__ . '.ALLCHANGES', 'All page changes'))
->setDescription(_t(
'SilverStripe\\VersionFeed\\VersionFeed.Warning',
"Publicising the history will also disclose the changes that have at the time been protected " .
"from the public view."
))
);
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>$Title</title>
<link>$Link</link>
<atom:link href="$Link" rel="self" type="application/rss+xml"></atom:link>
<% loop Entries %>
<item>
<title>$Title.XML</title>
<link>$GeneratedLink</link>
<description>
<% if DiffTitle %>
$DiffTitle.XML
<% end_if %>
<% if DiffContent %>
$DiffContent.AbsoluteLinks.XML
<% end_if %>
</description>
<pubDate>$LastEdited.Rfc822</pubDate>
<guid>$GeneratedLink</guid>
</item>
<% end_loop %>
</channel>
</rss>

View File

@ -0,0 +1,25 @@
<?xml version="1.0"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>$Title</title>
<link>$Link</link>
<atom:link href="$Link" rel="self" type="application/rss+xml"></atom:link>
<% loop Entries %>
<item>
<title>$Title.XML</title>
<link>$GeneratedLink</link>
<description>
<% if DiffTitle %>
$DiffTitle.XML
<% end_if %>
<% if DiffContent %>
$DiffContent.AbsoluteLinks.XML
<% end_if %>
</description>
<pubDate>$LastEdited.Rfc822</pubDate>
<guid>$GeneratedLink</guid>
</item>
<% end_loop %>
</channel>
</rss>

View File

@ -0,0 +1,274 @@
<?php
namespace SilverStripe\VersionFeed\Tests;
use Page;
use PageController;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Director;
use SilverStripe\Core\Cache\CacheFactory;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\Versioned\Versioned;
use SilverStripe\VersionFeed\Filters\CachedContentFilter;
use SilverStripe\VersionFeed\Filters\RateLimitFilter;
use SilverStripe\VersionFeed\VersionFeed;
use SilverStripe\VersionFeed\VersionFeedController;
class VersionFeedFunctionalTest extends FunctionalTest
{
protected $usesDatabase = true;
protected $baseURI = 'http://www.fakesite.test';
protected static $required_extensions = [
Page::class => [VersionFeed::class],
PageController::class => [VersionFeedController::class],
];
protected $userIP;
/**
* @var CacheInterface
*/
protected $cache;
protected function setUp(): void
{
Director::config()->set('alternate_base_url', $this->baseURI);
parent::setUp();
$this->cache = Injector::inst()->get(
CacheInterface::class . '.VersionFeedController'
);
$this->cache->clear();
$this->userIP = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
// Enable history by default
Config::modify()->set(VersionFeed::class, 'changes_enabled', true);
Config::modify()->set(VersionFeed::class, 'allchanges_enabled', true);
// Disable caching and locking by default
Config::modify()->set(CachedContentFilter::class, 'cache_enabled', false);
Config::modify()->set(RateLimitFilter::class, 'lock_timeout', 0);
Config::modify()->set(RateLimitFilter::class, 'lock_bypage', false);
Config::modify()->set(RateLimitFilter::class, 'lock_byuserip', false);
Config::modify()->set(RateLimitFilter::class, 'lock_cooldown', false);
// Ensure any version based caches read from the live cache
Versioned::set_reading_mode(Versioned::DEFAULT_MODE);
}
protected function tearDown(): void
{
Director::config()->set('alternate_base_url', null);
$_SERVER['REMOTE_ADDR'] = $this->userIP;
parent::tearDown();
}
public function testPublicHistoryPublicHistoryDisabled()
{
$page = $this->createPageWithChanges(['PublicHistory' => false]);
$response = $this->get($page->RelativeLink('changes'));
$this->assertEquals(
404,
$response->getStatusCode(),
'With Page\'s "PublicHistory" disabled, `changes` action response code should be 404'
);
$response = $this->get($page->RelativeLink('allchanges'));
$this->assertEquals(200, $response->getStatusCode());
$xml = simplexml_load_string($response->getBody() ?? '');
$this->assertFalse(
(bool)$xml->channel->item,
'With Page\'s "PublicHistory" disabled, `allchanges` action should not have an item in the channel'
);
}
public function testPublicHistoryPublicHistoryEnabled()
{
$page = $this->createPageWithChanges(['PublicHistory' => true]);
$response = $this->get($page->RelativeLink('changes'));
$this->assertEquals(200, $response->getStatusCode());
$xml = simplexml_load_string($response->getBody() ?? '');
$this->assertTrue(
(bool)$xml->channel->item,
'With Page\'s "PublicHistory" enabled, `changes` action should have an item in the channel'
);
$response = $this->get($page->RelativeLink('allchanges'));
$this->assertEquals(200, $response->getStatusCode());
$xml = simplexml_load_string($response->getBody() ?? '');
$this->assertTrue(
(bool)$xml->channel->item,
'With "PublicHistory" enabled, `allchanges` action should have an item in the channel'
);
}
public function testRateLimiting()
{
// Re-enable locking just for this test
Config::modify()->set(RateLimitFilter::class, 'lock_timeout', 20);
Config::modify()->set(CachedContentFilter::class, 'cache_enabled', true);
$page1 = $this->createPageWithChanges(['PublicHistory' => true, 'Title' => 'Page1']);
$page2 = $this->createPageWithChanges(['PublicHistory' => true, 'Title' => 'Page2']);
// Artifically set cache lock
$this->cache->set(RateLimitFilter::CACHE_PREFIX, time() + 10);
// Test normal hit
$response = $this->get($page1->RelativeLink('changes'));
$this->assertEquals(429, $response->getStatusCode());
$this->assertGreaterThan(0, $response->getHeader('Retry-After'));
$response = $this->get($page2->RelativeLink('changes'));
$this->assertEquals(429, $response->getStatusCode());
$this->assertGreaterThan(0, $response->getHeader('Retry-After'));
// Test page specific lock
Config::modify()->set(RateLimitFilter::class, 'lock_bypage', true);
$key = implode('_', [
'changes',
$page1->ID,
Versioned::get_versionnumber_by_stage(SiteTree::class, 'Live', $page1->ID, false)
]);
$key = RateLimitFilter::CACHE_PREFIX . '_' . md5($key ?? '');
$this->cache->set($key, time() + 10);
$response = $this->get($page1->RelativeLink('changes'));
$this->assertEquals(429, $response->getStatusCode());
$this->assertGreaterThan(0, $response->getHeader('Retry-After'));
$response = $this->get($page2->RelativeLink('changes'));
$this->assertEquals(200, $response->getStatusCode());
Config::modify()->set(RateLimitFilter::class, 'lock_bypage', false);
// Test rate limit hit by IP
Config::modify()->set(RateLimitFilter::class, 'lock_byuserip', true);
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$this->cache->set(RateLimitFilter::CACHE_PREFIX . '_' . md5('127.0.0.1'), time() + 10);
$response = $this->get($page1->RelativeLink('changes'));
$this->assertEquals(429, $response->getStatusCode());
$this->assertGreaterThan(0, $response->getHeader('Retry-After'));
// Test rate limit doesn't hit other IP
$_SERVER['REMOTE_ADDR'] = '127.0.0.20';
$this->cache->set(RateLimitFilter::CACHE_PREFIX . '_' . md5('127.0.0.1'), time() + 10);
$response = $this->get($page1->RelativeLink('changes'));
$this->assertEquals(200, $response->getStatusCode());
}
public function testChangesActionContainsChangesForCurrentPageOnly()
{
$page1 = $this->createPageWithChanges(['Title' => 'Page1']);
$page2 = $this->createPageWithChanges(['Title' => 'Page2']);
$response = $this->get($page1->RelativeLink('changes'));
$xml = simplexml_load_string($response->getBody() ?? '');
$titles = array_map(function ($item) {
return (string)$item->title;
}, $xml->xpath('//item') ?? []);
// TODO Unclear if this should contain the original version
$this->assertContains('Changed: Page1', $titles);
$this->assertNotContains('Changed: Page2', $titles);
$response = $this->get($page2->RelativeLink('changes'));
$xml = simplexml_load_string($response->getBody() ?? '');
$titles = array_map(function ($item) {
return (string)$item->title;
}, $xml->xpath('//item') ?? []);
// TODO Unclear if this should contain the original version
$this->assertNotContains('Changed: Page1', $titles);
$this->assertContains('Changed: Page2', $titles);
}
public function testAllChangesActionContainsAllChangesForAllPages()
{
$page1 = $this->createPageWithChanges(['Title' => 'Page1']);
$page2 = $this->createPageWithChanges(['Title' => 'Page2']);
$response = $this->get($page1->RelativeLink('allchanges'));
$xml = simplexml_load_string($response->getBody() ?? '');
$titles = array_map(function ($item) {
return str_replace('Changed: ', '', (string) $item->title);
}, $xml->xpath('//item') ?? []);
$this->assertContains('Page1', $titles);
$this->assertContains('Page2', $titles);
}
protected function createPageWithChanges($seed = null)
{
$page = new Page();
$seed = array_merge([
'Title' => 'My Title',
'Content' => 'My Content'
], $seed);
$page->update($seed);
$page->write();
$page->publishSingle();
$page->update([
'Title' => 'Changed: ' . $seed['Title'],
'Content' => 'Changed: ' . $seed['Content'],
]);
$page->write();
$page->publishSingle();
$page->update([
'Title' => 'Changed again: ' . $seed['Title'],
'Content' => 'Changed again: ' . $seed['Content'],
]);
$page->write();
$page->publishSingle();
$page->update([
'Title' => 'Unpublished: ' . $seed['Title'],
'Content' => 'Unpublished: ' . $seed['Content'],
]);
$page->write();
return $page;
}
/**
* Tests response code for globally disabled feeds
*/
public function testFeedViewability()
{
// Nested loop through each configuration
foreach ([true, false] as $publicHistory_Page) {
$page = $this->createPageWithChanges(['PublicHistory' => $publicHistory_Page, 'Title' => 'Page']);
// Test requests to 'changes' action
foreach ([true, false] as $publicHistory_Config) {
Config::modify()->set(VersionFeed::class, 'changes_enabled', $publicHistory_Config);
$expectedResponse = $publicHistory_Page && $publicHistory_Config ? 200 : 404;
$response = $this->get($page->RelativeLink('changes'));
$this->assertEquals($expectedResponse, $response->getStatusCode());
}
// Test requests to 'allchanges' action on each page
foreach ([true, false] as $allChanges_Config) {
foreach ([true, false] as $allChanges_SiteConfig) {
Config::modify()->set(VersionFeed::class, 'allchanges_enabled', $allChanges_Config);
$siteConfig = SiteConfig::current_site_config();
$siteConfig->AllChangesEnabled = $allChanges_SiteConfig;
$siteConfig->write();
$expectedResponse = $allChanges_Config && $allChanges_SiteConfig ? 200 : 404;
$response = $this->get($page->RelativeLink('allchanges'));
$this->assertEquals($expectedResponse, $response->getStatusCode());
}
}
}
}
}

62
tests/VersionFeedTest.php Normal file
View File

@ -0,0 +1,62 @@
<?php
namespace SilverStripe\VersionFeed\Tests;
use Page;
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Versioned\Versioned;
use SilverStripe\VersionFeed\VersionFeed;
use SilverStripe\VersionFeed\VersionFeedController;
class VersionFeedTest extends SapphireTest
{
protected $usesDatabase = true;
protected static $required_extensions = [
SiteTree::class => [VersionFeed::class],
ContentController::class => [VersionFeedController::class],
];
public function testDiffedChangesExcludesRestrictedItems()
{
$this->markTestIncomplete();
}
public function testDiffedChangesIncludesFullHistory()
{
$this->markTestIncomplete();
}
public function testDiffedChangesTitle()
{
$page = new Page(['Title' => 'My Title']);
$page->write();
$page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$page->Title = 'My Changed Title';
$page->write();
$page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$page->Title = 'My Unpublished Changed Title';
$page->write();
// Strip spaces from test output because they're not reliably maintained by the HTML Tidier
$cleanDiffOutput = function ($val) {
return str_replace(' ', '', strip_tags($val ?? ''));
};
$this->assertContains(
str_replace(' ', '', _t('RSSHistory.TITLECHANGED', 'Title has changed:') . 'My Changed Title'),
array_map($cleanDiffOutput, $page->getDiffList()->column('DiffTitle') ?? []),
'Detects published title changes'
);
$this->assertNotContains(
str_replace(' ', '', _t('RSSHistory.TITLECHANGED', 'Title has changed:') . 'My Unpublished Changed Title'),
array_map($cleanDiffOutput, $page->getDiffList()->column('DiffTitle') ?? []),
'Ignores unpublished title changes'
);
}
}