mirror of
https://github.com/silverstripe/silverstripe-versionfeed
synced 2024-10-22 11:05:31 +02:00
Compare commits
179 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
55d18d8392 | ||
|
0b94f4b8a9 | ||
|
a2b6f45abd | ||
|
b12eee33ed | ||
|
0a17d0c537 | ||
|
66da923502 | ||
|
3c922398ca | ||
|
82872780eb | ||
|
900aae6a25 | ||
|
d082606ded | ||
|
4129c23c0f | ||
|
84f1ed3914 | ||
|
b11db96d7a | ||
|
1f015f119c | ||
|
ebae2446a9 | ||
|
633b85537e | ||
|
df5845176f | ||
|
4afd8ce074 | ||
|
1e78dfc828 | ||
|
b56ae94e87 | ||
|
1ec4a141e3 | ||
|
d44e506d00 | ||
|
48220dad3a | ||
|
6699b9074a | ||
|
8041c15dc7 | ||
|
9f6b6f9d8a | ||
|
831c178514 | ||
|
2e1dc4a46e | ||
|
c5fdbcbd72 | ||
|
2ba0438277 | ||
|
656094763b | ||
|
d18a07213e | ||
|
e8da1e07bf | ||
|
abb248242f | ||
|
75237f89db | ||
|
befc7f07d3 | ||
|
c5f4a5ccdb | ||
|
9433c0542a | ||
|
f93f32d22b | ||
|
600a4b2cde | ||
|
7195e2a833 | ||
|
8171761d39 | ||
|
933c58128b | ||
|
feb6218035 | ||
|
2671406a86 | ||
|
52f303d497 | ||
|
baee472c9f | ||
|
25ff52c1fc | ||
|
b4ce6caead | ||
|
42ea538f0f | ||
|
a7251dba0f | ||
|
c4ba8bf3a9 | ||
|
5cc58a1269 | ||
|
747cf4d68d | ||
|
f473126c9f | ||
|
952c7c3592 | ||
|
1f832bcdc4 | ||
|
4d499524cd | ||
|
ece9566bcb | ||
|
8de3ec1dfc | ||
|
5aef1bace1 | ||
|
a56946b68b | ||
|
08539df0af | ||
|
aae09ae51f | ||
|
9260d8c744 | ||
|
831f03a2b8 | ||
|
247039ff0e | ||
|
2986d7e0c3 | ||
|
0be030efd1 | ||
|
bd59ce6200 | ||
|
0356e1e0a2 | ||
|
9065f82d79 | ||
|
3b1c682a54 | ||
|
d48fab6faa | ||
|
5b38663322 | ||
|
205d077384 | ||
|
3a6fe66e2f | ||
|
0c199bcae0 | ||
|
7b3d282802 | ||
|
093af10c8a | ||
|
29ef2a4920 | ||
|
5c6b5f5e2c | ||
|
f59ddb5578 | ||
|
ea21f4557f | ||
|
a9a346a53d | ||
|
989911acb2 | ||
|
de70e59d80 | ||
|
5bde86198b | ||
|
6b6f4ec622 | ||
|
952b67a5cb | ||
|
dd361929db | ||
|
921b7c7fb5 | ||
|
bfba519cc3 | ||
|
5096179825 | ||
|
d536df0d32 | ||
|
67e112fd12 | ||
|
17cf3d7487 | ||
|
23f2f45705 | ||
|
fe2b6597b3 | ||
|
229463547d | ||
|
17699f2b8a | ||
|
9a7651b017 | ||
|
f4c7a4d737 | ||
|
bfb7422981 | ||
|
3845294dfe | ||
|
b25d7d728e | ||
|
24b2d06424 | ||
|
6dffbdaee7 | ||
|
82600b37c5 | ||
|
fbb2baad15 | ||
|
e7861750b1 | ||
|
937eb88242 | ||
|
bbc4457f97 | ||
|
b87cfaadf8 | ||
|
33cc3a13e8 | ||
|
9a6c655f54 | ||
|
dc03856ca1 | ||
|
f53f353ba6 | ||
|
898c281ef5 | ||
|
c64e7ad2d7 | ||
|
73a033f22d | ||
|
843fb43979 | ||
|
c2e3f672dd | ||
|
7f7ecca151 | ||
|
a284681b39 | ||
|
83947f03d2 | ||
|
0426a38412 | ||
|
a71b15a437 | ||
|
fc8ad92b29 | ||
|
6d96c59ebd | ||
|
06d70e4593 | ||
|
04cc2c3f07 | ||
|
47de4e5112 | ||
|
6a748e0de1 | ||
|
e6f2a2f7a9 | ||
|
015819e6e2 | ||
|
84530d6678 | ||
|
2cad57d442 | ||
|
c297b7c229 | ||
|
2913245cb5 | ||
|
50661260c9 | ||
|
7f4f7bb1b4 | ||
|
1fe90397b3 | ||
|
5b6dae480f | ||
|
4d278f1695 | ||
|
f0c92002b6 | ||
|
b6179c5eef | ||
|
85c38311bc | ||
|
180a562b51 | ||
|
429b61036e | ||
|
fc530d601b | ||
|
66dd4622e0 | ||
|
73951e2623 | ||
|
8de4e31fca | ||
|
25c575fe16 | ||
|
d37785ff7b | ||
|
fd67399417 | ||
|
8a7d2ec9da | ||
|
2d6337dad9 | ||
|
797e5586c5 | ||
|
d718bf8758 | ||
|
094e3190df | ||
|
a3c4c52faf | ||
|
7f3adbf4d8 | ||
|
d6fca1a5c4 | ||
|
706ad85054 | ||
|
47991567b5 | ||
|
241f0604b0 | ||
|
a243a3510a | ||
|
4946376793 | ||
|
55fe8eb050 | ||
|
27bd513938 | ||
|
01e1136311 | ||
|
aabb774d20 | ||
|
13bb2cf270 | ||
|
2d035a6e94 | ||
|
778ae7d42c | ||
|
3ad4f401ee | ||
|
8b4fdd31c3 |
17
.editorconfig
Normal file
17
.editorconfig
Normal 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
7
.gitattributes
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/tests export-ignore
|
||||||
|
/docs export-ignore
|
||||||
|
/.gitattributes export-ignore
|
||||||
|
/.gitignore export-ignore
|
||||||
|
/.travis.yml export-ignore
|
||||||
|
/.scrutinizer.yml export-ignore
|
||||||
|
/codecov.yml export-ignore
|
11
.github/workflows/ci.yml
vendored
Normal file
11
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
name: CI
|
||||||
|
uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1
|
16
.github/workflows/deploy-userhelp-docs.yml
vendored
Normal file
16
.github/workflows/deploy-userhelp-docs.yml
vendored
Normal 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
16
.github/workflows/dispatch-ci.yml
vendored
Normal 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
17
.github/workflows/keepalive.yml
vendored
Normal 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
9
.tx/config
Normal 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
12
.upgrade.yml
Normal 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
17
LICENSE
@ -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.
|
|
23
README.md
23
README.md
@ -1,23 +1,28 @@
|
|||||||
# Version Feed
|
# 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
|
## Overview
|
||||||
|
|
||||||
The module creates an RSS feed on each page with their change history, as well as one for the entire site.
|
The module creates an RSS feed on each page with their change history, as well as one for the entire site.
|
||||||
|
|
||||||
## Requirements
|
## 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
|
## Installation
|
||||||
|
|
||||||
Install with composer by running:
|
Install with composer by running `composer require silverstripe/versionfeed` in the root of your Silverstripe project.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Usage
|
## 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.
|
||||||
|
@ -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
25
_config/versionfeed.yml
Normal 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
1
code-of-conduct.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
When having discussions about this module in issues or pull request please adhere to the [SilverStripe Community Code of Conduct](https://docs.silverstripe.org/en/contributing/code_of_conduct).
|
@ -1,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');
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
1
codecov.yml
Normal file
@ -0,0 +1 @@
|
|||||||
|
comment: false
|
@ -1,8 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "silverstripe/versionfeed",
|
"name": "silverstripe/versionfeed",
|
||||||
"description": "Adds RSS feeds of content changes to SilverStripe",
|
"description": "Adds RSS feeds of content changes to SilverStripe",
|
||||||
"type": "silverstripe-module",
|
"type": "silverstripe-vendormodule",
|
||||||
"keywords": ["silverstripe", "rss", "feed"],
|
"keywords": [
|
||||||
|
"silverstripe",
|
||||||
|
"rss",
|
||||||
|
"feed",
|
||||||
|
"cwp"
|
||||||
|
],
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
@ -10,9 +15,23 @@
|
|||||||
"email": "robert@silverstripe.com"
|
"email": "robert@silverstripe.com"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require":
|
"require": {
|
||||||
{
|
"php": "^7.4 || ^8.0",
|
||||||
"silverstripe/framework": "3.*",
|
"silverstripe/cms": "^4",
|
||||||
"silverstripe/cms": "3.*"
|
"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
|
||||||
}
|
}
|
@ -7,17 +7,66 @@
|
|||||||
Creating functions called `changes` or `allchanges` on any of your page types or controllers will cause confusion with
|
Creating functions called `changes` or `allchanges` on any of your page types or controllers will cause confusion with
|
||||||
the extensions defined on the extension.
|
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
|
### 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
|
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
|
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:
|
and returning the URL of your desired RSS feed:
|
||||||
|
|
||||||
:::php
|
```php
|
||||||
class MyPage extends Page {
|
class MyPage extends Page
|
||||||
function getDefaultRSSLink() {
|
{
|
||||||
|
public function getDefaultRSSLink()
|
||||||
|
{
|
||||||
return $this->Link('myrssfeed');
|
return $this->Link('myrssfeed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
This can be used in templates as `$DefaultRSSLink`.
|
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.
|
||||||
|
@ -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.
|
|
34
docs/en/userguide/index.md
Normal file
34
docs/en/userguide/index.md
Normal 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
0
lang/_manifest_exclude
Normal file
8
lang/ar.yml
Normal file
8
lang/ar.yml
Normal 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
13
lang/en.yml
Normal 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
13
lang/eo.yml
Normal 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
11
lang/fi_FI.yml
Normal 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
11
lang/hr.yml
Normal 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
3
lang/id.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
id:
|
||||||
|
SilverStripe\VersionFeed\VersionFeed:
|
||||||
|
TITLECHANGED: 'Judul telah diubah:'
|
11
lang/it.yml
Normal file
11
lang/it.yml
Normal 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
8
lang/mi.yml
Normal 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
11
lang/ru.yml
Normal 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
13
lang/sk.yml
Normal 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
12
lang/sl.yml
Normal 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
8
lang/zh.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
zh:
|
||||||
|
SilverStripe\VersionFeed\VersionFeed:
|
||||||
|
LABEL: 将历史记录公开
|
||||||
|
SINGLEPAGEFEEDTITLE: '更新至 {title} 页面'
|
||||||
|
SITEFEEDTITLE: '更新至 {title}'
|
||||||
|
TITLECHANGED: 标题已更改:
|
||||||
|
Warning: 发布历史记录还会在公开视图中显示受保护事件内进行的改动。
|
||||||
|
Warning2: 用这种方式更改访问设置会使得本页及下级页面变为公开的<br>可能还会使得这些页面的所有变动历史记录也变为公开的。请查阅<br>本节的“公开历史记录”设置,确保只将需要的信息披露出来。
|
12
license.md
Normal file
12
license.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Copyright (c) 2017, SilverStripe Limited
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
12
phpcs.xml.dist
Normal file
12
phpcs.xml.dist
Normal 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
16
phpunit.xml.dist
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit bootstrap="vendor/silverstripe/cms/tests/bootstrap.php" colors="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Default">
|
||||||
|
<directory>tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<filter>
|
||||||
|
<whitelist addUncoveredFilesFromWhitelist="true">
|
||||||
|
<directory suffix=".php">src/</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory suffix=".php">tests/</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
40
src/Filters/CachedContentFilter.php
Normal file
40
src/Filters/CachedContentFilter.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
66
src/Filters/ContentFilter.php
Normal file
66
src/Filters/ContentFilter.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
122
src/Filters/RateLimitFilter.php
Normal file
122
src/Filters/RateLimitFilter.php
Normal 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
241
src/VersionFeed.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
217
src/VersionFeedController.php
Normal file
217
src/VersionFeedController.php
Normal 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]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
47
src/VersionFeedSiteConfig.php
Normal file
47
src/VersionFeedSiteConfig.php
Normal 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."
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
25
templates/Page_allchanges_rss.ss
Normal file
25
templates/Page_allchanges_rss.ss
Normal 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>
|
25
templates/Page_changes_rss.ss
Normal file
25
templates/Page_changes_rss.ss
Normal 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>
|
274
tests/VersionFeedFunctionalTest.php
Normal file
274
tests/VersionFeedFunctionalTest.php
Normal 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
62
tests/VersionFeedTest.php
Normal 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'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user