From 95bcac796a3bcb65e7d9fc55e0af0b9d17407938 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Mon, 18 Jun 2018 14:44:31 +1200 Subject: [PATCH 01/35] ENHANCEMENT Ensure test DB is flushed on either DDL or transaction-disabled tests Fixes #8182 --- src/Dev/SapphireTest.php | 18 +++++++- src/Dev/State/FixtureTestState.php | 66 +++++++++++++++++++++--------- src/ORM/Connect/Database.php | 22 ++++++---- src/ORM/Connect/MySQLDatabase.php | 62 ++++++++++++++++++++++++---- src/ORM/Connect/TempDatabase.php | 25 ++++++++--- 5 files changed, 152 insertions(+), 41 deletions(-) diff --git a/src/Dev/SapphireTest.php b/src/Dev/SapphireTest.php index c4c39ddd4..988f7f087 100644 --- a/src/Dev/SapphireTest.php +++ b/src/Dev/SapphireTest.php @@ -78,6 +78,14 @@ class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly */ protected $usesDatabase = null; + /** + * This test will cleanup its state via transactions. + * If set to false a full schema is forced between tests, but at a performance cost. + * + * @var bool + */ + protected $usesTransactions = true; + /** * @var bool */ @@ -228,6 +236,14 @@ class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly return $this->usesDatabase; } + /** + * @return bool + */ + public function getUsesTransactions() + { + return $this->usesTransactions; + } + /** * @return array */ @@ -1188,7 +1204,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly if (strpos($fixtureFilePath, ':') !== false) { return ModuleResourceLoader::singleton()->resolvePath($fixtureFilePath); } - + // Support fixture paths relative to the test class, rather than relative to webroot // String checking is faster than file_exists() calls. $resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath); diff --git a/src/Dev/State/FixtureTestState.php b/src/Dev/State/FixtureTestState.php index ccbe67f1a..0272ebfc4 100644 --- a/src/Dev/State/FixtureTestState.php +++ b/src/Dev/State/FixtureTestState.php @@ -26,25 +26,31 @@ class FixtureTestState implements TestState */ public function setUp(SapphireTest $test) { - if ($this->testNeedsDB($test)) { - $tmpDB = $test::tempDB(); - if (!$tmpDB->isUsed()) { - $tmpDB->build(); - } - DataObject::singleton()->flushCache(); + if (!$this->testNeedsDB($test)) { + return; + } + $tmpDB = $test::tempDB(); + if (!$tmpDB->isUsed()) { + $tmpDB->build(); + } + DataObject::singleton()->flushCache(); - if (!$tmpDB->hasStarted()) { - foreach ($test->getRequireDefaultRecordsFrom() as $className) { - $instance = singleton($className); - if (method_exists($instance, 'requireDefaultRecords')) { - $instance->requireDefaultRecords(); - } - if (method_exists($instance, 'augmentDefaultRecords')) { - $instance->augmentDefaultRecords(); - } + // Ensure DB is built and populated + if (!$tmpDB->hasStarted()) { + foreach ($test->getRequireDefaultRecordsFrom() as $className) { + $instance = singleton($className); + if (method_exists($instance, 'requireDefaultRecords')) { + $instance->requireDefaultRecords(); + } + if (method_exists($instance, 'augmentDefaultRecords')) { + $instance->augmentDefaultRecords(); } - $this->loadFixtures($test); } + $this->loadFixtures($test); + } + + // Begin transactions if enabled + if ($test->getUsesTransactions()) { $tmpDB->startTransaction(); } } @@ -56,9 +62,21 @@ class FixtureTestState implements TestState */ public function tearDown(SapphireTest $test) { - if ($this->testNeedsDB($test)) { - $test::tempDB()->rollbackTransaction(); + if (!$this->testNeedsDB($test)) { + return; } + + // For transactional states, rollback if possible + if ($test->getUsesTransactions()) { + $success = $test::tempDB()->rollbackTransaction(); + if ($success) { + return; + } + } + + // Force reset if transaction failed, or disabled + $test::tempDB()->kill(); + $this->resetFixtureFactory(get_class($test)); } /** @@ -68,7 +86,7 @@ class FixtureTestState implements TestState */ public function setUpOnce($class) { - $this->fixtureFactories[strtolower($class)] = Injector::inst()->create(FixtureFactory::class); + $this->resetFixtureFactory($class); } /** @@ -220,4 +238,14 @@ class FixtureTestState implements TestState return false; } + + /** + * Bootstrap a clean fixture factory for the given class + * + * @param string $class + */ + protected function resetFixtureFactory($class) + { + $this->fixtureFactories[strtolower($class)] = Injector::inst()->create(FixtureFactory::class); + } } diff --git a/src/ORM/Connect/Database.php b/src/ORM/Connect/Database.php index 3e2910522..add4d6948 100644 --- a/src/ORM/Connect/Database.php +++ b/src/ORM/Connect/Database.php @@ -654,13 +654,17 @@ abstract class Database * * @param string|boolean $savepoint Name of savepoint, or leave empty to rollback * to last savepoint + * @return bool|null Boolean is returned if success state is known, or null if + * unknown. Note: For error checking purposes null should not be treated as error. */ abstract public function transactionRollback($savepoint = false); /** * Commit everything inside this transaction so far * - * @param boolean $chain + * @param bool $chain + * @return bool|null Boolean is returned if success state is known, or null if + * unknown. Note: For error checking purposes null should not be treated as error. */ abstract public function transactionEnd($chain = false); @@ -669,7 +673,7 @@ abstract class Database * which is different from table- or row-level locking. * See {@link getLock()} for details. * - * @return boolean Flag indicating that locking is available + * @return bool Flag indicating that locking is available */ public function supportsLocks() { @@ -681,7 +685,7 @@ abstract class Database * See {@link supportsLocks()} to check if locking is generally supported. * * @param string $name Name of the lock - * @return boolean + * @return bool */ public function canLock($name) { @@ -703,7 +707,7 @@ abstract class Database * * @param string $name Name of lock * @param integer $timeout Timeout in seconds - * @return boolean + * @return bool */ public function getLock($name, $timeout = 5) { @@ -715,7 +719,7 @@ abstract class Database * (if the execution aborts (e.g. due to an error) all locks are automatically released). * * @param string $name Name of the lock - * @return boolean Flag indicating whether the lock was successfully released + * @return bool Flag indicating whether the lock was successfully released */ public function releaseLock($name) { @@ -756,7 +760,7 @@ abstract class Database * Determine if the database with the specified name exists * * @param string $name Name of the database to check for - * @return boolean Flag indicating whether this database exists + * @return bool Flag indicating whether this database exists */ public function databaseExists($name) { @@ -778,12 +782,12 @@ abstract class Database * database if it doesn't exist in the current schema. * * @param string $name Name of the database - * @param boolean $create Flag indicating whether the database should be created + * @param bool $create Flag indicating whether the database should be created * if it doesn't exist. If $create is false and the database doesn't exist * then an error will be raised - * @param int|boolean $errorLevel The level of error reporting to enable for the query, or false if no error + * @param int|bool $errorLevel The level of error reporting to enable for the query, or false if no error * should be raised - * @return boolean Flag indicating success + * @return bool Flag indicating success */ public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR) { diff --git a/src/ORM/Connect/MySQLDatabase.php b/src/ORM/Connect/MySQLDatabase.php index fa3672db5..59c0c2b6c 100644 --- a/src/ORM/Connect/MySQLDatabase.php +++ b/src/ORM/Connect/MySQLDatabase.php @@ -329,25 +329,73 @@ class MySQLDatabase extends Database public function transactionRollback($savepoint = false) { + // Named transaction if ($savepoint) { $this->query('ROLLBACK TO ' . $savepoint); - } else { - --$this->transactionNesting; - if ($this->transactionNesting > 0) { - $this->transactionRollback('NESTEDTRANSACTION' . $this->transactionNesting); - } else { - $this->query('ROLLBACK'); - } + return true; } + + // Fail if transaction isn't available + if (!$this->transactionNesting) { + return false; + } + --$this->transactionNesting; + if ($this->transactionNesting > 0) { + $this->transactionRollback('NESTEDTRANSACTION' . $this->transactionNesting); + } else { + $this->query('ROLLBACK'); + } + return true; } public function transactionEnd($chain = false) { + // Fail if transaction isn't available + if (!$this->transactionNesting) { + return false; + } --$this->transactionNesting; if ($this->transactionNesting <= 0) { $this->transactionNesting = 0; $this->query('COMMIT AND ' . ($chain ? '' : 'NO ') . 'CHAIN'); } + return true; + } + + /** + * In error condition, set transactionNesting to zero + */ + protected function discardTransactions() + { + $this->transactionNesting = 0; + } + + public function query($sql, $errorLevel = E_USER_ERROR) + { + $this->inspectQuery($sql); + return parent::query($sql, $errorLevel); + } + + public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) + { + $this->inspectQuery($sql); + return parent::preparedQuery($sql, $parameters, $errorLevel); + } + + /** + * Inspect a SQL query prior to execution + * + * @param string $sql + */ + protected function inspectQuery($sql) + { + // Any DDL discards transactions. + // See https://dev.mysql.com/doc/internals/en/transactions-notes-on-ddl-and-normal-transaction.html + // on why we need to be over-eager + $isDDL = $this->getConnector()->isQueryDDL($sql); + if ($isDDL) { + $this->discardTransactions(); + } } public function comparisonClause( diff --git a/src/ORM/Connect/TempDatabase.php b/src/ORM/Connect/TempDatabase.php index 832acd606..b1fbecddf 100644 --- a/src/ORM/Connect/TempDatabase.php +++ b/src/ORM/Connect/TempDatabase.php @@ -102,15 +102,28 @@ class TempDatabase /** * Rollback a transaction (or trash all data if the DB doesn't support databases + * + * @return bool True if successfully rolled back, false otherwise. On error the DB is + * killed and must be re-created. Note that calling rollbackTransaction() when there + * is no transaction is counted as a failure, and will kill the DB. */ public function rollbackTransaction() { - if (static::getConn()->supportsTransactions()) { - static::getConn()->transactionRollback(); - } else { - $this->hasStarted = false; - static::clearAllData(); + $success = $this->hasStarted() && static::getConn()->supportsTransactions(); + if ($success) { + try { + // Explicit false = gnostic error from transactionRollback + if (static::getConn()->transactionRollback() === false) { + $success = false; + } + } catch (DatabaseException $ex) { + $success = false; + } } + if (!$success) { + static::kill(); + } + return $success; } /** @@ -118,6 +131,8 @@ class TempDatabase */ public function kill() { + $this->hasStarted = false; + // Delete our temporary database if (!$this->isUsed()) { return; From 11e0a3de43d7e05b7df63d52b0238e58baff3c2a Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Mon, 18 Jun 2018 17:34:05 +1200 Subject: [PATCH 02/35] BUG Ensure that build includes extra classes --- src/Dev/State/FixtureTestState.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Dev/State/FixtureTestState.php b/src/Dev/State/FixtureTestState.php index 0272ebfc4..a4df4d513 100644 --- a/src/Dev/State/FixtureTestState.php +++ b/src/Dev/State/FixtureTestState.php @@ -31,7 +31,14 @@ class FixtureTestState implements TestState } $tmpDB = $test::tempDB(); if (!$tmpDB->isUsed()) { + // Build base db $tmpDB->build(); + + // Reset schema + $extraObjects = $test->getExtraDataObjects(); + if ($extraObjects) { + $tmpDB->resetDBSchema($extraObjects); + } } DataObject::singleton()->flushCache(); From 225e61dc67f5ab550d41c42fa8c80b50b71db5b1 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Mon, 18 Jun 2018 17:56:42 +1200 Subject: [PATCH 03/35] BUG FIx manual resetDBSchema() calls breaking the database --- src/ORM/Connect/TempDatabase.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/ORM/Connect/TempDatabase.php b/src/ORM/Connect/TempDatabase.php index b1fbecddf..ce9065bbb 100644 --- a/src/ORM/Connect/TempDatabase.php +++ b/src/ORM/Connect/TempDatabase.php @@ -105,7 +105,8 @@ class TempDatabase * * @return bool True if successfully rolled back, false otherwise. On error the DB is * killed and must be re-created. Note that calling rollbackTransaction() when there - * is no transaction is counted as a failure, and will kill the DB. + * is no transaction is counted as a failure, user code should either kill or flush the DB + * as necessary */ public function rollbackTransaction() { @@ -120,9 +121,6 @@ class TempDatabase $success = false; } } - if (!$success) { - static::kill(); - } return $success; } @@ -247,7 +245,12 @@ class TempDatabase // pgsql doesn't allow schema updates inside transactions // so we need to rollback any transactions before commencing a schema reset if ($this->hasStarted()) { - $this->rollbackTransaction(); + // Sometimes transactions fail, rebuild + $success = $this->rollbackTransaction(); + if (!$success) { + $this->kill(); + $this->build(); + } } if (!$this->isUsed()) { return; From 8ea3bb36a0d599c85e66b4441bc862b5e7c98d02 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Tue, 19 Jun 2018 16:16:50 +1200 Subject: [PATCH 04/35] Maybe fix it? --- src/Dev/State/FixtureTestState.php | 26 +++++- src/ORM/Connect/Database.php | 14 ++++ src/ORM/Connect/MySQLDatabase.php | 9 ++- src/ORM/Connect/TempDatabase.php | 126 ++++++++++++++--------------- tests/php/Security/GroupTest.php | 5 ++ 5 files changed, 113 insertions(+), 67 deletions(-) diff --git a/src/Dev/State/FixtureTestState.php b/src/Dev/State/FixtureTestState.php index a4df4d513..ec5a32d32 100644 --- a/src/Dev/State/FixtureTestState.php +++ b/src/Dev/State/FixtureTestState.php @@ -19,6 +19,13 @@ class FixtureTestState implements TestState */ private $fixtureFactories = []; + /** + * Set if fixtures have been loaded + * + * @var bool + */ + protected $loaded = []; + /** * Called on setup * @@ -29,6 +36,8 @@ class FixtureTestState implements TestState if (!$this->testNeedsDB($test)) { return; } + + // Ensure DB is built $tmpDB = $test::tempDB(); if (!$tmpDB->isUsed()) { // Build base db @@ -40,10 +49,11 @@ class FixtureTestState implements TestState $tmpDB->resetDBSchema($extraObjects); } } + DataObject::singleton()->flushCache(); // Ensure DB is built and populated - if (!$tmpDB->hasStarted()) { + if (!$this->getIsLoaded(get_class($test))) { foreach ($test->getRequireDefaultRecordsFrom() as $className) { $instance = singleton($className); if (method_exists($instance, 'requireDefaultRecords')) { @@ -155,6 +165,8 @@ class FixtureTestState implements TestState foreach ($paths as $fixtureFile) { $this->loadFixture($fixtureFile, $test); } + // Flag as loaded + $this->loaded[get_class($test)] = true; } /** @@ -254,5 +266,17 @@ class FixtureTestState implements TestState protected function resetFixtureFactory($class) { $this->fixtureFactories[strtolower($class)] = Injector::inst()->create(FixtureFactory::class); + $this->loaded[$class] = false; + } + + /** + * Check if fixtures need to be loaded for this class + * + * @param string $class Name of test to check + * @return bool + */ + protected function getIsLoaded($class) + { + return !empty($this->loaded[$class]); } } diff --git a/src/ORM/Connect/Database.php b/src/ORM/Connect/Database.php index add4d6948..4c60ed085 100644 --- a/src/ORM/Connect/Database.php +++ b/src/ORM/Connect/Database.php @@ -668,6 +668,20 @@ abstract class Database */ abstract public function transactionEnd($chain = false); + /** + * Return depth of current transaction + * + * @return int Nesting level, or 0 if not in a transaction + */ + public function transactionDepth() + { + // Placeholder error for transactional DBs that don't expose depth + if ($this->supportsTransactions()) { + user_error(get_class($this) . " does not support transactionDepth", E_USER_WARNING); + } + return 0; + } + /** * Determines if the used database supports application-level locks, * which is different from table- or row-level locking. diff --git a/src/ORM/Connect/MySQLDatabase.php b/src/ORM/Connect/MySQLDatabase.php index 59c0c2b6c..49f6dd62d 100644 --- a/src/ORM/Connect/MySQLDatabase.php +++ b/src/ORM/Connect/MySQLDatabase.php @@ -348,6 +348,11 @@ class MySQLDatabase extends Database return true; } + public function transactionDepth() + { + return $this->transactionNesting; + } + public function transactionEnd($chain = false) { // Fail if transaction isn't available @@ -365,7 +370,7 @@ class MySQLDatabase extends Database /** * In error condition, set transactionNesting to zero */ - protected function discardTransactions() + protected function resetTransactionNesting() { $this->transactionNesting = 0; } @@ -394,7 +399,7 @@ class MySQLDatabase extends Database // on why we need to be over-eager $isDDL = $this->getConnector()->isQueryDDL($sql); if ($isDDL) { - $this->discardTransactions(); + $this->resetTransactionNesting(); } } diff --git a/src/ORM/Connect/TempDatabase.php b/src/ORM/Connect/TempDatabase.php index ce9065bbb..dce37959f 100644 --- a/src/ORM/Connect/TempDatabase.php +++ b/src/ORM/Connect/TempDatabase.php @@ -23,11 +23,6 @@ class TempDatabase */ protected $name = null; - /** - * @var bool If a transaction has been started - */ - protected $hasStarted = false; - /** * Create a new temp database * @@ -73,14 +68,6 @@ class TempDatabase return $this->isDBTemp($selected); } - /** - * @return bool - */ - public function hasStarted() - { - return $this->hasStarted; - } - /** * @return bool */ @@ -94,7 +81,6 @@ class TempDatabase */ public function startTransaction() { - $this->hasStarted = true; if (static::getConn()->supportsTransactions()) { static::getConn()->transactionStart(); } @@ -110,18 +96,21 @@ class TempDatabase */ public function rollbackTransaction() { - $success = $this->hasStarted() && static::getConn()->supportsTransactions(); - if ($success) { - try { - // Explicit false = gnostic error from transactionRollback - if (static::getConn()->transactionRollback() === false) { - $success = false; - } - } catch (DatabaseException $ex) { - $success = false; - } + // Ensure a rollback can be performed + $success = static::getConn()->supportsTransactions() + && static::getConn()->transactionDepth(); + if (!$success) { + return false; + } + try { + // Explicit false = gnostic error from transactionRollback + if (static::getConn()->transactionRollback() === false) { + return false; + } + return true; + } catch (DatabaseException $ex) { + return false; } - return $success; } /** @@ -129,13 +118,14 @@ class TempDatabase */ public function kill() { - $this->hasStarted = false; - - // Delete our temporary database + // Nothing to kill if (!$this->isUsed()) { return; } + // Rollback any transactions (note: Success ignored) + $this->rollbackTransaction(); + // Check the database actually exists $dbConn = $this->getConn(); $dbName = $dbConn->getSelectedDatabase(); @@ -160,7 +150,6 @@ class TempDatabase */ public function clearAllData() { - $this->hasStarted = false; if (!$this->isUsed()) { return; } @@ -218,44 +207,12 @@ class TempDatabase } /** - * Clear all temp DBs on this connection + * Rebuild all database tables * - * Note: This will output results to stdout unless suppressOutput - * is set on the current db schema + * @param array $extraDataObjects */ - public function deleteAll() + protected function rebuildTables($extraDataObjects = []) { - $schema = $this->getConn()->getSchemaManager(); - foreach ($schema->databaseList() as $dbName) { - if ($this->isDBTemp($dbName)) { - $schema->dropDatabase($dbName); - $schema->alterationMessage("Dropped database \"$dbName\"", 'deleted'); - flush(); - } - } - } - - /** - * Reset the testing database's schema. - * - * @param array $extraDataObjects List of extra dataobjects to build - */ - public function resetDBSchema(array $extraDataObjects = []) - { - // pgsql doesn't allow schema updates inside transactions - // so we need to rollback any transactions before commencing a schema reset - if ($this->hasStarted()) { - // Sometimes transactions fail, rebuild - $success = $this->rollbackTransaction(); - if (!$success) { - $this->kill(); - $this->build(); - } - } - if (!$this->isUsed()) { - return; - } - DataObject::reset(); // clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild() @@ -291,4 +248,45 @@ class TempDatabase ClassInfo::reset_db_cache(); DataObject::singleton()->flushCache(); } + + /** + * Clear all temp DBs on this connection + * + * Note: This will output results to stdout unless suppressOutput + * is set on the current db schema + */ + public function deleteAll() + { + $schema = $this->getConn()->getSchemaManager(); + foreach ($schema->databaseList() as $dbName) { + if ($this->isDBTemp($dbName)) { + $schema->dropDatabase($dbName); + $schema->alterationMessage("Dropped database \"$dbName\"", 'deleted'); + flush(); + } + } + } + + /** + * Reset the testing database's schema. + * + * @param array $extraDataObjects List of extra dataobjects to build + */ + public function resetDBSchema(array $extraDataObjects = []) + { + // Skip if no DB + if (!$this->isUsed()) { + return; + } + + try { + $this->rebuildTables($extraDataObjects); + } catch (DatabaseException $ex) { + // In case of error during build force a hard reset + // e.g. pgsql doesn't allow schema updates inside transactions + $this->kill(); + $this->build(); + $this->rebuildTables($extraDataObjects); + } + } } diff --git a/tests/php/Security/GroupTest.php b/tests/php/Security/GroupTest.php index 163257602..076e04584 100644 --- a/tests/php/Security/GroupTest.php +++ b/tests/php/Security/GroupTest.php @@ -20,6 +20,11 @@ class GroupTest extends FunctionalTest TestMember::class ]; + protected function setUp() + { + parent::setUp(); + } + public function testGroupCodeDefaultsToTitle() { $g1 = new Group(); From 1d06dcdd28feb0c6a215bd55aa90cfc0fd4f3a4e Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Tue, 19 Jun 2018 17:00:23 +1200 Subject: [PATCH 05/35] Use 2.1 of postgres --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 70e8be4a5..25b1b1b91 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,7 +67,7 @@ before_script: # Install composer dependencies - composer validate - mkdir ./public - - if [[ $DB == PGSQL ]]; then composer require silverstripe/postgresql:2.0.x-dev --no-update; fi + - if [[ $DB == PGSQL ]]; then composer require silverstripe/postgresql:2.1.x-dev --no-update; fi - if [[ $DB == SQLITE ]]; then composer require silverstripe/sqlite3:2.0.x-dev --no-update; fi - composer require silverstripe/recipe-testing:^1 silverstripe/recipe-core:4.2.x-dev silverstripe/admin:1.2.x-dev silverstripe/versioned:1.2.x-dev --no-update - if [[ $PHPUNIT_TEST == cms ]]; then composer require silverstripe/recipe-cms:4.2.x-dev --no-update; fi From cbdf547c1b0f891f378e8d2edc6fb51b356f0edc Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Tue, 19 Jun 2018 18:31:59 +0100 Subject: [PATCH 06/35] Address feedback --- src/Dev/State/FixtureTestState.php | 7 ++++--- src/ORM/Connect/Database.php | 2 +- src/ORM/Connect/MySQLDatabase.php | 5 +---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Dev/State/FixtureTestState.php b/src/Dev/State/FixtureTestState.php index ec5a32d32..80ac22e4f 100644 --- a/src/Dev/State/FixtureTestState.php +++ b/src/Dev/State/FixtureTestState.php @@ -166,7 +166,7 @@ class FixtureTestState implements TestState $this->loadFixture($fixtureFile, $test); } // Flag as loaded - $this->loaded[get_class($test)] = true; + $this->loaded[strtolower(get_class($test))] = true; } /** @@ -265,7 +265,8 @@ class FixtureTestState implements TestState */ protected function resetFixtureFactory($class) { - $this->fixtureFactories[strtolower($class)] = Injector::inst()->create(FixtureFactory::class); + $class = strtolower($class); + $this->fixtureFactories[$class] = Injector::inst()->create(FixtureFactory::class); $this->loaded[$class] = false; } @@ -277,6 +278,6 @@ class FixtureTestState implements TestState */ protected function getIsLoaded($class) { - return !empty($this->loaded[$class]); + return !empty($this->loaded[strtolower($class)]); } } diff --git a/src/ORM/Connect/Database.php b/src/ORM/Connect/Database.php index 4c60ed085..670b07b5b 100644 --- a/src/ORM/Connect/Database.php +++ b/src/ORM/Connect/Database.php @@ -676,7 +676,7 @@ abstract class Database public function transactionDepth() { // Placeholder error for transactional DBs that don't expose depth - if ($this->supportsTransactions()) { + if (!$this->supportsTransactions()) { user_error(get_class($this) . " does not support transactionDepth", E_USER_WARNING); } return 0; diff --git a/src/ORM/Connect/MySQLDatabase.php b/src/ORM/Connect/MySQLDatabase.php index 49f6dd62d..0477591de 100644 --- a/src/ORM/Connect/MySQLDatabase.php +++ b/src/ORM/Connect/MySQLDatabase.php @@ -360,10 +360,7 @@ class MySQLDatabase extends Database return false; } --$this->transactionNesting; - if ($this->transactionNesting <= 0) { - $this->transactionNesting = 0; - $this->query('COMMIT AND ' . ($chain ? '' : 'NO ') . 'CHAIN'); - } + $this->query('COMMIT AND ' . ($chain ? '' : 'NO ') . 'CHAIN'); return true; } From 1048520fbe809954852483f74fc91314a29edca5 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Wed, 20 Jun 2018 10:41:36 +1200 Subject: [PATCH 07/35] Restore check for zero or negative transaction nesting --- src/ORM/Connect/MySQLDatabase.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ORM/Connect/MySQLDatabase.php b/src/ORM/Connect/MySQLDatabase.php index 0477591de..49f6dd62d 100644 --- a/src/ORM/Connect/MySQLDatabase.php +++ b/src/ORM/Connect/MySQLDatabase.php @@ -360,7 +360,10 @@ class MySQLDatabase extends Database return false; } --$this->transactionNesting; - $this->query('COMMIT AND ' . ($chain ? '' : 'NO ') . 'CHAIN'); + if ($this->transactionNesting <= 0) { + $this->transactionNesting = 0; + $this->query('COMMIT AND ' . ($chain ? '' : 'NO ') . 'CHAIN'); + } return true; } From 793aafae917aa190e711ae5c7a18ed59c2812634 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Thu, 21 Jun 2018 14:26:21 +0100 Subject: [PATCH 08/35] FIX Transaction depth should error if not implemented by child classes --- src/ORM/Connect/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ORM/Connect/Database.php b/src/ORM/Connect/Database.php index 670b07b5b..4c60ed085 100644 --- a/src/ORM/Connect/Database.php +++ b/src/ORM/Connect/Database.php @@ -676,7 +676,7 @@ abstract class Database public function transactionDepth() { // Placeholder error for transactional DBs that don't expose depth - if (!$this->supportsTransactions()) { + if ($this->supportsTransactions()) { user_error(get_class($this) . " does not support transactionDepth", E_USER_WARNING); } return 0; From b78a89a76cd1c4596a159b8a5043517407de89bc Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Mon, 18 Jun 2018 10:25:56 +0100 Subject: [PATCH 09/35] FIX Default cache state should be `no-cache` --- src/Control/Middleware/HTTPCacheControlMiddleware.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Control/Middleware/HTTPCacheControlMiddleware.php b/src/Control/Middleware/HTTPCacheControlMiddleware.php index 048303eb1..13608d5e2 100644 --- a/src/Control/Middleware/HTTPCacheControlMiddleware.php +++ b/src/Control/Middleware/HTTPCacheControlMiddleware.php @@ -26,6 +26,8 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable const STATE_DISABLED = 'disabled'; + const STATE_DEFAULT = 'default'; + /** * Generate response for the given request * @@ -90,6 +92,9 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable self::STATE_ENABLED => [ 'must-revalidate' => true, ], + self::STATE_DEFAULT => [ + 'no-cache' => true, + ], ]; /** @@ -98,7 +103,7 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable * @config * @var string */ - protected static $defaultState = self::STATE_DISABLED; + protected static $defaultState = self::STATE_DEFAULT; /** * Current state From f7f567a12e95ffbf6bbb790f577dcd8c39175614 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Mon, 2 Jul 2018 13:54:11 +0100 Subject: [PATCH 10/35] Make config private (notation isnt working) --- src/Control/Middleware/HTTPCacheControlMiddleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Control/Middleware/HTTPCacheControlMiddleware.php b/src/Control/Middleware/HTTPCacheControlMiddleware.php index 13608d5e2..36b2f56ac 100644 --- a/src/Control/Middleware/HTTPCacheControlMiddleware.php +++ b/src/Control/Middleware/HTTPCacheControlMiddleware.php @@ -103,7 +103,7 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable * @config * @var string */ - protected static $defaultState = self::STATE_DEFAULT; + private static $defaultState = self::STATE_DEFAULT; /** * Current state From c52be7fe09bcf38f0ec34a34493ce24cea76de2c Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Mon, 2 Jul 2018 15:13:46 +0100 Subject: [PATCH 11/35] Consolidate disabling cache logic --- src/Control/Middleware/HTTPCacheControlMiddleware.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Control/Middleware/HTTPCacheControlMiddleware.php b/src/Control/Middleware/HTTPCacheControlMiddleware.php index 36b2f56ac..572e6fb2b 100644 --- a/src/Control/Middleware/HTTPCacheControlMiddleware.php +++ b/src/Control/Middleware/HTTPCacheControlMiddleware.php @@ -771,14 +771,9 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable } // Errors disable cache (unless some errors are cached intentionally by usercode) - if ($response->isError()) { + if ($response->isError() || $response->isRedirect()) { // Even if publicCache(true) is specified, errors will be uncacheable $this->disableCache(true); } - - // Don't cache redirects - if ($response->isRedirect()) { - $this->disableCache(true); - } } } From 997730aa7f507289da843974a386d0340842c14a Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Thu, 5 Jul 2018 22:50:35 +0100 Subject: [PATCH 12/35] FIX Allow cache control changes to affect default state --- src/Control/Middleware/HTTPCacheControlMiddleware.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Control/Middleware/HTTPCacheControlMiddleware.php b/src/Control/Middleware/HTTPCacheControlMiddleware.php index 572e6fb2b..18801f204 100644 --- a/src/Control/Middleware/HTTPCacheControlMiddleware.php +++ b/src/Control/Middleware/HTTPCacheControlMiddleware.php @@ -513,7 +513,7 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable public function setMaxAge($age) { // Affect all non-disabled states - $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC]; + $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC, self::STATE_DEFAULT]; $this->setStateDirective($applyTo, 'max-age', $age); return $this; } @@ -529,7 +529,7 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable public function setSharedMaxAge($age) { // Affect all non-disabled states - $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC]; + $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC, self::STATE_DEFAULT]; $this->setStateDirective($applyTo, 's-maxage', $age); return $this; } @@ -543,7 +543,7 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable */ public function setMustRevalidate($mustRevalidate = true) { - $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC]; + $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC, self::STATE_DEFAULT]; $this->setStateDirective($applyTo, 'must-revalidate', $mustRevalidate); return $this; } From 842b39e9884aefb91866cbcb4943f6aeae586d8b Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Thu, 5 Jul 2018 22:51:03 +0100 Subject: [PATCH 13/35] FIX Add must-revalidate to default state so its common on all our core states --- src/Control/Middleware/HTTPCacheControlMiddleware.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Control/Middleware/HTTPCacheControlMiddleware.php b/src/Control/Middleware/HTTPCacheControlMiddleware.php index 18801f204..940a4a757 100644 --- a/src/Control/Middleware/HTTPCacheControlMiddleware.php +++ b/src/Control/Middleware/HTTPCacheControlMiddleware.php @@ -94,6 +94,7 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable ], self::STATE_DEFAULT => [ 'no-cache' => true, + 'must-revalidate' => true, ], ]; From 2b1c55bc4e57d43490973452c8cc773a1a24c8ee Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Thu, 5 Jul 2018 22:51:39 +0100 Subject: [PATCH 14/35] FIX Allow setNoCache(false) to remove no-cache directive --- src/Control/Middleware/HTTPCacheControlMiddleware.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Control/Middleware/HTTPCacheControlMiddleware.php b/src/Control/Middleware/HTTPCacheControlMiddleware.php index 940a4a757..6ea8a2662 100644 --- a/src/Control/Middleware/HTTPCacheControlMiddleware.php +++ b/src/Control/Middleware/HTTPCacheControlMiddleware.php @@ -499,7 +499,13 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable { // Affect all non-disabled states $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC]; - $this->setStateDirective($applyTo, 'no-cache', $noCache); + if ($noCache) { + $this->setStateDirective($applyTo, 'no-cache'); + $this->removeStateDirective($applyTo, 'max-age'); + $this->removeStateDirective($applyTo, 's-maxage'); + } else { + $this->removeStateDirective($applyTo, 'no-cache'); + } return $this; } From cebed776ab93e8adfbb437dc0b1448ced9f71215 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Thu, 5 Jul 2018 22:52:04 +0100 Subject: [PATCH 15/35] FIX If theres a max-age set remove no-cache and no-store --- src/Control/Middleware/HTTPCacheControlMiddleware.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Control/Middleware/HTTPCacheControlMiddleware.php b/src/Control/Middleware/HTTPCacheControlMiddleware.php index 6ea8a2662..3bdf2d02b 100644 --- a/src/Control/Middleware/HTTPCacheControlMiddleware.php +++ b/src/Control/Middleware/HTTPCacheControlMiddleware.php @@ -522,6 +522,10 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable // Affect all non-disabled states $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC, self::STATE_DEFAULT]; $this->setStateDirective($applyTo, 'max-age', $age); + if ($age) { + $this->removeStateDirective($applyTo, 'no-cache'); + $this->removeStateDirective($applyTo, 'no-store'); + } return $this; } @@ -538,6 +542,10 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable // Affect all non-disabled states $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC, self::STATE_DEFAULT]; $this->setStateDirective($applyTo, 's-maxage', $age); + if ($age) { + $this->removeStateDirective($applyTo, 'no-cache'); + $this->removeStateDirective($applyTo, 'no-store'); + } return $this; } From 9f1471332d0a331ccd8cec00cb8ac2afd507adc8 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Thu, 5 Jul 2018 23:12:55 +0100 Subject: [PATCH 16/35] Make augmentState method more efficient --- .../Middleware/HTTPCacheControlMiddleware.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Control/Middleware/HTTPCacheControlMiddleware.php b/src/Control/Middleware/HTTPCacheControlMiddleware.php index 3bdf2d02b..9d710bf94 100644 --- a/src/Control/Middleware/HTTPCacheControlMiddleware.php +++ b/src/Control/Middleware/HTTPCacheControlMiddleware.php @@ -778,17 +778,16 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable */ protected function augmentState(HTTPRequest $request, HTTPResponse $response) { - // If sessions exist we assume that the responses should not be cached by CDNs / proxies as we are - // likely to be supplying information relevant to the current user only - if ($request->getSession()->getAll()) { - // Don't force in case user code chooses to opt in to public caching - $this->privateCache(); - } - // Errors disable cache (unless some errors are cached intentionally by usercode) if ($response->isError() || $response->isRedirect()) { // Even if publicCache(true) is specified, errors will be uncacheable $this->disableCache(true); + } elseif ($request->getSession()->getAll()) { + // If sessions exist we assume that the responses should not be cached by CDNs / proxies as we are + // likely to be supplying information relevant to the current user only + + // Don't force in case user code chooses to opt in to public caching + $this->privateCache(); } } } From a3687147fe2a0f25b7573b5971de7e62b924eb87 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Fri, 6 Jul 2018 11:48:06 +0100 Subject: [PATCH 17/35] State default should be state enabled (no-cache is an enabled state) --- .../Middleware/HTTPCacheControlMiddleware.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Control/Middleware/HTTPCacheControlMiddleware.php b/src/Control/Middleware/HTTPCacheControlMiddleware.php index 9d710bf94..b59b4e705 100644 --- a/src/Control/Middleware/HTTPCacheControlMiddleware.php +++ b/src/Control/Middleware/HTTPCacheControlMiddleware.php @@ -26,8 +26,6 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable const STATE_DISABLED = 'disabled'; - const STATE_DEFAULT = 'default'; - /** * Generate response for the given request * @@ -90,12 +88,9 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable 'must-revalidate' => true, ], self::STATE_ENABLED => [ - 'must-revalidate' => true, - ], - self::STATE_DEFAULT => [ 'no-cache' => true, 'must-revalidate' => true, - ], + ] ]; /** @@ -104,7 +99,7 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable * @config * @var string */ - private static $defaultState = self::STATE_DEFAULT; + private static $defaultState = self::STATE_ENABLED; /** * Current state @@ -520,7 +515,7 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable public function setMaxAge($age) { // Affect all non-disabled states - $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC, self::STATE_DEFAULT]; + $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC]; $this->setStateDirective($applyTo, 'max-age', $age); if ($age) { $this->removeStateDirective($applyTo, 'no-cache'); @@ -540,7 +535,7 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable public function setSharedMaxAge($age) { // Affect all non-disabled states - $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC, self::STATE_DEFAULT]; + $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC]; $this->setStateDirective($applyTo, 's-maxage', $age); if ($age) { $this->removeStateDirective($applyTo, 'no-cache'); @@ -558,7 +553,7 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable */ public function setMustRevalidate($mustRevalidate = true) { - $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC, self::STATE_DEFAULT]; + $applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC]; $this->setStateDirective($applyTo, 'must-revalidate', $mustRevalidate); return $this; } From 540c9aba69b8959ee106b01e3d1a2d4976b8b2dc Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Fri, 6 Jul 2018 11:48:30 +0100 Subject: [PATCH 18/35] TEST Add tests for (im)mutablity of states --- .../HTTPCacheControlMiddlewareTest.php | 169 +++++++++++++++++- 1 file changed, 166 insertions(+), 3 deletions(-) diff --git a/tests/php/Control/Middleware/HTTPCacheControlMiddlewareTest.php b/tests/php/Control/Middleware/HTTPCacheControlMiddlewareTest.php index 3eeff44f0..52bbab6a8 100644 --- a/tests/php/Control/Middleware/HTTPCacheControlMiddlewareTest.php +++ b/tests/php/Control/Middleware/HTTPCacheControlMiddlewareTest.php @@ -2,6 +2,7 @@ namespace SilverStripe\Control\Tests\Middleware; +use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware; use SilverStripe\Dev\SapphireTest; @@ -12,15 +13,177 @@ class HTTPCacheControlMiddlewareTest extends SapphireTest parent::setUp(); // Set to disabled at null forcing level HTTPCacheControlMiddleware::config() - ->set('defaultState', 'disabled') + ->set('defaultState', HTTPCacheControlMiddleware::STATE_ENABLED) ->set('defaultForcingLevel', 0); HTTPCacheControlMiddleware::reset(); } + public function provideCacheStates() + { + return [ + ['enableCache', false], + ['publicCache', false], + ['privateCache', false], + ['disableCache', true], + ]; + } + + /** + * @dataProvider provideCacheStates + */ + public function testCheckDefaultStates($state, $immutable) + { + $cc = HTTPCacheControlMiddleware::singleton(); + $cc->{$state}(); + + $response = new HTTPResponse(); + $cc->applyToResponse($response); + + $this->assertContains('must-revalidate', $response->getHeader('cache-control')); + } + + /** + * @dataProvider provideCacheStates + */ + public function testSetMaxAge($state, $immutable) + { + $cc = HTTPCacheControlMiddleware::singleton(); + $cc->{$state}(); + + $originalResponse = new HTTPResponse(); + $cc->applyToResponse($originalResponse); + + $cc->setMaxAge('300'); + + $response = new HTTPResponse(); + + $cc->applyToResponse($response); + + if ($immutable) { + $this->assertEquals($originalResponse->getHeader('cache-control'), $response->getHeader('cache-control')); + } else { + $this->assertContains('max-age=300', $response->getHeader('cache-control')); + $this->assertNotContains('no-cache', $response->getHeader('cache-control')); + $this->assertNotContains('no-store', $response->getHeader('cache-control')); + } + } + + /** + * @dataProvider provideCacheStates + */ + public function testSetNoStore($state, $immutable) + { + $cc = HTTPCacheControlMiddleware::singleton(); + $cc->setMaxAge('300'); + $cc->setSharedMaxAge('300'); + + $cc->{$state}(); + + $originalResponse = new HTTPResponse(); + $cc->applyToResponse($originalResponse); + + $cc->setNoStore(); + + $response = new HTTPResponse(); + + $cc->applyToResponse($response); + + if ($immutable) { + $this->assertEquals($originalResponse->getHeader('cache-control'), $response->getHeader('cache-control')); + } else { + $this->assertContains('no-store', $response->getHeader('cache-control')); + $this->assertNotContains('max-age', $response->getHeader('cache-control')); + $this->assertNotContains('s-maxage', $response->getHeader('cache-control')); + } + } + + /** + * @dataProvider provideCacheStates + */ + public function testSetNoCache($state, $immutable) + { + $cc = HTTPCacheControlMiddleware::singleton(); + $cc->setMaxAge('300'); + $cc->setSharedMaxAge('300'); + + $cc->{$state}(); + + $originalResponse = new HTTPResponse(); + $cc->applyToResponse($originalResponse); + + $cc->setNoCache(); + + $response = new HTTPResponse(); + + $cc->applyToResponse($response); + + if ($immutable) { + $this->assertEquals($originalResponse->getHeader('cache-control'), $response->getHeader('cache-control')); + } else { + $this->assertContains('no-cache', $response->getHeader('cache-control')); + $this->assertNotContains('max-age', $response->getHeader('cache-control')); + $this->assertNotContains('s-maxage', $response->getHeader('cache-control')); + } + } + + /** + * @dataProvider provideCacheStates + */ + public function testSetSharedMaxAge($state, $immutable) + { + $cc = HTTPCacheControlMiddleware::singleton(); + + $cc->{$state}(); + + $originalResponse = new HTTPResponse(); + $cc->applyToResponse($originalResponse); + + $cc->setSharedMaxAge('300'); + + $response = new HTTPResponse(); + + $cc->applyToResponse($response); + + if ($immutable) { + $this->assertEquals($originalResponse->getHeader('cache-control'), $response->getHeader('cache-control')); + } else { + $this->assertContains('s-maxage=300', $response->getHeader('cache-control')); + $this->assertNotContains('no-cache', $response->getHeader('cache-control')); + $this->assertNotContains('no-store', $response->getHeader('cache-control')); + } + } + + /** + * @dataProvider provideCacheStates + */ + public function testSetMustRevalidate($state, $immutable) + { + $cc = HTTPCacheControlMiddleware::singleton(); + + $cc->{$state}(); + + $originalResponse = new HTTPResponse(); + $cc->applyToResponse($originalResponse); + + $cc->setMustRevalidate(); + + $response = new HTTPResponse(); + + $cc->applyToResponse($response); + + if ($immutable) { + $this->assertEquals($originalResponse->getHeader('cache-control'), $response->getHeader('cache-control')); + } else { + $this->assertContains('must-revalidate', $response->getHeader('cache-control')); + $this->assertNotContains('max-age', $response->getHeader('cache-control')); + $this->assertNotContains('s-maxage', $response->getHeader('cache-control')); + } + } + public function testCachingPriorities() { $hcc = new HTTPCacheControlMiddleware(); - $this->assertTrue($this->isDisabled($hcc), 'caching starts as disabled'); + $this->assertFalse($this->isDisabled($hcc), 'caching starts as disabled'); $hcc->enableCache(); $this->assertFalse($this->isDisabled($hcc)); @@ -74,6 +237,6 @@ class HTTPCacheControlMiddlewareTest extends SapphireTest protected function isDisabled(HTTPCacheControlMiddleware $hcc) { - return $hcc->hasDirective('no-cache') && !$hcc->hasDirective('private') && !$hcc->hasDirective('public'); + return $hcc->hasDirective('no-store') && !$hcc->hasDirective('private') && !$hcc->hasDirective('public'); } } From e37b3b95f4e02eaa8bccd8acc30cdc001d7239c5 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Sat, 14 Jul 2018 19:30:29 +0100 Subject: [PATCH 19/35] FIX updateValidatePassword calls need to be masked from backtraces --- src/Dev/Backtrace.php | 6 ++++- tests/php/Dev/BacktraceTest.php | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/Dev/Backtrace.php b/src/Dev/Backtrace.php index 756945c52..daceca751 100644 --- a/src/Dev/Backtrace.php +++ b/src/Dev/Backtrace.php @@ -45,6 +45,7 @@ class Backtrace array('SilverStripe\\Security\\PasswordEncryptor_MySQLOldPassword', 'salt'), array('SilverStripe\\Security\\PasswordEncryptor_Blowfish', 'encrypt'), array('SilverStripe\\Security\\PasswordEncryptor_Blowfish', 'salt'), + array('*', 'updateValidatePassword'), ); /** @@ -106,7 +107,10 @@ class Backtrace $match = false; if (!empty($bt[$i]['class'])) { foreach ($ignoredArgs as $fnSpec) { - if (is_array($fnSpec) && $bt[$i]['class'] == $fnSpec[0] && $bt[$i]['function'] == $fnSpec[1]) { + if (is_array($fnSpec) && + ('*' == $fnSpec[0] || $bt[$i]['class'] == $fnSpec[0]) && + $bt[$i]['function'] == $fnSpec[1] + ) { $match = true; } } diff --git a/tests/php/Dev/BacktraceTest.php b/tests/php/Dev/BacktraceTest.php index 8a7f476f8..c10e47e79 100644 --- a/tests/php/Dev/BacktraceTest.php +++ b/tests/php/Dev/BacktraceTest.php @@ -68,4 +68,45 @@ class BacktraceTest extends SapphireTest $this->assertEquals('', $filtered[1]['args']['password'], 'Filters class functions'); $this->assertEquals('myval', $filtered[2]['args']['myarg'], 'Doesnt filter other functions'); } + + public function testFilteredWildCard() + { + $bt = array( + array( + 'type' => '->', + 'file' => 'MyFile.php', + 'line' => 99, + 'function' => 'myIgnoredGlobalFunction', + 'args' => array('password' => 'secred',) + ), + array( + 'class' => 'MyClass', + 'type' => '->', + 'file' => 'MyFile.php', + 'line' => 99, + 'function' => 'myIgnoredClassFunction', + 'args' => array('password' => 'secred',) + ), + array( + 'class' => 'MyClass', + 'type' => '->', + 'file' => 'MyFile.php', + 'line' => 99, + 'function' => 'myFunction', + 'args' => array('myarg' => 'myval') + ) + ); + Backtrace::config()->update( + 'ignore_function_args', + array( + array('*', 'myIgnoredClassFunction'), + ) + ); + + $filtered = Backtrace::filter_backtrace($bt); + + $this->assertEquals('secred', $filtered[0]['args']['password']); + $this->assertEquals('', $filtered[1]['args']['password']); + $this->assertEquals('myval', $filtered[2]['args']['myarg']); + } } From 76ac8465de768ab08c904e0e8c6075a9f4ceb56b Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 18 Jul 2018 14:24:27 +1200 Subject: [PATCH 20/35] BUG Lazy session state (fixes #8267) Fixes regression from 3.x, where sessions where lazy started as required: Either because an existing session identifier was sent through with the request, or because new session data needed to be persisted as part of the request execution. Without this lazy starting, *every* request will get a session, which makes all those responses uncacheable by HTTP layers. Note that 4.x also changed the $data vs. $changedData payloads: In 3.x, they both contained key/value pairs. In 4.x, $data contains key/value, while $changedData contains key/boolean to declare isChanged. While this reduces duplication in the class, it also surfaced a bug which was latent in 3.x: When an existing session is lazily resumed via start(), $data is set back to an empty array. In 3.x, any changed data before this point was *also* retained in $changedData, ensuring it gets merged into existing $_SESSION data. In 4.x, this clears out data - hence the need for a more complex merge logic. Since isset($this->data) is no longer an accurate indicator of a started session, we introduce a separate $this->started flag. Note that I've chosen not to make lazy an opt-in (e.g. via start($request, $lazy=false)). We already have a distinction between lazy starting via init(), and force starting via start(). --- src/Control/Session.php | 77 ++++++++++++------- .../SessionAuthenticationHandler.php | 9 ++- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/Control/Session.php b/src/Control/Session.php index d0e68f35e..17a2c295a 100644 --- a/src/Control/Session.php +++ b/src/Control/Session.php @@ -49,6 +49,9 @@ use SilverStripe\Dev\Deprecation; * * Once you have saved a value to the Session you can access it by using the {@link Session::get()} function. * Like the {@link Session::set()} function you can use this anywhere in your PHP files. + * Note that session data isn't persisted in PHP's own session store (via $_SESSION) + * until {@link Session::save()} is called, which happens automatically at the end of a standard request + * through {@link SilverStripe\Control\Middleware\SessionMiddleware}. * * The values in the comments are the values stored from the previous example. * @@ -84,7 +87,6 @@ use SilverStripe\Dev\Deprecation; * * * @see Cookie - * @todo This class is currently really basic and could do with a more well-thought-out implementation. */ class Session { @@ -128,6 +130,12 @@ class Session */ private static $cookie_secure = false; + /** + * @config + * @var string + */ + private static $cookie_name_secure = 'SECSESSID'; + /** * Name of session cache limiter to use. * Defaults to '' to disable cache limiter entirely. @@ -145,6 +153,11 @@ class Session */ protected $data = null; + /** + * @var bool + */ + protected $started = false; + /** * List of keys changed. This is a nested array which represents the * keys modified in $this->data. The value of each item is either "true" @@ -192,16 +205,21 @@ class Session } $this->data = $data; + $this->started = isset($data); } /** - * Init this session instance before usage + * Init this session instance before usage, + * if a session identifier is part of the passed in request. + * Otherwise, a session might be started in {@link save()} + * if session data needs to be written with a new session identifier. * * @param HTTPRequest $request */ public function init(HTTPRequest $request) { - if (!$this->isStarted()) { + + if (!$this->isStarted() && $this->requestContainsSessionId($request)) { $this->start($request); } @@ -210,6 +228,7 @@ class Session if ($this->data['HTTP_USER_AGENT'] !== $this->userAgent($request)) { $this->clearAll(); $this->destroy(); + $this->started = false; $this->start($request); } } @@ -233,11 +252,24 @@ class Session */ public function isStarted() { - return isset($this->data); + return $this->started; } /** - * Begin session + * @param HTTPRequest $request + * @return bool + */ + public function requestContainsSessionId(HTTPRequest $request) + { + $secure = Director::is_https($request) && $this->config()->get('cookie_secure'); + $name = $secure ? $this->config()->get('cookie_name_secure') : session_name(); + return (bool)Cookie::get($name); + } + + /** + * Begin session, regardless if a session identifier is present in the request, + * or whether any session data needs to be written. + * See {@link init()} if you want to "lazy start" a session. * * @param HTTPRequest $request The request for which to start a session */ @@ -281,7 +313,7 @@ class Session // If we want a secure cookie for HTTPS, use a seperate session name. This lets us have a // seperate (less secure) session for non-HTTPS requests if ($secure) { - session_name('SECSESSID'); + session_name($this->config()->get('cookie_name_secure')); } $limiter = $this->config()->get('sessionCacheLimiter'); @@ -291,7 +323,16 @@ class Session session_start(); - $this->data = isset($_SESSION) ? $_SESSION : array(); + if (isset($_SESSION)) { + // Initialise data from session store if present + $data = $_SESSION; + // Merge in existing in-memory data, taking priority over session store data + $this->recursivelyApply((array)$this->data, $data); + } else { + // Use in-memory data if the session is lazy started + $data = isset($this->data) ? $this->data : []; + } + $this->data = $data; } else { $this->data = []; } @@ -302,6 +343,8 @@ class Session Cookie::set(session_name(), session_id(), $timeout/86400, $path, $domain ? $domain : null, $secure, true); } + + $this->started = true; } /** @@ -335,9 +378,6 @@ class Session */ public function set($name, $val) { - if (!$this->isStarted()) { - throw new BadMethodCallException("Session cannot be modified until it's started"); - } $var = &$this->nestedValueRef($name, $this->data); // Mark changed @@ -380,10 +420,6 @@ class Session */ public function addToArray($name, $val) { - if (!$this->isStarted()) { - throw new BadMethodCallException("Session cannot be modified until it's started"); - } - $names = explode('.', $name); // We still want to do this even if we have strict path checking for legacy code @@ -407,9 +443,6 @@ class Session */ public function get($name) { - if (!$this->isStarted()) { - throw new BadMethodCallException("Session cannot be accessed until it's started"); - } return $this->nestedValue($name, $this->data); } @@ -421,10 +454,6 @@ class Session */ public function clear($name) { - if (!$this->isStarted()) { - throw new BadMethodCallException("Session cannot be modified until it's started"); - } - // Get var by path $var = $this->nestedValue($name, $this->data); @@ -449,10 +478,6 @@ class Session */ public function clearAll() { - if (!$this->isStarted()) { - throw new BadMethodCallException("Session cannot be modified until it's started"); - } - if ($this->data && is_array($this->data)) { foreach (array_keys($this->data) as $key) { $this->clear($key); @@ -495,7 +520,7 @@ class Session $this->start($request); } - // Apply all changes recursively + // Apply all changes recursively, implicitly writing them to the actual PHP session store. $this->recursivelyApplyChanges($this->changedData, $this->data, $_SESSION); } } diff --git a/src/Security/MemberAuthenticator/SessionAuthenticationHandler.php b/src/Security/MemberAuthenticator/SessionAuthenticationHandler.php index a5c9ed240..1aafc020c 100644 --- a/src/Security/MemberAuthenticator/SessionAuthenticationHandler.php +++ b/src/Security/MemberAuthenticator/SessionAuthenticationHandler.php @@ -45,9 +45,16 @@ class SessionAuthenticationHandler implements AuthenticationHandler */ public function authenticateRequest(HTTPRequest $request) { + $session = $request->getSession(); + + // Sessions are only started when a session cookie is detected + if (!$session->isStarted()) { + return null; + } + // If ID is a bad ID it will be treated as if the user is not logged in, rather than throwing a // ValidationException - $id = $request->getSession()->get($this->getSessionVariable()); + $id = $session->get($this->getSessionVariable()); if (!$id) { return null; } From 74b655d3fcb28db877d839c75f2f3f98b41448cd Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 18 Jul 2018 21:15:56 +1200 Subject: [PATCH 21/35] Fix tests on unset session data Thanks Robbie! --- src/Control/Director.php | 2 +- src/Control/Session.php | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Control/Director.php b/src/Control/Director.php index 79622d213..6e7aef593 100644 --- a/src/Control/Director.php +++ b/src/Control/Director.php @@ -206,7 +206,7 @@ class Director implements TemplateGlobalProvider if ($session instanceof Session) { // Note: If passing $session as object, ensure that changes are written back // This is important for classes such as FunctionalTest which emulate cross-request persistence - $newVars['_SESSION'] = $sessionArray = $session->getAll(); + $newVars['_SESSION'] = $sessionArray = $session->getAll() ?: []; $finally[] = function () use ($session, $sessionArray) { if (isset($_SESSION)) { // Set new / updated keys diff --git a/src/Control/Session.php b/src/Control/Session.php index 17a2c295a..f9f95d1cf 100644 --- a/src/Control/Session.php +++ b/src/Control/Session.php @@ -262,8 +262,8 @@ class Session public function requestContainsSessionId(HTTPRequest $request) { $secure = Director::is_https($request) && $this->config()->get('cookie_secure'); - $name = $secure ? $this->config()->get('cookie_name_secure') : session_name(); - return (bool)Cookie::get($name); + $name = $secure ? $this->config()->get('cookie_name_secure') : session_name(); + return (bool)Cookie::get($name); } /** @@ -330,9 +330,9 @@ class Session $this->recursivelyApply((array)$this->data, $data); } else { // Use in-memory data if the session is lazy started - $data = isset($this->data) ? $this->data : []; + $data = $this->data; } - $this->data = $data; + $this->data = $data ?: []; } else { $this->data = []; } @@ -615,6 +615,7 @@ class Session */ protected function recursivelyApplyChanges($changes, $source, &$destination) { + $source = $source ?: []; foreach ($changes as $key => $changed) { if ($changed === true) { // Determine if replacement or removal From 73026292bf3a82f02d0b140dfd9cbd49ba676ecb Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 18 Jul 2018 21:16:06 +1200 Subject: [PATCH 22/35] Test coverage for session data change --- tests/php/Control/SessionTest.php | 142 ++++++++++++++++++ .../SessionAuthenticationHandlerTest.php | 61 ++++++++ 2 files changed, 203 insertions(+) create mode 100644 tests/php/Security/MemberAuthenticator/SessionAuthenticationHandlerTest.php diff --git a/tests/php/Control/SessionTest.php b/tests/php/Control/SessionTest.php index f1b5a5430..724e778f5 100644 --- a/tests/php/Control/SessionTest.php +++ b/tests/php/Control/SessionTest.php @@ -2,6 +2,8 @@ namespace SilverStripe\Control\Tests; +use http\Exception\BadMessageException; +use SilverStripe\Control\Cookie; use SilverStripe\Control\Session; use SilverStripe\Dev\SapphireTest; use SilverStripe\Control\HTTPRequest; @@ -22,6 +24,127 @@ class SessionTest extends SapphireTest return parent::setUp(); } + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testInitDoesNotStartSessionWithoutIdentifier() + { + $req = new HTTPRequest('GET', '/'); + $session = new Session(null); // unstarted session + $session->init($req); + $this->assertFalse($session->isStarted()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testInitStartsSessionWithIdentifier() + { + $req = new HTTPRequest('GET', '/'); + Cookie::set(session_name(), '1234'); + $session = new Session(null); // unstarted session + $session->init($req); + $this->assertTrue($session->isStarted()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testInitStartsSessionWithData() + { + $req = new HTTPRequest('GET', '/'); + $session = new Session([]); + $session->init($req); + $this->assertTrue($session->isStarted()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testStartUsesDefaultCookieNameWithHttp() + { + $req = (new HTTPRequest('GET', '/')) + ->setScheme('http'); + Cookie::set(session_name(), '1234'); + $session = new Session(null); // unstarted session + $session->start($req); + $this->assertNotEquals(session_name(), $session->config()->get('cookie_name_secure')); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testStartUsesDefaultCookieNameWithHttpsAndCookieSecureOff() + { + $req = (new HTTPRequest('GET', '/')) + ->setScheme('https'); + Cookie::set(session_name(), '1234'); + $session = new Session(null); // unstarted session + $session->start($req); + $this->assertNotEquals(session_name(), $session->config()->get('cookie_name_secure')); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testStartUsesSecureCookieNameWithHttpsAndCookieSecureOn() + { + $req = (new HTTPRequest('GET', '/')) + ->setScheme('https'); + Cookie::set(session_name(), '1234'); + $session = new Session(null); // unstarted session + $session->config()->update('cookie_secure', true); + $session->start($req); + $this->assertEquals(session_name(), $session->config()->get('cookie_name_secure')); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + * @expectedException BadMethodCallException + * @expectedExceptionMessage Session has already started + */ + public function testStartErrorsWhenStartingTwice() + { + $req = new HTTPRequest('GET', '/'); + $session = new Session(null); // unstarted session + $session->start($req); + $session->start($req); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testStartRetainsInMemoryData() + { + $this->markTestIncomplete('Test'); + // TODO Figure out how to simulate session vars without a session_start() resetting them + // $_SESSION['existing'] = true; + // $_SESSION['merge'] = 1; + $req = new HTTPRequest('GET', '/'); + $session = new Session(null); // unstarted session + $session->set('new', true); + $session->set('merge', 2); + $session->start($req); // simulate lazy start + $this->assertEquals( + [ + // 'existing' => true, + 'new' => true, + 'merge' => 2 + ], + $session->getAll() + ); + + unset($_SESSION); + } + public function testGetSetBasics() { $this->session->set('Test', 'Test'); @@ -124,6 +247,25 @@ class SessionTest extends SapphireTest ); } + public function testRequestContainsSessionId() + { + $req = new HTTPRequest('GET', '/'); + $session = new Session(null); // unstarted session + $this->assertFalse($session->requestContainsSessionId($req)); + Cookie::set(session_name(), '1234'); + $this->assertTrue($session->requestContainsSessionId($req)); + } + + public function testRequestContainsSessionIdRespectsCookieNameSecure() + { + $req = (new HTTPRequest('GET', '/')) + ->setScheme('https'); + $session = new Session(null); // unstarted session + Cookie::set($session->config()->get('cookie_name_secure'), '1234'); + $session->config()->update('cookie_secure', true); + $this->assertTrue($session->requestContainsSessionId($req)); + } + public function testUserAgentLockout() { // Set a user agent diff --git a/tests/php/Security/MemberAuthenticator/SessionAuthenticationHandlerTest.php b/tests/php/Security/MemberAuthenticator/SessionAuthenticationHandlerTest.php new file mode 100644 index 000000000..800e49563 --- /dev/null +++ b/tests/php/Security/MemberAuthenticator/SessionAuthenticationHandlerTest.php @@ -0,0 +1,61 @@ + 'test@example.com']); + $member->write(); + + $handler = new SessionAuthenticationHandler(); + + $session = new Session(null); // unstarted, simulates lack of session cookie + $session->set($handler->getSessionVariable(), $member->ID); + + $req = new HTTPRequest('GET', '/'); + $req->setSession($session); + + $matchedMember = $handler->authenticateRequest($req); + $this->assertNull($matchedMember); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testAuthenticateRequestStartsSessionWithSessionIdentifier() + { + $member = new Member(['Email' => 'test@example.com']); + $member->write(); + + $handler = new SessionAuthenticationHandler(); + + $session = new Session(null); // unstarted + $session->set($handler->getSessionVariable(), $member->ID); + + $req = new HTTPRequest('GET', '/'); + $req->setSession($session); + + Cookie::set(session_name(), '1234'); + $session->start($req); // simulate detection of session cookie + + $matchedMember = $handler->authenticateRequest($req); + $this->assertNotNull($matchedMember); + $this->assertEquals($matchedMember->Email, $member->Email); + } +} From 3fa2c056d71a3286ce83017d7cafc180cee076c2 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Fri, 15 Jun 2018 17:57:17 +1200 Subject: [PATCH 23/35] FIX Don't reload form session data using FormField::setSubmittedValue (#8056) --- src/Forms/Form.php | 16 ++- tests/php/Forms/FormTest.php | 123 ++++++++++++++++++ ...trollerWithSpecialSubmittedValueFields.php | 103 +++++++++++++++ 3 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 tests/php/Forms/FormTest/ControllerWithSpecialSubmittedValueFields.php diff --git a/src/Forms/Form.php b/src/Forms/Form.php index 0619f78f5..fe3fcbd92 100644 --- a/src/Forms/Form.php +++ b/src/Forms/Form.php @@ -346,7 +346,7 @@ class Form extends ViewableData implements HasRequestHandler // load data in from previous submission upon error $data = $this->getSessionData(); if (isset($data)) { - $this->loadDataFrom($data); + $this->loadDataFrom($data, self::MERGE_AS_INTERNAL_VALUE); } return $this; } @@ -1317,9 +1317,10 @@ class Form extends ViewableData implements HasRequestHandler return $result; } - const MERGE_DEFAULT = 0; - const MERGE_CLEAR_MISSING = 1; - const MERGE_IGNORE_FALSEISH = 2; + const MERGE_DEFAULT = 0b0000; + const MERGE_CLEAR_MISSING = 0b0001; + const MERGE_IGNORE_FALSEISH = 0b0010; + const MERGE_AS_INTERNAL_VALUE = 0b0100; /** * Load data from the given DataObject or array. @@ -1340,6 +1341,7 @@ class Form extends ViewableData implements HasRequestHandler * {@link saveInto()}. * * @uses FieldList->dataFields() + * @uses FormField->setSubmittedValue() * @uses FormField->setValue() * * @param array|DataObject $data @@ -1359,6 +1361,10 @@ class Form extends ViewableData implements HasRequestHandler * Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace * a field's value. * + * Passing MERGE_AS_INTERNAL_VALUE forces the data to be parsed using the internal representation of the matching + * form field. This is helpful if you are loading an array of values retrieved from `Form::getData()` and you + * do not want them parsed as submitted data. + * * For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing * CLEAR_MISSING * @@ -1468,7 +1474,7 @@ class Form extends ViewableData implements HasRequestHandler // pass original data as well so composite fields can act on the additional information if ($setValue) { - if ($submitted) { + if ($submitted && ($mergeStrategy & self::MERGE_AS_INTERNAL_VALUE) != self::MERGE_AS_INTERNAL_VALUE) { $field->setSubmittedValue($val, $data); } else { $field->setValue($val, $data); diff --git a/tests/php/Forms/FormTest.php b/tests/php/Forms/FormTest.php index ae7e1d7bd..39bc95cf1 100644 --- a/tests/php/Forms/FormTest.php +++ b/tests/php/Forms/FormTest.php @@ -8,6 +8,7 @@ use SilverStripe\Control\Session; use SilverStripe\Dev\CSSContentParser; use SilverStripe\Dev\FunctionalTest; use SilverStripe\Forms\DateField; +use SilverStripe\Forms\DatetimeField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FileField; use SilverStripe\Forms\Form; @@ -17,12 +18,15 @@ use SilverStripe\Forms\LookupField; use SilverStripe\Forms\NumericField; use SilverStripe\Forms\PasswordField; use SilverStripe\Forms\Tests\FormTest\ControllerWithSecurityToken; +use SilverStripe\Forms\Tests\FormTest\ControllerWithSpecialSubmittedValueFields; use SilverStripe\Forms\Tests\FormTest\ControllerWithStrictPostCheck; use SilverStripe\Forms\Tests\FormTest\Player; use SilverStripe\Forms\Tests\FormTest\Team; use SilverStripe\Forms\Tests\FormTest\TestController; +use SilverStripe\Forms\Tests\ValidatorTest\TestValidator; use SilverStripe\Forms\TextareaField; use SilverStripe\Forms\TextField; +use SilverStripe\Forms\TimeField; use SilverStripe\ORM\ValidationResult; use SilverStripe\Security\NullSecurityToken; use SilverStripe\Security\RandomGenerator; @@ -46,6 +50,7 @@ class FormTest extends FunctionalTest TestController::class, ControllerWithSecurityToken::class, ControllerWithStrictPostCheck::class, + ControllerWithSpecialSubmittedValueFields::class ]; protected static $disable_themes = true; @@ -261,6 +266,65 @@ class FormTest extends FunctionalTest ); } + public function testLoadDataFromWithForceSetValueFlag() + { + // Get our data formatted in internal value and in submitted value + // We're using very esoteric date and time format + $dataInSubmittedValue = [ + 'SomeDateTimeField' => 'Fri, Jun 15, \'18 17:28:05', + 'SomeTimeField' => '05 o\'clock PM 28 05' + ]; + $dataInInternalValue = [ + 'SomeDateTimeField' => '2018-06-15 17:28:05', + 'SomeTimeField' => '17:28:05' + ]; + + // Test loading our data with the MERGE_FORCE_SET_VALUE + $form = new Form( + Controller::curr(), + 'Form', + new FieldList( + $dateField = DatetimeField::create('SomeDateTimeField') + ->setHTML5(false) + ->setDatetimeFormat("EEE, MMM d, ''yy HH:mm:ss"), + $timeField = TimeField::create('SomeTimeField') + ->setHTML5(false) + ->setTimeFormat("hh 'o''clock' a mm ss") // Swatch Internet Time format + ), + new FieldList() + ); + + $form->loadDataFrom($dataInInternalValue, Form::MERGE_AS_INTERNAL_VALUE); + + $this->assertEquals( + $dataInInternalValue, + $form->getData() + ); + + + // Test loading our data without the MERGE_FORCE_SET_VALUE + $form = new Form( + Controller::curr(), + 'Form', + new FieldList( + $dateField = DatetimeField::create('SomeDateTimeField') + ->setHTML5(false) + ->setDatetimeFormat("EEE, MMM d, ''yy HH:mm:ss"), + $timeField = TimeField::create('SomeTimeField') + ->setHTML5(false) + ->setTimeFormat("hh 'o''clock' a mm ss") // Swatch Internet Time format + ), + new FieldList() + ); + + $form->loadDataFrom($dataInSubmittedValue); + + $this->assertEquals( + $dataInInternalValue, + $form->getData() + ); + } + public function testLookupFieldDisabledSaving() { $object = new Team(); @@ -969,6 +1033,65 @@ class FormTest extends FunctionalTest } } + /** + * This test confirms that when a form validation fails, the submitted value are stored in the session and are + * reloaded correctly once the form is re-rendered. This indirectly test `Form::restoreFormState`, + * `Form::setSessionData`, `Form::getSessionData` and `Form::clearFormState`. + */ + public function testRestoreFromState() + { + // Use a specially crafted controlled for this request. The associated form contains fields that override the + // `setSubmittedValue` and require an internal format that differs from the submitted format. + $this->get('FormTest_ControllerWithSpecialSubmittedValueFields')->getBody(); + + // Posting our form. This should fail and redirect us to the form page and preload our submit value + $response = $this->post( + 'FormTest_ControllerWithSpecialSubmittedValueFields/Form', + array( + 'SomeDateField' => '15/06/2018', + 'SomeFrenchNumericField' => '9 876,5432', + 'SomeFrenchMoneyField' => [ + 'Amount' => '9 876,54', + 'Currency' => 'NZD' + ] + // Validation will fail because we leave out SomeRequiredField + ), + [] + ); + + // Test our reloaded form field + $body = $response->getBody(); + $this->assertContains( + 'assertContains( + 'assertContains( + 'assertContains( + 'assertEmpty( + $this->mainSession->session()->get('FormInfo.Form_Form'), + 'Our form was reloaded successfully. That should have cleared our session.' + ); + + } + protected function getStubForm() { return new Form( diff --git a/tests/php/Forms/FormTest/ControllerWithSpecialSubmittedValueFields.php b/tests/php/Forms/FormTest/ControllerWithSpecialSubmittedValueFields.php new file mode 100644 index 000000000..c373b124a --- /dev/null +++ b/tests/php/Forms/FormTest/ControllerWithSpecialSubmittedValueFields.php @@ -0,0 +1,103 @@ +setRequest(Controller::curr()->getRequest()); + } + } + + private static $allowed_actions = array('Form'); + + private static $url_handlers = array( + '$Action//$ID/$OtherID' => "handleAction", + ); + + protected $template = 'BlankPage'; + + public function Link($action = null) + { + return Controller::join_links( + 'FormTest_ControllerWithSpecialSubmittedValueFields', + $this->getRequest()->latestParam('Action'), + $this->getRequest()->latestParam('ID'), + $action + ); + } + + public function Form() + { + $form = new Form( + $this, + 'Form', + new FieldList( + new TextField('SomeRequiredField'), + DateField::create('SomeDateField') + ->setHTML5(false) + ->setDateFormat('dd/MM/yyyy') + ->setValue('2000-01-01'), + NumericField::create('SomeFrenchNumericField') + ->setHTML5(false) + ->setLocale('fr_FR') + ->setScale(4) + ->setValue(12345.6789), + MoneyField::create('SomeFrenchMoneyField') + ->setValue('100.5 EUR') + ->setLocale('fr_FR') + ), + new FieldList( + FormAction::create('doSubmit') + ), + new RequiredFields( + 'SomeRequiredField' + ) + ); + $form->setValidationExemptActions(array('doSubmitValidationExempt')); + $form->disableSecurityToken(); // Disable CSRF protection for easier form submission handling + + return $form; + } + + public function doSubmit($data, $form, $request) + { + $form->sessionMessage('Test save was successful', 'good'); + return $this->redirectBack(); + } + + public function doTriggerException($data, $form, $request) + { + $result = new ValidationResult(); + $result->addFieldError('Email', 'Error on Email field'); + $result->addError('Error at top of form'); + throw new ValidationException($result); + } + + public function getViewer($action = null) + { + return new SSViewer('BlankPage'); + } +} From 7fda52b2cdc55e8a7f635a8b50a5b211011eba00 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Mon, 18 Jun 2018 11:51:52 +1200 Subject: [PATCH 24/35] Add a MERGE_AS_SUBMITTED_VALUE flag for Form::loadDataFrom --- src/Forms/Form.php | 22 ++++++++--- tests/php/Forms/FormTest.php | 72 +++++++++++++++++++----------------- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/src/Forms/Form.php b/src/Forms/Form.php index fe3fcbd92..4ce24d70f 100644 --- a/src/Forms/Form.php +++ b/src/Forms/Form.php @@ -1317,10 +1317,11 @@ class Form extends ViewableData implements HasRequestHandler return $result; } - const MERGE_DEFAULT = 0b0000; - const MERGE_CLEAR_MISSING = 0b0001; - const MERGE_IGNORE_FALSEISH = 0b0010; - const MERGE_AS_INTERNAL_VALUE = 0b0100; + const MERGE_DEFAULT = 0b0000; + const MERGE_CLEAR_MISSING = 0b0001; + const MERGE_IGNORE_FALSEISH = 0b0010; + const MERGE_AS_INTERNAL_VALUE = 0b0100; + const MERGE_AS_SUBMITTED_VALUE = 0b1000; /** * Load data from the given DataObject or array. @@ -1363,7 +1364,8 @@ class Form extends ViewableData implements HasRequestHandler * * Passing MERGE_AS_INTERNAL_VALUE forces the data to be parsed using the internal representation of the matching * form field. This is helpful if you are loading an array of values retrieved from `Form::getData()` and you - * do not want them parsed as submitted data. + * do not want them parsed as submitted data. MERGE_AS_SUBMITTED_VALUE does the opposite and forces the data to be + * parsed as it would be submitted from a form. * * For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing * CLEAR_MISSING @@ -1395,6 +1397,14 @@ class Form extends ViewableData implements HasRequestHandler $submitted = false; } + // Using the `MERGE_AS_INTERNAL_VALUE` or `MERGE_AS_SUBMITTED_VALUE` flags users can explicitly specify which + // `setValue` method to use. + if (($mergeStrategy & self::MERGE_AS_INTERNAL_VALUE) == self::MERGE_AS_INTERNAL_VALUE) { + $submitted = false; + } else if (($mergeStrategy & self::MERGE_AS_SUBMITTED_VALUE) == self::MERGE_AS_SUBMITTED_VALUE) { + $submitted = true; + } + // dont include fields without data $dataFields = $this->Fields()->dataFields(); if (!$dataFields) { @@ -1474,7 +1484,7 @@ class Form extends ViewableData implements HasRequestHandler // pass original data as well so composite fields can act on the additional information if ($setValue) { - if ($submitted && ($mergeStrategy & self::MERGE_AS_INTERNAL_VALUE) != self::MERGE_AS_INTERNAL_VALUE) { + if ($submitted) { $field->setSubmittedValue($val, $data); } else { $field->setValue($val, $data); diff --git a/tests/php/Forms/FormTest.php b/tests/php/Forms/FormTest.php index 39bc95cf1..8b9a2948b 100644 --- a/tests/php/Forms/FormTest.php +++ b/tests/php/Forms/FormTest.php @@ -31,6 +31,7 @@ use SilverStripe\ORM\ValidationResult; use SilverStripe\Security\NullSecurityToken; use SilverStripe\Security\RandomGenerator; use SilverStripe\Security\SecurityToken; +use SilverStripe\View\ArrayData; use SilverStripe\View\SSViewer; /** @@ -279,21 +280,8 @@ class FormTest extends FunctionalTest 'SomeTimeField' => '17:28:05' ]; - // Test loading our data with the MERGE_FORCE_SET_VALUE - $form = new Form( - Controller::curr(), - 'Form', - new FieldList( - $dateField = DatetimeField::create('SomeDateTimeField') - ->setHTML5(false) - ->setDatetimeFormat("EEE, MMM d, ''yy HH:mm:ss"), - $timeField = TimeField::create('SomeTimeField') - ->setHTML5(false) - ->setTimeFormat("hh 'o''clock' a mm ss") // Swatch Internet Time format - ), - new FieldList() - ); - + // Test loading our data with the MERGE_AS_INTERNAL_VALUE + $form = $this->getStubFormWithWeirdValueFormat(); $form->loadDataFrom($dataInInternalValue, Form::MERGE_AS_INTERNAL_VALUE); $this->assertEquals( @@ -301,22 +289,16 @@ class FormTest extends FunctionalTest $form->getData() ); - - // Test loading our data without the MERGE_FORCE_SET_VALUE - $form = new Form( - Controller::curr(), - 'Form', - new FieldList( - $dateField = DatetimeField::create('SomeDateTimeField') - ->setHTML5(false) - ->setDatetimeFormat("EEE, MMM d, ''yy HH:mm:ss"), - $timeField = TimeField::create('SomeTimeField') - ->setHTML5(false) - ->setTimeFormat("hh 'o''clock' a mm ss") // Swatch Internet Time format - ), - new FieldList() + // Test loading our data with the MERGE_AS_SUBMITTED_VALUE and an data passed as an object + $form = $this->getStubFormWithWeirdValueFormat(); + $form->loadDataFrom(ArrayData::create($dataInSubmittedValue), Form::MERGE_AS_SUBMITTED_VALUE); + $this->assertEquals( + $dataInInternalValue, + $form->getData() ); + // Test loading our data without the MERGE_AS_INTERNAL_VALUE and without MERGE_AS_SUBMITTED_VALUE + $form = $this->getStubFormWithWeirdValueFormat(); $form->loadDataFrom($dataInSubmittedValue); $this->assertEquals( @@ -339,10 +321,10 @@ class FormTest extends FunctionalTest $form->loadDataFrom( array( 'Players' => array( - 14, - 18, - 22 - ), + 14, + 18, + 22 + ), ) ); $form->saveInto($object); @@ -1101,4 +1083,28 @@ class FormTest extends FunctionalTest new FieldList() ); } + + /** + * Some fields handle submitted values differently from their internal values. This forms contains 2 such fields + * * a SomeDateTimeField that expect a date such as `Fri, Jun 15, '18 17:28:05`, + * * a SomeTimeField that expects it's time as `05 o'clock PM 28 05` + * + * @return Form + */ + protected function getStubFormWithWeirdValueFormat() + { + return new Form( + Controller::curr(), + 'Form', + new FieldList( + $dateField = DatetimeField::create('SomeDateTimeField') + ->setHTML5(false) + ->setDatetimeFormat("EEE, MMM d, ''yy HH:mm:ss"), + $timeField = TimeField::create('SomeTimeField') + ->setHTML5(false) + ->setTimeFormat("hh 'o''clock' a mm ss") // Swatch Internet Time format + ), + new FieldList() + ); + } } From c77042aa8b5ba70f4c9dc6924d2ccccf3d5588a5 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Tue, 19 Jun 2018 09:11:16 +1200 Subject: [PATCH 25/35] Fix linting. --- src/Forms/Form.php | 2 +- tests/php/Forms/FormTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Forms/Form.php b/src/Forms/Form.php index 4ce24d70f..3f5f95771 100644 --- a/src/Forms/Form.php +++ b/src/Forms/Form.php @@ -1401,7 +1401,7 @@ class Form extends ViewableData implements HasRequestHandler // `setValue` method to use. if (($mergeStrategy & self::MERGE_AS_INTERNAL_VALUE) == self::MERGE_AS_INTERNAL_VALUE) { $submitted = false; - } else if (($mergeStrategy & self::MERGE_AS_SUBMITTED_VALUE) == self::MERGE_AS_SUBMITTED_VALUE) { + } elseif (($mergeStrategy & self::MERGE_AS_SUBMITTED_VALUE) == self::MERGE_AS_SUBMITTED_VALUE) { $submitted = true; } diff --git a/tests/php/Forms/FormTest.php b/tests/php/Forms/FormTest.php index 8b9a2948b..3322eb1b5 100644 --- a/tests/php/Forms/FormTest.php +++ b/tests/php/Forms/FormTest.php @@ -1071,7 +1071,6 @@ class FormTest extends FunctionalTest $this->mainSession->session()->get('FormInfo.Form_Form'), 'Our form was reloaded successfully. That should have cleared our session.' ); - } protected function getStubForm() From 6e1c7c2781fe8c30806f628038471faba072bb51 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Tue, 26 Jun 2018 13:29:59 +0100 Subject: [PATCH 26/35] FIX remove personal information from password reset confirmation screen --- lang/en.yml | 4 +-- .../LostPasswordHandler.php | 30 +++++-------------- tests/php/Security/MemberTest.php | 2 +- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/lang/en.yml b/lang/en.yml index 56654a39d..6e3bdd7cb 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -325,5 +325,5 @@ en: NOTEPAGESECURED: 'That page is secured. Enter your credentials below and we will send you right along.' NOTERESETLINKINVALID: '

