mirror of
https://github.com/silverstripe/silverstripe-versionfeed
synced 2024-10-22 11:05:31 +02:00
Compare commits
116 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 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -4,3 +4,4 @@
|
||||
/.gitignore export-ignore
|
||||
/.travis.yml export-ignore
|
||||
/.scrutinizer.yml export-ignore
|
||||
/codecov.yml export-ignore
|
||||
|
11
.github/workflows/ci.yml
vendored
Normal file
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
|
@ -1,9 +0,0 @@
|
||||
inherit: true
|
||||
|
||||
checks:
|
||||
php:
|
||||
code_rating: true
|
||||
duplication: true
|
||||
|
||||
filter:
|
||||
paths: [code/*, tests/*]
|
36
.travis.yml
36
.travis.yml
@ -1,36 +0,0 @@
|
||||
# See https://github.com/silverstripe-labs/silverstripe-travis-support for setup details
|
||||
|
||||
sudo: false
|
||||
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 5.3
|
||||
- 5.4
|
||||
- 5.5
|
||||
- 5.6
|
||||
- 7.0
|
||||
|
||||
env:
|
||||
- DB=MYSQL CORE_RELEASE=3.2
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- php: 5.6
|
||||
env: DB=MYSQL CORE_RELEASE=3
|
||||
- php: 5.6
|
||||
env: DB=MYSQL CORE_RELEASE=3.1
|
||||
- php: 5.6
|
||||
env: DB=PGSQL CORE_RELEASE=3.2
|
||||
allow_failures:
|
||||
- php: 7.0
|
||||
|
||||
before_script:
|
||||
- composer self-update || true
|
||||
- git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support
|
||||
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
|
||||
- cd ~/builds/ss
|
||||
- composer install
|
||||
|
||||
script:
|
||||
- vendor/bin/phpunit versionfeed/tests
|
@ -1,8 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[silverstripe-versionfeed.master]
|
||||
[o:silverstripe:p:silverstripe-versionfeed:r:master]
|
||||
file_filter = lang/<lang>.yml
|
||||
source_file = lang/en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
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
|
21
README.md
21
README.md
@ -1,6 +1,7 @@
|
||||
# Version Feed
|
||||
|
||||
[![Build Status](https://secure.travis-ci.org/silverstripe-labs/silverstripe-versionfeed.png)](http://travis-ci.org/silverstripe-labs/silverstripe-versionfeed)
|
||||
[![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
|
||||
|
||||
@ -8,26 +9,20 @@ The module creates an RSS feed on each page with their change history, as well a
|
||||
|
||||
## 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/userguide/index.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
### Translations
|
||||
## 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.
|
||||
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,21 +1,25 @@
|
||||
---
|
||||
Name: versionedfeedconfig
|
||||
---
|
||||
Injector:
|
||||
RateLimitFilter: \VersionFeed\Filters\RateLimitFilter
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
RateLimitFilter: SilverStripe\VersionFeed\Filters\RateLimitFilter
|
||||
ContentFilter:
|
||||
class: \VersionFeed\Filters\CachedContentFilter
|
||||
class: SilverStripe\VersionFeed\Filters\CachedContentFilter
|
||||
constructor:
|
||||
- %$RateLimitFilter
|
||||
SiteTree:
|
||||
- '%$RateLimitFilter'
|
||||
Psr\SimpleCache\CacheInterface.VersionFeedController:
|
||||
factory: SilverStripe\Core\Cache\CacheFactory
|
||||
constructor:
|
||||
namespace: 'VersionFeedController'
|
||||
SilverStripe\CMS\Model\SiteTree:
|
||||
extensions:
|
||||
- VersionFeed
|
||||
SiteConfig:
|
||||
- SilverStripe\VersionFeed\VersionFeed
|
||||
SilverStripe\SiteConfig\SiteConfig:
|
||||
extensions:
|
||||
- VersionFeedSiteConfig
|
||||
ContentController:
|
||||
- SilverStripe\VersionFeed\VersionFeedSiteConfig
|
||||
SilverStripe\CMS\Controllers\ContentController:
|
||||
extensions:
|
||||
- VersionFeed_Controller
|
||||
VersionFeed_Controller:
|
||||
- SilverStripe\VersionFeed\VersionFeedController
|
||||
SilverStripe\VersionFeed\VersionFeedController:
|
||||
dependencies:
|
||||
ContentFilter: %$ContentFilter
|
||||
ContentFilter: '%$ContentFilter'
|
||||
|
29
changelog.md
29
changelog.md
@ -1,29 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [1.2.2]
|
||||
|
||||
* Consistent sorting for getDiffList()
|
||||
|
||||
## [1.2.1]
|
||||
|
||||
* Updated userguide documentation
|
||||
* Added standard Scrutinizer config
|
||||
* Added standard code of conduct
|
||||
* Added standard gitattributes
|
||||
* Added standard editor config
|
||||
* Added standard license
|
||||
* Added standard Travis config
|
||||
* Updated license year
|
||||
* Updated translations
|
||||
|
||||
## [1.2.0]
|
||||
|
||||
* Changelog added.
|
||||
* Added Scrutinizer support
|
||||
* Add `ContentFilter.cache_lifetime` config to set the cache lifetime.
|
||||
* Update alternate xml link tag to be W3 compliant
|
||||
* Added CWP keyword
|
@ -1,205 +0,0 @@
|
||||
<?php
|
||||
|
||||
class VersionFeed extends SiteTreeExtension {
|
||||
|
||||
private static $db = array(
|
||||
'PublicHistory' => 'Boolean(true)'
|
||||
);
|
||||
|
||||
private static $defaults = array(
|
||||
'PublicHistory' => true
|
||||
);
|
||||
|
||||
public function updateFieldLabels(&$labels) {
|
||||
$labels['PublicHistory'] = _t('RSSHistory.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->allVersions(
|
||||
"\"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 = new HTMLText();
|
||||
$version->DiffTitle->setValue(
|
||||
sprintf(
|
||||
'<div><em>%s</em> ' . $diffTitle . '</div>',
|
||||
_t('RSSHistory.TITLECHANGED', 'Title has changed:')
|
||||
)
|
||||
);
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if ($version->Content != $previous->Content) {
|
||||
$diffContent = Diff::compareHTML($version->Content, $previous->Content);
|
||||
|
||||
$version->DiffContent = new HTMLText();
|
||||
$version->DiffContent->setValue('<div>'.$diffContent.'</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;
|
||||
}
|
||||
|
||||
// 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 = new HTMLText();
|
||||
$first->DiffContent->setValue('<div>' . $first->Content . '</div>');
|
||||
// Copy the link so it can be cached by SS_Cache.
|
||||
$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.
|
||||
*
|
||||
* @returns DataObject 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) {
|
||||
return $this->getDiffList(
|
||||
$highestVersion,
|
||||
$fullHistory ? $limit : 1
|
||||
);
|
||||
}
|
||||
|
||||
public function updateSettingsFields(FieldList $fields) {
|
||||
if(!Config::inst()->get(get_class(), 'changes_enabled')) return;
|
||||
|
||||
// Add public history field.
|
||||
$fields->addFieldToTab('Root.Settings', $publicHistory = new FieldGroup(
|
||||
new CheckboxField('PublicHistory', $this->owner->fieldLabel('PublicHistory')
|
||||
)));
|
||||
|
||||
$warning = _t(
|
||||
'VersionFeed.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 = _t(
|
||||
'VersionFeed.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."
|
||||
);
|
||||
|
||||
$fields->addFieldToTab('Root.Settings', new LiteralField('PublicHistoryWarning2', $warning), 'CanViewType');
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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('VersionFeedSiteConfig.ALLCHANGESLABEL', 'Make global changes feed public');
|
||||
}
|
||||
|
||||
public function updateCMSFields(\FieldList $fields) {
|
||||
if(!Config::inst()->get('VersionFeed', 'allchanges_enabled')) return;
|
||||
|
||||
$fields->addFieldToTab('Root.Access',
|
||||
FieldGroup::create(new CheckboxField('AllChangesEnabled', $this->owner->fieldLabel('AllChangesEnabled')))
|
||||
->setTitle(_t('VersionFeedSiteConfig.ALLCHANGES', 'All page changes'))
|
||||
->setDescription(_t(
|
||||
'VersionFeed.Warning',
|
||||
"Publicising the history will also disclose the changes that have at the time been protected " .
|
||||
"from the public view."
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
@ -1,173 +0,0 @@
|
||||
<?php
|
||||
|
||||
class VersionFeed_Controller extends Extension {
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'changes',
|
||||
'allchanges'
|
||||
);
|
||||
|
||||
/**
|
||||
* Content handler
|
||||
*
|
||||
* @var \VersionFeed\Filters\ContentFilter
|
||||
*/
|
||||
protected $contentFilter;
|
||||
|
||||
/**
|
||||
* Sets the content filter
|
||||
*
|
||||
* @param \VersionFeed\Filters\ContentFilter $contentFilter
|
||||
*/
|
||||
public function setContentFilter(\VersionFeed\Filters\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', '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', $this->owner->ID, $this->owner->Version));
|
||||
$entries = $this->filterContent($key, function() use ($target) {
|
||||
return $target->getDiffList(null, Config::inst()->get('VersionFeed', 'changes_limit'));
|
||||
});
|
||||
|
||||
// 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.
|
||||
*/
|
||||
public function allchanges() {
|
||||
// Check viewability of allchanges
|
||||
if(!Config::inst()->get('VersionFeed', 'allchanges_enabled')
|
||||
|| !SiteConfig::current_site_config()->AllChangesEnabled
|
||||
) {
|
||||
return $this->owner->httpError(404, 'Global history not viewable');
|
||||
}
|
||||
|
||||
$limit = (int)Config::inst()->get('VersionFeed', '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'])
|
||||
. (Member::currentUserID() ?: '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 = SiteTree::get()->byID($id);
|
||||
$canView[$id] = $page && $page->canView(new Member());
|
||||
}
|
||||
if (!$canView[$id]) continue;
|
||||
|
||||
// Get the diff to the previous version.
|
||||
$version = new Versioned_Version($record);
|
||||
if ($diff = $version->getDiff()) {
|
||||
$changeList->push($diff);
|
||||
}
|
||||
}
|
||||
|
||||
return $changeList;
|
||||
});
|
||||
} else {
|
||||
$changeList = new ArrayList();
|
||||
}
|
||||
|
||||
// Produce output
|
||||
$rss = new RSSFeed($changeList, $this->owner->request->getURL(), $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', 'changes_enabled') || !$this->owner->PublicHistory) {
|
||||
return;
|
||||
}
|
||||
|
||||
RSSFeed::linkToFeed(
|
||||
$this->owner->Link('changes'),
|
||||
sprintf(
|
||||
_t('RSSHistory.SINGLEPAGEFEEDTITLE', 'Updates to %s page'),
|
||||
$this->owner->Title
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and embeds the RSS header link for the global version rss feed
|
||||
*/
|
||||
public function linkToAllSiteRSSFeed() {
|
||||
if(!Config::inst()->get('VersionFeed', 'allchanges_enabled')
|
||||
|| !SiteConfig::current_site_config()->AllChangesEnabled
|
||||
) {
|
||||
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 sprintf(_t('RSSHistory.SITEFEEDTITLE', 'Updates to %s'), SiteConfig::current_site_config()->Title);
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace VersionFeed\Filters;
|
||||
|
||||
/**
|
||||
* 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->load($key);
|
||||
if($result) return $result;
|
||||
|
||||
// Fallback to generate result
|
||||
$result = parent::getContent($key, $callback);
|
||||
$cache->save($result, $key);
|
||||
return $result;
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace VersionFeed\Filters;
|
||||
|
||||
/**
|
||||
* Conditionally executes a given callback, attempting to return the desired results
|
||||
* of its execution.
|
||||
*/
|
||||
abstract class ContentFilter {
|
||||
|
||||
/**
|
||||
* 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 Zend_Cache_Frontend
|
||||
*/
|
||||
protected function getCache() {
|
||||
$cache = \SS_Cache::factory('VersionFeed_Controller');
|
||||
$cache->setOption('automatic_serialization', true);
|
||||
|
||||
// Map 0 to null for unlimited lifetime
|
||||
$lifetime = \Config::inst()->get(get_class($this), 'cache_lifetime') ?: null;
|
||||
$cache->setLifetime($lifetime);
|
||||
return $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace VersionFeed\Filters;
|
||||
|
||||
/**
|
||||
* 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(\Config::inst()->get(get_class(), 'lock_bypage')) {
|
||||
$key .= '_' . md5($itemkey);
|
||||
}
|
||||
|
||||
// Add user-specific identifier
|
||||
if(\Config::inst()->get(get_class(), '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 = \Config::inst()->get(get_class(), '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->load($limitKey)) {
|
||||
if(time() < $lockedUntil) {
|
||||
// Politely inform visitor of limit
|
||||
$response = new \SS_HTTPResponse_Exception('Too Many Requests.', 429);
|
||||
$response->getResponse()->addHeader('Retry-After', 1 + $lockedUntil - time());
|
||||
throw $response;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply rate limit
|
||||
$cache->save(time() + $timeout, $limitKey);
|
||||
|
||||
// Generate results
|
||||
$result = parent::getContent($key, $callback);
|
||||
|
||||
// Reset rate limit with optional cooldown
|
||||
if($cooldown = \Config::inst()->get(get_class(), 'lock_cooldown')) {
|
||||
// Set cooldown on successful query execution
|
||||
$cache->save(time() + $cooldown, $limitKey);
|
||||
} else {
|
||||
// Without cooldown simply disable lock
|
||||
$cache->remove($limitKey);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
1
codecov.yml
Normal file
1
codecov.yml
Normal file
@ -0,0 +1 @@
|
||||
comment: false
|
@ -1,25 +1,37 @@
|
||||
{
|
||||
"name": "silverstripe/versionfeed",
|
||||
"description": "Adds RSS feeds of content changes to SilverStripe",
|
||||
"type": "silverstripe-module",
|
||||
"keywords": ["silverstripe", "rss", "feed", "cwp"],
|
||||
"license": "BSD-3-Clause",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Robert Curry",
|
||||
"email": "robert@silverstripe.com"
|
||||
}
|
||||
],
|
||||
"require":
|
||||
{
|
||||
"silverstripe/framework": ">=3.1",
|
||||
"silverstripe/cms": ">=3.1"
|
||||
},
|
||||
"extra":
|
||||
{
|
||||
"branch-alias":
|
||||
{
|
||||
"dev-master": "1.2.x-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
"name": "silverstripe/versionfeed",
|
||||
"description": "Adds RSS feeds of content changes to SilverStripe",
|
||||
"type": "silverstripe-vendormodule",
|
||||
"keywords": [
|
||||
"silverstripe",
|
||||
"rss",
|
||||
"feed",
|
||||
"cwp"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Robert Curry",
|
||||
"email": "robert@silverstripe.com"
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
# 1.0.3
|
||||
|
||||
## Upgrading
|
||||
|
||||
* Caching and rate limiting rules now apply in this version, and it may be necessary to adjust the values
|
||||
for `RateLimitFilter.lock_timeout`, `RateLimitFilter.lock_bypage` and `RateLimitFilter.lock_byuserip` configs.
|
||||
See the [Developer Documentation](../developer.md) for information on how these affect cache performance and behaviour.
|
@ -9,12 +9,15 @@ the extensions defined on the extension.
|
||||
|
||||
### Enabling / Disabling
|
||||
|
||||
The `allchanges` feed can be disabled by setting the `VersionFeed.allchanges_enabled` config to false.
|
||||
By default the `allchanges` and `changes` feed are disabled.
|
||||
|
||||
Likewise, the `changes` feed for each page can be globally disabled by setting the `VersionFeed.changes_enabled`
|
||||
config to false. If this left 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](user.md#enabling--disabling).
|
||||
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
|
||||
|
||||
@ -22,45 +25,48 @@ Templates can offer a "Subscribe" link with a link to the most relevant RSS 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() {
|
||||
return $this->Link('myrssfeed');
|
||||
}
|
||||
```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 `versionfeed/_config/versionfeed.yml`.
|
||||
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:
|
||||
|
||||
* `CachedContentFilter` provides caching of versions based on an identifier built up of the record ID and the
|
||||
* `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.
|
||||
* `RateLimitFilter` provides rate limiting to ensure that requests to uncached data does not overload the
|
||||
server. This filter will only be applied if the `CachedContentFilter` does not have any cached record
|
||||
* `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 `VersionFeed_Controller.dependencies`
|
||||
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 `RateLimitFilter.lock_bypage` config defaults to false, meaning that a
|
||||
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.
|
||||
|
||||
`RateLimitFilter.lock_byuserip` can be set to true in order to prevent requests from different users
|
||||
`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 `RateLimitFilter.lock_timeout` config, which is set to 5 seconds by default.
|
||||
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 `ContentFilter.cache_lifetime` config in order to control the maximum age of the cache.
|
||||
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.
|
||||
cache unlimited.
|
||||
|
@ -1,5 +1,7 @@
|
||||
---
|
||||
title: Content change RSS
|
||||
summary: Adds page or site wide RSS feeds that display content changes
|
||||
---
|
||||
|
||||
# Content change RSS
|
||||
|
||||
@ -16,12 +18,12 @@ Make sure that your SilverStripe installation has the [versionfeed](http://addon
|
||||
|
||||
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`
|
||||
* 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 *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.
|
||||
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.
|
||||
|
||||
@ -29,4 +31,4 @@ The allchanges feed can also be disabled by unchecking the "All page changes" ch
|
||||
|
||||
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.
|
||||
There is a warning explaining this fact next to the *Make history public* checkbox.
|
||||
|
@ -1,9 +1,8 @@
|
||||
ar:
|
||||
RSSHistory:
|
||||
SilverStripe\VersionFeed\VersionFeed:
|
||||
LABEL: 'اجعل التاريخ متاح للمشاهدة من قبل الجميع'
|
||||
SINGLEPAGEFEEDTITLE: 'التحديثات و صلت ل s% من الصفحة'
|
||||
SITEFEEDTITLE: 'التحديثات إلى s%'
|
||||
TITLECHANGED: 'تم تغيير العنوان:'
|
||||
VersionFeed:
|
||||
Warning: 'إن نشر التاريخ سوف يظهر التغييرات التى تملكها و التى كنت تمنعها من العرض على العامة.'
|
||||
Warning2: 'إن تغيير إعدادات الدخول بهذه الطريقة كى تكون هذه الصفحة أو الصفحات التى تحتها تصبح معلنة<br>قد يؤدي الوصول إليها في نشر جميع التغييرات التاريخية على هذه الصفحات أيضا. من فضلك قم بمراجعة<br>"التاريخ العام" لهذا القطاع لكى يتم التأكد من أن المعلومات المقصودة فقط هى التى يتم الإفصاح عنها.'
|
||||
|
13
lang/en.yml
13
lang/en.yml
@ -1,12 +1,13 @@
|
||||
en:
|
||||
RSSHistory:
|
||||
SilverStripe\VersionFeed\VersionFeed:
|
||||
LABEL: 'Make history public'
|
||||
SINGLEPAGEFEEDTITLE: 'Updates to %s page'
|
||||
SITEFEEDTITLE: 'Updates to %s'
|
||||
SINGLEPAGEFEEDTITLE: 'Updates to {title} page'
|
||||
SITEFEEDTITLE: 'Updates to {title}'
|
||||
TITLECHANGED: 'Title has changed:'
|
||||
VersionFeed:
|
||||
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.'
|
||||
VersionFeedSiteConfig:
|
||||
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'
|
||||
|
11
lang/eo.yml
11
lang/eo.yml
@ -1,12 +1,13 @@
|
||||
eo:
|
||||
RSSHistory:
|
||||
SilverStripe\VersionFeed\VersionFeed:
|
||||
LABEL: 'Publikigu historion'
|
||||
SINGLEPAGEFEEDTITLE: 'Ĝisdatigoj al paĝo %s'
|
||||
SITEFEEDTITLE: 'Ĝisdatigoj al %s'
|
||||
SINGLEPAGEFEEDTITLE: 'Ĝisdatigoj al paĝo {title}'
|
||||
SITEFEEDTITLE: 'Ĝisdatigoj al {title}'
|
||||
TITLECHANGED: 'Titolo estas ŝanĝita:'
|
||||
VersionFeed:
|
||||
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.'
|
||||
VersionFeedSiteConfig:
|
||||
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'
|
||||
|
@ -1,12 +1,11 @@
|
||||
fi_FI:
|
||||
RSSHistory:
|
||||
SilverStripe\VersionFeed\VersionFeed:
|
||||
LABEL: 'Tee historiasta julkinen'
|
||||
SINGLEPAGEFEEDTITLE: 'Päivityksiä %s sivuun'
|
||||
SITEFEEDTITLE: 'Päivityksiä: %s'
|
||||
SINGLEPAGEFEEDTITLE: 'Päivityksiä {title} sivuun'
|
||||
SITEFEEDTITLE: 'Päivityksiä: {title}'
|
||||
TITLECHANGED: 'Otsikko on muuttunut:'
|
||||
VersionFeed:
|
||||
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.'
|
||||
VersionFeedSiteConfig:
|
||||
SilverStripe\VersionFeed\VersionFeedSiteConfig:
|
||||
ALLCHANGES: 'Kaikki sivumuokkaukset'
|
||||
ALLCHANGESLABEL: 'Tee muutoksista julkisia'
|
||||
|
@ -1,12 +1,11 @@
|
||||
hr:
|
||||
RSSHistory:
|
||||
SilverStripe\VersionFeed\VersionFeed:
|
||||
LABEL: 'Učini povijest dostupnu svima'
|
||||
SINGLEPAGEFEEDTITLE: 'Ažuriranja za %s stranicu'
|
||||
SITEFEEDTITLE: 'Ažuriranja za %s'
|
||||
SINGLEPAGEFEEDTITLE: 'Ažuriranja za {title} stranicu'
|
||||
SITEFEEDTITLE: 'Ažuriranja za {title}'
|
||||
TITLECHANGED: 'Naziv se promijenio:'
|
||||
VersionFeed:
|
||||
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.'
|
||||
VersionFeedSiteConfig:
|
||||
SilverStripe\VersionFeed\VersionFeedSiteConfig:
|
||||
ALLCHANGES: 'Sve promjene stranice'
|
||||
ALLCHANGESLABEL: 'Učini sveobuhvatne promjene feeda'
|
||||
|
@ -1,3 +1,3 @@
|
||||
id:
|
||||
RSSHistory:
|
||||
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'
|
@ -1,9 +1,8 @@
|
||||
mi:
|
||||
RSSHistory:
|
||||
SilverStripe\VersionFeed\VersionFeed:
|
||||
LABEL: 'Meinga kia tūmatanui te hītori'
|
||||
SINGLEPAGEFEEDTITLE: 'Ngā whakahou ki te whārangi %s'
|
||||
SITEFEEDTITLE: 'Ngā whakahou ki te %s'
|
||||
SINGLEPAGEFEEDTITLE: 'Ngā whakahou ki te whārangi {title}'
|
||||
SITEFEEDTITLE: 'Ngā whakahou ki te {title}'
|
||||
TITLECHANGED: 'Kua hurihia te taitara:'
|
||||
VersionFeed:
|
||||
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'
|
15
lang/zh.yml
15
lang/zh.yml
@ -1,9 +1,8 @@
|
||||
zh:
|
||||
RSSHistory:
|
||||
LABEL: '将历史记录公开'
|
||||
SINGLEPAGEFEEDTITLE: '更新至 %s 页面'
|
||||
SITEFEEDTITLE: '更新至 %s'
|
||||
TITLECHANGED: '标题已更改:'
|
||||
VersionFeed:
|
||||
Warning: '发布历史记录还会在公开视图中显示受保护事件内进行的改动。'
|
||||
Warning2: '用这种方式更改访问设置会使得本页及下级页面变为公开的<br>可能还会使得这些页面的所有变动历史记录也变为公开的。请查阅<br>本节的“公开历史记录”设置,确保只将需要的信息披露出来。'
|
||||
SilverStripe\VersionFeed\VersionFeed:
|
||||
LABEL: 将历史记录公开
|
||||
SINGLEPAGEFEEDTITLE: '更新至 {title} 页面'
|
||||
SITEFEEDTITLE: '更新至 {title}'
|
||||
TITLECHANGED: 标题已更改:
|
||||
Warning: 发布历史记录还会在公开视图中显示受保护事件内进行的改动。
|
||||
Warning2: 用这种方式更改访问设置会使得本页及下级页面变为公开的<br>可能还会使得这些页面的所有变动历史记录也变为公开的。请查阅<br>本节的“公开历史记录”设置,确保只将需要的信息披露出来。
|
||||
|
@ -1,4 +1,4 @@
|
||||
Copyright (c) 2016, SilverStripe Limited
|
||||
Copyright (c) 2017, SilverStripe Limited
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
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."
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
@ -1,216 +1,274 @@
|
||||
<?php
|
||||
class VersionFeedFunctionalTest extends FunctionalTest {
|
||||
|
||||
protected $requiredExtensions = array(
|
||||
'Page' => array('VersionFeed'),
|
||||
'Page_Controller' => array('VersionFeed_Controller'),
|
||||
);
|
||||
|
||||
protected $userIP;
|
||||
namespace SilverStripe\VersionFeed\Tests;
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
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;
|
||||
|
||||
$cache = SS_Cache::factory('VersionFeed_Controller');
|
||||
$cache->clean(Zend_Cache::CLEANING_MODE_ALL);
|
||||
|
||||
$this->userIP = isset($_SERVER['HTTP_CLIENT_IP']) ? $_SERVER['HTTP_CLIENT_IP'] : null;
|
||||
|
||||
Config::nest();
|
||||
// Disable caching and locking by default
|
||||
Config::inst()->update('VersionFeed\Filters\CachedContentFilter', 'cache_enabled', false);
|
||||
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_timeout', 0);
|
||||
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_bypage', false);
|
||||
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_byuserip', false);
|
||||
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_cooldown', false);
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
Config::unnest();
|
||||
|
||||
$_SERVER['HTTP_CLIENT_IP'] = $this->userIP;
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
class VersionFeedFunctionalTest extends FunctionalTest
|
||||
{
|
||||
protected $usesDatabase = true;
|
||||
|
||||
public function testPublicHistory() {
|
||||
$page = $this->createPageWithChanges(array('PublicHistory' => false));
|
||||
protected $baseURI = 'http://www.fakesite.test';
|
||||
|
||||
$response = $this->get($page->RelativeLink('changes'));
|
||||
$this->assertEquals(404, $response->getStatusCode());
|
||||
protected static $required_extensions = [
|
||||
Page::class => [VersionFeed::class],
|
||||
PageController::class => [VersionFeedController::class],
|
||||
];
|
||||
|
||||
$response = $this->get($page->RelativeLink('allchanges'));
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$xml = simplexml_load_string($response->getBody());
|
||||
$this->assertFalse((bool)$xml->channel->item);
|
||||
protected $userIP;
|
||||
|
||||
$page = $this->createPageWithChanges(array('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);
|
||||
/**
|
||||
* @var CacheInterface
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
$response = $this->get($page->RelativeLink('allchanges'));
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$xml = simplexml_load_string($response->getBody());
|
||||
$this->assertTrue((bool)$xml->channel->item);
|
||||
}
|
||||
|
||||
public function testRateLimiting() {
|
||||
// Re-enable locking just for this test
|
||||
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_timeout', 20);
|
||||
Config::inst()->update('VersionFeed\Filters\CachedContentFilter', 'cache_enabled', true);
|
||||
protected function setUp(): void
|
||||
{
|
||||
Director::config()->set('alternate_base_url', $this->baseURI);
|
||||
|
||||
$page1 = $this->createPageWithChanges(array('PublicHistory' => true, 'Title' => 'Page1'));
|
||||
$page2 = $this->createPageWithChanges(array('PublicHistory' => true, 'Title' => 'Page2'));
|
||||
|
||||
// Artifically set cache lock
|
||||
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_byuserip', false);
|
||||
$cache = SS_Cache::factory('VersionFeed_Controller');
|
||||
$cache->setOption('automatic_serialization', true);
|
||||
$cache->save(time() + 10, \VersionFeed\Filters\RateLimitFilter::CACHE_PREFIX);
|
||||
|
||||
// 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::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_bypage', true);
|
||||
$key = implode('_', array(
|
||||
'changes',
|
||||
$page1->ID,
|
||||
Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $page1->ID, false)
|
||||
));
|
||||
$key = \VersionFeed\Filters\RateLimitFilter::CACHE_PREFIX . '_' . md5($key);
|
||||
$cache->save(time() + 10, $key);
|
||||
$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::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_bypage', false);
|
||||
|
||||
// Test rate limit hit by IP
|
||||
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_byuserip', true);
|
||||
$_SERVER['HTTP_CLIENT_IP'] = '127.0.0.1';
|
||||
$cache->save(time() + 10, \VersionFeed\Filters\RateLimitFilter::CACHE_PREFIX . '_' . md5('127.0.0.1'));
|
||||
$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['HTTP_CLIENT_IP'] = '127.0.0.20';
|
||||
$cache->save(time() + 10, \VersionFeed\Filters\RateLimitFilter::CACHE_PREFIX . '_' . md5('127.0.0.1'));
|
||||
$response = $this->get($page1->RelativeLink('changes'));
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
// Restore setting
|
||||
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_byuserip', false);
|
||||
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_timeout', 0);
|
||||
Config::inst()->update('VersionFeed\Filters\CachedContentFilter', 'cache_enabled', false);
|
||||
}
|
||||
parent::setUp();
|
||||
|
||||
public function testContainsChangesForPageOnly() {
|
||||
$page1 = $this->createPageWithChanges(array('Title' => 'Page1'));
|
||||
$page2 = $this->createPageWithChanges(array('Title' => 'Page2'));
|
||||
$this->cache = Injector::inst()->get(
|
||||
CacheInterface::class . '.VersionFeedController'
|
||||
);
|
||||
$this->cache->clear();
|
||||
|
||||
$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);
|
||||
$this->userIP = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
|
||||
|
||||
$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);
|
||||
}
|
||||
// Enable history by default
|
||||
Config::modify()->set(VersionFeed::class, 'changes_enabled', true);
|
||||
Config::modify()->set(VersionFeed::class, 'allchanges_enabled', true);
|
||||
|
||||
public function testContainsAllChangesForAllPages() {
|
||||
$page1 = $this->createPageWithChanges(array('Title' => 'Page1'));
|
||||
$page2 = $this->createPageWithChanges(array('Title' => 'Page2'));
|
||||
// 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);
|
||||
|
||||
$response = $this->get($page1->RelativeLink('allchanges'));
|
||||
$xml = simplexml_load_string($response->getBody());
|
||||
$titles = array_map(function($item) {return (string)$item->title;}, $xml->xpath('//item'));
|
||||
$this->assertContains('Page1', $titles);
|
||||
$this->assertContains('Page2', $titles);
|
||||
}
|
||||
// Ensure any version based caches read from the live cache
|
||||
Versioned::set_reading_mode(Versioned::DEFAULT_MODE);
|
||||
}
|
||||
|
||||
protected function createPageWithChanges($seed = null) {
|
||||
$page = new Page();
|
||||
|
||||
$seed = array_merge(array(
|
||||
'Title' => 'My Title',
|
||||
'Content' => 'My Content'
|
||||
), $seed);
|
||||
$page->update($seed);
|
||||
$page->write();
|
||||
$page->publish('Stage', 'Live');
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Director::config()->set('alternate_base_url', null);
|
||||
|
||||
$page->update(array(
|
||||
'Title' => 'Changed: ' . $seed['Title'],
|
||||
'Content' => 'Changed: ' . $seed['Content'],
|
||||
));
|
||||
$page->write();
|
||||
$page->publish('Stage', 'Live');
|
||||
$_SERVER['REMOTE_ADDR'] = $this->userIP;
|
||||
|
||||
$page->update(array(
|
||||
'Title' => 'Changed again: ' . $seed['Title'],
|
||||
'Content' => 'Changed again: ' . $seed['Content'],
|
||||
));
|
||||
$page->write();
|
||||
$page->publish('Stage', 'Live');
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
$page->update(array(
|
||||
'Title' => 'Unpublished: ' . $seed['Title'],
|
||||
'Content' => 'Unpublished: ' . $seed['Content'],
|
||||
));
|
||||
$page->write();
|
||||
public function testPublicHistoryPublicHistoryDisabled()
|
||||
{
|
||||
$page = $this->createPageWithChanges(['PublicHistory' => false]);
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests response code for globally disabled feedss
|
||||
*/
|
||||
public function testFeedViewability() {
|
||||
|
||||
// Nested loop through each configuration
|
||||
foreach(array(true, false) as $publicHistory_Page) {
|
||||
$page = $this->createPageWithChanges(array('PublicHistory' => $publicHistory_Page, 'Title' => 'Page'));
|
||||
|
||||
// Test requests to 'changes' action
|
||||
foreach(array(true, false) as $publicHistory_Config) {
|
||||
Config::inst()->update('VersionFeed', '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(array(true, false) as $allChanges_Config) {
|
||||
foreach(array(true, false) as $allChanges_SiteConfig) {
|
||||
Config::inst()->update('VersionFeed', '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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,53 +1,62 @@
|
||||
<?php
|
||||
class VersionFeedTest extends SapphireTest {
|
||||
|
||||
protected $usesDatabase = true;
|
||||
namespace SilverStripe\VersionFeed\Tests;
|
||||
|
||||
protected $requiredExtensions = array(
|
||||
'SiteTree' => array('VersionFeed'),
|
||||
'ContentController' => array('VersionFeed_Controller'),
|
||||
);
|
||||
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;
|
||||
|
||||
protected $illegalExtensions = array(
|
||||
'SiteTree' => array('Translatable')
|
||||
);
|
||||
class VersionFeedTest extends SapphireTest
|
||||
{
|
||||
protected $usesDatabase = true;
|
||||
|
||||
public function testDiffedChangesExcludesRestrictedItems() {
|
||||
$this->markTestIncomplete();
|
||||
}
|
||||
protected static $required_extensions = [
|
||||
SiteTree::class => [VersionFeed::class],
|
||||
ContentController::class => [VersionFeedController::class],
|
||||
];
|
||||
|
||||
public function testDiffedChangesIncludesFullHistory() {
|
||||
$this->markTestIncomplete();
|
||||
}
|
||||
public function testDiffedChangesExcludesRestrictedItems()
|
||||
{
|
||||
$this->markTestIncomplete();
|
||||
}
|
||||
|
||||
public function testDiffedChangesTitle() {
|
||||
$page = new Page(array('Title' => 'My Title'));
|
||||
$page->write();
|
||||
$page->publish('Stage', 'Live');
|
||||
|
||||
$page->Title = 'My Changed Title';
|
||||
$page->write();
|
||||
$page->publish('Stage', 'Live');
|
||||
public function testDiffedChangesIncludesFullHistory()
|
||||
{
|
||||
$this->markTestIncomplete();
|
||||
}
|
||||
|
||||
$page->Title = 'My Unpublished Changed Title';
|
||||
$page->write();
|
||||
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);
|
||||
|
||||
// Strip spaces from test output because they're not reliably maintained by the HTML Tidier
|
||||
$cleanDiffOutput = function($val) {
|
||||
return str_replace(' ','',strip_tags($val));
|
||||
};
|
||||
$page->Title = 'My Unpublished Changed Title';
|
||||
$page->write();
|
||||
|
||||
$this->assertContains(
|
||||
str_replace(' ' ,'',_t('RSSHistory.TITLECHANGED', 'Title has changed:') . 'My Changed Title'),
|
||||
array_map($cleanDiffOutput, $page->getDiffList()->column('DiffTitle')),
|
||||
'Detects published title changes'
|
||||
);
|
||||
// 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->assertNotContains(
|
||||
str_replace(' ' ,'',_t('RSSHistory.TITLECHANGED', 'Title has changed:') . 'My Unpublished Changed Title'),
|
||||
array_map($cleanDiffOutput, $page->getDiffList()->column('DiffTitle')),
|
||||
'Ignores unpublished title changes'
|
||||
);
|
||||
}
|
||||
$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