FIX: Minor functional alterations and CI improvements

FIX: PSR-2 codebase. Formatting via phpcbf
FIX: rendering bug in allchanges
FIX: update .gitattributes to not export codecov's config file
FIX: Update SiteTree_versions to the ss4 equivalent SiteTree_Versions
This commit is contained in:
Dylan Wagstaff 2017-12-12 09:47:35 +13:00
parent 17cf3d7487
commit 67e112fd12
12 changed files with 805 additions and 773 deletions

1
.gitattributes vendored
View File

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

View File

@ -22,7 +22,6 @@ before_script:
# Install composer dependencies # Install composer dependencies
- composer validate - composer validate
- composer require --no-update silverstripe/recipe-core:1.0.x-dev
- if [[ $DB == PGSQL ]]; then composer require --no-update silverstripe/postgresql 2.0.x-dev; fi - if [[ $DB == PGSQL ]]; then composer require --no-update silverstripe/postgresql 2.0.x-dev; fi
- composer install --prefer-dist --no-interaction --no-progress --no-suggest --optimize-autoloader --verbose --profile - composer install --prefer-dist --no-interaction --no-progress --no-suggest --optimize-autoloader --verbose --profile

View File

@ -12,7 +12,9 @@
], ],
"require": "require":
{ {
"silverstripe/recipe-cms": "^1" "silverstripe/cms": "^4",
"silverstripe/versioned": "^1",
"silverstripe/siteconfig": "^4"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^5.7", "phpunit/phpunit": "^5.7",

View File

@ -1,4 +1,4 @@
<phpunit bootstrap="vendor/silverstripe/framework/tests/bootstrap.php" colors="true"> <phpunit bootstrap="vendor/silverstripe/cms/tests/bootstrap.php" colors="true">
<testsuite name="Default"> <testsuite name="Default">
<directory>tests/</directory> <directory>tests/</directory>
</testsuite> </testsuite>

View File

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

View File

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

View File

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

View File

@ -13,206 +13,215 @@ use SilverStripe\Forms\LiteralField;
use SilverStripe\SiteConfig\SiteConfig; use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\CMS\Model\SiteTreeExtension; use SilverStripe\CMS\Model\SiteTreeExtension;
class VersionFeed extends SiteTreeExtension { class VersionFeed extends SiteTreeExtension
{
private static $db = array( private static $db = array(
'PublicHistory' => 'Boolean(true)' 'PublicHistory' => 'Boolean(true)'
); );
private static $defaults = array( private static $defaults = array(
'PublicHistory' => true 'PublicHistory' => true
); );
public function updateFieldLabels(&$labels) { public function updateFieldLabels(&$labels)
$labels['PublicHistory'] = _t('RSSHistory.LABEL', 'Make history public'); {
} $labels['PublicHistory'] = _t('RSSHistory.LABEL', 'Make history public');
}
/** /**
* Enable the allchanges feed * Enable the allchanges feed
* *
* @config * @config
* @var bool * @var bool
*/ */
private static $allchanges_enabled = true; private static $allchanges_enabled = true;
/** /**
* Allchanges feed limit of items. * Allchanges feed limit of items.
* *
* @config * @config
* @var int * @var int
*/ */
private static $allchanges_limit = 20; private static $allchanges_limit = 20;
/** /**
* Enables RSS feed for page-specific changes * Enables RSS feed for page-specific changes
* *
* @config * @config
* @var bool * @var bool
*/ */
private static $changes_enabled = true; private static $changes_enabled = true;
/** /**
* Changes feed limit of items. * Changes feed limit of items.
* *
* @config * @config
* @var int * @var int
*/ */
private static $changes_limit = 100; private static $changes_limit = 100;
/** /**
* Compile a list of changes to the current page, excluding non-published and explicitly secured versions. * 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 $highestVersion Top version number to consider.
* @param int $limit Limit to the amount of items returned. * @param int $limit Limit to the amount of items returned.
* *
* @returns ArrayList List of cleaned records. * @returns ArrayList List of cleaned records.
*/ */
public function getDiffList($highestVersion = null, $limit = 100) { 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. // This can leak secured content if it was protected via inherited setting.
$offset = $highestVersion ? "AND \"SiteTree_versions\".\"Version\"<='".(int)$highestVersion."'" : ''; // For now the users will need to be aware about this shortcoming.
// Get just enough elements for diffing. We need one more than desired to have something to compare to. $offset = $highestVersion ? "AND \"SiteTree_Versions\".\"Version\"<='".(int)$highestVersion."'" : '';
$qLimit = (int)$limit + 1; // Get just enough elements for diffing. We need one more than desired to have something to compare to.
$versions = $this->owner->allVersions( $qLimit = (int)$limit + 1;
"\"WasPublished\"='1' AND \"CanViewType\" IN ('Anyone', 'Inherit') $offset", $versions = $this->owner->allVersions(
"\"SiteTree\".\"LastEdited\" DESC, \"SiteTree\".\"ID\" DESC", "\"WasPublished\"='1' AND \"CanViewType\" IN ('Anyone', 'Inherit') $offset",
$qLimit "\"SiteTree\".\"LastEdited\" DESC, \"SiteTree\".\"ID\" DESC",
); $qLimit
);
// Process the list to add the comparisons. // Process the list to add the comparisons.
$changeList = new ArrayList(); $changeList = new ArrayList();
$previous = null; $previous = null;
$count = 0; $count = 0;
foreach ($versions as $version) { foreach ($versions as $version) {
$changed = false; $changed = false;
// Check if we have something to compare with. // Check if we have something to compare with.
if (isset($previous)) { if (isset($previous)) {
// Produce the diff fields for use in the template.
if ($version->Title != $previous->Title) {
$diffTitle = Diff::compareHTML($version->Title, $previous->Title);
// Produce the diff fields for use in the template. $version->DiffTitle = DBField::create_field('HTMLText', null);
if ($version->Title != $previous->Title) { $version->DiffTitle->setValue(
$diffTitle = Diff::compareHTML($version->Title, $previous->Title); sprintf(
'<div><em>%s</em> ' . $diffTitle . '</div>',
_t('RSSHistory.TITLECHANGED', 'Title has changed:')
)
);
$changed = true;
}
$version->DiffTitle = DBField::create_field('HTMLText', null); if ($version->Content != $previous->Content) {
$version->DiffTitle->setValue( $diffContent = Diff::compareHTML($version->Content, $previous->Content);
sprintf(
'<div><em>%s</em> ' . $diffTitle . '</div>',
_t('RSSHistory.TITLECHANGED', 'Title has changed:')
)
);
$changed = true;
}
if ($version->Content != $previous->Content) { $version->DiffContent = DBField::create_field('HTMLText', null);
$diffContent = Diff::compareHTML($version->Content, $previous->Content); $version->DiffContent->setValue('<div>'.$diffContent.'</div>');
$changed = true;
}
$version->DiffContent = DBField::create_field('HTMLText', null); // Copy the link so it can be cached.
$version->DiffContent->setValue('<div>'.$diffContent.'</div>'); $version->GeneratedLink = $version->AbsoluteLink();
$changed = true; }
}
// Copy the link so it can be cached. // Omit the versions that haven't been visibly changed (only takes the above fields into consideration).
$version->GeneratedLink = $version->AbsoluteLink(); if ($changed) {
} $changeList->push($version);
$count++;
}
// Omit the versions that haven't been visibly changed (only takes the above fields into consideration). // Store the last version for comparison.
if ($changed) { $previous = $version;
$changeList->push($version); }
$count++;
}
// Store the last version for comparison. // Make sure enough diff items have been generated to satisfy the $limit. If we ran out, add the final,
$previous = $version; // 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);
}
// Make sure enough diff items have been generated to satisfy the $limit. If we ran out, add the final, return $changeList;
// 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.
*
* @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;
* 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)
* Compile a list of changes to the current page, excluding non-published and explicitly secured versions. {
* if (!Config::inst()->get(get_class(), 'changes_enabled')) {
* @deprecated 2.0.0 Use VersionFeed::getDiffList instead return;
* }
* @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) { // Add public history field.
if(!Config::inst()->get(get_class(), 'changes_enabled')) return; $fields->addFieldToTab('Root.Settings', $publicHistory = new FieldGroup(
new CheckboxField('PublicHistory', $this->owner->fieldLabel('PublicHistory'))
));
// Add public history field. $warning = _t(
$fields->addFieldToTab('Root.Settings', $publicHistory = new FieldGroup( 'VersionFeed.Warning',
new CheckboxField('PublicHistory', $this->owner->fieldLabel('PublicHistory') "Publicising the history will also disclose the changes that have at the time been protected " .
))); "from the public view."
);
$warning = _t( $fields->addFieldToTab('Root.Settings', new LiteralField('PublicHistoryWarning', $warning), 'PublicHistory');
'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."
);
if ($this->owner->CanViewType!='Anyone') { $fields->addFieldToTab('Root.Settings', new LiteralField('PublicHistoryWarning2', $warning), 'CanViewType');
$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 getSiteRSSLink() { public function getDefaultRSSLink()
// TODO: This link should be from the homepage, not this page. {
if(Config::inst()->get(get_class(), 'allchanges_enabled') if (Config::inst()->get(get_class(), 'changes_enabled') && $this->owner->PublicHistory) {
&& SiteConfig::current_site_config()->AllChangesEnabled return $this->owner->Link('changes');
) { }
return $this->owner->Link('allchanges'); }
}
}
public function getDefaultRSSLink() {
if(Config::inst()->get(get_class(), 'changes_enabled') && $this->owner->PublicHistory) {
return $this->owner->Link('changes');
}
}
} }

View File

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

View File

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

View File

@ -17,230 +17,242 @@ use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
class VersionFeedFunctionalTest extends FunctionalTest
{
protected $usesDatabase = true;
class VersionFeedFunctionalTest extends FunctionalTest { protected $baseURI = 'http://www.fakesite.test';
protected $usesDatabase = true;
protected $baseURI = 'http://www.fakesite.test'; protected static $required_extensions = array(
'Page' => array(VersionFeed::class),
'PageController' => array(VersionFeedController::class),
);
protected static $required_extensions = array( protected $userIP;
'Page' => array(VersionFeed::class),
'PageController' => array(VersionFeedController::class),
);
protected $userIP; protected function setUp()
{
Director::config()->set('alternate_base_url', $this->baseURI);
protected function setUp() { parent::setUp();
Director::config()->set('alternate_base_url', $this->baseURI);
parent::setUp(); $cache = Injector::inst()->get(
CacheInterface::class . '.VersionFeedController'
);
$cache->clear();
$cache = Injector::inst()->get( $this->userIP = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
CacheInterface::class . '.VersionFeedController'
);
$cache->clear();
$this->userIP = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
// Enable history by default // Enable history by default
Config::modify()->set(VersionFeed::class, 'changes_enabled', true); Config::modify()->set(VersionFeed::class, 'changes_enabled', true);
Config::modify()->set(VersionFeed::class, 'allchanges_enabled', true); Config::modify()->set(VersionFeed::class, 'allchanges_enabled', true);
// Disable caching and locking by default // Disable caching and locking by default
Config::modify()->set(CachedContentFilter::class, 'cache_enabled', false); Config::modify()->set(CachedContentFilter::class, 'cache_enabled', false);
Config::modify()->set(RateLimitFilter::class, 'lock_timeout', 0); Config::modify()->set(RateLimitFilter::class, 'lock_timeout', 0);
Config::modify()->set(RateLimitFilter::class, 'lock_bypage', false); Config::modify()->set(RateLimitFilter::class, 'lock_bypage', false);
Config::modify()->set(RateLimitFilter::class, 'lock_byuserip', false); Config::modify()->set(RateLimitFilter::class, 'lock_byuserip', false);
Config::modify()->set(RateLimitFilter::class, 'lock_cooldown', false); Config::modify()->set(RateLimitFilter::class, 'lock_cooldown', false);
} }
public function tearDown() { public function tearDown()
Director::config()->set('alternate_base_url', null); {
Director::config()->set('alternate_base_url', null);
$_SERVER['REMOTE_ADDR'] = $this->userIP; $_SERVER['REMOTE_ADDR'] = $this->userIP;
parent::tearDown(); parent::tearDown();
} }
public function testPublicHistory() { public function testPublicHistory()
$page = $this->createPageWithChanges(array('PublicHistory' => false)); {
$page = $this->createPageWithChanges(array('PublicHistory' => false));
$response = $this->get($page->RelativeLink('changes')); $response = $this->get($page->RelativeLink('changes'));
$this->assertEquals(404, $response->getStatusCode()); $this->assertEquals(404, $response->getStatusCode());
$response = $this->get($page->RelativeLink('allchanges')); $response = $this->get($page->RelativeLink('allchanges'));
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
$xml = simplexml_load_string($response->getBody()); $xml = simplexml_load_string($response->getBody());
$this->assertFalse((bool)$xml->channel->item); $this->assertFalse((bool)$xml->channel->item);
$page = $this->createPageWithChanges(array('PublicHistory' => true)); $page = $this->createPageWithChanges(array('PublicHistory' => true));
$response = $this->get($page->RelativeLink('changes')); $response = $this->get($page->RelativeLink('changes'));
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
$xml = simplexml_load_string($response->getBody()); $xml = simplexml_load_string($response->getBody());
$this->assertTrue((bool)$xml->channel->item); $this->assertTrue((bool)$xml->channel->item);
$response = $this->get($page->RelativeLink('allchanges')); $response = $this->get($page->RelativeLink('allchanges'));
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
$xml = simplexml_load_string($response->getBody()); $xml = simplexml_load_string($response->getBody());
$this->assertTrue((bool)$xml->channel->item); $this->assertTrue((bool)$xml->channel->item);
} }
public function testRateLimiting() { public function testRateLimiting()
// Re-enable locking just for this test {
Config::modify()->set(RateLimitFilter::class, 'lock_timeout', 20); // Re-enable locking just for this test
Config::modify()->set(CachedContentFilter::class, 'cache_enabled', true); Config::modify()->set(RateLimitFilter::class, 'lock_timeout', 20);
Config::modify()->set(CachedContentFilter::class, 'cache_enabled', true);
$page1 = $this->createPageWithChanges(array('PublicHistory' => true, 'Title' => 'Page1')); $page1 = $this->createPageWithChanges(array('PublicHistory' => true, 'Title' => 'Page1'));
$page2 = $this->createPageWithChanges(array('PublicHistory' => true, 'Title' => 'Page2')); $page2 = $this->createPageWithChanges(array('PublicHistory' => true, 'Title' => 'Page2'));
// Artifically set cache lock // Artifically set cache lock
Config::modify()->set(RateLimitFilter::class, 'lock_byuserip', false); Config::modify()->set(RateLimitFilter::class, 'lock_byuserip', false);
$cache = Injector::inst()->get( $cache = Injector::inst()->get(
CacheInterface::class . '.VersionFeedController' CacheInterface::class . '.VersionFeedController'
); );
$cache->set(RateLimitFilter::CACHE_PREFIX, time() + 10); $cache->set(RateLimitFilter::CACHE_PREFIX, time() + 10);
// Test normal hit // Test normal hit
$response = $this->get($page1->RelativeLink('changes')); $response = $this->get($page1->RelativeLink('changes'));
$this->assertEquals(429, $response->getStatusCode()); $this->assertEquals(429, $response->getStatusCode());
$this->assertGreaterThan(0, $response->getHeader('Retry-After')); $this->assertGreaterThan(0, $response->getHeader('Retry-After'));
$response = $this->get($page2->RelativeLink('changes')); $response = $this->get($page2->RelativeLink('changes'));
$this->assertEquals(429, $response->getStatusCode()); $this->assertEquals(429, $response->getStatusCode());
$this->assertGreaterThan(0, $response->getHeader('Retry-After')); $this->assertGreaterThan(0, $response->getHeader('Retry-After'));
// Test page specific lock // Test page specific lock
Config::modify()->set(RateLimitFilter::class, 'lock_bypage', true); Config::modify()->set(RateLimitFilter::class, 'lock_bypage', true);
$key = implode('_', array( $key = implode('_', array(
'changes', 'changes',
$page1->ID, $page1->ID,
Versioned::get_versionnumber_by_stage(SiteTree::class, 'Live', $page1->ID, false) Versioned::get_versionnumber_by_stage(SiteTree::class, 'Live', $page1->ID, false)
)); ));
$key = RateLimitFilter::CACHE_PREFIX . '_' . md5($key); $key = RateLimitFilter::CACHE_PREFIX . '_' . md5($key);
$cache->set($key, time() + 10); $cache->set($key, time() + 10);
$response = $this->get($page1->RelativeLink('changes')); $response = $this->get($page1->RelativeLink('changes'));
$this->assertEquals(429, $response->getStatusCode()); $this->assertEquals(429, $response->getStatusCode());
$this->assertGreaterThan(0, $response->getHeader('Retry-After')); $this->assertGreaterThan(0, $response->getHeader('Retry-After'));
$response = $this->get($page2->RelativeLink('changes')); $response = $this->get($page2->RelativeLink('changes'));
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
Config::modify()->set(RateLimitFilter::class, 'lock_bypage', false); Config::modify()->set(RateLimitFilter::class, 'lock_bypage', false);
// Test rate limit hit by IP // Test rate limit hit by IP
Config::modify()->set(RateLimitFilter::class, 'lock_byuserip', true); Config::modify()->set(RateLimitFilter::class, 'lock_byuserip', true);
$_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$cache->set(RateLimitFilter::CACHE_PREFIX . '_' . md5('127.0.0.1'), time() + 10); $cache->set(RateLimitFilter::CACHE_PREFIX . '_' . md5('127.0.0.1'), time() + 10);
$response = $this->get($page1->RelativeLink('changes')); $response = $this->get($page1->RelativeLink('changes'));
$this->assertEquals(429, $response->getStatusCode()); $this->assertEquals(429, $response->getStatusCode());
$this->assertGreaterThan(0, $response->getHeader('Retry-After')); $this->assertGreaterThan(0, $response->getHeader('Retry-After'));
// Test rate limit doesn't hit other IP // Test rate limit doesn't hit other IP
$_SERVER['REMOTE_ADDR'] = '127.0.0.20'; $_SERVER['REMOTE_ADDR'] = '127.0.0.20';
$cache->set(RateLimitFilter::CACHE_PREFIX . '_' . md5('127.0.0.1'), time() + 10); $cache->set(RateLimitFilter::CACHE_PREFIX . '_' . md5('127.0.0.1'), time() + 10);
$response = $this->get($page1->RelativeLink('changes')); $response = $this->get($page1->RelativeLink('changes'));
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
// Restore setting // Restore setting
Config::modify()->set(RateLimitFilter::class, 'lock_byuserip', false); Config::modify()->set(RateLimitFilter::class, 'lock_byuserip', false);
Config::modify()->set(RateLimitFilter::class, 'lock_timeout', 0); Config::modify()->set(RateLimitFilter::class, 'lock_timeout', 0);
Config::modify()->set(CachedContentFilter::class, 'cache_enabled', false); Config::modify()->set(CachedContentFilter::class, 'cache_enabled', false);
} }
public function testContainsChangesForPageOnly() { public function testContainsChangesForPageOnly()
$page1 = $this->createPageWithChanges(array('Title' => 'Page1')); {
$page2 = $this->createPageWithChanges(array('Title' => 'Page2')); $page1 = $this->createPageWithChanges(array('Title' => 'Page1'));
$page2 = $this->createPageWithChanges(array('Title' => 'Page2'));
$response = $this->get($page1->RelativeLink('changes')); $response = $this->get($page1->RelativeLink('changes'));
$xml = simplexml_load_string($response->getBody()); $xml = simplexml_load_string($response->getBody());
$titles = array_map(function($item) {return (string)$item->title;}, $xml->xpath('//item')); $titles = array_map(function ($item) {
// TODO Unclear if this should contain the original version return (string)$item->title;
$this->assertContains('Changed: Page1', $titles); }, $xml->xpath('//item'));
$this->assertNotContains('Changed: Page2', $titles); // 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')); $response = $this->get($page2->RelativeLink('changes'));
$xml = simplexml_load_string($response->getBody()); $xml = simplexml_load_string($response->getBody());
$titles = array_map(function($item) {return (string)$item->title;}, $xml->xpath('//item')); $titles = array_map(function ($item) {
// TODO Unclear if this should contain the original version return (string)$item->title;
$this->assertNotContains('Changed: Page1', $titles); }, $xml->xpath('//item'));
$this->assertContains('Changed: Page2', $titles); // TODO Unclear if this should contain the original version
} $this->assertNotContains('Changed: Page1', $titles);
$this->assertContains('Changed: Page2', $titles);
}
public function testContainsAllChangesForAllPages() { public function testContainsAllChangesForAllPages()
$page1 = $this->createPageWithChanges(array('Title' => 'Page1')); {
$page2 = $this->createPageWithChanges(array('Title' => 'Page2')); $page1 = $this->createPageWithChanges(array('Title' => 'Page1'));
$page2 = $this->createPageWithChanges(array('Title' => 'Page2'));
$response = $this->get($page1->RelativeLink('allchanges')); $response = $this->get($page1->RelativeLink('allchanges'));
$xml = simplexml_load_string($response->getBody()); $xml = simplexml_load_string($response->getBody());
$titles = array_map(function($item) {return (string)$item->title;}, $xml->xpath('//item')); $titles = array_map(function ($item) {
$this->assertContains('Page1', $titles); return (string)$item->title;
$this->assertContains('Page2', $titles); }, $xml->xpath('//item'));
} $this->assertContains('Page1', $titles);
$this->assertContains('Page2', $titles);
}
protected function createPageWithChanges($seed = null) { protected function createPageWithChanges($seed = null)
$page = new Page(); {
$page = new Page();
$seed = array_merge(array( $seed = array_merge(array(
'Title' => 'My Title', 'Title' => 'My Title',
'Content' => 'My Content' 'Content' => 'My Content'
), $seed); ), $seed);
$page->update($seed); $page->update($seed);
$page->write(); $page->write();
$page->publish('Stage', 'Live'); $page->publish('Stage', 'Live');
$page->update(array( $page->update(array(
'Title' => 'Changed: ' . $seed['Title'], 'Title' => 'Changed: ' . $seed['Title'],
'Content' => 'Changed: ' . $seed['Content'], 'Content' => 'Changed: ' . $seed['Content'],
)); ));
$page->write(); $page->write();
$page->publish('Stage', 'Live'); $page->publish('Stage', 'Live');
$page->update(array( $page->update(array(
'Title' => 'Changed again: ' . $seed['Title'], 'Title' => 'Changed again: ' . $seed['Title'],
'Content' => 'Changed again: ' . $seed['Content'], 'Content' => 'Changed again: ' . $seed['Content'],
)); ));
$page->write(); $page->write();
$page->publish('Stage', 'Live'); $page->publish('Stage', 'Live');
$page->update(array( $page->update(array(
'Title' => 'Unpublished: ' . $seed['Title'], 'Title' => 'Unpublished: ' . $seed['Title'],
'Content' => 'Unpublished: ' . $seed['Content'], 'Content' => 'Unpublished: ' . $seed['Content'],
)); ));
$page->write(); $page->write();
return $page; return $page;
} }
/** /**
* Tests response code for globally disabled feedss * Tests response code for globally disabled feedss
*/ */
public function testFeedViewability() { public function testFeedViewability()
{
// Nested loop through each configuration // Nested loop through each configuration
foreach(array(true, false) as $publicHistory_Page) { foreach (array(true, false) as $publicHistory_Page) {
$page = $this->createPageWithChanges(array('PublicHistory' => $publicHistory_Page, 'Title' => 'Page')); $page = $this->createPageWithChanges(array('PublicHistory' => $publicHistory_Page, 'Title' => 'Page'));
// Test requests to 'changes' action // Test requests to 'changes' action
foreach(array(true, false) as $publicHistory_Config) { foreach (array(true, false) as $publicHistory_Config) {
Config::modify()->set(VersionFeed::class, 'changes_enabled', $publicHistory_Config); Config::modify()->set(VersionFeed::class, 'changes_enabled', $publicHistory_Config);
$expectedResponse = $publicHistory_Page && $publicHistory_Config ? 200 : 404; $expectedResponse = $publicHistory_Page && $publicHistory_Config ? 200 : 404;
$response = $this->get($page->RelativeLink('changes')); $response = $this->get($page->RelativeLink('changes'));
$this->assertEquals($expectedResponse, $response->getStatusCode()); $this->assertEquals($expectedResponse, $response->getStatusCode());
} }
// Test requests to 'allchanges' action on each page // Test requests to 'allchanges' action on each page
foreach(array(true, false) as $allChanges_Config) { foreach (array(true, false) as $allChanges_Config) {
foreach(array(true, false) as $allChanges_SiteConfig) { foreach (array(true, false) as $allChanges_SiteConfig) {
Config::modify()->set(VersionFeed::class, 'allchanges_enabled', $allChanges_Config); Config::modify()->set(VersionFeed::class, 'allchanges_enabled', $allChanges_Config);
$siteConfig = SiteConfig::current_site_config(); $siteConfig = SiteConfig::current_site_config();
$siteConfig->AllChangesEnabled = $allChanges_SiteConfig; $siteConfig->AllChangesEnabled = $allChanges_SiteConfig;
$siteConfig->write(); $siteConfig->write();
$expectedResponse = $allChanges_Config && $allChanges_SiteConfig ? 200 : 404;
$response = $this->get($page->RelativeLink('allchanges'));
$this->assertEquals($expectedResponse, $response->getStatusCode());
}
}
}
}
$expectedResponse = $allChanges_Config && $allChanges_SiteConfig ? 200 : 404;
$response = $this->get($page->RelativeLink('allchanges'));
$this->assertEquals($expectedResponse, $response->getStatusCode());
}
}
}
}
} }

View File

@ -2,7 +2,6 @@
namespace SilverStripe\VersionFeed\Tests; namespace SilverStripe\VersionFeed\Tests;
use Page; use Page;
use SilverStripe\VersionFeed\VersionFeed; use SilverStripe\VersionFeed\VersionFeed;
use SilverStripe\VersionFeed\VersionFeedController; use SilverStripe\VersionFeed\VersionFeedController;
@ -10,56 +9,58 @@ use SilverStripe\Dev\SapphireTest;
use SilverStripe\CMS\Model\SiteTree; use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\CMS\Controllers\ContentController; use SilverStripe\CMS\Controllers\ContentController;
class VersionFeedTest extends SapphireTest
{
class VersionFeedTest extends SapphireTest { protected $usesDatabase = true;
protected $usesDatabase = true; protected static $required_extensions = [
SiteTree::class => [VersionFeed::class],
ContentController::class => [VersionFeedController::class],
];
protected static $required_extensions = [ protected $illegalExtensions = [
SiteTree::class => [VersionFeed::class], 'SiteTree' => ['Translatable']
ContentController::class => [VersionFeedController::class], ];
];
protected $illegalExtensions = [ public function testDiffedChangesExcludesRestrictedItems()
'SiteTree' => ['Translatable'] {
]; $this->markTestIncomplete();
}
public function testDiffedChangesExcludesRestrictedItems() { public function testDiffedChangesIncludesFullHistory()
$this->markTestIncomplete(); {
} $this->markTestIncomplete();
}
public function testDiffedChangesIncludesFullHistory() { public function testDiffedChangesTitle()
$this->markTestIncomplete(); {
} $page = new Page(['Title' => 'My Title']);
$page->write();
$page->publish('Stage', 'Live');
public function testDiffedChangesTitle() { $page->Title = 'My Changed Title';
$page = new Page(['Title' => 'My Title']); $page->write();
$page->write(); $page->publish('Stage', 'Live');
$page->publish('Stage', 'Live');
$page->Title = 'My Changed Title'; $page->Title = 'My Unpublished Changed Title';
$page->write(); $page->write();
$page->publish('Stage', 'Live');
$page->Title = 'My Unpublished Changed Title'; // Strip spaces from test output because they're not reliably maintained by the HTML Tidier
$page->write(); $cleanDiffOutput = function ($val) {
return str_replace(' ', '', strip_tags($val));
};
// Strip spaces from test output because they're not reliably maintained by the HTML Tidier $this->assertContains(
$cleanDiffOutput = function($val) { str_replace(' ', '', _t('RSSHistory.TITLECHANGED', 'Title has changed:') . 'My Changed Title'),
return str_replace(' ','',strip_tags($val)); array_map($cleanDiffOutput, $page->getDiffList()->column('DiffTitle')),
}; 'Detects published 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'
);
}
$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'
);
}
} }