The password reset link is invalid or expired.

You can request a new one here or change your password after you logged in.

' NOTERESETPASSWORD: 'Enter your e-mail address and we will send you a link with which you can reset your password' - PASSWORDSENTHEADER: 'Password reset link sent to ''{email}''' - PASSWORDSENTTEXT: 'Thank you! A reset link has been sent to ''{email}'', provided an account exists for this email address.' + PASSWORDRESETSENTHEADER: 'Password reset link sent' + PASSWORDRESETSENTTEXT: 'Thank you. A reset link has been sent, provided an account exists for this email address.' diff --git a/src/Security/MemberAuthenticator/LostPasswordHandler.php b/src/Security/MemberAuthenticator/LostPasswordHandler.php index 5086e525c..dfd8aeebc 100644 --- a/src/Security/MemberAuthenticator/LostPasswordHandler.php +++ b/src/Security/MemberAuthenticator/LostPasswordHandler.php @@ -27,8 +27,8 @@ class LostPasswordHandler extends RequestHandler * @var array */ private static $url_handlers = [ - 'passwordsent/$EmailAddress' => 'passwordsent', - '' => 'lostpassword', + 'passwordsent' => 'passwordsent', + '' => 'lostpassword', ]; /** @@ -101,27 +101,17 @@ class LostPasswordHandler extends RequestHandler */ public function passwordsent() { - $request = $this->getRequest(); - $email = Convert::raw2xml(rawurldecode($request->param('EmailAddress'))); - if ($request->getExtension()) { - $email = $email . '.' . Convert::raw2xml($request->getExtension()); - } - $message = _t( - 'SilverStripe\\Security\\Security.PASSWORDSENTTEXT', - "Thank you! A reset link has been sent to '{email}', provided an account exists for this email" - . " address.", - ['email' => Convert::raw2xml($email)] + 'SilverStripe\\Security\\Security.PASSWORDRESETSENTTEXT', + "Thank you. A reset link has been sent, provided an account exists for this email address." ); return [ - 'Title' => _t( - 'SilverStripe\\Security\\Security.PASSWORDSENTHEADER', - "Password reset link sent to '{email}'", - array('email' => $email) + 'Title' => _t( + 'SilverStripe\\Security\\Security.PASSWORDRESETSENTHEADER', + "Password reset link sent" ), 'Content' => DBField::create_field('HTMLFragment', "

$message

"), - 'Email' => $email ]; } @@ -263,11 +253,7 @@ class LostPasswordHandler extends RequestHandler */ protected function redirectToSuccess(array $data) { - $link = Controller::join_links( - $this->Link('passwordsent'), - rawurlencode($data['Email']), - '/' - ); + $link = $this->link('passwordsent'); return $this->redirect($this->addBackURLParam($link)); } diff --git a/tests/php/Security/MemberTest.php b/tests/php/Security/MemberTest.php index 53bb4883c..942b732b0 100644 --- a/tests/php/Security/MemberTest.php +++ b/tests/php/Security/MemberTest.php @@ -239,7 +239,7 @@ class MemberTest extends FunctionalTest // We should get redirected to Security/passwordsent $this->assertContains( - 'Security/lostpassword/passwordsent/testuser@example.com', + 'Security/lostpassword/passwordsent', urldecode($response->getHeader('Location')) ); From ccbbcd45a223ffaba4ab33a4fce0375952c20c1d Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Thu, 29 Mar 2018 14:46:34 +0100 Subject: [PATCH 27/35] FIX Fixed bug in config merging priorities so that config values set by extensions are now least important instead of most important --- .../Config/Middleware/ExtensionMiddleware.php | 2 +- tests/php/ORM/DataObjectTest.php | 116 ++++++++++-------- 2 files changed, 67 insertions(+), 51 deletions(-) diff --git a/src/Core/Config/Middleware/ExtensionMiddleware.php b/src/Core/Config/Middleware/ExtensionMiddleware.php index 3edbc5b0d..a79eaa44d 100644 --- a/src/Core/Config/Middleware/ExtensionMiddleware.php +++ b/src/Core/Config/Middleware/ExtensionMiddleware.php @@ -39,7 +39,7 @@ class ExtensionMiddleware implements Middleware } foreach ($this->getExtraConfig($class, $config, $excludeMiddleware) as $extra) { - $config = Priority::mergeArray($extra, $config); + $config = Priority::mergeArray($config, $extra); } return $config; } diff --git a/tests/php/ORM/DataObjectTest.php b/tests/php/ORM/DataObjectTest.php index 7a9422abf..c0d81c15f 100644 --- a/tests/php/ORM/DataObjectTest.php +++ b/tests/php/ORM/DataObjectTest.php @@ -1102,72 +1102,88 @@ class DataObjectTest extends SapphireTest // Test logical fields (including composite) $teamSpecifications = $schema->fieldSpecs(DataObjectTest\Team::class); + $expected = array( + 'ID', + 'ClassName', + 'LastEdited', + 'Created', + 'Title', + 'DatabaseField', + 'ExtendedDatabaseField', + 'CaptainID', + 'FounderID', + 'HasOneRelationshipID', + 'ExtendedHasOneRelationshipID' + ); + $actual = array_keys($teamSpecifications); + sort($expected); + sort($actual); $this->assertEquals( - array( - 'ID', - 'ClassName', - 'LastEdited', - 'Created', - 'Title', - 'DatabaseField', - 'ExtendedDatabaseField', - 'CaptainID', - 'FounderID', - 'HasOneRelationshipID', - 'ExtendedHasOneRelationshipID' - ), - array_keys($teamSpecifications), + $expected, + $actual, 'fieldSpecifications() contains all fields defined on instance: base, extended and foreign keys' ); $teamFields = $schema->databaseFields(DataObjectTest\Team::class, false); + $expected = array( + 'ID', + 'ClassName', + 'LastEdited', + 'Created', + 'Title', + 'DatabaseField', + 'ExtendedDatabaseField', + 'CaptainID', + 'FounderID', + 'HasOneRelationshipID', + 'ExtendedHasOneRelationshipID' + ); + $actual = array_keys($teamFields); + sort($expected); + sort($actual); $this->assertEquals( - array( - 'ID', - 'ClassName', - 'LastEdited', - 'Created', - 'Title', - 'DatabaseField', - 'ExtendedDatabaseField', - 'CaptainID', - 'FounderID', - 'HasOneRelationshipID', - 'ExtendedHasOneRelationshipID' - ), - array_keys($teamFields), + $expected, + $actual, 'databaseFields() contains only fields defined on instance, including base, extended and foreign keys' ); $subteamSpecifications = $schema->fieldSpecs(DataObjectTest\SubTeam::class); + $expected = array( + 'ID', + 'ClassName', + 'LastEdited', + 'Created', + 'Title', + 'DatabaseField', + 'ExtendedDatabaseField', + 'CaptainID', + 'FounderID', + 'HasOneRelationshipID', + 'ExtendedHasOneRelationshipID', + 'SubclassDatabaseField', + 'ParentTeamID', + ); + $actual = array_keys($subteamSpecifications); + sort($expected); + sort($actual); $this->assertEquals( - array( - 'ID', - 'ClassName', - 'LastEdited', - 'Created', - 'Title', - 'DatabaseField', - 'ExtendedDatabaseField', - 'CaptainID', - 'FounderID', - 'HasOneRelationshipID', - 'ExtendedHasOneRelationshipID', - 'SubclassDatabaseField', - 'ParentTeamID', - ), - array_keys($subteamSpecifications), + $expected, + $actual, 'fieldSpecifications() on subclass contains all fields, including base, extended and foreign keys' ); $subteamFields = $schema->databaseFields(DataObjectTest\SubTeam::class, false); + $expected = array( + 'ID', + 'SubclassDatabaseField', + 'ParentTeamID', + ); + $actual = array_keys($subteamFields); + sort($expected); + sort($actual); $this->assertEquals( - array( - 'ID', - 'SubclassDatabaseField', - 'ParentTeamID', - ), - array_keys($subteamFields), + $expected, + $actual, 'databaseFields() on subclass contains only fields defined on instance' ); } From ef97164c0a83efee99556ceab84f7d6f1aaa2974 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Thu, 12 Jul 2018 01:09:17 +0100 Subject: [PATCH 28/35] DOCS Update docs to reflect true config merge priorities --- .../04_Configuration/00_Configuration.md | 4 ++-- docs/en/04_Changelogs/4.0.5.md | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 docs/en/04_Changelogs/4.0.5.md diff --git a/docs/en/02_Developer_Guides/04_Configuration/00_Configuration.md b/docs/en/02_Developer_Guides/04_Configuration/00_Configuration.md index 09f940bc0..3d543b2fe 100644 --- a/docs/en/02_Developer_Guides/04_Configuration/00_Configuration.md +++ b/docs/en/02_Developer_Guides/04_Configuration/00_Configuration.md @@ -168,12 +168,12 @@ the result will be the higher priority false-ish value. The locations that configuration values are taken from in highest -> lowest priority order are: -- Any values set via a call to Config#merge / Config#set +- Runtime modifications, ie: any values set via a call to `Config::inst()->update()` - The configuration values taken from the YAML files in `_config/` directories (internally sorted in before / after order, where the item that is latest is highest priority) -- Any static set on an "additional static source" class (such as an extension) named the same as the name of the property - Any static set on the class named the same as the name of the property - The composite configuration value of the parent class of this class +- Any static set on an "additional static source" class (such as an extension) named the same as the name of the property
It is an error to have mixed types of the same named property in different locations. An error will not necessarily diff --git a/docs/en/04_Changelogs/4.0.5.md b/docs/en/04_Changelogs/4.0.5.md new file mode 100644 index 000000000..0fc65d7f7 --- /dev/null +++ b/docs/en/04_Changelogs/4.0.5.md @@ -0,0 +1,12 @@ +# 4.0.5 + +## Notable changes + +Fix [#7971](https://github.com/silverstripe/silverstripe-framework/pull/7971) introduces a subtle change of behaviour +to how `Config` settings are prioritised. In SilverStripe 4 there was a change where `Extension` objects took the +highest importance when determining Config values; this was deemed to be unexpected and unintuitive as well as making it +cumbersome and difficult for developers to override module defaults defined in `Extension`s. This change reverts +behaviour to that of SilverStripe 3 where `Extension` instances are of lowest importance and are used only to set a +default value. If you rely on your `Extension` or module providing an overriding config value, please move this to yaml. + + From bde3121a33c893eca3bdf2ffff349d1bf15a3e06 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Wed, 4 Jul 2018 13:35:47 +1200 Subject: [PATCH 29/35] FIX: Remove X-Requested-With from default Vary header 3.x forward port of https://github.com/silverstripe/silverstripe-framework/pull/8242 --- .../08_Performance/02_HTTP_Cache_Headers.md | 5 ++++- docs/en/04_Changelogs/4.2.0.md | 3 +++ src/Control/Director.php | 3 +++ src/Control/Middleware/HTTPCacheControlMiddleware.php | 1 - 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/en/02_Developer_Guides/08_Performance/02_HTTP_Cache_Headers.md b/docs/en/02_Developer_Guides/08_Performance/02_HTTP_Cache_Headers.md index d1274d7c6..ea8c10fcf 100644 --- a/docs/en/02_Developer_Guides/08_Performance/02_HTTP_Cache_Headers.md +++ b/docs/en/02_Developer_Guides/08_Performance/02_HTTP_Cache_Headers.md @@ -228,7 +228,7 @@ when calculating a cache key, usually in addition to the full URL. By default, SilverStripe will output a `Vary` header with the following content: ``` -Vary: X-Requested-With, X-Forwarded-Protocol +Vary: X-Forwarded-Protocol ``` To change the value of the `Vary` header, you can change this value by specifying the header in configuration. @@ -237,3 +237,6 @@ To change the value of the `Vary` header, you can change this value by specifyin SilverStripe\Control\HTTP: vary: "" ``` + +Note that if you use `Director::is_ajax()` on cached pages +then you should add `X-Requested-With` to the vary header. \ No newline at end of file diff --git a/docs/en/04_Changelogs/4.2.0.md b/docs/en/04_Changelogs/4.2.0.md index 8543e735e..9147bd5b4 100644 --- a/docs/en/04_Changelogs/4.2.0.md +++ b/docs/en/04_Changelogs/4.2.0.md @@ -357,6 +357,9 @@ class PageController extends ContentController Note this is different from `Vary: Accept-Encoding`, which is important for compression (e.g. gzip), and usually added by other layers such as Apache's mod_gzip. + * Removed `Vary: X-Requested-With` since it's only applicable when varying + content based on the client context, mostly for returning different XHR responses + as determined through `Director::is_ajax()`. * No longer sets `Last-Modified` date in HTTP response headers in `DataObject::__construct()`. Uses `ETag` calculation based on response body which is more accurate, and resilient against partial and object caching which can produce stale `Last-Modified` values. diff --git a/src/Control/Director.php b/src/Control/Director.php index 79622d213..617b91b2a 100644 --- a/src/Control/Director.php +++ b/src/Control/Director.php @@ -993,6 +993,9 @@ class Director implements TemplateGlobalProvider * Checks if the current HTTP-Request is an "Ajax-Request" by checking for a custom header set by * jQuery or whether a manually set request-parameter 'ajax' is present. * + * Note that if you plan to use this to alter your HTTP response on a cached page, + * you should add X-Requested-With to the Vary header. + * * @param HTTPRequest $request * @return bool */ diff --git a/src/Control/Middleware/HTTPCacheControlMiddleware.php b/src/Control/Middleware/HTTPCacheControlMiddleware.php index 048303eb1..13f40c818 100644 --- a/src/Control/Middleware/HTTPCacheControlMiddleware.php +++ b/src/Control/Middleware/HTTPCacheControlMiddleware.php @@ -135,7 +135,6 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable * @var array */ private static $defaultVary = [ - "X-Requested-With" => true, "X-Forwarded-Protocol" => true, ]; From d426ecbb89329f91a3aa4eaf371fe2790212fd37 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 19 Jul 2018 11:20:58 +1200 Subject: [PATCH 30/35] Add $maxAge arg for caching API See https://github.com/silverstripe/silverstripe-framework/issues/8272 --- .../Middleware/HTTPCacheControlMiddleware.php | 23 ++++++-- .../HTTPCacheControlMiddlewareTest.php | 59 +++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/Control/Middleware/HTTPCacheControlMiddleware.php b/src/Control/Middleware/HTTPCacheControlMiddleware.php index b59b4e705..efd97dc66 100644 --- a/src/Control/Middleware/HTTPCacheControlMiddleware.php +++ b/src/Control/Middleware/HTTPCacheControlMiddleware.php @@ -507,7 +507,8 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable /** * Specifies the maximum amount of time (seconds) a resource will be considered fresh. * This directive is relative to the time of the request. - * Affects all non-disabled states. Use setStateDirective() instead to set for a single state. + * Affects all non-disabled states. Use enableCache(), publicCache() or + * setStateDirective() instead to set the max age for a single state. * * @param int $age * @return $this @@ -560,6 +561,7 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable /** * Simple way to set cache control header to a cacheable state. + * Needs either `setMaxAge()` or the `$maxAge` method argument in order to activate caching. * * The resulting cache-control headers will be chosen from the 'enabled' set of directives. * @@ -568,14 +570,20 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable * * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/ * @param bool $force Force the cache to public even if its unforced private or public + * @param int $maxAge Shortcut for `setMaxAge()`, which is required to actually enable the cache. * @return $this */ - public function enableCache($force = false) + public function enableCache($force = false, $maxAge = null) { // Only execute this if its forcing level is high enough if ($this->applyChangeLevel(self::LEVEL_ENABLED, $force)) { $this->setState(self::STATE_ENABLED); } + + if (!is_null($maxAge)) { + $this->setMaxAge($maxAge); + } + return $this; } @@ -627,20 +635,27 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable /** * Advanced way to set cache control header to a cacheable state. - * Indicates that the response may be cached by any cache. (eg: CDNs, Proxies, Web browsers) + * Indicates that the response may be cached by any cache. (eg: CDNs, Proxies, Web browsers). + * Needs either `setMaxAge()` or the `$maxAge` method argument in order to activate caching. * * The resulting cache-control headers will be chosen from the 'private' set of directives. * * @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/ * @param bool $force Force the cache to public even if it's private, unless it's been forced private + * @param int $maxAge Shortcut for `setMaxAge()`, which is required to actually enable the cache. * @return $this */ - public function publicCache($force = false) + public function publicCache($force = false, $maxAge = null) { // Only execute this if its forcing level is high enough if ($this->applyChangeLevel(self::LEVEL_PUBLIC, $force)) { $this->setState(self::STATE_PUBLIC); } + + if (!is_null($maxAge)) { + $this->setMaxAge($maxAge); + } + return $this; } diff --git a/tests/php/Control/Middleware/HTTPCacheControlMiddlewareTest.php b/tests/php/Control/Middleware/HTTPCacheControlMiddlewareTest.php index 52bbab6a8..1a8b3e4b2 100644 --- a/tests/php/Control/Middleware/HTTPCacheControlMiddlewareTest.php +++ b/tests/php/Control/Middleware/HTTPCacheControlMiddlewareTest.php @@ -68,6 +68,65 @@ class HTTPCacheControlMiddlewareTest extends SapphireTest } } + public function testEnableCacheWithMaxAge() + { + $maxAge = 300; + + $cc = HTTPCacheControlMiddleware::singleton(); + $cc->enableCache(false, $maxAge); + + $response = new HTTPResponse(); + $cc->applyToResponse($response); + + $this->assertContains('max-age=300', $response->getHeader('cache-control')); + $this->assertNotContains('no-cache', $response->getHeader('cache-control')); + $this->assertNotContains('no-store', $response->getHeader('cache-control')); + } + + public function testEnableCacheWithMaxAgeAppliesWhenLevelDoesNot() + { + $maxAge = 300; + + $cc = HTTPCacheControlMiddleware::singleton(); + $cc->privateCache(true); + $cc->enableCache(false, $maxAge); + + $response = new HTTPResponse(); + $cc->applyToResponse($response); + + $this->assertContains('max-age=300', $response->getHeader('cache-control')); + } + + public function testPublicCacheWithMaxAge() + { + $maxAge = 300; + + $cc = HTTPCacheControlMiddleware::singleton(); + $cc->publicCache(false, $maxAge); + + $response = new HTTPResponse(); + $cc->applyToResponse($response); + + $this->assertContains('max-age=300', $response->getHeader('cache-control')); + // STATE_PUBLIC doesn't contain no-cache or no-store headers to begin with, + // so can't test their removal effectively + $this->assertNotContains('no-cache', $response->getHeader('cache-control')); + } + + public function testPublicCacheWithMaxAgeAppliesWhenLevelDoesNot() + { + $maxAge = 300; + + $cc = HTTPCacheControlMiddleware::singleton(); + $cc->privateCache(true); + $cc->publicCache(false, $maxAge); + + $response = new HTTPResponse(); + $cc->applyToResponse($response); + + $this->assertContains('max-age=300', $response->getHeader('cache-control')); + } + /** * @dataProvider provideCacheStates */ From 9300e802f7367e1b9a589a65d5b32539383065a3 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 19 Jul 2018 11:21:36 +1200 Subject: [PATCH 31/35] Updated caching docs (out of sync with implementation) See https://github.com/silverstripe/silverstripe-framework/issues/8272 --- .../08_Performance/02_HTTP_Cache_Headers.md | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/docs/en/02_Developer_Guides/08_Performance/02_HTTP_Cache_Headers.md b/docs/en/02_Developer_Guides/08_Performance/02_HTTP_Cache_Headers.md index d1274d7c6..163248ecc 100644 --- a/docs/en/02_Developer_Guides/08_Performance/02_HTTP_Cache_Headers.md +++ b/docs/en/02_Developer_Guides/08_Performance/02_HTTP_Cache_Headers.md @@ -8,7 +8,7 @@ summary: Set the correct HTTP cache headers for your responses. By default, SilverStripe sends headers which signal to HTTP caches that the response should be not considered cacheable. HTTP caches can either be intermediary caches (e.g. CDNs and proxies), or clients (e.g. browsers). -The cache headers sent are `Cache-Control: no-store, no-cache, must-revalidate`; +The cache headers sent are `Cache-Control: no-cache, must-revalidate`; HTTP caching can be a great way to speed up your website, but needs to be properly applied. Getting it wrong can accidentally expose draft pages or other protected content. @@ -59,8 +59,8 @@ Does not set `private` directive, use `privateCache()` if this is explicitly req Simple way to set cache control header to a cacheable state. Use this method over `publicCache()` if you are unsure about caching details. -Removes `no-store` and `no-cache` directives; other directives will remain in place. -Use alongside `setMaxAge()` to indicate caching. +Removes the `no-store` directive unless a `max-age` is set; other directives will remain in place. +Use alongside `setMaxAge()` to activate caching. Does not set `public` directive. Usually, `setMaxAge()` is sufficient. Use `publicCache()` if this is explicitly required ([details](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private)) @@ -184,24 +184,13 @@ class PageController extends ContentController } ``` -## Defaults - -By default, PHP adds caching headers that make the page appear purely dynamic. This isn't usually appropriate for most -sites, even ones that are updated reasonably frequently. SilverStripe overrides the default settings with the following -headers: - - * The `Last-Modified` date is set to be most recent modification date of any database record queried in the generation - of the page. - * The `Expiry` date is set by taking the age of the page and adding that to the current time. - * `Cache-Control` is set to `max-age=86400, must-revalidate` - * Since a visitor cookie is set, the site won't be cached by proxies. - * Ajax requests are never cached. - ## Max Age The cache age determines the lifetime of your cache, in seconds. It only takes effect if you instruct the cache control -that your response is cacheable in the first place (via `enableCache()` or via modifying the `HTTP.cache_control` defaults). +that your response is cacheable in the first place +(via `enableCache()`, `publicCache()` or `privateCache()`), +or via modifying the `HTTP.cache_control` defaults). ```php use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware; @@ -209,7 +198,8 @@ HTTPCacheControlMiddleware::singleton() ->setMaxAge(60) ``` -Note that `setMaxAge(0)` is NOT sufficient to disable caching in all cases. +Note that `setMaxAge(0)` is NOT sufficient to disable caching in all cases, +use `disableCache()` instead. ### Last Modified From 0f5420b6a5b247a93819b5df5fbe8e7d8b748aa7 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 19 Jul 2018 11:21:42 +1200 Subject: [PATCH 32/35] Removed unused classes --- src/ORM/DataObject.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ORM/DataObject.php b/src/ORM/DataObject.php index d66a25d2b..40059ff5f 100644 --- a/src/ORM/DataObject.php +++ b/src/ORM/DataObject.php @@ -6,8 +6,6 @@ use BadMethodCallException; use Exception; use InvalidArgumentException; use LogicException; -use SilverStripe\Control\HTTP; -use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; From d12c2fe631532ed60e0ef36545b02101d285aca6 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 19 Jul 2018 12:25:27 +1200 Subject: [PATCH 33/35] Properly deprecate HTTP.cache_control --- src/Control/HTTP.php | 35 +++++++++++++++- tests/php/Control/HTTPTest.php | 74 ++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/Control/HTTP.php b/src/Control/HTTP.php index 1e5bcb25a..8fc3741b9 100644 --- a/src/Control/HTTP.php +++ b/src/Control/HTTP.php @@ -71,6 +71,7 @@ class HTTP /** * List of names to add to the Cache-Control header. * + * @deprecated 4.2..5.0 Handled by HTTPCacheControlMiddleware instead * @see HTTPCacheControlMiddleware::__construct() * @config * @var array Keys are cache control names, values are boolean flags @@ -80,7 +81,7 @@ class HTTP /** * Vary string; A comma separated list of var header names * - * @deprecated 4.2..5.0 Handled by HTTPCacheMiddleware instead + * @deprecated 4.2..5.0 Handled by HTTPCacheControlMiddleware instead * @config * @var string|null */ @@ -473,6 +474,7 @@ class HTTP * Ensure that all deprecated HTTP cache settings are respected * * @deprecated 4.2..5.0 Use HTTPCacheControlMiddleware instead + * @throws \LogicException * @param HTTPRequest $request * @param HTTPResponse $response */ @@ -509,6 +511,37 @@ class HTTP $cacheControlMiddleware->addVary($configVary); } + // Pass cache_control to middleware + $configCacheControl = $config->get('cache_control'); + if ($configCacheControl) { + Deprecation::notice('5.0', 'Use HTTPCacheControlMiddleware API instead'); + + $supportedDirectives = ['max-age', 'no-cache', 'no-store', 'must-revalidate']; + if ($foundUnsupported = array_diff(array_keys($configCacheControl), $supportedDirectives)) { + throw new \LogicException( + 'Found unsupported legacy directives in HTTP.cache_control: ' . + implode(', ', $foundUnsupported) . + '. Please use HTTPCacheControlMiddleware API instead' + ); + } + + if (isset($configCacheControl['max-age'])) { + $cacheControlMiddleware->setMaxAge($configCacheControl['max-age']); + } + + if (isset($configCacheControl['no-cache'])) { + $cacheControlMiddleware->setNoCache((bool)$configCacheControl['no-cache']); + } + + if (isset($configCacheControl['no-store'])) { + $cacheControlMiddleware->setNoStore((bool)$configCacheControl['no-store']); + } + + if (isset($configCacheControl['must-revalidate'])) { + $cacheControlMiddleware->setMustRevalidate((bool)$configCacheControl['must-revalidate']); + } + } + // Set modification date if (self::$modification_date) { Deprecation::notice('5.0', 'Use HTTPCacheControlMiddleware::registerModificationDate() instead'); diff --git a/tests/php/Control/HTTPTest.php b/tests/php/Control/HTTPTest.php index 517a57020..70cd7c974 100644 --- a/tests/php/Control/HTTPTest.php +++ b/tests/php/Control/HTTPTest.php @@ -9,6 +9,7 @@ use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware; use SilverStripe\Control\Session; +use SilverStripe\Core\Config\Config; use SilverStripe\Dev\FunctionalTest; /** @@ -112,6 +113,79 @@ class HTTPTest extends FunctionalTest $this->assertEmpty($v); } + public function testDeprecatedVaryHandling() + { + /** @var Config */ + Config::modify()->set( + HTTP::class, + 'vary', + 'X-Foo' + ); + $response = new HTTPResponse('', 200); + $this->addCacheHeaders($response); + $header = $response->getHeader('Vary'); + $this->assertContains('X-Foo', $header); + } + + public function testDeprecatedCacheControlHandling() + { + HTTPCacheControlMiddleware::singleton()->publicCache(); + + /** @var Config */ + Config::modify()->set( + HTTP::class, + 'cache_control', + [ + 'no-store' => true, + 'no-cache' => true, + ] + ); + $response = new HTTPResponse('', 200); + $this->addCacheHeaders($response); + $header = $response->getHeader('Cache-Control'); + $this->assertContains('no-store', $header); + $this->assertContains('no-cache', $header); + } + + public function testDeprecatedCacheControlHandlingOnMaxAge() + { + HTTPCacheControlMiddleware::singleton()->publicCache(); + + /** @var Config */ + Config::modify()->set( + HTTP::class, + 'cache_control', + [ + // Needs to be separate from no-cache and no-store, + // since that would unset max-age + 'max-age' => 99, + ] + ); + $response = new HTTPResponse('', 200); + $this->addCacheHeaders($response); + $header = $response->getHeader('Cache-Control'); + $this->assertContains('max-age=99', $header); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessageRegExp /Found unsupported legacy directives in HTTP\.cache_control: unknown/ + */ + public function testDeprecatedCacheControlHandlingThrowsWithUnknownDirectives() + { + /** @var Config */ + Config::modify()->set( + HTTP::class, + 'cache_control', + [ + 'no-store' => true, + 'unknown' => true, + ] + ); + $response = new HTTPResponse('', 200); + $this->addCacheHeaders($response); + } + /** * Tests {@link HTTP::getLinksIn()} */ From bdf3bc965c91fab305731523b1e93abfdbba643e Mon Sep 17 00:00:00 2001 From: root Date: Tue, 24 Jul 2018 19:07:41 +0000 Subject: [PATCH 34/35] Update translations --- lang/af.yml | 3 - lang/ar.yml | 5 -- lang/az.yml | 3 - lang/bg.yml | 5 -- lang/bs.yml | 3 - lang/ca.yml | 3 - lang/cs.yml | 5 -- lang/de.yml | 5 -- lang/en.yml | 9 +- lang/eo.yml | 5 -- lang/es.yml | 5 -- lang/es_AR.yml | 3 - lang/es_MX.yml | 2 - lang/et_EE.yml | 5 -- lang/fa_IR.yml | 5 -- lang/fi.yml | 5 -- lang/fo.yml | 3 - lang/fr.yml | 4 +- lang/gl_ES.yml | 2 - lang/hr.yml | 5 -- lang/id.yml | 5 -- lang/id_ID.yml | 5 -- lang/is.yml | 3 - lang/it.yml | 5 -- lang/ja.yml | 5 -- lang/lt.yml | 5 -- lang/lv.yml | 3 - lang/mi.yml | 5 -- lang/nb.yml | 5 -- lang/nl.yml | 5 -- lang/pl.yml | 201 ++++++++++++++++++++++++++++++++++++++++++- lang/pt.yml | 3 - lang/pt_BR.yml | 3 - lang/ro.yml | 2 - lang/ru.yml | 5 -- lang/sk.yml | 5 -- lang/sl.yml | 5 -- lang/sl_SI.yml | 5 -- lang/sr.yml | 5 -- lang/sr@latin.yml | 5 -- lang/sr_RS.yml | 5 -- lang/sr_RS@latin.yml | 5 -- lang/sv.yml | 5 -- lang/th.yml | 4 - lang/tr.yml | 3 - lang/uk.yml | 3 - lang/zh.yml | 5 -- 47 files changed, 203 insertions(+), 197 deletions(-) diff --git a/lang/af.yml b/lang/af.yml index ddf2abee0..bbaf0c9cb 100644 --- a/lang/af.yml +++ b/lang/af.yml @@ -4,8 +4,6 @@ af: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: R SilverStripe\Forms\DateField: - NOTSET: 'Nie gestel nie' - TODAY: vandag VALIDDATEFORMAT2: 'Sleutel asseblief ''n geldige datum formaat in ({format})' VALIDDATEMAXDATE: 'Jou datum moet gelykstaande of ouer wees as die maksimum toelaatbare datum ({date})' VALIDDATEMINDATE: 'Jou datum moet net so out of nuwer wees as die minimum toelaatbare datum ({date})' @@ -36,7 +34,6 @@ af: RelationSearch: 'Soek vir verwantskap' ResetFilter: Herstel SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Verwyder Delete: Verwyder DeletePermissionsFailure: 'Geen toestemming om te verwyder nie' UnlinkRelation: Ontkoppel diff --git a/lang/ar.yml b/lang/ar.yml index ef2f216cf..19381589f 100644 --- a/lang/ar.yml +++ b/lang/ar.yml @@ -5,8 +5,6 @@ ar: MAXIMUM: 'يجب أن تكون كلمات المرور على الأكثر {الحد الأقصى} حرفاً.' SHOWONCLICKTITLE: 'تغيير كلمة المرور' SilverStripe\Forms\DateField: - NOTSET: 'غير محدد' - TODAY: اليوم VALIDDATEFORMAT2: 'الرجاء إدخال صيغة تاريخ صحيحة ({صيغة})' VALIDDATEMAXDATE: 'التسجيل الخاص بك قد يكون أقدم أو مطابق لأقصى تاريخ مسموح به ({تاريخ})' VALIDDATEMINDATE: 'التسجيل الخاص بك قد يكون أحدث أو مطابق للحد الأدنى للتاريخ المسموح به ({تاريخ})' @@ -39,7 +37,6 @@ ar: RelationSearch: 'ابحث عن علاقة' ResetFilter: 'إعادة تعيين' SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: حذف Delete: حذف DeletePermissionsFailure: 'لا يوجد أية تصريحات حذف' EditPermissionsFailure: 'لا يوجد تصريح لإلغاء الربط بين السجلات' @@ -51,8 +48,6 @@ ar: DeletePermissionsFailure: 'لا يوجد تصريحات بالحذف' Deleted: '{type} {name} تم حذفه' Save: حفظ - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: تعديل SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: الكمية FIELDLABELCURRENCY: العملة diff --git a/lang/az.yml b/lang/az.yml index 392264a1b..7629d5d3b 100644 --- a/lang/az.yml +++ b/lang/az.yml @@ -1,9 +1,6 @@ az: SilverStripe\Forms\ConfirmedPasswordField: SHOWONCLICKTITLE: 'Parolu dəyiş' - SilverStripe\Forms\DateField: - NOTSET: 'təyin edilməyib' - TODAY: 'bu gün' SilverStripe\Forms\DropdownField: CHOOSE: (Seçin) SilverStripe\Forms\Form: diff --git a/lang/bg.yml b/lang/bg.yml index 91c831c04..3e1eba6a0 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -37,8 +37,6 @@ bg: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'не е зададена' - TODAY: днес VALIDDATEFORMAT2: 'Моля, въведете валиден формат за дата ({format})' VALIDDATEMAXDATE: 'Датата трябва да бъде същата или преди ({date})' VALIDDATEMINDATE: 'Датата трябва да бъде същата или след ({date})' @@ -86,7 +84,6 @@ bg: RelationSearch: 'Търсене на връзка' ResetFilter: Изчистване SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Изтриване Delete: Изтрий DeletePermissionsFailure: 'Изтриването не е разрешено' EditPermissionsFailure: 'Нямате права за премахване на връзката' @@ -98,8 +95,6 @@ bg: DeletePermissionsFailure: 'Изтриването не е разрешено' Deleted: 'Изтрити {type} {name}' Save: Запис - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Редактиране SilverStripe\Forms\GridField\GridFieldPaginator: OF: от Page: Страница diff --git a/lang/bs.yml b/lang/bs.yml index eca1e7246..3a2ac1a35 100644 --- a/lang/bs.yml +++ b/lang/bs.yml @@ -4,9 +4,6 @@ bs: YESANSWER: Da SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: KM - SilverStripe\Forms\DateField: - NOTSET: 'nije postavljeno' - TODAY: danas SilverStripe\Forms\DropdownField: CHOOSE: (Izaberite) SilverStripe\Forms\Form: diff --git a/lang/ca.yml b/lang/ca.yml index b93a85cc0..4ffdc98fe 100644 --- a/lang/ca.yml +++ b/lang/ca.yml @@ -3,9 +3,6 @@ ca: SHOWONCLICKTITLE: 'Canvia la contrasenya' SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: €E - SilverStripe\Forms\DateField: - NOTSET: 'no definit' - TODAY: avui SilverStripe\Forms\DropdownField: CHOOSE: (Trieu) SilverStripe\Forms\Form: diff --git a/lang/cs.yml b/lang/cs.yml index 9e7bf5d6a..4a3cae3ab 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -13,8 +13,6 @@ cs: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: Kč SilverStripe\Forms\DateField: - NOTSET: nenastaveno - TODAY: dnes VALIDDATEFORMAT2: 'Prosím zadejte platný formát datumu ({format})' VALIDDATEMAXDATE: 'Váš datum musí být starší nebo odpovídající maximu povoleného datumu ({date})' VALIDDATEMINDATE: 'Váš datum musí být novější nebo odpovídající minimu povoleného datumu ({date})' @@ -51,7 +49,6 @@ cs: RelationSearch: 'Vztah hledání' ResetFilter: Resetovat SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Smazat Delete: Smazat DeletePermissionsFailure: 'Žádná oprávnění mazat' EditPermissionsFailure: 'Žádné oprávnění pro rozpojení záznamu' @@ -63,8 +60,6 @@ cs: DeletePermissionsFailure: 'Žádná oprávnění mazat' Deleted: 'Smazáno {type} {name}' Save: Uložit - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Editovat SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Částka FIELDLABELCURRENCY: Měna diff --git a/lang/de.yml b/lang/de.yml index 3bbf8cdb5..1e9f16e5e 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -11,8 +11,6 @@ de: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: € SilverStripe\Forms\DateField: - NOTSET: 'nicht gesetzt' - TODAY: heute VALIDDATEFORMAT2: 'Bitte geben sie das Datum im korrekten Format ein ({format})' VALIDDATEMAXDATE: 'Ihr Datum muss vor dem erlaubtem Datum ({date}) liegen oder gleich sein' VALIDDATEMINDATE: 'Ihr Datum muss nach dem erlaubtem Datum ({date}) liegen oder gleich sein' @@ -49,7 +47,6 @@ de: RelationSearch: Relationssuche ResetFilter: Zurücksetzen SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Löschen Delete: Löschen DeletePermissionsFailure: 'Keine Berechtigung zum Löschen' EditPermissionsFailure: 'Sie haben keine Berechtigung, die Verknüpfung zu lösen' @@ -61,8 +58,6 @@ de: DeletePermissionsFailure: 'Keine Berechtigungen zum löschen' Deleted: 'Gelöscht {type} {name}' Save: Speichern - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Bearbeiten SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Betrag FIELDLABELCURRENCY: Währung diff --git a/lang/en.yml b/lang/en.yml index 6e3bdd7cb..37da731e4 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -84,6 +84,7 @@ en: RelationSearch: 'Relation search' ResetFilter: Reset SilverStripe\Forms\GridField\GridFieldDeleteAction: + DELETE_DESCRIPTION: Delete Delete: Delete DeletePermissionsFailure: 'No delete permissions' EditPermissionsFailure: 'No permission to unlink record' @@ -95,7 +96,7 @@ en: DeletePermissionsFailure: 'No delete permissions' Deleted: 'Deleted {type} {name}' Save: Save - SilverStripe\Forms\GridField\GridFieldEditButton: + SilverStripe\Forms\GridField\GridFieldEditButton_ss: EDIT: Edit SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: UnlinkSelfFailure: 'Cannot remove yourself from this group, you will lose admin rights' @@ -149,8 +150,6 @@ en: other: '{count} years' SilverStripe\ORM\FieldType\DBEnum: ANY: Any - SilverStripe\ORM\FieldType\DBForeignKey: - DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'Too many related objects; fallback field in use' SilverStripe\ORM\Hierarchy: LIMITED_TITLE: 'Too many children ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: @@ -325,5 +324,5 @@ en: NOTEPAGESECURED: 'That page is secured. Enter your credentials below and we will send you right along.' NOTERESETLINKINVALID: '

The password reset link is invalid or expired.

You can request a new one here or change your password after you logged in.

' NOTERESETPASSWORD: 'Enter your e-mail address and we will send you a link with which you can reset your password' - PASSWORDRESETSENTHEADER: 'Password reset link sent' - PASSWORDRESETSENTTEXT: 'Thank you. A reset link has been sent, provided an account exists for this email address.' + PASSWORDSENTHEADER: 'Password reset link sent to ''{email}''' + PASSWORDSENTTEXT: 'Thank you! A reset link has been sent to ''{email}'', provided an account exists for this email address.' diff --git a/lang/eo.yml b/lang/eo.yml index f0ca41ca2..59806b953 100644 --- a/lang/eo.yml +++ b/lang/eo.yml @@ -37,8 +37,6 @@ eo: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'ne agordita' - TODAY: hodiaŭ VALIDDATEFORMAT2: 'Bonvole enigu validan datan formaton ({format})' VALIDDATEMAXDATE: 'Necesas ke via dato estu pli aĝa ol, aŭ egala al la maksimuma permesita dato ({date})' VALIDDATEMINDATE: 'Necesas ke via dato estu pli nova ol, aŭ egala al la minimuma permesita dato ({date})' @@ -86,7 +84,6 @@ eo: RelationSearch: 'Serĉi rilatojn' ResetFilter: Restartigi SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Forigi Delete: Forigi DeletePermissionsFailure: 'Mankas permeso forigi' EditPermissionsFailure: 'Mankas permeso malligi rikordon' @@ -98,8 +95,6 @@ eo: DeletePermissionsFailure: 'Mankas permeso forigi' Deleted: 'Forigita {type} {name}' Save: Konservi - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Redakti SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: UnlinkSelfFailure: 'Ne povas forigi vin el ĉi tiu grupo; vi perdus administrajn rajtojn' SilverStripe\Forms\GridField\GridFieldPaginator: diff --git a/lang/es.yml b/lang/es.yml index 5251bdbf6..0009c7a4a 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -15,8 +15,6 @@ es: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: Símbolo SilverStripe\Forms\DateField: - NOTSET: 'sin establecer' - TODAY: hoy VALIDDATEFORMAT2: 'Por favor, introduzca un formato de fecha válido ({format})' VALIDDATEMAXDATE: 'La fecha tiene que ser mayor o igual a la fecha máxima permitida ({date})' VALIDDATEMINDATE: 'La fecha tiene que ser posterior o coincidente a la fecha mínima permitida ({date})' @@ -55,7 +53,6 @@ es: RelationSearch: 'Búsqueda de relación' ResetFilter: Restaurar SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Borrar Delete: Borrar DeletePermissionsFailure: 'Sin permiso para borrar' EditPermissionsFailure: 'No se pudo editar permisos' @@ -67,8 +64,6 @@ es: DeletePermissionsFailure: 'Sin permiso para borrar' Deleted: 'Borrado {type} {name}' Save: Guardar - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Editar SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Cantidad FIELDLABELCURRENCY: Moneda diff --git a/lang/es_AR.yml b/lang/es_AR.yml index 3539b9101..773d79948 100644 --- a/lang/es_AR.yml +++ b/lang/es_AR.yml @@ -1,9 +1,6 @@ es_AR: SilverStripe\Forms\ConfirmedPasswordField: SHOWONCLICKTITLE: 'Cambiar Contraseña' - SilverStripe\Forms\DateField: - NOTSET: 'no especificada' - TODAY: hoy SilverStripe\Forms\DropdownField: CHOOSE: (Selecciona) SilverStripe\Forms\Form: diff --git a/lang/es_MX.yml b/lang/es_MX.yml index 5fd337559..04baa70a6 100644 --- a/lang/es_MX.yml +++ b/lang/es_MX.yml @@ -2,8 +2,6 @@ es_MX: SilverStripe\Forms\ConfirmedPasswordField: SHOWONCLICKTITLE: 'Cambiar contraseña' SilverStripe\Forms\DateField: - NOTSET: 'no especificada' - TODAY: ahora VALIDDATEFORMAT2: 'Por favor ingresar un formato válido de fecha ({format})' VALIDDATEMAXDATE: 'Tu fecha tiene que ser más antigua o al menos coincidir con la fecha mínima permitida ({date})' VALIDDATEMINDATE: 'Tu fecha tiene que ser nueva o al menos coincidir con la fecha mínima permitida ({date})' diff --git a/lang/et_EE.yml b/lang/et_EE.yml index 2c44eee9b..63fc4cd5d 100644 --- a/lang/et_EE.yml +++ b/lang/et_EE.yml @@ -8,8 +8,6 @@ et_EE: MAXIMUM: 'Parool võib olla kuni {max} märki pikk.' SHOWONCLICKTITLE: 'Muuda parool' SilverStripe\Forms\DateField: - NOTSET: 'Pole seadistatud' - TODAY: Täna VALIDDATEFORMAT2: 'Sisestage sobivas vormingus kuupäev ({format})' VALIDDATEMAXDATE: 'Teie kuupäev peab oleme lubatud kuupäevast ({date}) varasem või ühtima sellega.' VALIDDATEMINDATE: 'Teie kuupäev peab oleme lubatud kuupäevast ({date}) hilisem või ühtima sellega.' @@ -41,7 +39,6 @@ et_EE: RelationSearch: 'Seose otsing' ResetFilter: Lähtesta SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Kustuta Delete: Kustuta DeletePermissionsFailure: 'Kustutamisõigused puuduvad' UnlinkRelation: 'Tühista linkimine' @@ -52,8 +49,6 @@ et_EE: DeletePermissionsFailure: 'Kustutamisõigused puuduvad' Deleted: '{type} {name} on kustutatud' Save: Salvesta - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Redigeeri SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Hulk FIELDLABELCURRENCY: Valuuta diff --git a/lang/fa_IR.yml b/lang/fa_IR.yml index 14c967ba9..b292b25d7 100644 --- a/lang/fa_IR.yml +++ b/lang/fa_IR.yml @@ -13,8 +13,6 @@ fa_IR: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: ﷼ SilverStripe\Forms\DateField: - NOTSET: نامشخص - TODAY: امروز VALIDDATEFORMAT2: 'لطفاً یک قالب تاریخ معتبر وارد نمایید ({format})' VALIDDATEMAXDATE: 'تاریخ شما باید قدیمی‌تر یا برابر با حداکثر تاریخ مجاز ({date}) باشد' VALIDDATEMINDATE: 'تاریخ شما باید جدیدتر یا برابر با حداقل تاریخ مجاز ({date}) باشد' @@ -50,7 +48,6 @@ fa_IR: RelationSearch: 'جستجوی رابط' ResetFilter: 'از نو' SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: حذف Delete: حذف DeletePermissionsFailure: 'دسترسی‌های حذف وجود ندارد' EditPermissionsFailure: 'دسترسی‌های حذف لینک وجود ندارد' @@ -62,8 +59,6 @@ fa_IR: DeletePermissionsFailure: 'دسترسی‌های حذف وجود ندارد' Deleted: 'حذف شده {type} {name}' Save: ذخیره - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: ویرایش SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: مقدار FIELDLABELCURRENCY: 'واحد پول' diff --git a/lang/fi.yml b/lang/fi.yml index 8bbb331f5..364762d3a 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -37,8 +37,6 @@ fi: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'ei asetettu' - TODAY: tänään VALIDDATEFORMAT2: 'Ole hyvä ja kirjaa päivämäärä sallitussa muodossa ({format})' VALIDDATEMAXDATE: 'Päivämäärän on oltava vanhempi tai sovittava asetettuun maksimiin ({date})' VALIDDATEMINDATE: 'Päivämäärän on oltava uudempi tai sovittava asetettuun minimiin ({date})' @@ -86,7 +84,6 @@ fi: RelationSearch: Relaatiohaku ResetFilter: Nollaa SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Poista Delete: Poista DeletePermissionsFailure: 'Ei oikeuksia poistamiseen' EditPermissionsFailure: 'Ei oikeuksia purkaa linkitystä tietueeseen' @@ -98,8 +95,6 @@ fi: DeletePermissionsFailure: 'Ei oikeuksia poistamiseen' Deleted: 'Poistettiin {type} {name}' Save: Tallenna - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Muokkaa SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: UnlinkSelfFailure: 'Et voi siirtää itseäsi pois tästä ryhmästä: menettäisit pääkäyttäjän oikeudet' SilverStripe\Forms\GridField\GridFieldPaginator: diff --git a/lang/fo.yml b/lang/fo.yml index b9f881ac6..e95dfc85c 100644 --- a/lang/fo.yml +++ b/lang/fo.yml @@ -1,9 +1,6 @@ fo: SilverStripe\Forms\ConfirmedPasswordField: SHOWONCLICKTITLE: 'Broyt loyniorð' - SilverStripe\Forms\DateField: - NOTSET: 'ikki ásett' - TODAY: 'í dag' SilverStripe\Forms\DropdownField: CHOOSE: (Áset) SilverStripe\Forms\Form: diff --git a/lang/fr.yml b/lang/fr.yml index 3b533783b..5a18188da 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -37,8 +37,6 @@ fr: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'non renseigné' - TODAY: 'aujourd''hui' VALIDDATEFORMAT2: 'Saisissez une date au format valide ({format})' VALIDDATEMAXDATE: 'La date doit être antérieure ou égale à celle autorisée ({date})' VALIDDATEMINDATE: 'La date doit être postérieure ou égale à celle autorisée ({date})' @@ -99,7 +97,7 @@ fr: Deleted: '{type} {name} supprimés' Save: Enregistrer SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Editer + EDIT: Éditer SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: UnlinkSelfFailure: 'Impossible de retirer votre propre profil de ce groupe, vous perdriez vos droits d''administration' SilverStripe\Forms\GridField\GridFieldPaginator: diff --git a/lang/gl_ES.yml b/lang/gl_ES.yml index c165b63a1..b9773f123 100644 --- a/lang/gl_ES.yml +++ b/lang/gl_ES.yml @@ -3,8 +3,6 @@ gl_ES: SHOWONCLICKTITLE: 'Mudar Contrasinal' SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: € - SilverStripe\Forms\DateField: - NOTSET: 'sen establecer' SilverStripe\Forms\DropdownField: CHOOSE: (Escoller) SilverStripe\Forms\Form: diff --git a/lang/hr.yml b/lang/hr.yml index c0823ee2b..f7087f039 100644 --- a/lang/hr.yml +++ b/lang/hr.yml @@ -7,8 +7,6 @@ hr: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'nije postavljeno' - TODAY: danas VALIDDATEFORMAT2: 'Molimo unesite datum u ispravnom formatu ({format})' SilverStripe\Forms\DropdownField: CHOOSE: (Odaberite) @@ -26,7 +24,6 @@ hr: Find: Pronađi Print: Ispiši SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Obriši Delete: Obriši SilverStripe\Forms\GridField\GridFieldDetailForm: CancelBtn: Odustani @@ -35,8 +32,6 @@ hr: DeletePermissionsFailure: 'Nema dozvole brisanja' Deleted: 'Obrisano {type} {name}' Save: Spremi - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Uredi SilverStripe\Forms\MoneyField: FIELDLABELCURRENCY: Valuta SilverStripe\ORM\FieldType\DBBoolean: diff --git a/lang/id.yml b/lang/id.yml index 4e8bf8875..d17eac338 100644 --- a/lang/id.yml +++ b/lang/id.yml @@ -10,8 +10,6 @@ id: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'tidak diatur' - TODAY: 'hari ini' VALIDDATEFORMAT2: 'Mohon isikan format tanggal yang valid ({format})' VALIDDATEMAXDATE: 'Tanggal Anda harus lebih lama atau sama dengan tanggal maksimum ({date})' VALIDDATEMINDATE: 'Tanggal Anda harus lebih baru atau sama dengan tanggal minimum ({date})' @@ -47,7 +45,6 @@ id: RelationSearch: 'Cari yang terkait' ResetFilter: Reset SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Hapus Delete: Hapus DeletePermissionsFailure: 'Tidak ada ijin menghapus' EditPermissionsFailure: 'Tidak ada ijin membuka tautan' @@ -59,8 +56,6 @@ id: DeletePermissionsFailure: 'Tidak ada ijin menghapus' Deleted: '{type} {name} dihapus' Save: Simpan - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Edit SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Jumlah FIELDLABELCURRENCY: 'Mata Uang' diff --git a/lang/id_ID.yml b/lang/id_ID.yml index 9d4b6a922..22ebcb2e5 100644 --- a/lang/id_ID.yml +++ b/lang/id_ID.yml @@ -10,8 +10,6 @@ id_ID: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'tidak diatur' - TODAY: 'hari ini' VALIDDATEFORMAT2: 'Mohon isikan format tanggal yang valid ({format})' VALIDDATEMAXDATE: 'Tanggal Anda harus lebih lama atau sama dengan tanggal maksimum ({date})' VALIDDATEMINDATE: 'Tanggal Anda harus lebih baru atau sama dengan tanggal minimum ({date})' @@ -47,7 +45,6 @@ id_ID: RelationSearch: 'Cari yang terkait' ResetFilter: Reset SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Hapus Delete: Hapus DeletePermissionsFailure: 'Tidak ada ijin menghapus' EditPermissionsFailure: 'Tidak ada ijin membuka tautan' @@ -59,8 +56,6 @@ id_ID: DeletePermissionsFailure: 'Tidak ada ijin menghapus' Deleted: '{type} {name} dihapus' Save: Simpan - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Edit SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Jumlah FIELDLABELCURRENCY: 'Mata Uang' diff --git a/lang/is.yml b/lang/is.yml index 9da7485ab..dd322dfa5 100644 --- a/lang/is.yml +++ b/lang/is.yml @@ -3,9 +3,6 @@ is: SHOWONCLICKTITLE: 'Breyta lykliorði' SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: kr - SilverStripe\Forms\DateField: - NOTSET: 'ekki valið' - TODAY: 'í dag' SilverStripe\Forms\DropdownField: CHOOSE: (Veldu) SilverStripe\Forms\Form: diff --git a/lang/it.yml b/lang/it.yml index e3dedc079..05bf5dc83 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -37,8 +37,6 @@ it: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'non impostato' - TODAY: oggi VALIDDATEFORMAT2: 'Inserisci un formato di data valido ({format})' VALIDDATEMAXDATE: 'La tua data deve essere più vecchia o uguale alla data massima consentita ({date})' VALIDDATEMINDATE: 'La tua data deve essere più nuova o uguale alla data minima consentita ({date})' @@ -86,7 +84,6 @@ it: RelationSearch: 'Cerca relazione' ResetFilter: Azzera SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Elimina Delete: Elimina DeletePermissionsFailure: 'Non hai i permessi per eliminare' EditPermissionsFailure: 'Non hai i permessi per modificare' @@ -98,8 +95,6 @@ it: DeletePermissionsFailure: 'Non hai i permessi per eliminare' Deleted: 'Eliminato {type} {name}' Save: Salva - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Modifica SilverStripe\Forms\GridField\GridFieldPaginator: OF: di Page: Pagina diff --git a/lang/ja.yml b/lang/ja.yml index 7f76ad481..b2f92eb1e 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -5,8 +5,6 @@ ja: MAXIMUM: 'パスワードは{max} 文字以内でなければなりません。' SHOWONCLICKTITLE: パスワード変更 SilverStripe\Forms\DateField: - NOTSET: セットされていません - TODAY: 今日 VALIDDATEFORMAT2: '{{format}}日付フォーマットの正しい日付を入力してください。' VALIDDATEMAXDATE: '許可されている最も新しい日付{{date}}より古い日付か同じ日付である必要があります。' VALIDDATEMINDATE: '許可されている最も古い日付{{date}}より新しい日付か同じ日付である必要があります' @@ -39,7 +37,6 @@ ja: RelationSearch: 関連検索 ResetFilter: リセット SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: 削除 Delete: 削除 DeletePermissionsFailure: 削除権限がありません EditPermissionsFailure: レコードのリンクを解除するための権限がありません @@ -51,8 +48,6 @@ ja: DeletePermissionsFailure: 削除権限がありません Deleted: '削除済み {type} {name}' Save: 保存 - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: 編集 SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: 総計 FIELDLABELCURRENCY: 通貨 diff --git a/lang/lt.yml b/lang/lt.yml index 5f70dd239..abdbaea25 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -10,8 +10,6 @@ lt: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: € SilverStripe\Forms\DateField: - NOTSET: nenustatyta - TODAY: šiandien VALIDDATEFORMAT2: 'Prašome suvesti datą reikiamu formatu ({format})' VALIDDATEMAXDATE: 'Data privalo būti senesnė arba lygi vėliausiai galimai datai ({date})' VALIDDATEMINDATE: 'Data privalo būti naujesnė arba lygi anksčiausiai galimai datai ({date})' @@ -47,7 +45,6 @@ lt: RelationSearch: 'Sąryšių paieška' ResetFilter: Atstatyti SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Ištrinti Delete: Ištrinti DeletePermissionsFailure: 'Nėra leidimų trynimui' EditPermissionsFailure: 'Nėra leidimų atjungti įrašą' @@ -59,8 +56,6 @@ lt: DeletePermissionsFailure: 'Nėra leidimų trynimui' Deleted: 'Ištrinta {type} {name}' Save: Išsaugoti - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Redaguoti SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Kiekis FIELDLABELCURRENCY: Valiuta diff --git a/lang/lv.yml b/lang/lv.yml index d7106a435..cfa2299d9 100644 --- a/lang/lv.yml +++ b/lang/lv.yml @@ -3,9 +3,6 @@ lv: SHOWONCLICKTITLE: 'Mainīt paroli' SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: Ls - SilverStripe\Forms\DateField: - NOTSET: 'nav uzstādīts' - TODAY: šodien SilverStripe\Forms\DropdownField: CHOOSE: (Izvēlieties) SilverStripe\Forms\Form: diff --git a/lang/mi.yml b/lang/mi.yml index 954732f27..7a2601ed5 100644 --- a/lang/mi.yml +++ b/lang/mi.yml @@ -7,8 +7,6 @@ mi: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'kāore i tautuhia' - TODAY: 'tēnei rā' VALIDDATEFORMAT2: 'Tāurua he hōputu rā tika ({format})' VALIDDATEMAXDATE: 'Me tawhito ake tō rā, kia ōrite rānei ki te rā mōrahi ({date}) kua whakaaetia' VALIDDATEMINDATE: 'Me hōu ake tō rā, kia ōrite rānei ki te rā moroiti ({date}) kua whakaaetia' @@ -41,7 +39,6 @@ mi: RelationSearch: 'Rapu whanaunga' ResetFilter: 'Tautuhi anō' SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Muku Delete: Muku DeletePermissionsFailure: 'Kāore he muku whakaaetanga' EditPermissionsFailure: 'Kāore ō whakaaetanga kia wetehono pūkete' @@ -53,8 +50,6 @@ mi: DeletePermissionsFailure: 'Kāore he whakaaetanga muku' Deleted: 'Kua mukua {type} {name}' Save: Tiaki - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Whakatika SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Rahinga FIELDLABELCURRENCY: Moni diff --git a/lang/nb.yml b/lang/nb.yml index 2825f7543..91dd9f758 100644 --- a/lang/nb.yml +++ b/lang/nb.yml @@ -7,8 +7,6 @@ nb: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'Ikke satt' - TODAY: 'i dag' VALIDDATEFORMAT2: 'Vennligst skriv inn et gyldig datoformat ({format})' VALIDDATEMAXDATE: 'Din dato må være eldre eller i samsvar med maksimum tillatte dato ({date})' VALIDDATEMINDATE: 'Din dato må være nyere eller i samsvar med minimum tillatte dato ({date})' @@ -41,7 +39,6 @@ nb: RelationSearch: Relasjonssøk ResetFilter: Tilbakestille SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Slett Delete: Slett DeletePermissionsFailure: 'Ikke tillatt å slette' EditPermissionsFailure: 'Ikke tilgang til å fjerne oppføringer' @@ -53,8 +50,6 @@ nb: DeletePermissionsFailure: 'Ikke tillatt å slette' Deleted: 'Slettet {type} {name}' Save: Lagre - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Rediger SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Mengde FIELDLABELCURRENCY: Valuta diff --git a/lang/nl.yml b/lang/nl.yml index 2903dff05..64f191213 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -13,8 +13,6 @@ nl: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'niet ingesteld' - TODAY: vandaag VALIDDATEFORMAT2: 'Vul een geldig datumformaat in ({format})' VALIDDATEMAXDATE: 'De datum moet ouder of gelijk zijn aan de maximale datum ({date})' VALIDDATEMINDATE: 'De datum moet nieuwer of gelijk zijn aan de minimale datum ({date})' @@ -51,7 +49,6 @@ nl: RelationSearch: 'Zoek relatie' ResetFilter: Resetten SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Verwijderen Delete: Verwijder DeletePermissionsFailure: 'Onvoldoende rechten om te verwijderen' EditPermissionsFailure: 'Geen toelating om te ontkoppelen' @@ -63,8 +60,6 @@ nl: DeletePermissionsFailure: 'Onvoldoende rechten om te verwijderen' Deleted: '{type} {name} verwijderd' Save: Opslaan - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Bewerken SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Aantal FIELDLABELCURRENCY: Munteenheid diff --git a/lang/pl.yml b/lang/pl.yml index 03753f33b..3774d27f3 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -1,34 +1,77 @@ pl: + SilverStripe\Admin\LeftAndMain: + VersionUnknown: Nieznany + SilverStripe\AssetAdmin\Forms\UploadField: + Dimensions: Rozmiar + EDIT: Edytuj + EDITINFO: 'Edytuj plik' + REMOVE: Usuń + SilverStripe\Control\ChangePasswordEmail_ss: + CHANGEPASSWORDFOREMAIL: 'Hasło do konta o adresie e-mail {email} zostało zmienione. Jeśli nie zmieniłeś swojego hasła, zmień hasło, korzystając z poniższego linku' + CHANGEPASSWORDTEXT1: 'Zmieniłeś hasło na' + CHANGEPASSWORDTEXT3: 'Zmień hasło' + HELLO: 'Witaj,' + SilverStripe\Control\Email\ForgotPasswordEmail_ss: + HELLO: 'Witaj,' + TEXT1: 'Oto twój' + TEXT2: 'link zmiany hasła' + TEXT3: dla + SilverStripe\Control\RequestProcessor: + INVALID_REQUEST: 'Nieprawidłowe żądanie' + REQUEST_ABORTED: 'Żądanie zostało przerwane' + SilverStripe\Core\Manifest\VersionProvider: + VERSIONUNKNOWN: Nieznany + SilverStripe\Forms\CheckboxField: + NOANSWER: Nie + YESANSWER: Tak + SilverStripe\Forms\CheckboxSetField_ss: + NOOPTIONSAVAILABLE: 'Brak dostępnych opcji' SilverStripe\Forms\ConfirmedPasswordField: ATLEAST: 'Hasła muszą mieć przynajmniej {min} znaków.' BETWEEN: 'Hasła muszą mieć długość pomiędzy {min} a {max} znaków.' + CURRENT_PASSWORD_ERROR: 'Podane hasło jest nieprawidłowe' + CURRENT_PASSWORD_MISSING: 'Musisz podać swoje aktualne hasło.' + LOGGED_IN_ERROR: 'Musisz być zalogowany aby zmienić hasło' MAXIMUM: 'Hasła mogą mieć co najwyżej {max} znaków.' SHOWONCLICKTITLE: 'Zmiana Hasła' SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'nie ustawiono' - TODAY: dzisiaj VALIDDATEFORMAT2: 'Proszę wprowadź prawidłowy format daty ({format})' VALIDDATEMAXDATE: 'Twoja data musi być wcześniejsza lub taka sama, jak maksymalna dozwolona data ({date})' VALIDDATEMINDATE: 'Twoja data musi być późniejsza lub taka sama, jak minimalna dozwolona data ({date})' + SilverStripe\Forms\DatetimeField: + VALIDDATEMAXDATETIME: 'Twoja data musi być wcześniejsza lub taka sama, jak maksymalna dozwolona data ({date})' + VALIDDATETIMEFORMAT: 'Proszę wprowadź prawidłowy format czasu ({format})' + VALIDDATETIMEMINDATE: 'Twoja data musi być późniejsza lub taka sama, jak minimalna dozwolona data ({date})' SilverStripe\Forms\DropdownField: CHOOSE: (wybierz) + CHOOSE_MODEL: '(Wybierz {name})' + SOURCE_VALIDATION: 'Wybierz wartość z podanej listy. {value} nie jest poprawną opcją' SilverStripe\Forms\EmailField: VALIDATION: 'Proszę podaj adres e-mail' + SilverStripe\Forms\FileUploadReceiver: + FIELDNOTSET: 'Nie znaleziono informacji o pliku' SilverStripe\Forms\Form: + BAD_METHOD: 'Ten formularz wymaga {metody} przesłania' + CSRF_EXPIRED_MESSAGE: 'Twoja sesja wygasła. Prześlij ponownie formularz.' + CSRF_FAILED_MESSAGE: 'Wygląda na to, że wystąpił błąd techniczny. Kliknij przycisk wstecz, następnie odśwież przeglądarkę aby wczytać stronę ponownie.' VALIDATIONPASSWORDSDONTMATCH: 'Hasła nie są takie same' VALIDATIONPASSWORDSNOTEMPTY: 'Hasło nie może być puste' VALIDATIONSTRONGPASSWORD: 'Hasła muszą mieć przynajmniej jedną cyfrę oraz jeden znak alfanumeryczny.' VALIDATOR: Walidator VALIDCURRENCY: 'Proszę podaj prawidłową walutę' SilverStripe\Forms\FormField: + EXAMPLE: 'na przykład {format}' NONE: brak + SilverStripe\Forms\FormScaffolder: + TABMAIN: Główny SilverStripe\Forms\GridField\GridField: Add: 'Dodaj {name}' CSVEXPORT: 'Eksportuj do CSV' + CSVIMPORT: 'Import z CSV' Filter: Filtr - FilterBy: 'Filtruj wg' + FilterBy: 'Filtruj wg ' Find: Wyszukaj LinkExisting: 'Linkuj istniejący' NewRecord: 'Nowy {type}' @@ -55,55 +98,145 @@ pl: Save: Zapisz SilverStripe\Forms\GridField\GridFieldEditButton_ss: EDIT: Edytuj + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'Nie możesz usunąć siebie z tej grupy, stracone zostałby prawa administratora' + SilverStripe\Forms\GridField\GridFieldPaginator: + OF: z + Page: Strona + View: Widok SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Ilość FIELDLABELCURRENCY: waluta + INVALID_CURRENCY: 'Waluta {currency} nie znajduje się na liście dozwolonych walut' + SilverStripe\Forms\MultiSelectField: + SOURCE_VALIDATION: 'Wybierz wartości z podanej listy. Podano niepoprawną opcję {value}' SilverStripe\Forms\NullableField: IsNullLabel: 'Jest Pusty' SilverStripe\Forms\NumericField: VALIDATION: '''{value}'' nie jest liczbą, to pole przyjmuje tylko liczby' SilverStripe\Forms\TimeField: VALIDATEFORMAT: 'Proszę wprowadź prawidłowy format czasu ({format})' + SilverStripe\ORM\DataObject: + PLURALNAME: 'Obiekty danych' + PLURALS: + one: 'Obiekt danych' + few: 'Obiektów danych' + many: 'Obiektów danych' + other: 'Obiektów danych {count}' + SINGULARNAME: 'Obiekt danych' SilverStripe\ORM\FieldType\DBBoolean: ANY: Jakikolwiek + NOANSWER: Nie + YESANSWER: Tak SilverStripe\ORM\FieldType\DBDate: + DAYS_SHORT_PLURALS: + one: '{count} dzień' + few: '{count} dni' + many: '{count} dni' + other: '{count} dni' + HOURS_SHORT_PLURALS: + one: '{count} godzina' + few: '{count} godzin' + many: '{count} godzin' + other: '{count} godzin' LessThanMinuteAgo: 'mniej niż minuta' + MINUTES_SHORT_PLURALS: + one: '{count} minuta' + few: '{count} minut' + many: '{count} minut' + other: '{count} minut' + MONTHS_SHORT_PLURALS: + one: '{count} miesiąc' + few: '{count} miesięcy' + many: '{count} miesięcy' + other: '{count} miesięcy' + SECONDS_SHORT_PLURALS: + one: '{count} sekunda' + few: '{count} sekund' + many: '{count} sekund' + other: '{count} sekund' TIMEDIFFAGO: '{difference} temu' TIMEDIFFIN: 'w {difference}' + YEARS_SHORT_PLURALS: + one: '{count} rok' + few: '{count} lat' + many: '{count} lat' + other: '{count} lat' SilverStripe\ORM\FieldType\DBEnum: ANY: Jakikolwiek + SilverStripe\ORM\Hierarchy: + LIMITED_TITLE: 'Zbyt wiele dzieci ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: InfiniteLoopNotAllowed: 'Znaleziono nieskończoną pętlę wewnątrz hierarchii "{type}". Proszę zmień rodzica by to rozwiązać.' + LIMITED_TITLE: 'Zbyt wiele dzieci ({count})' + SilverStripe\ORM\ValidationException: + DEFAULT_ERROR: 'Niepoprawne dane' SilverStripe\Security\BasicAuth: ENTERINFO: 'Wprowadź username i hasło' ERRORNOTADMIN: 'Ten użytkownik nie jest administratorem' ERRORNOTREC: 'Nie istnieje taki username/hasło' + SilverStripe\Security\CMSMemberLoginForm: + PASSWORDEXPIRED: '

Twoje hasło wygasło. Prosimy wybrać nowe.

' + SilverStripe\Security\CMSSecurity: + INVALIDUSER: '

Niepoprawny użytkownik. Prosimy o ponownie uwierzytelnienie – aby kontynuować.

' + LOGIN_MESSAGE: '

Twoja sesja wygasła z powodu braku aktywności

' + LOGIN_TITLE: 'Wróć do strony, z którym połączenie zostało przerwane, logując się ponownie' + SUCCESS: Sukces + SUCCESSCONTENT: '

Zalogowano poprawnie! Jeżeli nie zostaniesz automatycznie przekierowany kliknij tutaj

' + SUCCESS_TITLE: 'Zalogowano poprawne' + SilverStripe\Security\DefaultAdminService: + DefaultAdminFirstname: 'Domyślny administrator' SilverStripe\Security\Group: AddRole: 'Dodaj rolę dla tej grupy' Code: 'Kod Grupy' DefaultGroupTitleAdministrators: Administratorzy DefaultGroupTitleContentAuthors: 'Autor treści' Description: Opis + GROUPNAME: 'Nazwa Grupy' GroupReminder: 'Jeśli wybierzesz nadrzędną grupę, obecna grupa otrzyma wszystkie jej role' HierarchyPermsError: 'Nie można przyporządkować uprzywilejowanej grupy "{group}" (wymagane uprawnienie ADMIN)' Locked: 'Zablokowana?' + MEMBERS: Użytkownicy + NEWGROUP: 'Nowa grupa' NoRoles: 'Nie znaleziono ról' + PERMISSIONS: Uprawnienia + PLURALNAME: Grupy + PLURALS: + one: Grupa + few: Grup + many: Grup + other: '{count} grup' Parent: 'Grupa nadrzędna' + ROLES: Role + ROLESDESCRIPTION: 'Role są wstępnie zdefiniowanymi zestawami uprawnień i można je przypisać do grup.
TW razie potrzeby są one dziedziczone z grup nadrzędnych.' RolesAddEditLink: 'Zarządzaj rolami' + SINGULARNAME: Grupa Sort: 'Kolejność Sortowania' has_many_Permissions: Zezwolenia many_many_Members: Użytkownicy SilverStripe\Security\LoginAttempt: + Email: 'Adres e-mail' + EmailHashed: 'Adres e-mail (hashed)' IP: 'Adres IP' + PLURALNAME: 'Próby logowania' + PLURALS: + one: 'Próba logowania' + few: 'Prób logowania' + many: 'Próby logowania {count}' + other: 'Próby logowania {count}' + SINGULARNAME: 'Próba logowania' Status: Status SilverStripe\Security\Member: ADDGROUP: 'Dodaj grupę' BUTTONCHANGEPASSWORD: 'Zmień hasło' BUTTONLOGIN: Zaloguj BUTTONLOGINOTHER: 'Zaloguj jako ktoś inny' + BUTTONLOGOUT: 'Wyloguj się' BUTTONLOSTPASSWORD: 'Zgubiłem hasło' CONFIRMNEWPASSWORD: 'Potwierdź nowe hasło' CONFIRMPASSWORD: 'Potwierdź hasło' + CURRENT_PASSWORD: 'Aktualne hasło' + EDIT_PASSWORD: 'Nowe hasło' EMAIL: E-mail EMPTYNEWPASSWORD: 'Nowe hasło nie może być puste, spróbuj ponownie.' ENTEREMAIL: 'Wpisz adres e-mail aby otrzymać link do zmiany hasła.' @@ -113,12 +246,23 @@ pl: ERRORWRONGCRED: 'Podane dane są niepoprawne. Proszę spróbować ponownie.' FIRSTNAME: Imię INTERFACELANG: 'Język interfejsu' + KEEPMESIGNEDIN: 'Zapamiętaj mnie' LOGGEDINAS: 'Zostałeś zalogowany jako {name}.' NEWPASSWORD: 'Nowe hasło' PASSWORD: Hasło + PASSWORDEXPIRED: 'Twoje hasło wygasło. Prosimy ustawić nowe.' + PLURALNAME: Użytkownicy + PLURALS: + one: Użytkownik + few: '{count} użytkowników' + many: '{count} użytkowników' + other: '{count} użytkowników' + REMEMBERME: 'Pamiętaj mnie następnym razem? (przez {count} dni na tym urządzeniu)' + SINGULARNAME: Użytkownik SUBJECTPASSWORDCHANGED: 'Twoje hasło zostało zmienione' SUBJECTPASSWORDRESET: 'Twój link do zmiany hasła' SURNAME: Nazwisko + VALIDATIONADMINLOSTACCESS: 'Nie można usunąć wszystkich grup administracyjnych z Twojego profilu' ValidationIdentifierFailed: 'Nie można nadpisać istniejącego użytkownika #{id} o identycznym identyfikatorze ({name} = {value})' WELCOMEBACK: 'Witaj ponownie, {firstname}' YOUROLDPASSWORD: 'Twoje stare hasło' @@ -127,15 +271,42 @@ pl: db_LockedOutUntil: 'Zablokowany do' db_Password: Hasło db_PasswordExpiry: 'Data wygaśnięcia hasła' + SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm: + AUTHENTICATORNAME: 'Formularz logowania użytkownika CMS' + BUTTONFORGOTPASSWORD: 'Zapomniałeś hasła?' + BUTTONLOGIN: 'Zaloguj mnie spowrotem' + BUTTONLOGOUT: 'Wyloguj się' + SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: + ERRORWRONGCRED: 'Podane dane są niepoprawne. Proszę spróbować ponownie.' + NoPassword: 'Hasło nie zostało skonfigurowane dla tego użytkownika.' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'E-mail i hasło' + SilverStripe\Security\MemberPassword: + PLURALNAME: 'Hasła użytkownika' + PLURALS: + one: 'Hasło użytkownika' + few: 'Haseł użytkownika' + many: 'Haseł użytkownika' + other: '{count} haseł użytkownika ' + SINGULARNAME: 'Hasło użytkownika' SilverStripe\Security\PasswordValidator: LOWCHARSTRENGTH: 'Proszę zwiększyć siłę hasła, dodając niektóre z następujących znaków: % s' PREVPASSWORD: 'Użyłeś już tego hasła wcześniej, proszę wybrać nowe' TOOSHORT: 'Hasło jest za krótkie, proszę podać {minimum} znaków lub więcej' SilverStripe\Security\Permission: AdminGroup: Administrator + CMS_ACCESS_CATEGORY: 'Dostęp do CMS''a' CONTENT_CATEGORY: 'Uprawnienie edycji treści' FULLADMINRIGHTS: 'Pełne prawa administracyjne' FULLADMINRIGHTS_HELP: 'Zatwierdza i nadpisuje wszystkie istniejące uprawnienia' + PERMISSIONS_CATEGORY: 'Uprawnienia ról i dostępu' + PLURALNAME: Uprawnienia + PLURALS: + one: Uprawnienie + few: Uprawnień + many: Uprawnień + other: '{count} uprawnień' + SINGULARNAME: Uprawnienie UserPermissionsIntro: 'Przydzielenie grup temu użytkownikowi spowoduje zmianę jego uprawnień. Odwołaj się do sekcji Grupy aby dowiedzieć się więcej o uprawnieniach grupowych.' SilverStripe\Security\PermissionCheckboxSetField: AssignedTo: 'przypisany do "{title}"' @@ -144,17 +315,41 @@ pl: FromRoleOnGroup: 'odziedziczone z roli "{roletitle}" w grupie "{grouptitle}"' SilverStripe\Security\PermissionRole: OnlyAdminCanApply: 'Tylko administrator może to zastosować' + PLURALNAME: Role + PLURALS: + one: Rola + few: ról + many: ról + other: '{count} ról' + SINGULARNAME: Rola Title: Tytuł SilverStripe\Security\PermissionRoleCode: + PLURALNAME: 'Kod roli uprawnienia' + PLURALS: + one: 'Kod roli uprawnienia' + few: 'Kodów ról uprawnień' + many: 'Kodów ról uprawnień' + other: '{count} kodów ról uprawnień' PermsError: 'Nie można przyporządkować uprzywilejowanego uprawnienia "{code}" (wymagane uprawnienie ADMIN)' + SINGULARNAME: 'Kod roli uprawnienia' + SilverStripe\Security\RememberLoginHash: + PLURALNAME: 'Hasła logowania' + PLURALS: + one: 'Hasło logowania' + few: 'Haseł logowania' + many: 'Haseł logowania' + other: '{count} haseł logowania' + SINGULARNAME: 'Hasło logowania' SilverStripe\Security\Security: ALREADYLOGGEDIN: 'Nie masz dostępu do tej strony. Jeśli posiadasz inne konto, które umożliwi Ci dostęp do tej strony, możesz się zalogować poniżej' BUTTONSEND: 'Wyślij mi link do zresetowania hasła' CHANGEPASSWORDBELOW: 'Możesz zmienić swoje hasło poniżej' CHANGEPASSWORDHEADER: 'Zmień swoje hasło' + CONFIRMLOGOUT: 'Kliknij przycisk poniżej, aby potwierdzić, że chcesz się wylogować.' ENTERNEWPASSWORD: 'Proszę wprowadż nowe hasło' ERRORPASSWORDPERMISSION: 'Musisz być zalogowany aby zmienić hasło' LOGIN: Logowanie + LOGOUT: 'Wyloguj się' LOSTPASSWORDHEADER: 'Nie pamiętam hasła' NOTEPAGESECURED: 'Ta strona jest zabezpieczona. Wpisz swoje dane a my wyślemy Ci potwierdzenie niebawem' NOTERESETLINKINVALID: '

Link resetujący hasło wygasł lub jest nieprawidłowy.

Możesz poprosić o nowy tutaj lub zmień swoje hasło po zalogowaniu się.

' diff --git a/lang/pt.yml b/lang/pt.yml index b4b030690..2a7c645d2 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -1,9 +1,6 @@ pt: SilverStripe\Forms\ConfirmedPasswordField: SHOWONCLICKTITLE: 'Mudar password' - SilverStripe\Forms\DateField: - NOTSET: 'Não inserido' - TODAY: Hoje SilverStripe\Forms\DropdownField: CHOOSE: (Escolha) SilverStripe\Forms\Form: diff --git a/lang/pt_BR.yml b/lang/pt_BR.yml index fc7c41713..10d37fa3d 100644 --- a/lang/pt_BR.yml +++ b/lang/pt_BR.yml @@ -3,9 +3,6 @@ pt_BR: SHOWONCLICKTITLE: 'Trocar senha' SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: R$ - SilverStripe\Forms\DateField: - NOTSET: 'não informado' - TODAY: hoje SilverStripe\Forms\DropdownField: CHOOSE: Selecione SilverStripe\Forms\Form: diff --git a/lang/ro.yml b/lang/ro.yml index aa855a0d0..e02135a75 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -4,8 +4,6 @@ ro: BETWEEN: 'Parola trebuie să conțină între {min} și {max} caractere.' MAXIMUM: 'Parola trebuie să conțină cel mult {max} caractere.' SHOWONCLICKTITLE: 'Schimbare Parola' - SilverStripe\Forms\DateField: - TODAY: astăzi SilverStripe\Forms\DropdownField: CHOOSE: (Alege) SilverStripe\Forms\GridField\GridField: diff --git a/lang/ru.yml b/lang/ru.yml index 5d8cf119d..73602e013 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -33,8 +33,6 @@ ru: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'не установлено' - TODAY: сегодня VALIDDATEFORMAT2: 'Пожалуйста, задайте верный формат даты ({format})' VALIDDATEMAXDATE: 'Требуется значение даты, совпадающее с максимальным ({date}) или более старое' VALIDDATEMINDATE: 'Требуется значение даты, совпадающее с минимальным ({date}) или более новое' @@ -82,7 +80,6 @@ ru: RelationSearch: 'Поиск отношений' ResetFilter: Сброс SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Удалить Delete: Удалить DeletePermissionsFailure: 'Нет прав на удаление' EditPermissionsFailure: 'Не достаточно прав для удаления записи' @@ -94,8 +91,6 @@ ru: DeletePermissionsFailure: 'Нет прав на удаление' Deleted: 'Удалено {type} {name}' Save: Сохранить - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Редактировать SilverStripe\Forms\GridField\GridFieldPaginator: OF: из Page: Страница diff --git a/lang/sk.yml b/lang/sk.yml index 5d82a7d8a..861045fc6 100644 --- a/lang/sk.yml +++ b/lang/sk.yml @@ -37,8 +37,6 @@ sk: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: nezadané - TODAY: dnes VALIDDATEFORMAT2: 'Prosím zadajte platný formát dátumu ({format})' VALIDDATEMAXDATE: 'Váš dátum musí byť starší alebo odpovedajúci maximu povoleného dátumu ({date})' VALIDDATEMINDATE: 'Váš dátum musí byť novší alebo odpovedajúci minimu povoleného dátumu ({date})' @@ -78,7 +76,6 @@ sk: RelationSearch: 'Vzťah hľadania' ResetFilter: Reset SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Zmazať Delete: Zmazať DeletePermissionsFailure: 'Žiadne oprávnenia zmazať' EditPermissionsFailure: 'Žiadne oprávnenie pre odpojenie záznamu' @@ -90,8 +87,6 @@ sk: DeletePermissionsFailure: 'Žiadne oprávnenia zmazať' Deleted: 'Zmazané {type} {name}' Save: Uložiť - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Editovať SilverStripe\Forms\GridField\GridFieldPaginator: Page: Stránka SilverStripe\Forms\MoneyField: diff --git a/lang/sl.yml b/lang/sl.yml index 7e5138547..bccd51826 100644 --- a/lang/sl.yml +++ b/lang/sl.yml @@ -5,8 +5,6 @@ sl: MAXIMUM: 'Geslo je lahko dolgo največ {max} znakov.' SHOWONCLICKTITLE: 'Spremeni geslo' SilverStripe\Forms\DateField: - NOTSET: 'ni nastavljeno' - TODAY: danes VALIDDATEFORMAT2: 'Prosim, vnesite ustrezno obliko datuma ({format})' VALIDDATEMAXDATE: 'Datum mora biti starejši ali enak največjemu dovoljenemu datumu ({date})' VALIDDATEMINDATE: 'Datum mora biti novejši ali enak najmanjšemu dovoljenemu datumu ({date})' @@ -40,7 +38,6 @@ sl: RelationSearch: 'Povezano iskanje' ResetFilter: Ponastavi SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Izbriši Delete: Izbriši DeletePermissionsFailure: 'Ni dovoljenja za brisanje' UnlinkRelation: 'Odstrani povezavo' @@ -51,8 +48,6 @@ sl: DeletePermissionsFailure: 'Ni dovoljenja za brisanje' Deleted: 'Izbrisanih {type} {name}' Save: Shrani - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Uredi SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Znesek FIELDLABELCURRENCY: Valuta diff --git a/lang/sl_SI.yml b/lang/sl_SI.yml index 01fb8b0a3..ec4ad09e8 100644 --- a/lang/sl_SI.yml +++ b/lang/sl_SI.yml @@ -7,8 +7,6 @@ sl_SI: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: € SilverStripe\Forms\DateField: - NOTSET: 'ni nastavljeno' - TODAY: danes VALIDDATEFORMAT2: 'Prosimo vnesite veljavno obliko datuma ({format})' SilverStripe\Forms\DropdownField: CHOOSE: (Izberi) @@ -24,7 +22,6 @@ sl_SI: Print: Natisni ResetFilter: Resetiraj SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Izbriši Delete: Izbriši UnlinkRelation: 'Odstrani povezavo' SilverStripe\Forms\GridField\GridFieldDetailForm: @@ -34,8 +31,6 @@ sl_SI: DeletePermissionsFailure: 'Nimate pravic za brisanje' Deleted: 'Izbrisano {type} {name}' Save: Shrani - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Uredi SilverStripe\ORM\FieldType\DBBoolean: NOANSWER: Ne YESANSWER: Da diff --git a/lang/sr.yml b/lang/sr.yml index 4a989ee97..07f642278 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -7,8 +7,6 @@ sr: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: дин. SilverStripe\Forms\DateField: - NOTSET: 'није подешено' - TODAY: данас VALIDDATEFORMAT2: 'Молимо Вас да унесете исправан формат датума ({format})' VALIDDATEMAXDATE: 'Датум не сме бити после ({date})' VALIDDATEMINDATE: 'Датум не сме бити пре ({date})' @@ -41,7 +39,6 @@ sr: RelationSearch: 'Претраживање релације' ResetFilter: 'Врати у пређашње стање' SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Избриши Delete: Избриши DeletePermissionsFailure: 'Немате дозволу за брисање' EditPermissionsFailure: 'Немате дозволу да раскинете линк са записом' @@ -53,8 +50,6 @@ sr: DeletePermissionsFailure: 'Немате право брисања' Deleted: 'Избрисано {type} {name}' Save: Сачувај - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Измени SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Износ FIELDLABELCURRENCY: Валута diff --git a/lang/sr@latin.yml b/lang/sr@latin.yml index 957457b82..1b210ae51 100644 --- a/lang/sr@latin.yml +++ b/lang/sr@latin.yml @@ -7,8 +7,6 @@ sr@latin: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: din. SilverStripe\Forms\DateField: - NOTSET: 'nije podešeno' - TODAY: danas VALIDDATEFORMAT2: 'Molimo Vas da unesete ispravan format datuma ({format})' VALIDDATEMAXDATE: 'Datum ne sme biti posle ({date})' VALIDDATEMINDATE: 'Datum ne sme biti pre ({date})' @@ -41,7 +39,6 @@ sr@latin: RelationSearch: 'Pretraživanje relacije' ResetFilter: 'Vrati u pređašnje stanje' SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Izbriši Delete: Izbriši DeletePermissionsFailure: 'Nemate dozvolu za brisanje' EditPermissionsFailure: 'Nemate dozvolu da raskinete link sa zapisom' @@ -53,8 +50,6 @@ sr@latin: DeletePermissionsFailure: 'Nemate pravo brisanja' Deleted: 'Izbrisano {type} {name}' Save: Sačuvaj - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Izmeni SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Iznos FIELDLABELCURRENCY: Valuta diff --git a/lang/sr_RS.yml b/lang/sr_RS.yml index 62d6aef15..fc79812da 100644 --- a/lang/sr_RS.yml +++ b/lang/sr_RS.yml @@ -7,8 +7,6 @@ sr_RS: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: дин. SilverStripe\Forms\DateField: - NOTSET: 'није подешено' - TODAY: данас VALIDDATEFORMAT2: 'Молимо Вас да унесете исправан формат датума ({format})' VALIDDATEMAXDATE: 'Датум не сме бити после ({date})' VALIDDATEMINDATE: 'Датум не сме бити пре ({date})' @@ -41,7 +39,6 @@ sr_RS: RelationSearch: 'Претраживање релације' ResetFilter: 'Врати у пређашње стање' SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Избриши Delete: Избриши DeletePermissionsFailure: 'Немате дозволу за брисање' EditPermissionsFailure: 'Немате дозволу да раскинете линк са записом' @@ -53,8 +50,6 @@ sr_RS: DeletePermissionsFailure: 'Немате право брисања' Deleted: 'Избрисано {type} {name}' Save: Сачувај - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Измени SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Износ FIELDLABELCURRENCY: Валута diff --git a/lang/sr_RS@latin.yml b/lang/sr_RS@latin.yml index c4c4b6900..f39b36058 100644 --- a/lang/sr_RS@latin.yml +++ b/lang/sr_RS@latin.yml @@ -7,8 +7,6 @@ sr_RS@latin: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: din. SilverStripe\Forms\DateField: - NOTSET: 'nije podešeno' - TODAY: danas VALIDDATEFORMAT2: 'Molimo Vas da unesete ispravan format datuma ({format})' VALIDDATEMAXDATE: 'Datum ne sme biti posle ({date})' VALIDDATEMINDATE: 'Datum ne sme biti pre ({date})' @@ -41,7 +39,6 @@ sr_RS@latin: RelationSearch: 'Pretraživanje relacije' ResetFilter: 'Vrati u pređašnje stanje' SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Izbriši Delete: Izbriši DeletePermissionsFailure: 'Nemate dozvolu za brisanje' EditPermissionsFailure: 'Nemate dozvolu da raskinete link sa zapisom' @@ -53,8 +50,6 @@ sr_RS@latin: DeletePermissionsFailure: 'Nemate pravo brisanja' Deleted: 'Izbrisano {type} {name}' Save: Sačuvaj - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Izmeni SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Iznos FIELDLABELCURRENCY: Valuta diff --git a/lang/sv.yml b/lang/sv.yml index 27f943efd..f35c2ee99 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -37,8 +37,6 @@ sv: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'inte angivet' - TODAY: 'i dag' VALIDDATEFORMAT2: 'Var god att ange datumet i ett giltigt format ({format})' VALIDDATEMAXDATE: 'Angivet datum måste vara tidigare eller samma som det senaste godkända datumet ({date})' VALIDDATEMINDATE: 'Angivet datum måste vara senare eller samma som det tidigaste godkända datumet ({date})' @@ -84,7 +82,6 @@ sv: RelationSearch: Relationssökning ResetFilter: Rensa SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Radera Delete: Radera DeletePermissionsFailure: 'Rättighet för att radera saknas' EditPermissionsFailure: 'Rättigheter för avlänkning saknas' @@ -96,8 +93,6 @@ sv: DeletePermissionsFailure: 'Rättighet för att radera saknas' Deleted: 'Raderade {type} {name}' Save: Spara - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Redigera SilverStripe\Forms\GridField\GridFieldPaginator: Page: Sida View: Visa diff --git a/lang/th.yml b/lang/th.yml index b30883ed4..a94e91ed4 100644 --- a/lang/th.yml +++ b/lang/th.yml @@ -2,9 +2,6 @@ th: SilverStripe\Forms\ConfirmedPasswordField: BETWEEN: 'รหัสผ่านต้องมีความยาวตัวอักษรอย่างน้อย {min} ถึง {max} ตัวอักษร' SHOWONCLICKTITLE: เปลี่ยนรหัสผ่าน - SilverStripe\Forms\DateField: - NOTSET: ไม่ต้องตั้งค่า - TODAY: วันนี้ SilverStripe\Forms\DropdownField: CHOOSE: (เลือก) SilverStripe\Forms\EmailField: @@ -30,7 +27,6 @@ th: Print: พิมพ์ ResetFilter: รีเซ็ต SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: ลบ Delete: ลบ DeletePermissionsFailure: ไม่ได้รับสิทธิ์ให้ลบได้ UnlinkRelation: ยกเลิกการลิงก์ diff --git a/lang/tr.yml b/lang/tr.yml index d354d3444..e40237592 100644 --- a/lang/tr.yml +++ b/lang/tr.yml @@ -6,9 +6,6 @@ tr: SHOWONCLICKTITLE: 'Parola Değiştir' SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: USD - SilverStripe\Forms\DateField: - NOTSET: ayarlanmamış - TODAY: bugün SilverStripe\Forms\DropdownField: CHOOSE: (Seçiniz) SilverStripe\Forms\Form: diff --git a/lang/uk.yml b/lang/uk.yml index ca0be2b5a..9c30b7d1f 100644 --- a/lang/uk.yml +++ b/lang/uk.yml @@ -4,9 +4,6 @@ uk: BETWEEN: 'Пароль повинен містити від {min} до {max} символів.' MAXIMUM: 'Пароль повинен містити не більше ніж {max} символів.' SHOWONCLICKTITLE: 'Змінити пароль' - SilverStripe\Forms\DateField: - NOTSET: 'не встановлено' - TODAY: сьогодні SilverStripe\Forms\DropdownField: CHOOSE: (Оберіть) SilverStripe\Forms\Form: diff --git a/lang/zh.yml b/lang/zh.yml index 792565ac6..26c8e2893 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -10,8 +10,6 @@ zh: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: 货币字符 SilverStripe\Forms\DateField: - NOTSET: 未设置 - TODAY: 今天 VALIDDATEFORMAT2: '请输入一个有效的日期格式 ({format})' VALIDDATEMAXDATE: '您的日期必须更早或者符合最大允许日期 ({date})' VALIDDATEMINDATE: '您的日期必须更迟或者符合最小允许日期 ({date})' @@ -46,7 +44,6 @@ zh: RelationSearch: 关系搜索 ResetFilter: 重设 SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: 删除 Delete: 删除 DeletePermissionsFailure: 没有删除权限 EditPermissionsFailure: 没有解除记录链接的权限 @@ -58,8 +55,6 @@ zh: DeletePermissionsFailure: 没有删除权限 Deleted: '已删除的 {type} {name}' Save: 保存 - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: 编辑 SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: 金额 FIELDLABELCURRENCY: 货币 From 1bcd449acecf74c782feecbd11991b1ea0c06e2a Mon Sep 17 00:00:00 2001 From: root Date: Tue, 24 Jul 2018 19:10:40 +0000 Subject: [PATCH 35/35] Added 4.2.0 changelog --- docs/en/04_Changelogs/4.2.0.md | 195 ++++++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 1 deletion(-) diff --git a/docs/en/04_Changelogs/4.2.0.md b/docs/en/04_Changelogs/4.2.0.md index 9147bd5b4..c23c232ee 100644 --- a/docs/en/04_Changelogs/4.2.0.md +++ b/docs/en/04_Changelogs/4.2.0.md @@ -370,4 +370,197 @@ class PageController extends ContentController * Deprecated `HTTP.disable_http_cache`. Use `HTTPCacheControlMiddleware.defaultState` and `defaultForcingLevel` instead * Deprecated `HTTP::register_modification_date()`. Use `HTTPCacheControlMiddleware::registerModificationDate()` instead * Deprecated `HTTP::register_modification_timestamp()`. Use `HTTPCacheControlMiddleware::registerModificationDate()` instead - * Deprecated `HTTP::register_etag()`. Use `HTTPCacheControlMiddleware::ETagMiddleware()` instead \ No newline at end of file + * Deprecated `HTTP::register_etag()`. Use `HTTPCacheControlMiddleware::ETagMiddleware()` instead + +## Change Log + +### Security + + * 2018-04-23 [d42bd6e](https://github.com/silverstripe/silverstripe-assets/commit/d42bd6ec3e1d15d48074de04bcabfd90ce0cbdc9) File.allowed_extensions can have values removed via YAML configuration (Robbie Averill) - See [ss-2018-014](https://www.silverstripe.org/download/security-releases/ss-2018-014) + * 2018-04-23 [30e2d9c](https://github.com/silverstripe/silverstripe-framework/commit/30e2d9c4df0e8d431bdf41446c7dc4d5e74ad507) Allow forced redirects to HTTPS for responses with basic authentication (Robbie Averill) - See [ss-2018-009](https://www.silverstripe.org/download/security-releases/ss-2018-009) + * 2018-04-10 [0b7e665](https://github.com/silverstripe/silverstripe-asset-admin/commit/0b7e665266c08fb0262af15096c0eaa2ce872a00) Enable oembed to be disabled serverside (Damian Mooyman) - See [ss-2018-003](https://www.silverstripe.org/download/security-releases/ss-2018-003) + * 2018-04-10 [7c2886d](https://github.com/silverstripe/silverstripe-framework/commit/7c2886d87cd8dc01181b69f5252a7012137668f9) Update docs for oembed (Damian Mooyman) - See [ss-2018-003](https://www.silverstripe.org/download/security-releases/ss-2018-003) + * 2018-04-09 [326b1ff](https://github.com/silverstripe/silverstripe-asset-admin/commit/326b1ffa3976d8be30de3f8cfc0e7a55c14764aa) Implement stronger oembed white/blacklist (Damian Mooyman) - See [ss-2018-002](https://www.silverstripe.org/download/security-releases/ss-2018-002) + +### API Changes + + * 2018-06-15 [53dded8](https://github.com/silverstripe/silverstripe-framework/commit/53dded8cff678c6fee4163f91ceac12b6c960a77) Remove @internal from new 4.2 methods (Damian Mooyman) + * 2018-06-12 [ec956a6](https://github.com/silverstripe/silverstripe-framework/commit/ec956a682dce2876322a3bc7c169a8eccec558ec) Moving tests to use transactions (Daniel Hensby) + * 2018-04-16 [b1e8db1](https://github.com/silverstripe/silverstripe-versioned/commit/b1e8db1848c410abfd917d2b7104221d5004b66c) Implement rollbackRecursive() / rollbackSingle() (Damian Mooyman) + * 2018-04-16 [c8b3593](https://github.com/silverstripe/silverstripe-framework/commit/c8b3593090d7543df59c33f1323b9b188e663efd) Form::makeReadonly() returns self (Damian Mooyman) + * 2018-04-06 [6c616f5](https://github.com/silverstripe/silverstripe-cms/commit/6c616f5f7a4d2611e7a8f2cbb66360079c3edb6b) Implement polymorphic sitetree link tracking (#2123) (Damian Mooyman) + * 2018-03-22 [7351caf](https://github.com/silverstripe/silverstripe-framework/commit/7351caf48702558d20a93bd83fdc4c36d4be50c3) Allow non-DataExtension Extensions to decorate dataobject (Damian Mooyman) + * 2018-03-21 [257ff69](https://github.com/silverstripe/silverstripe-framework/commit/257ff69e321e08af743de019ae338a291cb62615) Implement many_many through polymorphic (from only) (#7928) (Damian Mooyman) + * 2018-03-21 [32dcc4d](https://github.com/silverstripe/silverstripe-versioned/commit/32dcc4d34fa3490dfa4b1dd877448673176ca236) add withVersionedMode() to safely isolate reading mode modifications (Damian Mooyman) + * 2018-03-20 [87afe84](https://github.com/silverstripe/silverstripe-graphql/commit/87afe84e86d40ba4e9ebd0c37ea52e806650ba59) Customise type names and operation names (#143) (Aaron Carlino) + * 2018-03-05 [3a1c813](https://github.com/silverstripe/silverstripe-framework/commit/3a1c813b288090853942296d52f12157ae83c946) Add getContentCSS() / setContentCSS() to allow per-config customisation of content_css (Damian Mooyman) + * 2018-02-21 [ced2ba1](https://github.com/silverstripe/silverstripe-framework/commit/ced2ba1f64015c92dfeea6b473cf3f1999e449c6) Move CSV writing/reading to league/csv library (Daniel Hensby) + * 2018-02-07 [860fa2a](https://github.com/silverstripe/silverstripe-framework/commit/860fa2a05a8d55d97a9508858215502e94c24810) Add excludeAny() and tests for complicated excludes/filters (#7838) (Andrew Aitken-Fincham) + +### Features and Enhancements + + * 2018-06-18 [95bcac7](https://github.com/silverstripe/silverstripe-framework/commit/95bcac796a3bcb65e7d9fc55e0af0b9d17407938) Ensure test DB is flushed on either DDL or transaction-disabled tests (Damian Mooyman) + * 2018-06-13 [a88257e](https://github.com/silverstripe/silverstripe-framework/commit/a88257efac1a222607d413c070ab4bd7dc20df3d) Add version to HTTPRequest and create raw string representation (Daniel Hensby) + * 2018-05-21 [865ebb3](https://github.com/silverstripe/silverstripe-framework/commit/865ebb3398a692d9f39b4f64ff86054ccc291177) Improve upgrading experience. (#8025) (Damian Mooyman) + * 2018-05-20 [1d34d19](https://github.com/silverstripe/silverstripe-admin/commit/1d34d198f8ed661a987c8a10ee24c568e4528e39) Make FormAlert injectable (Robbie Averill) + * 2018-05-17 [e3237f9](https://github.com/silverstripe/silverstripe-cms/commit/e3237f9638d72d49e1e192738e00eacac2308c69) Add revert mutation and refactor injector transformations (#2158) (Robbie Averill) + * 2018-05-17 [8ffa9dd](https://github.com/silverstripe/silverstripe-admin/commit/8ffa9dda6aa53da4f701cf61b32c64b3318053aa) Make Preview component injectable (#505) (Robbie Averill) + * 2018-05-11 [1a57c7c](https://github.com/silverstripe/silverstripe-framework/commit/1a57c7c1d0623fdc1b5a2ba2428306dfaf618bb0) Add getJoinTable to MMTL (Daniel Hensby) + * 2018-05-02 [660e8bd](https://github.com/silverstripe/silverstripe-graphql/commit/660e8bde0e4b80ef770672436ae1c05262c61665) static caching of schema types, as well as dynamic endpoint (Aaron Carlino) + * 2018-05-01 [aae318e](https://github.com/silverstripe/silverstripe-admin/commit/aae318efd7a87b6ebcd57ab0d251095721651400) Register fieldHolder HOCs with injector (Dylan Wagstaff) + * 2018-04-27 [e0b4d50](https://github.com/silverstripe/silverstripe-admin/commit/e0b4d506a1eea6daa8b66f037f2a140b8f76eb1a) Add Loading indicator component, implement into FormBuilderLoader (#490) (Robbie Averill) + * 2018-04-26 [0494be7](https://github.com/silverstripe/silverstripe-admin/commit/0494be71d8f35c7c45e76e4b0110e6a8f948769a) Ensure that popover has correct container assigned (Damian Mooyman) + * 2018-04-23 [1b24bf6](https://github.com/silverstripe/silverstripe-graphql/commit/1b24bf635949fac871ba6180b83986505c0e01db) Consolidate type / operation name generation (#151) (Damian Mooyman) + * 2018-04-23 [f50438e](https://github.com/silverstripe/silverstripe-versioned/commit/f50438e448209372f1f19c4d0c566182758c4a35) Ensure that default caches are segmented based on versioned state (Damian Mooyman) + * 2018-04-19 [7c3980a](https://github.com/silverstripe/silverstripe-graphql/commit/7c3980a1c8dd10f24aabddcc770c102d13a32089) Refactor for more consistent use of union and inheritance types (#150) (Aaron Carlino) + * 2018-04-11 [4ddee82](https://github.com/silverstripe/silverstripe-admin/commit/4ddee82ccd989963581d7bd8168f7d0899571096) Allow Preview class names to be overridden, and add i18n to messages (Robbie Averill) + * 2018-04-11 [c4f8af5](https://github.com/silverstripe/silverstripe-cms/commit/c4f8af543cd362dfc54d477cac956f7418747e50) Add AbsoluteLink to history viewer page GraphQL query (#2142) (Robbie Averill) + * 2018-04-10 [0fa15f4](https://github.com/silverstripe/silverstripe-versioned/commit/0fa15f45e8c19f0c2c560597bdff1a61d93c6660) Ensure invalid stage values are throws as exceptions (Damian Mooyman) + * 2018-04-09 [19e45a9](https://github.com/silverstripe/silverstripe-asset-admin/commit/19e45a9b966490be55b79c2667e2e18c16c3e0e6) Open modal default upload folder (#763) (Maxime Rainville) + * 2018-04-04 [2c266c2](https://github.com/silverstripe/silverstripe-versioned/commit/2c266c220918c93fd45ad7ecb23d153bb529821e) Allow cleanupVersionedOrphans to be disabled (Damian Mooyman) + * 2018-04-03 [47bcac9](https://github.com/silverstripe/silverstripe-framework/commit/47bcac930df8bde71ffeb9144ac07d429ea9ee87) Add config var to skip confirm logout (#7977) (Andrew Aitken-Fincham) + * 2018-04-02 [14af3b8](https://github.com/silverstripe/silverstripe-admin/commit/14af3b851906c04211f1a72de27e0a939e1b3656) Add --inverted modifier for Badge component with pattern library examples (Robbie Averill) + * 2018-03-21 [d88415b](https://github.com/silverstripe/silverstripe-versioned/commit/d88415b57d3042935eb4e08f46e51a6b0d9b802b) Decorate TestSession with stage params (Damian Mooyman) + * 2018-03-21 [26402f3](https://github.com/silverstripe/silverstripe-framework/commit/26402f3bb52b2effa4218ada4b06ce9199216190) Enable request handlers to be extended (Damian Mooyman) + * 2018-03-21 [9a6d18a](https://github.com/silverstripe/silverstripe-admin/commit/9a6d18a8e456ca45d050d6b5306f4aa2a9e66ad1) Set default reading mode in admin (disables stage=Stage rewrite links) (Damian Mooyman) + * 2018-03-14 [f51ea4d](https://github.com/silverstripe/silverstripe-admin/commit/f51ea4d4209958b5dd0809eb8f96351cbd5c713f) use scss variable than hard-coded color (#460) (Chris Joe) + * 2018-03-12 [8294ab3](https://github.com/silverstripe/silverstripe-admin/commit/8294ab3cea88ec44f7aebdd658f1b2dbecbf3686) Allow badge-pill class to be modified in Badge component (Robbie Averill) + * 2018-03-12 [79db975](https://github.com/silverstripe/silverstripe-asset-admin/commit/79db975a190c0178699164d63d1fd92bbd6cf9f0) add status badge to uploadfield item (Christopher Joe) + * 2018-03-12 [c92e5fe](https://github.com/silverstripe/silverstripe-versioned/commit/c92e5fef0e422b54bff9fcf453b7346a2a1e0186) Ensure that publishSingle() updates local version (Damian Mooyman) + * 2018-03-08 [5db03d0](https://github.com/silverstripe/silverstripe-versioned/commit/5db03d06fb88face772af77c93a21978d25ab2c4) Add isLiveVersion and isLatestDraftVersion to Versioned and GraphQL DataObject scaffolding (Robbie Averill) + * 2018-03-05 [1a82f03](https://github.com/silverstripe/silverstripe-cms/commit/1a82f0364ad33e18145d4f7e7729c7c0fdce4413) Add page GraphQL query HOC for history viewer component (Robbie Averill) + * 2018-03-05 [083308f](https://github.com/silverstripe/silverstripe-admin/commit/083308fa699dcc8298b3a80ee60b0d1ff63ed034) Update table border colour to lighter grey (Robbie Averill) + * 2018-02-28 [4d424dd](https://github.com/silverstripe/silverstripe-framework/commit/4d424dd3407542641300f6064db0952ddc9acb48) get_by_id: alternate signature to allow MyDataObject::get_by_id($id) (Damian Mooyman) + * 2018-02-28 [5735bee](https://github.com/silverstripe/silverstripe-cms/commit/5735beeb90f97127ae7cb42cecd031b8f168cdf3) Upgrade to Bootstrap 4.0.0-stable and change to reactstrap 5.0.0-beta (#2101) (Luke Edwards) + * 2018-02-28 [62eb29e](https://github.com/silverstripe/silverstripe-campaign-admin/commit/62eb29e5c461af2279bb007c2d18759945124c76) Upgrade to Bootstrap 4.0.0-stable and change to reactstrap 5.0.0-beta (#88) (Luke Edwards) + * 2018-02-27 [f181ba3](https://github.com/silverstripe/silverstripe-asset-admin/commit/f181ba32f750ae2b6bf33f87c632fd3f4419ad93) Upgrade to Bootstrap 4.0.0-stable and change to reactstrap 5.0.0-beta (#737) (Luke Edwards) + * 2018-02-27 [8094c26](https://github.com/silverstripe/silverstripe-admin/commit/8094c2673049c359fd8c061b58191b0886b7a391) Decouple preview from campaign admin (Damian Mooyman) + * 2018-02-27 [5825958](https://github.com/silverstripe/silverstripe-admin/commit/5825958eafe63e6cf1ab88d7940ae1afe7865499) Upgrade to Bootstrap 4.0.0-stable and change to reactstrap 5.0.0-beta (#441) (Luke Edwards) + * 2018-02-27 [9474deb](https://github.com/silverstripe/silverstripe-asset-admin/commit/9474debfd256e3d0401cc8a0a4080f2b09bdf795) Add bulk insert feature for UploadField (Christopher Joe) + * 2018-02-26 [85dae1b](https://github.com/silverstripe/silverstripe-asset-admin/commit/85dae1bbde06e01c8bb19b38eb7e4a3d750f3610) Add warning when unpublishing owned files (#739) (Aaron Carlino) + * 2018-02-26 [c4e705a](https://github.com/silverstripe/silverstripe-campaign-admin/commit/c4e705a7fe117e4d6042c435ea368a2db48ed866) removed max width for content in intro screen (Christopher Joe) + * 2018-02-25 [1202807](https://github.com/silverstripe/silverstripe-admin/commit/12028073f48273179ea12adbbaf84f3db25c0fc7) Add warning for unpublishing owned records #444 (Aaron Carlino) + * 2018-02-25 [fe9f729](https://github.com/silverstripe/silverstripe-versioned/commit/fe9f72950534e669676ca50d9ad781c00aa5148c) Add warning when unpublishing owned records (#122) (Aaron Carlino) + * 2018-02-17 [a214368](https://github.com/silverstripe/silverstripe-framework/commit/a2143680e8ae819d3e9404aba636eb6cb168c319) Add record count to dev/build output. (Sam Minnee) + * 2018-02-15 [de0b76d](https://github.com/silverstripe/silverstripe-framework/commit/de0b76dff6f4519cbe788c6e778b60e589f50b02) Fall back to SSViewer::get_themes when using themeResourceLoaders (Andrew Aitken-Fincham) + * 2018-02-12 [00ff3ba](https://github.com/silverstripe/silverstripe-framework/commit/00ff3ba4b22df024a8fe98c47fb76a70739498fd) Make dropdownFieldThreshold configurable on DBForeignKey (#7789) (Andrew Aitken-Fincham) + * 2018-02-09 [0151449](https://github.com/silverstripe/silverstripe-cms/commit/01514490fe4ef1ea99d4e528e752afa3df0c3575) remove File extension for backlink tracking in favour of UsedOnTable form field (Christopher Joe) + * 2018-02-08 [5f0a7cc](https://github.com/silverstripe/silverstripe-asset-admin/commit/5f0a7ccce69a6a7d9c354709654a463405c1ff0d) add a Usage tab showing owners of files (Christopher Joe) + * 2018-02-08 [c370e3c](https://github.com/silverstripe/silverstripe-admin/commit/c370e3cfdf3171f3b7f2042d0ded5af3619eb5f2) Add a used-on table component for recorded ownerships (Christopher Joe) + * 2018-02-07 [dd82820](https://github.com/silverstripe/silverstripe-framework/commit/dd8282024232eb849693e68320e0f0b6bb02bab5) Allow GridFieldConfig::addComponents to accept an array (#7844) (Robbie Averill) + * 2018-02-07 [b084fe8](https://github.com/silverstripe/silverstripe-cms/commit/b084fe817779b7abd74583c5ddb93f6f7a53d48d) Convert page history notice to use Bootstrap 4 info alert (Robbie Averill) + * 2017-11-30 [9103816](https://github.com/silverstripe/silverstripe-framework/commit/9103816333e790a9b7cd84994e00e0941e34de39) Add php 7.2 support (Daniel Hensby) + * 2017-09-26 [2c121e8](https://github.com/silverstripe/silverstripe-framework/commit/2c121e8a07297baa89c0bedad086b1a62de2ba0a) approach (Daniel Hensby) + +### Bugfixes + + * 2018-07-20 [78adff9](https://github.com/silverstripe/silverstripe-errorpage/commit/78adff9a99c8778953f891e24f5f818335cea359) Build Static error page from live URL (Maxime Rainville) + * 2018-07-18 [74b655d](https://github.com/silverstripe/silverstripe-framework/commit/74b655d3fcb28db877d839c75f2f3f98b41448cd) tests on unset session data (Ingo Schommer) + * 2018-07-18 [76ac846](https://github.com/silverstripe/silverstripe-framework/commit/76ac8465de768ab08c904e0e8c6075a9f4ceb56b) Lazy session state (fixes #8267) (Ingo Schommer) + * 2018-07-14 [e37b3b9](https://github.com/silverstripe/silverstripe-framework/commit/e37b3b95f4e02eaa8bccd8acc30cdc001d7239c5) updateValidatePassword calls need to be masked from backtraces (Daniel Hensby) + * 2018-07-05 [cebed77](https://github.com/silverstripe/silverstripe-framework/commit/cebed776ab93e8adfbb437dc0b1448ced9f71215) If theres a max-age set remove no-cache and no-store (Daniel Hensby) + * 2018-07-05 [2b1c55b](https://github.com/silverstripe/silverstripe-framework/commit/2b1c55bc4e57d43490973452c8cc773a1a24c8ee) Allow setNoCache(false) to remove no-cache directive (Daniel Hensby) + * 2018-07-05 [842b39e](https://github.com/silverstripe/silverstripe-framework/commit/842b39e9884aefb91866cbcb4943f6aeae586d8b) Add must-revalidate to default state so its common on all our core states (Daniel Hensby) + * 2018-07-05 [997730a](https://github.com/silverstripe/silverstripe-framework/commit/997730aa7f507289da843974a386d0340842c14a) Allow cache control changes to affect default state (Daniel Hensby) + * 2018-07-04 [bde3121](https://github.com/silverstripe/silverstripe-framework/commit/bde3121a33c893eca3bdf2ffff349d1bf15a3e06) Remove X-Requested-With from default Vary header (Sam Minnee) + * 2018-06-26 [6e1c7c2](https://github.com/silverstripe/silverstripe-framework/commit/6e1c7c2781fe8c30806f628038471faba072bb51) remove personal information from password reset confirmation screen (Daniel Hensby) + * 2018-06-21 [793aafa](https://github.com/silverstripe/silverstripe-framework/commit/793aafae917aa190e711ae5c7a18ed59c2812634) Transaction depth should error if not implemented by child classes (Daniel Hensby) + * 2018-06-18 [c77042a](https://github.com/silverstripe/silverstripe-framework/commit/c77042aa8b5ba70f4c9dc6924d2ccccf3d5588a5) linting. (Maxime Rainville) + * 2018-06-18 [b78a89a](https://github.com/silverstripe/silverstripe-framework/commit/b78a89a76cd1c4596a159b8a5043517407de89bc) Default cache state should be `no-cache` (Daniel Hensby) + * 2018-06-18 [225e61d](https://github.com/silverstripe/silverstripe-framework/commit/225e61dc67f5ab550d41c42fa8c80b50b71db5b1) FIx manual resetDBSchema() calls breaking the database (Damian Mooyman) + * 2018-06-18 [11e0a3d](https://github.com/silverstripe/silverstripe-framework/commit/11e0a3de43d7e05b7df63d52b0238e58baff3c2a) Ensure that build includes extra classes (Damian Mooyman) + * 2018-06-15 [3fa2c05](https://github.com/silverstripe/silverstripe-framework/commit/3fa2c056d71a3286ce83017d7cafc180cee076c2) Don't reload form session data using FormField::setSubmittedValue (#8056) (Maxime Rainville) + * 2018-06-15 [74ef975](https://github.com/silverstripe/silverstripe-assets/commit/74ef975459a7f4779c2f4a91c9e277be0ffd81e0) mark legacy migration test as skipped temporarily (Damian Mooyman) + * 2018-06-15 [e70e46e](https://github.com/silverstripe/silverstripe-graphql/commit/e70e46e21628e17f56f542e910379cc2713dfe8c) Fix missing .graphql file category (Damian Mooyman) + * 2018-06-15 [8f7893f](https://github.com/silverstripe/silverstripe-versioned/commit/8f7893fe9af856a712952a8e992a79a24fade84b) Fix unit tests for 4.2 core regressions (Damian Mooyman) + * 2018-06-14 [d52c4dd](https://github.com/silverstripe/silverstripe-framework/commit/d52c4dd602929db5b37cf33261b41c0d3d2098bc) Make regression in #7839 safer (Damian Mooyman) + * 2018-06-14 [acc8d48](https://github.com/silverstripe/silverstripe-framework/commit/acc8d48b11890ec8a4f725e8e330ea56950a43d6) SapphireTest can load relative fixtures in subfolders, switch "needs db" priority check order (Robbie Averill) + * 2018-06-13 [9274692](https://github.com/silverstripe/silverstripe-framework/commit/92746924159e9667429d0fa5ee8c7d5ed34ea514) core tests (Damian Mooyman) + * 2018-06-13 [59ba208](https://github.com/silverstripe/silverstripe-framework/commit/59ba208df010e6a26afeed8c650a5b705ea9361b) HTTPTest (Damian Mooyman) + * 2018-06-13 [6f32762](https://github.com/silverstripe/silverstripe-framework/commit/6f32762268941ba42ad2a129c67172e51cb1a4d3) unit tests (Damian Mooyman) + * 2018-06-13 [aa1ba0e](https://github.com/silverstripe/silverstripe-framework/commit/aa1ba0ef90109fe084a8d736ed4ca08b32a4f40b) inverted condition (Damian Mooyman) + * 2018-06-12 [befd81d](https://github.com/silverstripe/silverstripe-framework/commit/befd81d0c238125b8b82dc8af69b58a1e639a6c4) Bug with forms being cached (Daniel Hensby) + * 2018-06-12 [7c87591](https://github.com/silverstripe/silverstripe-framework/commit/7c875918c7bc48d615b24bc959f659afbad7883b) make sure we create ETags from the body, not the request (Daniel Hensby) + * 2018-06-10 [d842225](https://github.com/silverstripe/silverstripe-framework/commit/d842225df6df2eef1dd6ebd592120ffa5b344c11) Codesniffer style violations with comments (Robbie Averill) + * 2018-06-09 [4e6f45c](https://github.com/silverstripe/silverstripe-framework/commit/4e6f45c2ea4cb3d2349a97a231edaa023f2af219) updateCMSFields example (Juan Molina) + * 2018-06-06 [31ad3cd](https://github.com/silverstripe/silverstripe-framework/commit/31ad3cdaab9623230ce6bdad4e53f6a9a6c9baba) Allow buttons to opt out of display (#8113) (Aaron Carlino) + * 2018-06-05 [bf07ba3](https://github.com/silverstripe/silverstripe-framework/commit/bf07ba30f4b11e1ddc26be50b0366d281bcd4967) Make error messages available to extensions (Jonathon Menz) + * 2018-06-01 [a9e2af6](https://github.com/silverstripe/silverstripe-installer/commit/a9e2af6863c2b32cddf417e43a228a743b3fade7) Remove incorrect classmap for Page and PageController (Robbie Averill) + * 2018-05-31 [4b3e76a](https://github.com/silverstripe/silverstripe-framework/commit/4b3e76a976a11703f2d269d225874269ac2e37a3) missing braces (Aaron Carlino) + * 2018-05-30 [d1af098](https://github.com/silverstripe/silverstripe-versioned/commit/d1af098796dd647023adbde53d5752f14744fd9c) linting (Aaron Carlino) + * 2018-05-16 [396ac65](https://github.com/silverstripe/silverstripe-asset-admin/commit/396ac65106ce1c98e24a43fc11ded2fa54af9455) es per flameohr (Aaron Carlino) + * 2018-05-15 [11c85c6](https://github.com/silverstripe/silverstripe-asset-admin/commit/11c85c63ee8792e443511f2f6582a586bf30ab60) Infinite render loop due to unchecked setState, incorrect binding of handleDrop() (Aaron Carlino) + * 2018-05-10 [e22d1ec](https://github.com/silverstripe/silverstripe-asset-admin/commit/e22d1ec1769e0673f6e7e076c20db506c08a69ec) behat test (Aaron Carlino) + * 2018-05-10 [1993454](https://github.com/silverstripe/silverstripe-asset-admin/commit/19934542d901339625c5b2da5a82d94a4ccbae17) broken delete close editor (Aaron Carlino) + * 2018-05-10 [553ab92](https://github.com/silverstripe/silverstripe-asset-admin/commit/553ab92b85df7fc679294adb32b58aad47c85340) checkbox select behat step (Aaron Carlino) + * 2018-04-30 [abb1011](https://github.com/silverstripe/silverstripe-admin/commit/abb10117a5ea9fa9b8b6322da5ae8f9288d68a38) Expose Badge in injector registrations (Robbie Averill) + * 2018-04-30 [1d3e838](https://github.com/silverstripe/silverstripe-framework/commit/1d3e83838d22744ed51e50469d38c2a0559e893c) phpdoc for relation method (namespace) (JorisDebonnet) + * 2018-04-20 [3f5c3ec](https://github.com/silverstripe/silverstripe-versioned/commit/3f5c3ec9a2be7788cf4f204dff859977860a7104) Fix graphql regressions (Damian Mooyman) + * 2018-04-19 [9727052](https://github.com/silverstripe/silverstripe-versioned/commit/97270527aba45cdf598179fd985dfa112bdb4a1c) Ensure that nested rollbacks don't reset root version (Damian Mooyman) + * 2018-04-18 [4585b0b](https://github.com/silverstripe/silverstripe-framework/commit/4585b0b3828e3574f62aed71e07505a42cbd6893) a broken link to the versioning page. (Maxime Rainville) + * 2018-04-18 [a71b821](https://github.com/silverstripe/silverstripe-asset-admin/commit/a71b8219ba22939adfffccd1cb437b2f47910104) behat test (Damian Mooyman) + * 2018-04-17 [57b006c](https://github.com/silverstripe/silverstripe-campaign-admin/commit/57b006cf2eafdfe2427a914ff4a337327c0550c3) Re-implement flexbox classes for preview window (Robbie Averill) + * 2018-04-16 [0d40b54](https://github.com/silverstripe/silverstripe-cms/commit/0d40b54537664a09dd232ba5fb44cb0cf24f7c82) FIx rollback button from not working (Damian Mooyman) + * 2018-04-16 [02d7989](https://github.com/silverstripe/silverstripe-admin/commit/02d79897edff9f6a17a2657d82b4347bf2d6821d) Ensure FormBuilder loading indicator has a minimum height of the image (Robbie Averill) + * 2018-04-14 [dfa0915](https://github.com/silverstripe/silverstripe-admin/commit/dfa0915e858aec6d643c188aab2df000ba4f1e11) Make 'id' a required prop (Raissa North) + * 2018-04-12 [d21e03d](https://github.com/silverstripe/silverstripe-framework/commit/d21e03d4ed6d8c1c2f60caec183c9301a493afd6) branch alias (Damian Mooyman) + * 2018-04-11 [51173a7](https://github.com/silverstripe/silverstripe-siteconfig/commit/51173a727c016fcfc3d528577bf747324a133ba9) Fixture the 'app' dir instead of mysite in unit tests (Robbie Averill) + * 2018-04-11 [5da708d](https://github.com/silverstripe/silverstripe-cms/commit/5da708d223723f91ffe47672846a3967c52a5326) Fixture the 'app' dir instead of mysite in unit tests (Robbie Averill) + * 2018-04-11 [1041b65](https://github.com/silverstripe/silverstripe-admin/commit/1041b6542ee8ad09714d79b9b6dbdd676735f06e) Add loading indicator for FormBuilderLoader (#481) (Luke Edwards) + * 2018-04-10 [80e0f4d](https://github.com/silverstripe/silverstripe-asset-admin/commit/80e0f4db26e93241ee4a95bfde1c823abe3a5202) File modified indicator missing and draft indicator incorrect style (Luke Edwards) + * 2018-04-09 [79e4f9c](https://github.com/silverstripe/silverstripe-admin/commit/79e4f9cbed4ae230d322ed0fbe1f25f11bed856f) Don't add redundant href="#" to tabs (Damian Mooyman) + * 2018-04-09 [f569785](https://github.com/silverstripe/silverstripe-admin/commit/f56978577b96c525796e3577a9a2eecef32e0a6d) gridfield style issues with negative margins (#474) (Luke Edwards) + * 2018-04-06 [be8287f](https://github.com/silverstripe/silverstripe-framework/commit/be8287fef87244f98b0cbdfd89933bd1ec6754f6) Prevent failover / extensions interfering with composite field properties (#7988) (Damian Mooyman) + * 2018-04-05 [e15a5af](https://github.com/silverstripe/silverstripe-campaign-admin/commit/e15a5afed389a8081d66be96b3511dc64a6721c8) Fix gridfield being cut off on sides (Luke Edwards) + * 2018-04-04 [85f4e65](https://github.com/silverstripe/silverstripe-versioned/commit/85f4e65f8f5ca4eb509837473cbca60f974176d6) Ensure extra fields have correct casting (Damian Mooyman) + * 2018-04-04 [b127422](https://github.com/silverstripe/silverstripe-assets/commit/b1274224d9aeea7d9e14e55d9aaf52aa9f17f26d) linting (Aaron Carlino) + * 2018-04-04 [50ad0ad](https://github.com/silverstripe/silverstripe-asset-admin/commit/50ad0ad15b548d0f23aa41d5bc29972ed9ebeeb8) Fix double popup for unpublish and incorrect confirm (#758) (Damian Mooyman) + * 2018-03-29 [ccbbcd4](https://github.com/silverstripe/silverstripe-framework/commit/ccbbcd45a223ffaba4ab33a4fce0375952c20c1d) Fixed bug in config merging priorities so that config values set by extensions are now least important instead of most important (Daniel Hensby) + * 2018-03-28 [878dc1f](https://github.com/silverstripe/silverstripe-framework/commit/878dc1f86dadbaf501298511177517360ded980e) Change inverse polymorphic inference to silent-empty instead of explicit error (Damian Mooyman) + * 2018-03-27 [484e0a7](https://github.com/silverstripe/silverstripe-versioned/commit/484e0a7f90cb00e77ca32f30554471a8a3dc4461) Ensure polymorphic ownership works (Damian Mooyman) + * 2018-03-27 [9cb974c](https://github.com/silverstripe/silverstripe-framework/commit/9cb974c61925fe3bb1283c179a421e3e97ccdd4b) several mistakes in example code (Aaron Carlino) + * 2018-03-27 [ec37e67](https://github.com/silverstripe/silverstripe-versioned/commit/ec37e6796068f725366310192c63949c1cf366a7) Don't crash on polymorphic ownership (Damian Mooyman) + * 2018-03-27 [e35971b](https://github.com/silverstripe/silverstripe-assets/commit/e35971b32f92452398e092c459ffbd71b329c83c) revert to live (Damian Mooyman) + * 2018-03-22 [436d473](https://github.com/silverstripe/silverstripe-versioned/commit/436d4734d056c5c43a2a08e3bc9a23fb793926e1) Fix regression in canViewVersioned (Damian Mooyman) + * 2018-03-21 [ba94e02](https://github.com/silverstripe/silverstripe-framework/commit/ba94e020e7deadd5deb9b4c6744f72bec37453b3) FunctionalTest not setting persistent versioned mode (Damian Mooyman) + * 2018-03-20 [e277f19](https://github.com/silverstripe/silverstripe-graphql/commit/e277f198d6aba0a9929ba79f5bfb198495d59c6e) merge regressions (Damian Mooyman) + * 2018-03-15 [8568259](https://github.com/silverstripe/silverstripe-admin/commit/8568259061e5bb5f7523b82521355f7e1802e812) id for scaffolded objects (Christopher Joe) + * 2018-03-15 [b2e2a6b](https://github.com/silverstripe/silverstripe-admin/commit/b2e2a6b5133d526c8ac9e54beec264118b2b56a5) bugs with execution (Christopher Joe) + * 2018-03-15 [61ce477](https://github.com/silverstripe/silverstripe-framework/commit/61ce4771f91367cbb4b8a1bf61e2af51964714df) ing HTMLEditorField API documentation (3Dgoo) + * 2018-03-14 [23af7ea](https://github.com/silverstripe/silverstripe-versioned/commit/23af7ea302a654ec21114fef8ccc4eb0eb09b355) Ensure consistent strict equality checks in version numbers (Robbie Averill) + * 2018-03-14 [97f22cb](https://github.com/silverstripe/silverstripe-framework/commit/97f22cbaa5d683cca2f65370a9b827314317436d) ing FormAction API documentation (3Dgoo) + * 2018-03-13 [b06bcc2](https://github.com/silverstripe/silverstripe-versioned/commit/b06bcc24fded9e4718b4c8b182df5d3ed7eb1047) Fix regressions from testing cms unit tests (Damian Mooyman) + * 2018-03-13 [e3e5edb](https://github.com/silverstripe/silverstripe-admin/commit/e3e5edbf4856cbc2eb71a89b600a8542aeba5b34) "container" logic for FormBuilder and PopoverField, improves accessibility (#459) (Chris Joe) + * 2018-03-11 [6f18e35](https://github.com/silverstripe/silverstripe-versioned/commit/6f18e3596fa20720e523f9b2e670e1d5a418d920) sorting issue with versioned test (Damian Mooyman) + * 2018-03-09 [9bed12b](https://github.com/silverstripe/silverstripe-versioned/commit/9bed12b8b696ce849e9eaff72a9ff14f8d9688af) Fix issue in latest_version (Damian Mooyman) + * 2018-03-08 [2e43291](https://github.com/silverstripe/silverstripe-versioned/commit/2e432910fb0d161919474491e3d837641a4bce32) Fix archive date erroring if stage omitted (Damian Mooyman) + * 2018-03-08 [3e698c1](https://github.com/silverstripe/silverstripe-versioned/commit/3e698c1bd0690027bd6cd0082a51b3c1d79974e1) remaining tests (Damian Mooyman) + * 2018-03-08 [be0b274](https://github.com/silverstripe/silverstripe-versioned/commit/be0b2743917a1d064acde3989b3ac89a3a2ae727) testDeleteNestedOwnedWithoutRepublishingOwner (Damian Mooyman) + * 2018-03-08 [0be2a9d](https://github.com/silverstripe/silverstripe-versioned/commit/0be2a9dae588ecee332f9c183ae8bc8a92e5b53b) Fix WasDraft not being written (Damian Mooyman) + * 2018-03-08 [35cff90](https://github.com/silverstripe/silverstripe-versioned/commit/35cff907607cc02def1f53e9a238b99d39ba471b) missing date for deleted _Versioned rows (Damian Mooyman) + * 2018-03-08 [3454600](https://github.com/silverstripe/silverstripe-versioned/commit/3454600ea499741591324bf072876f982d8ae62c) archive / unpublish / delete creating deleted row (Damian Mooyman) + * 2018-03-08 [e839d10](https://github.com/silverstripe/silverstripe-asset-admin/commit/e839d100bd02d7c12c9529a517bbea2a386d802e) responsive modals (#744) (Chris Joe) + * 2018-03-08 [38fbb92](https://github.com/silverstripe/silverstripe-admin/commit/38fbb92decdc4fbbe4c55e6647c68e852ae4a2ac) responsive modals (#457) (Chris Joe) + * 2018-03-07 [ff78a3b](https://github.com/silverstripe/silverstripe-versioned/commit/ff78a3bffc30a6fbc5ee2684718188e817eab506) up testDeleteOwnedWithoutRepublishingOwner (Damian Mooyman) + * 2018-03-07 [640c8f7](https://github.com/silverstripe/silverstripe-versioned/commit/640c8f7b3f56aa958ae462f4195bdd7121c37e97) postgres issue (Damian Mooyman) + * 2018-03-06 [4f8a10d](https://github.com/silverstripe/silverstripe-versioned/commit/4f8a10dd3132f5b2126ee8f888ab504c8aaefd22) invalid joins (Damian Mooyman) + * 2018-03-06 [75168cf](https://github.com/silverstripe/silverstripe-asset-admin/commit/75168cf2fa0fcf1ffc649e67b8ee3c87a675b3ed) multi-select shouldn't show insert button on form (Christopher Joe) + * 2018-03-06 [67fa8e9](https://github.com/silverstripe/silverstripe-admin/commit/67fa8e923ac4a3c5ef457597402c96c21dcd65b1) aligns Form actions container with the rest of the "bottom bars" (Christopher Joe) + * 2018-03-06 [36b6b30](https://github.com/silverstripe/silverstripe-asset-admin/commit/36b6b3025119875110ffb4719591026f78cd123f) up progress indicators (Damian Mooyman) + * 2018-03-05 [c209aff](https://github.com/silverstripe/silverstripe-asset-admin/commit/c209afff96343899c38cc07ba788f9c0deb7fa93) Consolidate queued and read files for Gallery (Christopher Joe) + * 2018-03-01 [49a3970](https://github.com/silverstripe/silverstripe-versioned/commit/49a3970c8722236bf51c0bded8c20d32b2fc4d9a) Resolve Published version field to Versioned_Version::Published() correctly (#125) (Robbie Averill) + * 2018-03-01 [6523d7a](https://github.com/silverstripe/silverstripe-framework/commit/6523d7a6eb3905d5e3cf24120d33232e1eb5d789) ing HTMLEditorField API documentation (3Dgoo) + * 2018-03-01 [c96b6dc](https://github.com/silverstripe/silverstripe-asset-admin/commit/c96b6dceecfa1846990cd14b8cf6250a7879ca02) aesthetic annoyance where deselect+select code will make the bulk actions animate unnecessarily (Christopher Joe) + * 2018-03-01 [222eec8](https://github.com/silverstripe/silverstripe-versioned/commit/222eec81944ebcd2e3a7ad302f8ef8ff65f5e344) Add missing published state filter (Robbie Averill) + * 2018-02-27 [c755f77](https://github.com/silverstripe/silverstripe-framework/commit/c755f77288bcbd5e6777f94d8499264446b456f0) indentation (Aaron Carlino) + * 2018-02-27 [6274ccc](https://github.com/silverstripe/silverstripe-campaign-admin/commit/6274ccce2ee3d66fb361a6335295d3a3eb2dbfe9) behat failure (Aaron Carlino) + * 2018-02-27 [7677c68](https://github.com/silverstripe/silverstripe-admin/commit/7677c68b6b354910c1964f833a0df765daa4e346) Remove max-width from form-fields and items (#446) (Chris Joe) + * 2018-02-27 [efe5c0f](https://github.com/silverstripe/silverstripe-admin/commit/efe5c0f3d251286b27c73ededc68305230c10184) fileSchema abolishing actions from previous props (Christopher Joe) + * 2018-02-21 [0647dee](https://github.com/silverstripe/silverstripe-cms/commit/0647dee8f0e09a23046c1a556e6acb7f6185bf08) travis (Daniel Hensby) + * 2018-02-14 [d019f88](https://github.com/silverstripe/silverstripe-admin/commit/d019f8875aa1e7f42035529ee6de450a20bd1bec) php field to fallback to the form's record and added logic to handle no record available (or not saved) (Christopher Joe) + * 2018-02-13 [42fd4d6](https://github.com/silverstripe/silverstripe-admin/commit/42fd4d6218102042d26607c968c2a773ea093751) Fix display logic for password fields (Damian Mooyman) + * 2018-02-12 [6570599](https://github.com/silverstripe/silverstripe-framework/commit/6570599aa931224e65797d13b74714c44180a9e4) Fix incorrect display logic on password field (Damian Mooyman) + * 2018-02-07 [b5f68eb](https://github.com/silverstripe/silverstripe-cms/commit/b5f68ebed2e7dda145a5eff55b67402f03d6f88f) warning appearing when button is disabled for rollback (Christopher Joe) + * 2018-02-07 [1983200](https://github.com/silverstripe/silverstripe-framework/commit/19832000a53dc57cbd2029502d9064816e0508b1) Fix installer checking wrong location for files (Damian Mooyman) + * 2018-02-02 [1d17f40](https://github.com/silverstripe/silverstripe-graphql/commit/1d17f40f4a587598fb01351df7cd181efb7b05a3) travis builds (Christopher Joe) + * 2017-12-01 [74a3ba5](https://github.com/silverstripe/silverstripe-framework/commit/74a3ba54ae3f02158ba81622bd9933ae3e98c665) count size of $relations (Daniel Hensby)