diff --git a/.travis.yml b/.travis.yml
index 129244d26..05a390383 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,9 +7,6 @@ addons:
packages:
- tidy
-php:
- - 5.4
-
env:
global:
- CORE_RELEASE=3
@@ -17,28 +14,19 @@ env:
- "ARTIFACTS_S3_BUCKET=silverstripe-travis-artifacts"
- secure: "DjwZKhY/c0wXppGmd8oEMiTV0ayfOXiCmi9Lg1aXoSXNnj+sjLmhYwhUWjehjR6IX0MRtzJG6v7V5Y+4nSGe+i+XIrBQnhPQ95Jrkm1gKofX2mznWTl9npQElNS1DXi58NLPbiB3qxHWGFBRAWmRQrsAouyZabkPnChnSa9ldOg="
- secure: "UmbXCNLK0f2Dk+7qX8bOVcgIt4QhRvccoWvMUxaPtIU+95HCbG10eeCxvfOeBax+tHcRXmeCG4vM4tcuT/WoANkAma/VX74DylFjbWhks2tsKOcr2kjTrOwe6Q9CXOBjVAlcx0lnV/a+w83KARjXGnCrIbE7p7r4EDw31rkVufg="
- matrix:
- - DB=MYSQL
- - DB=SQLITE
- - DB=PGSQL
matrix:
- allow_failures:
- - php: hhvm
-
include:
- - php: 5.4
- env: DB=MYSQL PDO=1
- - php: 5.5
- env: DB=MYSQL
- - php: 5.6
- env: DB=MYSQL
- - php: 5.4
- env: DB=MYSQL BEHAT_TEST=1
- php: 5.3
env: DB=MYSQL
- - php: hhvm
- env: DB=MYSQL
+ - php: 5.4
+ env: DB=PGSQL
+ - php: 5.5
+ env: DB=SQLITE
+ - php: 5.6
+ env: DB=MYSQL PDO=1
+ - php: 5.6
+ env: DB=MYSQL BEHAT_TEST=1
before_script:
- composer self-update || true
@@ -52,6 +40,7 @@ before_script:
script:
- "if [ \"$BEHAT_TEST\" = \"\" ]; then vendor/bin/phpunit framework/tests; fi"
+ - "if [ \"$BEHAT_TEST\" = \"\" ]; then vendor/bin/phpunit framework/admin/tests; fi"
- "if [ \"$BEHAT_TEST\" = \"1\" ]; then vendor/bin/behat @framework; fi"
after_failure:
@@ -64,11 +53,6 @@ branches:
- 2.3
- translation-staging
-notifications:
- irc:
- channels:
- - "irc.freenode.org#silverstripe"
-
# global:
# - secure: "AZmjVPtUD8JBA7ag4ULlEwEKXSEZbIUjDHeRBFugaOtdsn5yigGLmwYbzsg2tq7k7UkdbbAlGct0SUbiRJb9F2wPA5+eUd/p49fgDIU6CTSWIlT87H2BwgOrxKwS9sDwxLptPFM6vWQ8JKYSNGmVIepie9kQZbu4L2k5k6B69jQ="
# - secure: "f3kKpUn9cS5K+p/E52cMqN18cDApol/43LanDmHO6mo3iRAztk3jZLyfNOUq6JASKMqdh8+9kencRpEoaAYbcQnDPoZsT9POResiJ9/ADKB6RwWy+lcFHUp9E2Zf/x2VRh9FmXEguDhpWzkJqzWYJGCSig1IBp/+TjzKnsjQHIY="
diff --git a/admin/code/CMSProfileController.php b/admin/code/CMSProfileController.php
index 55590c47c..259aded88 100644
--- a/admin/code/CMSProfileController.php
+++ b/admin/code/CMSProfileController.php
@@ -50,20 +50,20 @@ class CMSProfileController extends LeftAndMain {
}
public function canView($member = null) {
- if(!$member && $member !== FALSE) $member = Member::currentUser();
+ if(!$member && $member !== false) $member = Member::currentUser();
// cms menus only for logged-in members
if(!$member) return false;
- // Only check for generic CMS permissions
+ // Check they can access the CMS and that they are trying to edit themselves
if(
- !Permission::checkMember($member, "CMS_ACCESS_LeftAndMain")
- && !Permission::checkMember($member, "CMS_ACCESS_CMSMain")
+ Permission::checkMember($member, "CMS_ACCESS")
+ && $member->ID === Member::currentUserID()
) {
- return false;
+ return true;
}
- return true;
+ return false;
}
public function save($data, $form) {
diff --git a/admin/css/screen.css b/admin/css/screen.css
index 7687a2db8..afd223c55 100644
--- a/admin/css/screen.css
+++ b/admin/css/screen.css
@@ -669,7 +669,7 @@ body.cms-dialog { overflow: auto; background: url("../images/textures/bg_cms_mai
.htmleditorfield-dialog.ui-dialog-content { padding: 0; position: relative; }
.htmleditorfield-dialog .htmleditorfield-from-web .CompositeField { overflow: hidden; *zoom: 1; }
.htmleditorfield-dialog .htmleditorfield-from-web #RemoteURL { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; width: 55%; max-width: 512px; float: left; position: relative; }
-.htmleditorfield-dialog .htmleditorfield-from-web #RemoteURL label { position: absolute; left: 8px; top: 13px; font-weight: normal; color: #888; }
+.htmleditorfield-dialog .htmleditorfield-from-web #RemoteURL label { position: absolute; left: 8px; top: 13px; font-weight: normal; color: #888; width: 35px; padding-right: 0; }
.htmleditorfield-dialog .htmleditorfield-from-web #RemoteURL .middleColumn { margin-left: 0; }
.htmleditorfield-dialog .htmleditorfield-from-web #RemoteURL input.remoteurl { padding-left: 40px; max-width: 350px; }
.htmleditorfield-dialog .htmleditorfield-from-web button.add-url { margin-top: 20px; overflow: hidden; *zoom: 1; border: none; background: none; opacity: 0.8; cursor: hand; }
diff --git a/admin/javascript/LeftAndMain.Preview.js b/admin/javascript/LeftAndMain.Preview.js
index e4b60639f..068c360a0 100644
--- a/admin/javascript/LeftAndMain.Preview.js
+++ b/admin/javascript/LeftAndMain.Preview.js
@@ -179,18 +179,14 @@
* Store the preview options for this page.
*/
saveState : function(name, value) {
- if(!window.localStorage) return;
-
- window.localStorage.setItem('cms-preview-state-' + name, value);
+ if(this._supportsLocalStorage()) window.localStorage.setItem('cms-preview-state-' + name, value);
},
/**
* Load previously stored preferences
*/
loadState : function(name) {
- if(!window.localStorage) return;
-
- return window.localStorage.getItem('cms-preview-state-' + name);
+ if(this._supportsLocalStorage()) return window.localStorage.getItem('cms-preview-state-' + name);
},
/**
@@ -276,6 +272,23 @@
this._super();
},
+
+ /**
+ * Detect and use localStorage if available. In IE11 windows 8.1 call to window.localStorage was throwing out an access denied error in some cases which was causing the preview window not to display correctly in the CMS admin area.
+ */
+ _supportsLocalStorage: function() {
+ var uid = new Date;
+ var storage;
+ var result;
+ try {
+ (storage = window.localStorage).setItem(uid, uid);
+ result = storage.getItem(uid) == uid;
+ storage.removeItem(uid);
+ return result && storage;
+ } catch (exception) {
+ console.warn('localStorge is not available due to current browser / system settings.');
+ }
+ },
/**
* Set the preview to unavailable - could be still visible. This is purely visual.
diff --git a/admin/scss/_style.scss b/admin/scss/_style.scss
index 6c67072a0..8faa2128b 100644
--- a/admin/scss/_style.scss
+++ b/admin/scss/_style.scss
@@ -1422,11 +1422,14 @@ body.cms-dialog {
float:left;
position: relative;
- label {
- position: absolute;
- left: 8px;
- top: 13px;
- font-weight: normal; color: #888;
+ label {
+ position: absolute;
+ left: 8px;
+ top: 13px;
+ font-weight: normal;
+ color: #888;
+ width: 35px;
+ padding-right: 0;
}
.middleColumn {
diff --git a/admin/tests/LeftAndMainTest.php b/admin/tests/LeftAndMainTest.php
index f594b43a8..9672f80fa 100644
--- a/admin/tests/LeftAndMainTest.php
+++ b/admin/tests/LeftAndMainTest.php
@@ -17,7 +17,7 @@ class LeftAndMainTest extends FunctionalTest {
// @todo fix controller stack problems and re-activate
//$this->autoFollowRedirection = false;
- CMSMenu::populate_menu();
+ $this->resetMenu();
$this->backupCss = Config::inst()->get('LeftAndMain', 'extra_requirements_css');
$this->backupJs = Config::inst()->get('LeftAndMain', 'extra_requirements_javascript');
@@ -34,6 +34,23 @@ class LeftAndMainTest extends FunctionalTest {
Requirements::set_combined_files_enabled(false);
}
+ /**
+ * Clear menu to default state as per LeftAndMain::init()
+ */
+ protected function resetMenu() {
+ CMSMenu::clear_menu();
+ CMSMenu::populate_menu();
+ CMSMenu::add_link(
+ 'Help',
+ _t('LeftAndMain.HELP', 'Help', 'Menu title'),
+ LeftAndMain::config()->help_link,
+ -2,
+ array(
+ 'target' => '_blank'
+ )
+ );
+ }
+
public function tearDown() {
parent::tearDown();
@@ -130,7 +147,8 @@ class LeftAndMainTest extends FunctionalTest {
$adminuser = $this->objFromFixture('Member','admin');
$this->session()->inst_set('loggedInAs', $adminuser->ID);
- $menuItems = singleton('LeftAndMain')->MainMenu();
+ $this->resetMenu();
+ $menuItems = singleton('LeftAndMain')->MainMenu(false);
foreach($menuItems as $menuItem) {
$link = $menuItem->Link;
@@ -159,6 +177,7 @@ class LeftAndMainTest extends FunctionalTest {
// anonymous user
$this->session()->inst_set('loggedInAs', null);
+ $this->resetMenu();
$menuItems = singleton('LeftAndMain')->MainMenu(false);
$this->assertEquals(
array_map($allValsFn, $menuItems->column('Code')),
@@ -167,19 +186,25 @@ class LeftAndMainTest extends FunctionalTest {
);
// restricted cms user
- $this->session()->inst_set('loggedInAs', $securityonlyuser->ID);
+ $this->logInAs($securityonlyuser);
+ $this->resetMenu();
$menuItems = singleton('LeftAndMain')->MainMenu(false);
$menuItems = array_map($allValsFn, $menuItems->column('Code'));
sort($menuItems);
$this->assertEquals(
$menuItems,
- array('Help', 'SecurityAdmin'),
+ array('CMSProfileController', 'SecurityAdmin','Help'),
'Groups with limited access can only access the interfaces they have permissions for'
);
// all cms sections user
- $this->session()->inst_set('loggedInAs', $allcmssectionsuser->ID);
+ $this->logInAs($allcmssectionsuser);
+ $this->resetMenu();
$menuItems = singleton('LeftAndMain')->MainMenu(false);
+ $this->assertContains('CMSProfileController',
+ array_map($allValsFn, $menuItems->column('Code')),
+ 'Group with CMS_ACCESS_LeftAndMain permission can edit own profile'
+ );
$this->assertContains('SecurityAdmin',
array_map($allValsFn, $menuItems->column('Code')),
'Group with CMS_ACCESS_LeftAndMain permission can access all sections'
@@ -190,7 +215,8 @@ class LeftAndMainTest extends FunctionalTest {
);
// admin
- $this->session()->inst_set('loggedInAs', $adminuser->ID);
+ $this->logInAs($adminuser);
+ $this->resetMenu();
$menuItems = singleton('LeftAndMain')->MainMenu(false);
$this->assertContains(
'SecurityAdmin',
@@ -270,6 +296,8 @@ class LeftAndMainTest_Object extends DataObject implements TestOnly {
'Sort' => 'Int',
);
+ private static $default_sort = '"Sort"';
+
private static $extensions = array(
'Hierarchy'
);
diff --git a/control/HTTP.php b/control/HTTP.php
index 45abaec4f..586ecddef 100644
--- a/control/HTTP.php
+++ b/control/HTTP.php
@@ -307,6 +307,9 @@ class HTTP {
/**
* Add the appropriate caching headers to the response, including If-Modified-Since / 304 handling.
+ * Note that setting HTTP::$cache_age will overrule any cache headers set by PHP's
+ * session_cache_limiter functionality. It is your responsibility to ensure only cacheable data
+ * is in fact cached, and HTTP::$cache_age isn't set when the HTTP body contains session-specific content.
*
* @param SS_HTTPResponse $body The SS_HTTPResponse object to augment. Omitted the argument or passing a string is
* deprecated; in these cases, the headers are output directly.
@@ -346,6 +349,11 @@ class HTTP {
if($cacheAge > 0) {
$cacheControlHeaders['max-age'] = self::$cache_age;
+
+ // Set empty pragma to avoid PHP's session_cache_limiter adding conflicting caching information,
+ // defaulting to "nocache" on most PHP configurations (see http://php.net/session_cache_limiter).
+ // Since it's a deprecated HTTP 1.0 option, all modern HTTP clients and proxies should
+ // prefer the caching information indicated through the "Cache-Control" header.
$responseHeaders["Pragma"] = "";
// To do: User-Agent should only be added in situations where you *are* actually
@@ -370,6 +378,11 @@ class HTTP {
// (http://support.microsoft.com/kb/323308)
// Note: this is also fixable by ticking "Do not save encrypted pages to disk" in advanced options.
$cacheControlHeaders['max-age'] = 3;
+
+ // Set empty pragma to avoid PHP's session_cache_limiter adding conflicting caching information,
+ // defaulting to "nocache" on most PHP configurations (see http://php.net/session_cache_limiter).
+ // Since it's a deprecated HTTP 1.0 option, all modern HTTP clients and proxies should
+ // prefer the caching information indicated through the "Cache-Control" header.
$responseHeaders["Pragma"] = "";
} else {
$cacheControlHeaders['no-cache'] = "true";
diff --git a/dev/SapphireTest.php b/dev/SapphireTest.php
index 4db25575e..4ce8522f8 100644
--- a/dev/SapphireTest.php
+++ b/dev/SapphireTest.php
@@ -351,6 +351,27 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
* tearDown method that's called once per test class rather once per test method.
*/
public function tearDownOnce() {
+ // If we have made changes to the extensions present, then migrate the database schema.
+ if($this->extensionsToReapply || $this->extensionsToRemove) {
+ // @todo: This isn't strictly necessary to restore extensions, but only to ensure that
+ // Object::$extra_methods is properly flushed. This should be replaced with a simple
+ // flush mechanism for each $class.
+ //
+ // Remove extensions added for testing
+ foreach($this->extensionsToRemove as $class => $extensions) {
+ foreach($extensions as $extension) {
+ $class::remove_extension($extension);
+ }
+ }
+
+ // Reapply ones removed
+ foreach($this->extensionsToReapply as $class => $extensions) {
+ foreach($extensions as $extension) {
+ $class::add_extension($extension);
+ }
+ }
+ }
+
//unnest injector / config now that the test suite is over
// this will reset all the extensions on the object too (see setUpOnce)
Injector::unnest();
@@ -400,7 +421,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
* Will collate all IDs form all fixtures if multiple fixtures are provided.
*
* @param string $className
- * @return A map of fixture-identifier => object-id
+ * @return array A map of fixture-identifier => object-id
*/
protected function allFixtureIDs($className) {
return $this->getFixtureFactory()->getIds($className);
diff --git a/dev/TeamCityListener.php b/dev/TeamCityListener.php
index 731830617..02402f48f 100644
--- a/dev/TeamCityListener.php
+++ b/dev/TeamCityListener.php
@@ -46,7 +46,7 @@ class TeamCityListener implements PHPUnit_Framework_TestListener {
public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) {
$class = get_class($test);
- $message = $this->escape($e->getMessage());
+ $message = $this->escape(PHPUnit_Framework_TestFailure::exceptionToString($e));
$trace = $this->escape($e->getTraceAsString());
echo "##teamcity[testFailed type='failure' name='{$class}.{$test->getName()}' message='$message'"
. " details='$trace']\n";
diff --git a/dev/TestRunner.php b/dev/TestRunner.php
index 0b516ad76..41c40fda4 100755
--- a/dev/TestRunner.php
+++ b/dev/TestRunner.php
@@ -82,9 +82,9 @@ class TestRunner extends Controller {
* top of the loader stacks.
*/
public static function use_test_manifest() {
- $flush = true;
- if(isset($_GET['flush']) && $_GET['flush'] === '0') {
- $flush = false;
+ $flush = false;
+ if(isset($_GET['flush']) && ($_GET['flush'] === '1' || $_GET['flush'] == 'all')) {
+ $flush = true;
}
$classManifest = new SS_ClassManifest(
diff --git a/docs/en/00_Getting_Started/02_Composer.md b/docs/en/00_Getting_Started/02_Composer.md
index 9caab927a..19899daa1 100644
--- a/docs/en/00_Getting_Started/02_Composer.md
+++ b/docs/en/00_Getting_Started/02_Composer.md
@@ -114,7 +114,7 @@ So, your deployment process, as it relates to Composer, should be as follows:
Modules and themes managed by composer should not be committed with your projects source code. For more details read [Should I commit the dependencies in my vendor directory?](https://getcomposer.org/doc/faqs/should-i-commit-the-dependencies-in-my-vendor-directory.md).
-Since SilverStripe modules are installed in to thier own folder, you have to manage your [.gitignore](http://git-scm.com/docs/gitignore) to ensure they are ignored from your repository.
+Since SilverStripe modules are installed into their own folder, you have to manage your [.gitignore](http://git-scm.com/docs/gitignore) to ensure they are ignored from your repository.
Here is the default SilverStripe [.gitignore](http://git-scm.com/docs/gitignore) with the forum module ignored
diff --git a/docs/en/02_Developer_Guides/02_Controllers/01_Introduction.md b/docs/en/02_Developer_Guides/02_Controllers/01_Introduction.md
index bbd5ed239..bba0fdd2a 100644
--- a/docs/en/02_Developer_Guides/02_Controllers/01_Introduction.md
+++ b/docs/en/02_Developer_Guides/02_Controllers/01_Introduction.md
@@ -124,7 +124,7 @@ Action methods can return one of four main things:
$this->response->addHeader("Content-type", "application/json");
- return $this->response.
+ return $this->response;
}
For more information on how a URL gets mapped to an action see the [Routing](routing) documentation.
diff --git a/docs/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md b/docs/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md
index 7afd1894c..098b5e3d1 100644
--- a/docs/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md
+++ b/docs/en/02_Developer_Guides/06_Testing/00_Unit_Testing.md
@@ -73,6 +73,10 @@ The `phpunit` binary should be used from the root directory of your website.
phpunit framework/tests '' flush=all
# Run tests with optional `$_GET` parameters (you need an empty second argument)
+
+The manifest is not flushed when running tests. Add `flush=all` to the test command to do this (see above example.)
+
+
If phpunit is not installed globally on your machine, you may need to replace the above usage of `phpunit` with the full
path (e.g `vendor/bin/phpunit framework/tests`)
diff --git a/docs/en/02_Developer_Guides/09_Security/02_Permissions.md b/docs/en/02_Developer_Guides/09_Security/02_Permissions.md
index cb1aa1d8d..4e40af440 100644
--- a/docs/en/02_Developer_Guides/09_Security/02_Permissions.md
+++ b/docs/en/02_Developer_Guides/09_Security/02_Permissions.md
@@ -66,6 +66,36 @@ currently logged in member is assumed.
* On a request, $request->hasPermission("View", $member = null) can be called. See [datamodel](/topics/datamodel) for
information on request objects.
+## Special cases
+
+### ADMIN permissions
+
+By default the config option `admin_implies_all` is true - this means that any user granted the `ADMIN` permission has
+all other permissions granted to them. This is a type of cascading of permissions that is hard coded into the permission
+system.
+
+### CMS access permissions
+
+Access to the CMS has a couple of special cases where permission codes can imply other permissions.
+
+#### 1. Granting access to all CMS permissions
+
+The `CMS_ACCESS_LeftAndMain` grants access to every single area of the CMS, without exception. Internally, this works by
+adding the `CMS_ACCESS_LeftAndMain` code to the set of accepted codes when a `CMS_ACCESS_*` permission is required.
+This works much like ADMIN permissions (see above)
+
+
+#### 2. Checking for any access to the CMS
+
+You can check if a user has access to the CMS by simply performing a check against `CMS_ACCESS`.
+
+ :::php
+ if (Permission::checkMember($member, 'CMS_ACCESS')) {
+ //user can access the CMS
+ }
+
+Internally, this checks that the user has any of the defined `CMS_ACCESS_*` permissions.
+
## API Documentation
`[api:Permission]`
diff --git a/docs/en/04_Changelogs/rc/3.1.14-rc1.md b/docs/en/04_Changelogs/rc/3.1.14-rc1.md
new file mode 100644
index 000000000..cbffd2ba6
--- /dev/null
+++ b/docs/en/04_Changelogs/rc/3.1.14-rc1.md
@@ -0,0 +1,42 @@
+# 3.1.14-rc1
+
+
+
+## Change Log
+
+### API Changes
+
+ * 2015-01-28 [782c4cb](https://github.com/silverstripe/silverstripe-framework/commit/782c4cbf6f5cde2fa4d45cdbd17552773a67f88f) Enable single-column fulltext filter search as fallback (Damian Mooyman)
+
+### Bugfixes
+
+ * 2015-08-27 [899eb0b](https://github.com/silverstripe/silverstripe-framework/commit/899eb0b235859c843890c790e99c03f4fd4b825c) Use complete fieldlist for extracting data (Daniel Hensby)
+ * 2015-08-26 [2d4b743](https://github.com/silverstripe/silverstripe-framework/commit/2d4b743090935e7c10bd95e00398df7bfb5763af) Members can access their own profiles in CMS (Daniel Hensby)
+ * 2015-08-26 [0943b3b](https://github.com/silverstripe/silverstripe-framework/commit/0943b3b1a06e6c9130500532fd979c720b65c761) Recursion errors when sorting objects with circular dependencies (fixes #4464) (Loz Calver)
+ * 2015-08-20 [fc212e0](https://github.com/silverstripe/silverstripe-framework/commit/fc212e030c474d966ffb1821423ddcb3ae361b72) Fix illegalExtensions breaking tests. (Damian Mooyman)
+ * 2015-08-18 [8b638f5](https://github.com/silverstripe/silverstripe-framework/commit/8b638f56fb737dac18126c291297c87469eb7d0f) Using undefined var in ModelAdmin (Loz Calver)
+ * 2015-07-26 [5f5ce8a](https://github.com/silverstripe/silverstripe-framework/commit/5f5ce8a82c2bb1a29f9f8b7011d5cd990c34f128) Disable cache to prevent caching of build target (Damian Mooyman)
+ * 2015-07-16 [a3201d6](https://github.com/silverstripe/silverstripe-framework/commit/a3201d6ed9967179aa020802e6fb88d2a6a0e37e) $callerClass is undefined (Christopher Darling)
+ * 2015-07-08 [c7bd504](https://github.com/silverstripe/silverstripe-framework/commit/c7bd50427a4e0ad446502547b81648d78d354062) Fix cookie errors when running in CLI (Damian Mooyman)
+ * 2015-07-07 [5ace490](https://github.com/silverstripe/silverstripe-framework/commit/5ace4905c90be1373f49dbb0e1a579b279786a1c) Fix issue when SS_ALLOWED_HOSTS is run in CLI (Damian Mooyman)
+ * 2015-07-05 [a556b48](https://github.com/silverstripe/silverstripe-framework/commit/a556b4854a44b9dfe86c40140ec03d781d354d19) Fix of multiple i18nTextCollector issues: #3797, #3798, #3417 (Damian Mooyman)
+ * 2015-07-01 [6fabd01](https://github.com/silverstripe/silverstripe-framework/commit/6fabd0122be37faa671923b534a74e5684d58220) Fix potential XSS injection (Damian Mooyman)
+ * 2015-06-26 [d78d325](https://github.com/silverstripe/silverstripe-cms/commit/d78d3250736c5d2f48c5cfc1690fba8b98cc222b) RedirectorPage_Controller shouldn't attempt redirection if the response is finished (fixes #1230) (Loz Calver)
+ * 2015-06-18 [f7f92b3](https://github.com/silverstripe/silverstripe-installer/commit/f7f92b32260f31a5969dde4b1d8c55d81c289056) Invalid comment syntax for web.config (Daniel Hensby)
+ * 2015-06-16 [6169bf2](https://github.com/silverstripe/silverstripe-framework/commit/6169bf2760366b0aebf255c973803621472ce1fb) No longer caching has_one after ID change (Daniel Hensby)
+ * 2015-06-11 [6be0488](https://github.com/silverstripe/silverstripe-framework/commit/6be04887315522e5b95b83be1e301691441b985c) TreeDropdownField doesnt change label on unselect (Daniel Hensby)
+ * 2015-05-28 [0319f78](https://github.com/silverstripe/silverstripe-framework/commit/0319f7855bc4e8a6eb71d2766ac24a7d760d502e) Incorrect env setting in 3.1.13 (Damian Mooyman)
+ * 2015-05-22 [e0710ae](https://github.com/silverstripe/silverstripe-framework/commit/e0710ae4e4a03c191b841cc45a6c103a0e21ec7f) Fix DirectorTest failing when run with sake (Damian Mooyman)
+ * 2015-05-20 [94f6a13](https://github.com/silverstripe/silverstripe-framework/commit/94f6a137297d6638065583c388dffeeb9eccb55b) Fixed setting LastEdited for DataObject with class ancestry (Gregory Smirnov)
+ * 2015-05-20 [869e69a](https://github.com/silverstripe/silverstripe-framework/commit/869e69a9b2c1352e1fa6246432d9180eb81cf7e3) Clicking icon in site tree link fails (Jonathon Menz)
+ * 2015-05-20 [f9bdf61](https://github.com/silverstripe/silverstripe-framework/commit/f9bdf61b6f4cdd2f55ff2729a5b6be0a200f876a) Fixed handling of numbers in certain locales (Gregory Smirnov)
+ * 2015-05-19 [dbe2ad4](https://github.com/silverstripe/silverstripe-cms/commit/dbe2ad4f9fe818fe21755eff2ecf8d359c578736) Folder expansion icons (Jonathon Menz)
+ * 2015-05-19 [a56d08b](https://github.com/silverstripe/silverstripe-framework/commit/a56d08b1aeeb0a2dfc16e134ddc3bd7b699bd606) TreeDropdownField Folder expansion (Jonathon Menz)
+ * 2015-05-16 [c6bcfea](https://github.com/silverstripe/silverstripe-framework/commit/c6bcfea3e36a4211d2f69ff5c73db2fcab474ba8) FieldList::changeFieldOrder() leftovers discarded (Jonathon Menz)
+ * 2015-05-04 [1cca37c](https://github.com/silverstripe/silverstripe-framework/commit/1cca37c9082ef53f02633d1bdac27f4a815d4208) File::getFileType() was case sensitive (fixes #3631) (Loz Calver)
+ * 2015-04-01 [7ff131d](https://github.com/silverstripe/silverstripe-framework/commit/7ff131daa76d345cff90410469accdcca9049cf1) Fix default casted (boolean)false evaluating to true in templates (Damian Mooyman)
+ * 2014-12-31 [71a14c3](https://github.com/silverstripe/silverstripe-framework/commit/71a14c30352e69e4c0ac59e5ea72e1da0c79009b) Prevent url= querystring argument override (Damian Mooyman)
+ * 2014-10-25 [28be51c](https://github.com/silverstripe/silverstripe-framework/commit/28be51cab0b567b692632503e0f440d30a2fe09e) Config state leaking between unit tests (Loz Calver)
+ * 2014-09-20 [bbc1cb8](https://github.com/silverstripe/silverstripe-framework/commit/bbc1cb82702b678b21bef15394f067c146e47625) #3458 iframe transport multi file upload FIX #3343, FIX #3148 (Thierry François)
+ * 2014-05-25 [40c5b8b](https://github.com/silverstripe/silverstripe-framework/commit/40c5b8b6758676a3e2a5daf3c438a7720c49baaf) FulltextFilter did not work and was not usable (micmania1)
+ * 2014-03-24 [fd755a7](https://github.com/silverstripe/silverstripe-framework/commit/fd755a7ff9de69802f04763570f69e4c3b68c08c) ChangePasswordForm validation message should render HTML correctly. (Sean Harvey)
diff --git a/forms/Form.php b/forms/Form.php
index 519a7d76d..5b24754ac 100644
--- a/forms/Form.php
+++ b/forms/Form.php
@@ -693,8 +693,10 @@ class Form extends RequestHandler {
$extraFields = new FieldList();
$token = $this->getSecurityToken();
- $tokenField = $token->updateFieldSet($this->fields);
- if($tokenField) $tokenField->setForm($this);
+ if ($token) {
+ $tokenField = $token->updateFieldSet($this->fields);
+ if($tokenField) $tokenField->setForm($this);
+ }
$this->securityTokenAdded = true;
// add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
@@ -1378,7 +1380,7 @@ class Form extends RequestHandler {
if(is_object($data)) $this->record = $data;
// dont include fields without data
- $dataFields = $this->fields->dataFields();
+ $dataFields = $this->Fields()->dataFields();
if($dataFields) foreach($dataFields as $field) {
$name = $field->getName();
diff --git a/model/ArrayList.php b/model/ArrayList.php
index c9e428546..21db8740d 100644
--- a/model/ArrayList.php
+++ b/model/ArrayList.php
@@ -422,10 +422,15 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta
throw new InvalidArgumentException("Bad arguments passed to sort()");
}
+ // Store the original keys of the items as a sort fallback, so we can preserve the original order in the event
+ // that array_multisort is unable to work out a sort order for them. This also prevents array_multisort trying
+ // to inspect object properties which can result in errors with circular dependencies
+ $originalKeys = array_keys($this->items);
+
// This the main sorting algorithm that supports infinite sorting params
$multisortArgs = array();
$values = array();
- foreach($columnsToSort as $column => $direction ) {
+ foreach($columnsToSort as $column => $direction) {
// The reason these are added to columns is of the references, otherwise when the foreach
// is done, all $values and $direction look the same
$values[$column] = array();
@@ -442,6 +447,8 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta
$multisortArgs[] = &$sortDirection[$column];
}
+ $multisortArgs[] = &$originalKeys;
+
$list = clone $this;
// As the last argument we pass in a reference to the items that all the sorting will be applied upon
$multisortArgs[] = &$list->items;
diff --git a/model/Hierarchy.php b/model/Hierarchy.php
index 8267793a8..6ccc6ebc5 100644
--- a/model/Hierarchy.php
+++ b/model/Hierarchy.php
@@ -725,7 +725,7 @@ class Hierarchy extends DataExtension {
$children = $baseClass::get()
->filter('ParentID', (int)$this->owner->ID)
- ->sort('Sort', 'ASC');
+ ->sort('"Sort"', 'ASC');
if ($afterNode) {
$children = $children->filter('Sort:GreaterThan', $afterNode->Sort);
}
diff --git a/security/Permission.php b/security/Permission.php
index 5f81d6450..96890de94 100644
--- a/security/Permission.php
+++ b/security/Permission.php
@@ -163,16 +163,35 @@ class Permission extends DataObject implements TemplateGlobalProvider {
$memberID = (is_object($member)) ? $member->ID : $member;
}
+ // Turn the code into an array as we may need to add other permsissions to the set we check
+ if(!is_array($code)) $code = array($code);
+
if($arg == 'any') {
+ $adminImpliesAll = (bool)Config::inst()->get('Permission', 'admin_implies_all');
// Cache the permissions in memory
if(!isset(self::$cache_permissions[$memberID])) {
self::$cache_permissions[$memberID] = self::permissions_for_member($memberID);
}
-
- // If $admin_implies_all was false then this would be inefficient, but that's an edge
- // case and this keeps the code simpler
- if(!is_array($code)) $code = array($code);
- if(Config::inst()->get('Permission', 'admin_implies_all')) $code[] = "ADMIN";
+ foreach ($code as $permCode) {
+ if ($permCode === 'CMS_ACCESS') {
+ foreach (self::$cache_permissions[$memberID] as $perm) {
+ //if they have admin rights OR they have an explicit access to the CMS then give permission
+ if (($adminImpliesAll && $perm == 'ADMIN') || substr($perm, 0, 11) === 'CMS_ACCESS_') {
+ return true;
+ }
+ }
+ }
+ elseif (substr($permCode, 0, 11) === 'CMS_ACCESS_') {
+ //cms_access_leftandmain means access to all CMS areas
+ $code[] = 'CMS_ACCESS_LeftAndMain';
+ break;
+ }
+ }
+
+ // if ADMIN has all privileges, then we need to push that code in
+ if($adminImpliesAll) {
+ $code[] = "ADMIN";
+ }
// Multiple $code values - return true if at least one matches, ie, intersection exists
return (bool)array_intersect($code, self::$cache_permissions[$memberID]);
diff --git a/tests/control/CMSProfileControllerTest.php b/tests/control/CMSProfileControllerTest.php
index 9545a2c9a..2e62033c9 100644
--- a/tests/control/CMSProfileControllerTest.php
+++ b/tests/control/CMSProfileControllerTest.php
@@ -32,7 +32,7 @@ class CMSProfileControllerTest extends FunctionalTest {
}
public function testMemberEditsOwnProfile() {
- $member = $this->objFromFixture('Member', 'user1');
+ $member = $this->objFromFixture('Member', 'user3');
$this->session()->inst_set('loggedInAs', $member->ID);
$response = $this->post('admin/myprofile/EditForm', array(
@@ -46,9 +46,9 @@ class CMSProfileControllerTest extends FunctionalTest {
'Password[_ConfirmPassword]' => 'password',
));
- $member = $this->objFromFixture('Member', 'user1');
+ $member = $this->objFromFixture('Member', 'user3');
- $this->assertEquals($member->FirstName, 'JoeEdited', 'FirstName field was changed');
+ $this->assertEquals('JoeEdited', $member->FirstName, 'FirstName field was changed');
}
public function testExtendedPermissionsStopEditingOwnProfile() {
diff --git a/tests/control/CMSProfileControllerTest.yml b/tests/control/CMSProfileControllerTest.yml
index 4ab2d44f3..40795637a 100644
--- a/tests/control/CMSProfileControllerTest.yml
+++ b/tests/control/CMSProfileControllerTest.yml
@@ -1,27 +1,38 @@
Permission:
- admin:
- Code: ADMIN
- cmsmain:
- Code: CMS_ACCESS_LeftAndMain
- leftandmain:
- Code: CMS_ACCESS_CMSMain
+ admin:
+ Code: ADMIN
+ cmsmain:
+ Code: CMS_ACCESS_LeftAndMain
+ leftandmain:
+ Code: CMS_ACCESS_CMSMain
+ test:
+ Code: CMS_ACCESS_TestController
+
Group:
- admins:
- Title: Administrators
- Permissions: =>Permission.admin
- cmsusers:
- Title: CMS Users
- Permissions: =>Permission.cmsmain, =>Permission.leftandmain
+ admins:
+ Title: Administrators
+ Permissions: =>Permission.admin
+ cmsusers:
+ Title: CMS Users
+ Permissions: =>Permission.cmsmain, =>Permission.leftandmain
+ test:
+ Title: Test group
+ Permissions: =>Permission.test
+
Member:
- admin:
- FirstName: Admin
- Email: admin@user.com
- Groups: =>Group.admins
- user1:
- FirstName: Joe
- Email: user1@user.com
- Groups: =>Group.cmsusers
- user2:
- FirstName: Steve
- Email: user2@user.com
- Groups: =>Group.cmsusers
+ admin:
+ FirstName: Admin
+ Email: admin@user.com
+ Groups: =>Group.admins
+ user1:
+ FirstName: Joe
+ Email: user1@user.com
+ Groups: =>Group.cmsusers
+ user2:
+ FirstName: Steve
+ Email: user2@user.com
+ Groups: =>Group.cmsusers
+ user3:
+ FirstName: Files
+ Email: user3@example.com
+ Groups: =>Group.test
diff --git a/tests/forms/FormTest.php b/tests/forms/FormTest.php
index 99245c18c..409bac60a 100644
--- a/tests/forms/FormTest.php
+++ b/tests/forms/FormTest.php
@@ -587,6 +587,26 @@ class FormTest extends FunctionalTest {
$messageEls[0]->asXML()
);
}
+
+ public function testGetExtraFields()
+ {
+ $form = new FormTest_ExtraFieldsForm(
+ new FormTest_Controller(),
+ 'Form',
+ new FieldList(new TextField('key1')),
+ new FieldList()
+ );
+
+ $data = array(
+ 'key1' => 'test',
+ 'ExtraFieldCheckbox' => false,
+ );
+
+ $form->loadDataFrom($data);
+
+ $formData = $form->getData();
+ $this->assertEmpty($formData['ExtraFieldCheckbox']);
+ }
protected function getStubForm() {
return new Form(
@@ -732,41 +752,57 @@ class FormTest_ControllerWithSecurityToken extends Controller implements TestOnl
}
-class FormTest_ControllerWithStrictPostCheck extends Controller implements TestOnly {
+class FormTest_ControllerWithStrictPostCheck extends Controller implements TestOnly
+{
- private static $allowed_actions = array('Form');
+ private static $allowed_actions = array('Form');
- protected $template = 'BlankPage';
+ protected $template = 'BlankPage';
- public function Link($action = null) {
- return Controller::join_links(
- 'FormTest_ControllerWithStrictPostCheck',
- $this->getRequest()->latestParam('Action'),
- $this->getRequest()->latestParam('ID'),
- $action
- );
- }
+ public function Link($action = null)
+ {
+ return Controller::join_links(
+ 'FormTest_ControllerWithStrictPostCheck',
+ $this->request->latestParam('Action'),
+ $this->request->latestParam('ID'),
+ $action
+ );
+ }
- public function Form() {
- $form = new Form(
- $this,
- 'Form',
- new FieldList(
- new EmailField('Email')
- ),
- new FieldList(
- new FormAction('doSubmit')
- )
- );
- $form->setFormMethod('POST');
- $form->setStrictFormMethodCheck(true);
- $form->disableSecurityToken(); // Disable CSRF protection for easier form submission handling
+ public function Form()
+ {
+ $form = new Form(
+ $this,
+ 'Form',
+ new FieldList(
+ new EmailField('Email')
+ ),
+ new FieldList(
+ new FormAction('doSubmit')
+ )
+ );
+ $form->setFormMethod('POST');
+ $form->setStrictFormMethodCheck(true);
+ $form->disableSecurityToken(); // Disable CSRF protection for easier form submission handling
- return $form;
- }
+ return $form;
+ }
+
+ public function doSubmit($data, $form, $request)
+ {
+ $form->sessionMessage('Test save was successful', 'good');
+ return $this->redirectBack();
+ }
+}
+
+class FormTest_ExtraFieldsForm extends Form implements TestOnly {
+
+ public function getExtraFields() {
+ $fields = parent::getExtraFields();
+
+ $fields->push(new CheckboxField('ExtraFieldCheckbox', 'Extra Field Checkbox', 1));
+
+ return $fields;
+ }
- public function doSubmit($data, $form, $request) {
- $form->sessionMessage('Test save was successful', 'good');
- return $this->redirectBack();
- }
}
diff --git a/tests/model/ArrayListTest.php b/tests/model/ArrayListTest.php
index 95af4d3f1..1f91a58d6 100644
--- a/tests/model/ArrayListTest.php
+++ b/tests/model/ArrayListTest.php
@@ -425,6 +425,29 @@ class ArrayListTest extends SapphireTest {
$this->assertEquals($list->last()->ID, 3, 'Bert.3 should be last in the list');
}
+ /**
+ * Check that we don't cause recursion errors with array_multisort() and circular dependencies
+ */
+ public function testSortWithCircularDependencies() {
+ $itemA = new stdClass;
+ $childA = new stdClass;
+ $itemA->child = $childA;
+ $childA->parent = $itemA;
+ $itemA->Sort = 1;
+
+ $itemB = new stdClass;
+ $childB = new stdClass;
+ $itemB->child = $childB;
+ $childB->parent = $itemB;
+ $itemB->Sort = 1;
+
+ $items = new ArrayList;
+ $items->add($itemA);
+ $items->add($itemB);
+
+ // This call will trigger a fatal error if there are issues with circular dependencies
+ $items->sort('Sort');
+ }
/**
* $list->filter('Name', 'bob'); // only bob in the list
*/
diff --git a/tests/security/PermissionTest.php b/tests/security/PermissionTest.php
index b1c472121..dd76c3fdb 100644
--- a/tests/security/PermissionTest.php
+++ b/tests/security/PermissionTest.php
@@ -14,7 +14,7 @@ class PermissionTest extends SapphireTest {
}
public function testGetCodesUngrouped() {
- $codes = Permission::get_codes(null, false);
+ $codes = Permission::get_codes(false);
$this->assertArrayHasKey('SITETREE_VIEW_ALL', $codes);
}
@@ -23,6 +23,31 @@ class PermissionTest extends SapphireTest {
$this->assertTrue(Permission::checkMember($member, "SITETREE_VIEW_ALL"));
}
+ public function testCMSAccess() {
+ $members = Member::get()->byIDs($this->allFixtureIDs('Member'));
+ foreach ($members as $member) {
+ $this->assertTrue(Permission::checkMember($member, 'CMS_ACCESS'));
+ }
+
+ $member = new Member();
+ $member->update(array(
+ 'FirstName' => 'No CMS',
+ 'Surname' => 'Access',
+ 'Email' => 'no-access@example.com',
+ ));
+ $member->write();
+ $this->assertFalse(Permission::checkMember($member, 'CMS_ACCESS'));
+ }
+
+ public function testLeftAndMainAccessAll() {
+ //add user and group
+ $member = $this->objFromFixture('Member', 'leftandmain');
+
+ $this->assertTrue(Permission::checkMember($member, "CMS_ACCESS_MyAdmin"));
+ $this->assertTrue(Permission::checkMember($member, "CMS_ACCESS_AssetAdmin"));
+ $this->assertTrue(Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin"));
+ }
+
public function testPermissionAreInheritedFromOneRole() {
$member = $this->objFromFixture('Member', 'author');
$this->assertTrue(Permission::checkMember($member, "CMS_ACCESS_MyAdmin"));
@@ -39,7 +64,7 @@ class PermissionTest extends SapphireTest {
$this->assertFalse(Permission::checkMember($member, "SITETREE_VIEW_ALL"));
}
- function testPermissionsForMember() {
+ public function testPermissionsForMember() {
$member = $this->objFromFixture('Member', 'access');
$permissions = Permission::permissions_for_member($member->ID);
$this->assertEquals(4, count($permissions));
diff --git a/tests/security/PermissionTest.yml b/tests/security/PermissionTest.yml
index 58bc5fef3..f861e1d81 100644
--- a/tests/security/PermissionTest.yml
+++ b/tests/security/PermissionTest.yml
@@ -1,52 +1,63 @@
PermissionRole:
- author:
- Title: Author
- access:
- Title: Access Administrator
+ author:
+ Title: Author
+ access:
+ Title: Access Administrator
PermissionRoleCode:
- author1:
- Role: =>PermissionRole.author
- Code: CMS_ACCESS_MyAdmin
- author2:
- Role: =>PermissionRole.author
- Code: CMS_ACCESS_AssetAdmin
- access1:
- Role: =>PermissionRole.access
- Code: CMS_ACCESS_SecurityAdmin
- access2:
- Role: =>PermissionRole.access
- Code: EDIT_PERMISSIONS
+ author1:
+ Role: =>PermissionRole.author
+ Code: CMS_ACCESS_MyAdmin
+ author2:
+ Role: =>PermissionRole.author
+ Code: CMS_ACCESS_AssetAdmin
+ access1:
+ Role: =>PermissionRole.access
+ Code: CMS_ACCESS_SecurityAdmin
+ access2:
+ Role: =>PermissionRole.access
+ Code: EDIT_PERMISSIONS
+
Member:
- author:
- FirstName: Test
- Surname: Author
- access:
- FirstName: Test
- Surname: Access Administrator
- globalauthor:
- FirstName: Test
- Surname: Global Author
+ author:
+ FirstName: Test
+ Surname: Author
+ access:
+ FirstName: Test
+ Surname: Access Administrator
+ globalauthor:
+ FirstName: Test
+ Surname: Global Author
+ leftandmain:
+ FirstName: Left
+ Surname: AndMain
+ Email: leftandmain@example.com
Group:
- author:
- Title: Authors
- Members: =>Member.author
- Roles: =>PermissionRole.author
- access:
- Title: Access Administrators + Authors
- Members: =>Member.access
- Roles: =>PermissionRole.access,=>PermissionRole.author
- globalauthor:
- Parent: =>Group.author
- Title: Global Authors
- Members: =>Member.globalauthor
+ author:
+ Title: Authors
+ Members: =>Member.author
+ Roles: =>PermissionRole.author
+ access:
+ Title: Access Administrators + Authors
+ Members: =>Member.access
+ Roles: =>PermissionRole.access,=>PermissionRole.author
+ globalauthor:
+ Parent: =>Group.author
+ Title: Global Authors
+ Members: =>Member.globalauthor
+ leftandmain:
+ Title: LeftAndMain
+ Members: =>Member.leftandmain
Permission:
- extra1:
- Code: SITETREE_VIEW_ALL
- Group: =>Group.author
- globalauthor:
- Code: SITETREE_EDIT_ALL
- Group: =>Group.globalauthor
+ extra1:
+ Code: SITETREE_VIEW_ALL
+ Group: =>Group.author
+ globalauthor:
+ Code: SITETREE_EDIT_ALL
+ Group: =>Group.globalauthor
+ leftandmain:
+ Code: CMS_ACCESS_LeftAndMain
+ Group: =>Group.leftandmain