From 0a64b07b2c25b39ec89dbbe1f8aaa213e9184386 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Thu, 13 Sep 2018 19:00:04 +0200 Subject: [PATCH 001/175] NEW Use Bootstrap alerts throughout the CMS --- templates/SilverStripe/Security/CMSSecurity_login.ss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/SilverStripe/Security/CMSSecurity_login.ss b/templates/SilverStripe/Security/CMSSecurity_login.ss index a4834f0b6..cd02e2032 100644 --- a/templates/SilverStripe/Security/CMSSecurity_login.ss +++ b/templates/SilverStripe/Security/CMSSecurity_login.ss @@ -7,7 +7,7 @@ <% with $Form %> <% if $Message %> -
+

$Message

<% end_if %> From 274657f4f815cfb990c23b39ab81c1def91b37ad Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Fri, 14 Sep 2018 11:19:53 +0200 Subject: [PATCH 002/175] FIX Add support in "I should see a message" step definition for Bootstrap alerts --- tests/behat/src/CmsUiContext.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/behat/src/CmsUiContext.php b/tests/behat/src/CmsUiContext.php index 209835615..aea6f2ea1 100644 --- a/tests/behat/src/CmsUiContext.php +++ b/tests/behat/src/CmsUiContext.php @@ -85,7 +85,13 @@ class CmsUiContext implements Context */ public function iShouldSeeAMessage($message) { - $this->getMainContext()->assertElementContains('.message', $message); + $page = $this->getMainContext()->getSession()->getPage(); + if ($page->find('css', '.message')) { + $this->getMainContext()->assertElementContains('.message', $message); + } else { + // Support for new Bootstrap alerts + $this->getMainContext()->assertElementContains('.alert', $message); + } } protected function getCmsTabsElement() From a6a17439976710b2311558d363b5467fa429dcca Mon Sep 17 00:00:00 2001 From: bergice Date: Tue, 16 Oct 2018 18:49:51 +1300 Subject: [PATCH 003/175] BUG: Fix `ENTER` not triggering form save button as `GridField`s used `submit` type buttons --- src/Forms/GridField/GridField_FormAction.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Forms/GridField/GridField_FormAction.php b/src/Forms/GridField/GridField_FormAction.php index 627ef8b87..10dc772bc 100644 --- a/src/Forms/GridField/GridField_FormAction.php +++ b/src/Forms/GridField/GridField_FormAction.php @@ -101,6 +101,7 @@ class GridField_FormAction extends FormAction // will strip it from the requests 'name' => 'action_gridFieldAlterAction' . '?' . http_build_query($actionData), 'data-url' => $this->gridField->Link(), + 'type' => "button", ) ); } From e211e27470ec4ff21ccc30656741458d5344df2c Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 14:27:57 +0200 Subject: [PATCH 004/175] Add more unit tests for DebugViewFriendlyErrorFormatter, tidy up Director::is_ajax() return --- src/Control/Director.php | 10 +-- .../DebugViewFriendlyErrorFormatter.php | 7 ++- .../DebugViewFriendlyErrorFormatterTest.php | 61 ++++++++++++++++++- 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/src/Control/Director.php b/src/Control/Director.php index 8b85e1fee..86491bd05 100644 --- a/src/Control/Director.php +++ b/src/Control/Director.php @@ -1004,12 +1004,12 @@ class Director implements TemplateGlobalProvider $request = self::currentRequest($request); if ($request) { return $request->isAjax(); - } else { - return ( - isset($_REQUEST['ajax']) || - (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest") - ); } + + return ( + isset($_REQUEST['ajax']) || + (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest") + ); } /** diff --git a/src/Logging/DebugViewFriendlyErrorFormatter.php b/src/Logging/DebugViewFriendlyErrorFormatter.php index fca78fcf4..7356076c4 100644 --- a/src/Logging/DebugViewFriendlyErrorFormatter.php +++ b/src/Logging/DebugViewFriendlyErrorFormatter.php @@ -97,7 +97,7 @@ class DebugViewFriendlyErrorFormatter implements FormatterInterface public function format(array $record) { // Get error code - $code = empty($record['code']) ? $this->statusCode : $record['code']; + $code = empty($record['code']) ? $this->getStatusCode() : $record['code']; return $this->output($code); } @@ -127,8 +127,9 @@ class DebugViewFriendlyErrorFormatter implements FormatterInterface $output = $renderer->renderHeader(); $output .= $renderer->renderInfo("Website Error", $this->getTitle(), $this->getBody()); - if (Email::config()->admin_email) { - $mailto = Email::obfuscate(Email::config()->admin_email); + $adminEmail = Email::config()->get('admin_email'); + if ($adminEmail) { + $mailto = Email::obfuscate($adminEmail); $output .= $renderer->renderParagraph('Contact an administrator: ' . $mailto . ''); } diff --git a/tests/php/Logging/DebugViewFriendlyErrorFormatterTest.php b/tests/php/Logging/DebugViewFriendlyErrorFormatterTest.php index 49d834457..04f2c64c1 100644 --- a/tests/php/Logging/DebugViewFriendlyErrorFormatterTest.php +++ b/tests/php/Logging/DebugViewFriendlyErrorFormatterTest.php @@ -2,18 +2,66 @@ namespace SilverStripe\Logging\Tests; +use PHPUnit_Framework_MockObject_MockObject; use SilverStripe\Control\Email\Email; +use SilverStripe\Control\HTTPRequest; +use SilverStripe\Core\Injector\Injector; +use SilverStripe\Dev\DebugView; use SilverStripe\Dev\SapphireTest; use SilverStripe\Logging\DebugViewFriendlyErrorFormatter; class DebugViewFriendlyErrorFormatterTest extends SapphireTest { - public function setUp() + protected function setUp() { parent::setUp(); Email::config()->set('admin_email', 'testy@mctest.face'); } + public function testFormatPassesRecordCodeToOutput() + { + /** @var DebugViewFriendlyErrorFormatter|PHPUnit_Framework_MockObject_MockObject $mock */ + $mock = $this->getMockBuilder(DebugViewFriendlyErrorFormatter::class) + ->setMethods(['output']) + ->getMock(); + + $mock->expects($this->once())->method('output')->with(403)->willReturn('foo'); + $this->assertSame('foo', $mock->format(['code' => 403])); + } + + public function testFormatPassesInstanceStatusCodeToOutputWhenNotProvidedByRecord() + { + /** @var DebugViewFriendlyErrorFormatter|PHPUnit_Framework_MockObject_MockObject $mock */ + $mock = $this->getMockBuilder(DebugViewFriendlyErrorFormatter::class) + ->setMethods(['output']) + ->getMock(); + + $mock->setStatusCode(404); + + $mock->expects($this->once())->method('output')->with(404)->willReturn('foo'); + $this->assertSame('foo', $mock->format(['notacode' => 'bar'])); + } + + public function testFormatBatch() + { + $records = [ + ['message' => 'bar'], + ['open' => 'sausage'], + ['horse' => 'caballo'], + ]; + + /** @var DebugViewFriendlyErrorFormatter|PHPUnit_Framework_MockObject_MockObject $mock */ + $mock = $this->getMockBuilder(DebugViewFriendlyErrorFormatter::class) + ->setMethods(['format']) + ->getMock(); + + $mock->expects($this->exactly(3)) + ->method('format') + ->willReturn('foo'); + + $this->assertSame('foofoofoo', $mock->formatBatch($records)); + } + public function testOutput() { $formatter = new DebugViewFriendlyErrorFormatter(); @@ -34,4 +82,15 @@ TEXT $this->assertEquals($expected, $formatter->output(404)); } + + public function testOutputReturnsTitleWhenRequestIsAjax() + { + // Mock an AJAX request + Injector::inst()->registerService(new HTTPRequest('GET', '', ['ajax' => true])); + + $formatter = new DebugViewFriendlyErrorFormatter(); + $formatter->setTitle('The Diary of Anne Frank'); + + $this->assertSame('The Diary of Anne Frank', $formatter->output(200)); + } } From 73df3166b7d3eac4a3b7b4600a6033f668b8ca77 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 14:30:15 +0200 Subject: [PATCH 005/175] Switch to short array syntax in DetailedErrorFormatter, add spaces between array values --- src/Logging/DetailedErrorFormatter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Logging/DetailedErrorFormatter.php b/src/Logging/DetailedErrorFormatter.php index 2d47bd999..4b845d5d6 100644 --- a/src/Logging/DetailedErrorFormatter.php +++ b/src/Logging/DetailedErrorFormatter.php @@ -25,7 +25,7 @@ class DetailedErrorFormatter implements FormatterInterface ); } else { $context = isset($record['context']) ? $record['context'] : $record; - foreach (array('code','message','file','line') as $key) { + foreach (['code', 'message', 'file', 'line'] as $key) { if (!isset($context[$key])) { $context[$key] = isset($record[$key]) ? $record[$key] : null; } @@ -57,7 +57,7 @@ class DetailedErrorFormatter implements FormatterInterface public function formatBatch(array $records) { - return implode("\n", array_map(array($this, 'format'), $records)); + return implode("\n", array_map([$this, 'format'], $records)); } /** From 2694a47c45a01ae5f7228a61f665d0b15f300b31 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 14:41:45 +0200 Subject: [PATCH 006/175] Add more tests for DetailedErrorFormatter --- .../DebugViewFriendlyErrorFormatterTest.php | 1 - .../Logging/DetailedErrorFormatterTest.php | 46 ++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/tests/php/Logging/DebugViewFriendlyErrorFormatterTest.php b/tests/php/Logging/DebugViewFriendlyErrorFormatterTest.php index 04f2c64c1..0b40bcf38 100644 --- a/tests/php/Logging/DebugViewFriendlyErrorFormatterTest.php +++ b/tests/php/Logging/DebugViewFriendlyErrorFormatterTest.php @@ -6,7 +6,6 @@ use PHPUnit_Framework_MockObject_MockObject; use SilverStripe\Control\Email\Email; use SilverStripe\Control\HTTPRequest; use SilverStripe\Core\Injector\Injector; -use SilverStripe\Dev\DebugView; use SilverStripe\Dev\SapphireTest; use SilverStripe\Logging\DebugViewFriendlyErrorFormatter; diff --git a/tests/php/Logging/DetailedErrorFormatterTest.php b/tests/php/Logging/DetailedErrorFormatterTest.php index 40f9b75a7..b36786f81 100644 --- a/tests/php/Logging/DetailedErrorFormatterTest.php +++ b/tests/php/Logging/DetailedErrorFormatterTest.php @@ -8,7 +8,7 @@ use SilverStripe\Logging\Tests\DetailedErrorFormatterTest\ErrorGenerator; class DetailedErrorFormatterTest extends SapphireTest { - public function testFormat() + public function testFormatWithException() { $generator = new ErrorGenerator(); $formatter = new DetailedErrorFormatter(); @@ -27,4 +27,48 @@ class DetailedErrorFormatterTest extends SapphireTest $output ); } + + public function testFormatWithoutException() + { + $record = [ + 'code' => 401, + 'message' => 'Denied', + 'file' => 'index.php', + 'line' => 4, + ]; + + $formatter = new DetailedErrorFormatter(); + $result = $formatter->format($record); + + $this->assertContains('ERRNO 401', $result, 'Status code was not found in trace'); + $this->assertContains('Denied', $result, 'Message was not found in trace'); + $this->assertContains('Line 4 in index.php', $result, 'Line or filename were not found in trace'); + $this->assertContains(self::class, $result, 'Backtrace doesn\'t show current test class'); + } + + public function testFormatBatch() + { + $records = [ + [ + 'code' => 401, + 'message' => 'Denied', + 'file' => 'index.php', + 'line' => 4, + ], + [ + 'code' => 404, + 'message' => 'Not found', + 'file' => 'admin.php', + 'line' => 7, + ], + ]; + + $formatter = new DetailedErrorFormatter(); + $result = $formatter->formatBatch($records); + + $this->assertContains('ERRNO 401', $result, 'First status code was not found in trace'); + $this->assertContains('ERRNO 404', $result, 'Second status code was not found in trace'); + $this->assertContains('Denied', $result, 'First message was not found in trace'); + $this->assertContains('Not found', $result, 'Second message was not found in trace'); + } } From 9911c9c9ef0d78398f31431964d58a3055679045 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 14:51:08 +0200 Subject: [PATCH 007/175] Use single quotes and getters over direct prop access in HTTPOutputHandler --- src/Logging/HTTPOutputHandler.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Logging/HTTPOutputHandler.php b/src/Logging/HTTPOutputHandler.php index 206039cd1..45f51c785 100644 --- a/src/Logging/HTTPOutputHandler.php +++ b/src/Logging/HTTPOutputHandler.php @@ -18,7 +18,7 @@ class HTTPOutputHandler extends AbstractProcessingHandler /** * @var string */ - private $contentType = "text/html"; + private $contentType = 'text/html'; /** * @var int @@ -155,8 +155,8 @@ class HTTPOutputHandler extends AbstractProcessingHandler // If headers have been sent then these won't be used, and may throw errors that we wont' want to see. if (!headers_sent()) { - $response->setStatusCode($this->statusCode); - $response->addHeader("Content-Type", $this->contentType); + $response->setStatusCode($this->getStatusCode()); + $response->addHeader('Content-Type', $this->getContentType()); } else { // To supress errors aboot errors $response->setStatusCode(200); @@ -165,6 +165,6 @@ class HTTPOutputHandler extends AbstractProcessingHandler $response->setBody($record['formatted']); $response->output(); - return false === $this->bubble; + return false === $this->getBubble(); } } From 5bd05a2deba610e67836c1f6f292ce02b6aa743a Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 14:51:53 +0200 Subject: [PATCH 008/175] Reduce setUp visibility and remove check for CLI - tests always run on CLI now --- tests/php/Logging/HTTPOutputHandlerTest.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/php/Logging/HTTPOutputHandlerTest.php b/tests/php/Logging/HTTPOutputHandlerTest.php index 352fdf13d..198361bf5 100644 --- a/tests/php/Logging/HTTPOutputHandlerTest.php +++ b/tests/php/Logging/HTTPOutputHandlerTest.php @@ -3,7 +3,6 @@ namespace SilverStripe\Logging\Tests; use Monolog\Handler\HandlerInterface; -use PhpParser\Node\Scalar\MagicConst\Dir; use SilverStripe\Control\Director; use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; @@ -13,14 +12,12 @@ use SilverStripe\Logging\HTTPOutputHandler; class HTTPOutputHandlerTest extends SapphireTest { - public function setUp() + protected function setUp() { parent::setUp(); - if (!Director::is_cli()) { - $this->markTestSkipped("This test only runs in CLI mode"); - } + if (!Director::isDev()) { - $this->markTestSkipped("This test only runs in dev mode"); + $this->markTestSkipped('This test only runs in dev mode'); } } From 3cdb73bd44376161c79e0ccd38394f52203458c7 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 15:00:08 +0200 Subject: [PATCH 009/175] NEW Add getLogger() to MonologErrorHandler and add test for exception without one --- src/Logging/MonologErrorHandler.php | 16 ++++++++++++++-- tests/php/Logging/MonologErrorHandlerTest.php | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 tests/php/Logging/MonologErrorHandlerTest.php diff --git a/src/Logging/MonologErrorHandler.php b/src/Logging/MonologErrorHandler.php index 8d2ccdb80..3b5e7727f 100644 --- a/src/Logging/MonologErrorHandler.php +++ b/src/Logging/MonologErrorHandler.php @@ -16,19 +16,31 @@ class MonologErrorHandler implements ErrorHandler * Set the PSR-3 logger to send errors & exceptions to * * @param LoggerInterface $logger + * @return $this */ public function setLogger(LoggerInterface $logger) { $this->logger = $logger; + return $this; + } + + /** + * Get the PSR-3 logger to send errors & exceptions to + * + * @return LoggerInterface + */ + public function getLogger() + { + return $this->logger; } public function start() { - if (!$this->logger) { + if (!$this->getLogger()) { throw new \InvalidArgumentException("No Logger property passed to MonologErrorHandler." . "Is your Injector config correct?"); } - MonologHandler::register($this->logger); + MonologHandler::register($this->getLogger()); } } diff --git a/tests/php/Logging/MonologErrorHandlerTest.php b/tests/php/Logging/MonologErrorHandlerTest.php new file mode 100644 index 000000000..c7ec4628e --- /dev/null +++ b/tests/php/Logging/MonologErrorHandlerTest.php @@ -0,0 +1,19 @@ +start(); + } +} From 60b375d99500284e71c2c60fbda583f4c4a9cd7b Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 15:18:34 +0200 Subject: [PATCH 010/175] Add more tests for CheckboxField_Readonly and CompositeField, improve PHPDocs --- src/Forms/CompositeField.php | 10 +- .../php/Forms/CheckboxField_ReadonlyTest.php | 17 ++ tests/php/Forms/CompositeFieldTest.php | 150 +++++++++++++++++- 3 files changed, 171 insertions(+), 6 deletions(-) create mode 100644 tests/php/Forms/CheckboxField_ReadonlyTest.php diff --git a/src/Forms/CompositeField.php b/src/Forms/CompositeField.php index e86b55fd8..e02a3c9df 100644 --- a/src/Forms/CompositeField.php +++ b/src/Forms/CompositeField.php @@ -21,11 +21,13 @@ class CompositeField extends FormField /** * Set to true when this field is a readonly field + * + * @var bool */ protected $readonly; /** - * @var $columnCount int Toggle different css-rendering for multiple columns + * @var int Toggle different css-rendering for multiple columns * ("onecolumn", "twocolumns", "threecolumns"). The content is determined * by the $children-array, so wrap all items you want to have grouped in a * column inside a CompositeField. @@ -35,12 +37,12 @@ class CompositeField extends FormField protected $columnCount = null; /** - * @var String custom HTML tag to render with, e.g. to produce a
. + * @var string custom HTML tag to render with, e.g. to produce a
. */ protected $tag = 'div'; /** - * @var String Optional description for this set of fields. + * @var string Optional description for this set of fields. * If the {@link $tag} property is set to use a 'fieldset', this will be * rendered as a tag, otherwise its a 'title' attribute. */ @@ -214,7 +216,7 @@ class CompositeField extends FormField 'tabindex' => null, 'type' => null, 'value' => null, - 'title' => ($this->tag == 'fieldset') ? null : $this->legend + 'title' => ($this->tag === 'fieldset') ? null : $this->legend ) ); } diff --git a/tests/php/Forms/CheckboxField_ReadonlyTest.php b/tests/php/Forms/CheckboxField_ReadonlyTest.php new file mode 100644 index 000000000..4996deda8 --- /dev/null +++ b/tests/php/Forms/CheckboxField_ReadonlyTest.php @@ -0,0 +1,17 @@ +performReadonlyTransformation(); + $this->assertInstanceOf(CheckboxField_Readonly::class, $result); + $this->assertNotSame($result, $field); + } +} diff --git a/tests/php/Forms/CompositeFieldTest.php b/tests/php/Forms/CompositeFieldTest.php index 6a4629fd8..382c339dc 100644 --- a/tests/php/Forms/CompositeFieldTest.php +++ b/tests/php/Forms/CompositeFieldTest.php @@ -2,13 +2,14 @@ namespace SilverStripe\Forms\Tests; +use PHPUnit_Framework_Error; use SilverStripe\Dev\CSSContentParser; use SilverStripe\Dev\SapphireTest; -use SilverStripe\Forms\FieldList; -use SilverStripe\Forms\TextField; use SilverStripe\Forms\CompositeField; use SilverStripe\Forms\DropdownField; +use SilverStripe\Forms\FieldList; use SilverStripe\Forms\RequiredFields; +use SilverStripe\Forms\TextField; class CompositeFieldTest extends SapphireTest { @@ -36,6 +37,9 @@ class CompositeFieldTest extends SapphireTest $this->assertEquals(0, $compositeOuter->fieldPosition('A')); $this->assertEquals(1, $compositeOuter->fieldPosition('AB')); $this->assertEquals(2, $compositeOuter->fieldPosition('B')); + + $this->assertFalse($compositeOuter->fieldPosition(null), 'Falsy input should return false'); + $this->assertFalse($compositeOuter->fieldPosition('FOO'), 'Non-exitent child should return false'); } public function testTag() @@ -124,4 +128,146 @@ class CompositeFieldTest extends SapphireTest $this->assertEquals($expectedChildren, $field->getChildren()); $this->assertEquals($field, $expectedChildren->getContainerField()); } + + public function testExtraClass() + { + $field = CompositeField::create(); + $field->setColumnCount(3); + $result = $field->extraClass(); + + $this->assertContains('field', $result, 'Default class was not added'); + $this->assertContains('CompositeField', $result, 'Default class was not added'); + $this->assertContains('multicolumn', $result, 'Multi column field did not have extra class added'); + } + + public function testGetAttributes() + { + $field = CompositeField::create(); + $field->setLegend('test'); + $result = $field->getAttributes(); + + $this->assertNull($result['tabindex']); + $this->assertNull($result['type']); + $this->assertNull($result['value']); + $this->assertSame('test', $result['title']); + } + + public function testGetAttributesReturnsEmptyTitleForFieldSets() + { + $field = CompositeField::create(); + $field->setLegend('not used'); + $field->setTag('fieldset'); + $result = $field->getAttributes(); + $this->assertNull($result['title']); + } + + /** + * @expectedException PHPUnit_Framework_Error + * @expectedExceptionMessageRegExp /a field called 'Test' appears twice in your form.*TextField.*TextField/ + */ + public function testCollateDataFieldsThrowsErrorOnDuplicateChildren() + { + $field = CompositeField::create( + TextField::create('Test'), + TextField::create('Test') + ); + + $list = []; + $field->collateDataFields($list); + } + + public function testCollateDataFieldsWithSaveableOnly() + { + $field = CompositeField::create( + TextField::create('Test') + ->setReadonly(false) + ->setDisabled(true) + ); + + $list = []; + $field->collateDataFields($list, true); + $this->assertEmpty($list, 'Unsaveable fields should not be collated when $saveableOnly = true'); + + $field->collateDataFields($list, false); + $this->assertNotEmpty($list, 'Unsavable fields should be collated when $saveableOnly = false'); + } + + public function testSetDisabledPropagatesToChildren() + { + $field = CompositeField::create( + $testField = TextField::create('Test') + ->setDisabled(false) + )->setDisabled(true); + $this->assertTrue($field->fieldByName('Test')->isDisabled(), 'Children should also be set to disabled'); + } + + public function testIsComposite() + { + $this->assertTrue(CompositeField::create()->isComposite()); + } + + public function testMakeFieldReadonlyPassedFieldName() + { + $field = CompositeField::create( + TextField::create('Test')->setDisabled(false) + ); + + $this->assertFalse($field->fieldByName('Test')->isReadonly()); + $this->assertTrue($field->makeFieldReadonly('Test'), 'makeFieldReadonly should return true'); + $this->assertTrue($field->fieldByName('Test')->isReadonly(), 'Named child field should be made readonly'); + } + + public function testMakeFieldReadonlyPassedFormField() + { + $field = CompositeField::create( + $testField = TextField::create('Test')->setDisabled(false) + ); + + $this->assertFalse($field->fieldByName('Test')->isReadonly()); + $this->assertTrue($field->makeFieldReadonly($testField), 'makeFieldReadonly should return true'); + $this->assertTrue($field->fieldByName('Test')->isReadonly(), 'Named child field should be made readonly'); + } + + public function testMakeFieldReadonlyWithNestedCompositeFields() + { + $field = CompositeField::create( + CompositeField::create( + TextField::create('Test')->setDisabled(false) + ) + ); + + $this->assertFalse($field->getChildren()->first()->fieldByName('Test')->isReadonly()); + $this->assertTrue($field->makeFieldReadonly('Test'), 'makeFieldReadonly should return true'); + $this->assertTrue( + $field->getChildren()->first()->fieldByName('Test')->isReadonly(), + 'Named child field should be made readonly' + ); + } + + public function testMakeFieldReadonlyReturnsFalseWhenFieldNotFound() + { + $field = CompositeField::create( + CompositeField::create( + TextField::create('Test') + ) + ); + + $this->assertFalse( + $field->makeFieldReadonly('NonExistent'), + 'makeFieldReadonly should return false when field is not found' + ); + } + + public function testDebug() + { + $field = new CompositeField( + new TextField('TestTextField') + ); + $field->setName('TestComposite'); + + $result = $field->debug(); + $this->assertContains(CompositeField::class . ' (TestComposite)', $result); + $this->assertContains('TestTextField', $result); + $this->assertContains(''); + } } From c418ee2915634ba4277688589a1ecf1d89fa6fba Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 16:34:36 +0200 Subject: [PATCH 011/175] NEW Add getters and setters for public properties in ConfirmPasswordField, add tests Some of the validation parts of ConfirmPasswordField are previously untested, this adds tests --- src/Forms/ConfirmedPasswordField.php | 168 +++++++++++++----- .../php/Forms/ConfirmedPasswordFieldTest.php | 155 +++++++++++++--- 2 files changed, 256 insertions(+), 67 deletions(-) diff --git a/src/Forms/ConfirmedPasswordField.php b/src/Forms/ConfirmedPasswordField.php index 0b14bd037..e4b3840ec 100644 --- a/src/Forms/ConfirmedPasswordField.php +++ b/src/Forms/ConfirmedPasswordField.php @@ -140,12 +140,12 @@ class ConfirmedPasswordField extends FormField $title = isset($title) ? $title : _t('SilverStripe\\Security\\Member.PASSWORD', 'Password'); // naming with underscores to prevent values from actually being saved somewhere - $this->children = new FieldList( - $this->passwordField = new PasswordField( + $this->children = FieldList::create( + $this->passwordField = PasswordField::create( "{$name}[_Password]", $title ), - $this->confirmPasswordfield = new PasswordField( + $this->confirmPasswordfield = PasswordField::create( "{$name}[_ConfirmPassword]", (isset($titleConfirmField)) ? $titleConfirmField : _t('SilverStripe\\Security\\Member.CONFIRMPASSWORD', 'Confirm Password') ) @@ -153,11 +153,11 @@ class ConfirmedPasswordField extends FormField // has to be called in constructor because Field() isn't triggered upon saving the instance if ($showOnClick) { - $this->children->push($this->hiddenField = new HiddenField("{$name}[_PasswordFieldVisible]")); + $this->getChildren()->push($this->hiddenField = HiddenField::create("{$name}[_PasswordFieldVisible]")); } // disable auto complete - foreach ($this->children as $child) { + foreach ($this->getChildren() as $child) { /** @var FormField $child */ $child->setAttribute('autocomplete', 'off'); } @@ -176,8 +176,8 @@ class ConfirmedPasswordField extends FormField public function setTitle($title) { - parent::setTitle($title); - $this->passwordField->setTitle($title); + $this->getPasswordField()->setTitle($title); + return parent::setTitle($title); } /** @@ -189,7 +189,7 @@ class ConfirmedPasswordField extends FormField { // Build inner content $fieldContent = ''; - foreach ($this->children as $field) { + foreach ($this->getChildren() as $field) { /** @var FormField $field */ $field->setDisabled($this->isDisabled()); $field->setReadonly($this->isReadonly()); @@ -207,8 +207,8 @@ class ConfirmedPasswordField extends FormField return $fieldContent; } - if ($this->showOnClickTitle) { - $title = $this->showOnClickTitle; + if ($this->getShowOnClickTitle()) { + $title = $this->getShowOnClickTitle(); } else { $title = _t( __CLASS__ . '.SHOWONCLICKTITLE', @@ -286,11 +286,11 @@ class ConfirmedPasswordField extends FormField /** * @param string $title * - * @return ConfirmedPasswordField + * @return $this */ public function setRightTitle($title) { - foreach ($this->children as $field) { + foreach ($this->getChildren() as $field) { /** @var FormField $field */ $field->setRightTitle($title); } @@ -310,8 +310,8 @@ class ConfirmedPasswordField extends FormField public function setChildrenTitles($titles) { $expectedChildren = $this->getRequireExistingPassword() ? 3 : 2; - if (is_array($titles) && count($titles) == $expectedChildren) { - foreach ($this->children as $field) { + if (is_array($titles) && count($titles) === $expectedChildren) { + foreach ($this->getChildren() as $field) { if (isset($titles[0])) { /** @var FormField $field */ $field->setTitle($titles[0]); @@ -350,7 +350,7 @@ class ConfirmedPasswordField extends FormField : null; if ($this->showOnClick && isset($value['_PasswordFieldVisible'])) { - $this->children->fieldByName($this->getName() . '[_PasswordFieldVisible]') + $this->getChildren()->fieldByName($this->getName() . '[_PasswordFieldVisible]') ->setValue($value['_PasswordFieldVisible']); } } else { @@ -362,10 +362,10 @@ class ConfirmedPasswordField extends FormField //looking up field by name is expensive, so lets check it needs to change if ($oldValue != $this->value) { - $this->children->fieldByName($this->getName() . '[_Password]') + $this->getChildren()->fieldByName($this->getName() . '[_Password]') ->setValue($this->value); - $this->children->fieldByName($this->getName() . '[_ConfirmPassword]') + $this->getChildren()->fieldByName($this->getName() . '[_ConfirmPassword]') ->setValue($this->confirmValue); } @@ -380,8 +380,8 @@ class ConfirmedPasswordField extends FormField */ public function setName($name) { - $this->passwordField->setName($name . '[_Password]'); - $this->confirmPasswordfield->setName($name . '[_ConfirmPassword]'); + $this->getPasswordField()->setName($name . '[_Password]'); + $this->getConfirmPasswordField()->setName($name . '[_ConfirmPassword]'); if ($this->hiddenField) { $this->hiddenField->setName($name . '[_PasswordFieldVisible]'); } @@ -417,12 +417,12 @@ class ConfirmedPasswordField extends FormField return true; } - $this->passwordField->setValue($this->value); - $this->confirmPasswordfield->setValue($this->confirmValue); - $value = $this->passwordField->Value(); + $this->getPasswordField()->setValue($this->value); + $this->getConfirmPasswordField()->setValue($this->confirmValue); + $value = $this->getPasswordField()->Value(); // both password-fields should be the same - if ($value != $this->confirmPasswordfield->Value()) { + if ($value != $this->getConfirmPasswordField()->Value()) { $validator->validationError( $name, _t('SilverStripe\\Forms\\Form.VALIDATIONPASSWORDSDONTMATCH', "Passwords don't match"), @@ -434,7 +434,7 @@ class ConfirmedPasswordField extends FormField if (!$this->canBeEmpty) { // both password-fields shouldn't be empty - if (!$value || !$this->confirmPasswordfield->Value()) { + if (!$value || !$this->getConfirmPasswordField()->Value()) { $validator->validationError( $name, _t('SilverStripe\\Forms\\Form.VALIDATIONPASSWORDSNOTEMPTY', "Passwords can't be empty"), @@ -446,29 +446,31 @@ class ConfirmedPasswordField extends FormField } // lengths - if (($this->minLength || $this->maxLength)) { + $minLength = $this->getMinLength(); + $maxLength = $this->getMaxLength(); + if ($minLength || $maxLength) { $errorMsg = null; $limit = null; - if ($this->minLength && $this->maxLength) { - $limit = "{{$this->minLength},{$this->maxLength}}"; + if ($minLength && $maxLength) { + $limit = "{{$minLength},{$maxLength}}"; $errorMsg = _t( - 'SilverStripe\\Forms\\ConfirmedPasswordField.BETWEEN', + __CLASS__ . '.BETWEEN', 'Passwords must be {min} to {max} characters long.', - array('min' => $this->minLength, 'max' => $this->maxLength) + ['min' => $minLength, 'max' => $maxLength] ); - } elseif ($this->minLength) { - $limit = "{{$this->minLength}}.*"; + } elseif ($minLength) { + $limit = "{{$minLength}}.*"; $errorMsg = _t( - 'SilverStripe\\Forms\\ConfirmedPasswordField.ATLEAST', + __CLASS__ . '.ATLEAST', 'Passwords must be at least {min} characters long.', - array('min' => $this->minLength) + ['min' => $minLength] ); - } elseif ($this->maxLength) { - $limit = "{0,{$this->maxLength}}"; + } elseif ($maxLength) { + $limit = "{0,{$maxLength}}"; $errorMsg = _t( - 'SilverStripe\\Forms\\ConfirmedPasswordField.MAXIMUM', + __CLASS__ . '.MAXIMUM', 'Passwords must be at most {max} characters long.', - array('max' => $this->maxLength) + ['max' => $maxLength] ); } $limitRegex = '/^.' . $limit . '$/'; @@ -478,16 +480,18 @@ class ConfirmedPasswordField extends FormField $errorMsg, "validation" ); + + return false; } } - if ($this->requireStrongPassword) { + if ($this->getRequireStrongPassword()) { if (!preg_match('/^(([a-zA-Z]+\d+)|(\d+[a-zA-Z]+))[a-zA-Z0-9]*$/', $value)) { $validator->validationError( $name, _t( 'SilverStripe\\Forms\\Form.VALIDATIONSTRONGPASSWORD', - "Passwords must have at least one digit and one alphanumeric character" + 'Passwords must have at least one digit and one alphanumeric character' ), "validation" ); @@ -502,8 +506,8 @@ class ConfirmedPasswordField extends FormField $validator->validationError( $name, _t( - 'SilverStripe\\Forms\\ConfirmedPasswordField.CURRENT_PASSWORD_MISSING', - "You must enter your current password." + __CLASS__ . '.CURRENT_PASSWORD_MISSING', + 'You must enter your current password.' ), "validation" ); @@ -516,7 +520,7 @@ class ConfirmedPasswordField extends FormField $validator->validationError( $name, _t( - 'SilverStripe\\Forms\\ConfirmedPasswordField.LOGGED_IN_ERROR', + __CLASS__ . '.LOGGED_IN_ERROR', "You must be logged in to change your password." ), "validation" @@ -532,7 +536,7 @@ class ConfirmedPasswordField extends FormField $validator->validationError( $name, _t( - 'SilverStripe\\Forms\\ConfirmedPasswordField.CURRENT_PASSWORD_ERROR', + __CLASS__ . '.CURRENT_PASSWORD_ERROR', "The current password you have entered is not correct." ), "validation" @@ -608,10 +612,84 @@ class ConfirmedPasswordField extends FormField $currentName = "{$name}[_CurrentPassword]"; if ($show) { $confirmField = PasswordField::create($currentName, _t('SilverStripe\\Security\\Member.CURRENT_PASSWORD', 'Current Password')); - $this->children->unshift($confirmField); + $this->getChildren()->unshift($confirmField); } else { - $this->children->removeByName($currentName, true); + $this->getChildren()->removeByName($currentName, true); } return $this; } + + /** + * @return PasswordField + */ + public function getPasswordField() + { + return $this->passwordField; + } + + /** + * @return PasswordField + */ + public function getConfirmPasswordField() + { + return $this->confirmPasswordfield; + } + + /** + * Set the minimum length required for passwords + * + * @param int $minLength + * @return $this + */ + public function setMinLength($minLength) + { + $this->minLength = (int) $minLength; + return $this; + } + + /** + * @return int + */ + public function getMinLength() + { + return $this->minLength; + } + + /** + * Set the maximum length required for passwords + * + * @param int $maxLength + * @return $this + */ + public function setMaxLength($maxLength) + { + $this->maxLength = (int) $maxLength; + return $this; + } + + /** + * @return int + */ + public function getMaxLength() + { + return $this->maxLength; + } + + /** + * @param bool $requireStrongPassword + * @return $this + */ + public function setRequireStrongPassword($requireStrongPassword) + { + $this->requireStrongPassword = (bool) $requireStrongPassword; + return $this; + } + + /** + * @return bool + */ + public function getRequireStrongPassword() + { + return $this->requireStrongPassword; + } } diff --git a/tests/php/Forms/ConfirmedPasswordFieldTest.php b/tests/php/Forms/ConfirmedPasswordFieldTest.php index c034ef0cb..f4fd8b253 100644 --- a/tests/php/Forms/ConfirmedPasswordFieldTest.php +++ b/tests/php/Forms/ConfirmedPasswordFieldTest.php @@ -83,33 +83,40 @@ class ConfirmedPasswordFieldTest extends SapphireTest $field = new ConfirmedPasswordField( 'Test', 'Testing', - array( - "_Password" => "abc123", - "_ConfirmPassword" => "abc123" - ) + [ + '_Password' => 'abc123', + '_ConfirmPassword' => 'abc123', + ] ); $validator = new RequiredFields(); - /** @skipUpgrade */ - new Form(Controller::curr(), 'Form', new FieldList($field), new FieldList(), $validator); $this->assertTrue( $field->validate($validator), - "Validates when both passwords are the same" + 'Validates when both passwords are the same' ); - $field->setName("TestNew"); //try changing name of field + $field->setName('TestNew'); //try changing name of field $this->assertTrue( $field->validate($validator), - "Validates when field name is changed" + 'Validates when field name is changed' ); //non-matching password should make the field invalid - $field->setValue( - array( - "_Password" => "abc123", - "_ConfirmPassword" => "123abc" - ) - ); + $field->setValue([ + '_Password' => 'abc123', + '_ConfirmPassword' => '123abc', + ]); $this->assertFalse( $field->validate($validator), - "Does not validate when passwords differ" + 'Does not validate when passwords differ' + ); + + // Empty passwords should make the field invalid + $field->setCanBeEmpty(false); + $field->setValue([ + '_Password' => '', + '_ConfirmPassword' => '', + ]); + $this->assertFalse( + $field->validate($validator), + 'Empty passwords should not be allowed when canBeEmpty is false' ); } @@ -123,17 +130,121 @@ class ConfirmedPasswordFieldTest extends SapphireTest new FieldList() ); - $form->loadDataFrom( - array( - 'Password' => array( + $form->loadDataFrom([ + 'Password' => [ '_Password' => '123', '_ConfirmPassword' => '999', - ) - ) - ); + ], + ]); $this->assertEquals('123', $field->children->first()->Value()); $this->assertEquals('999', $field->children->last()->Value()); $this->assertNotEquals($field->children->first()->Value(), $field->children->last()->Value()); } + + /** + * @param int|null $minLength + * @param int|null $maxLength + * @param bool $expectValid + * @param string $expectedMessage + * @dataProvider lengthValidationProvider + */ + public function testLengthValidation($minLength, $maxLength, $expectValid, $expectedMessage = '') + { + $field = new ConfirmedPasswordField('Test', 'Testing', [ + '_Password' => 'abc123', + '_ConfirmPassword' => 'abc123', + ]); + $field->setMinLength($minLength)->setMaxLength($maxLength); + + $validator = new RequiredFields(); + $result = $field->validate($validator); + + $this->assertSame($expectValid, $result, 'Validate method should return its result'); + $this->assertSame($expectValid, $validator->getResult()->isValid()); + if ($expectedMessage) { + $this->assertContains($expectedMessage, $validator->getResult()->serialize()); + } + } + + /** + * @return array[] + */ + public function lengthValidationProvider() + { + return [ + 'valid: within min and max' => [3, 8, true], + 'invalid: lower than min with max' => [8, 12, false, 'Passwords must be 8 to 12 characters long'], + 'valid: greater than min' => [3, null, true], + 'invalid: lower than min' => [8, null, false, 'Passwords must be at least 8 characters long'], + 'valid: less than max' => [null, 8, true], + 'invalid: greater than max' => [null, 4, false, 'Passwords must be at most 4 characters long'], + + ]; + } + + public function testStrengthValidation() + { + $field = new ConfirmedPasswordField('Test', 'Testing', [ + '_Password' => 'abc', + '_ConfirmPassword' => 'abc', + ]); + $field->setRequireStrongPassword(true); + + $validator = new RequiredFields(); + $result = $field->validate($validator); + + $this->assertFalse($result, 'Validate method should return its result'); + $this->assertFalse($validator->getResult()->isValid()); + $this->assertContains( + 'Passwords must have at least one digit and one alphanumeric character', + $validator->getResult()->serialize() + ); + } + + public function testTitle() + { + $this->assertNull(ConfirmedPasswordField::create('Test')->Title(), 'Should not have it\'s own title'); + } + + public function testSetTitlePropagatesToPasswordField() + { + /** @var ConfirmedPasswordField $field */ + $field = ConfirmedPasswordField::create('Test') + ->setTitle('My password'); + + $this->assertSame('My password', $field->getPasswordField()->Title()); + } + + public function testSetRightTitlePropagatesToChildren() + { + /** @var ConfirmedPasswordField $field */ + $field = ConfirmedPasswordField::create('Test'); + + $this->assertCount(2, $field->getChildren()); + foreach ($field->getChildren() as $child) { + $this->assertEmpty($child->RightTitle()); + } + + $field->setRightTitle('Please confirm'); + foreach ($field->getChildren() as $child) { + $this->assertSame('Please confirm', $child->RightTitle()); + } + } + + public function testSetChildrenTitles() + { + /** @var ConfirmedPasswordField $field */ + $field = ConfirmedPasswordField::create('Test'); + $field->setRequireExistingPassword(true); + $field->setChildrenTitles([ + 'Current Password', + 'Password', + 'Confirm Password', + ]); + + $this->assertSame('Current Password', $field->getChildren()->shift()->Title()); + $this->assertSame('Password', $field->getChildren()->shift()->Title()); + $this->assertSame('Confirm Password', $field->getChildren()->shift()->Title()); + } } From 8929b8204fdb9a9d1a48331feb38c2b4ba48ab1e Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 17:18:31 +0200 Subject: [PATCH 012/175] More validation tests for ConfirmedPasswordField --- src/Forms/ConfirmedPasswordField.php | 2 +- .../php/Forms/ConfirmedPasswordFieldTest.php | 112 +++++++++++++++++- 2 files changed, 108 insertions(+), 6 deletions(-) diff --git a/src/Forms/ConfirmedPasswordField.php b/src/Forms/ConfirmedPasswordField.php index e4b3840ec..dd022be49 100644 --- a/src/Forms/ConfirmedPasswordField.php +++ b/src/Forms/ConfirmedPasswordField.php @@ -573,7 +573,7 @@ class ConfirmedPasswordField extends FormField public function performReadonlyTransformation() { /** @var ReadonlyField $field */ - $field = $this->castedCopy('SilverStripe\\Forms\\ReadonlyField') + $field = $this->castedCopy(ReadonlyField::class) ->setTitle($this->title ? $this->title : _t('SilverStripe\\Security\\Member.PASSWORD', 'Password')) ->setValue('*****'); diff --git a/tests/php/Forms/ConfirmedPasswordFieldTest.php b/tests/php/Forms/ConfirmedPasswordFieldTest.php index f4fd8b253..d5b7218d5 100644 --- a/tests/php/Forms/ConfirmedPasswordFieldTest.php +++ b/tests/php/Forms/ConfirmedPasswordFieldTest.php @@ -7,12 +7,12 @@ use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\ConfirmedPasswordField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\Form; +use SilverStripe\Forms\ReadonlyField; use SilverStripe\Forms\RequiredFields; use SilverStripe\Security\Member; class ConfirmedPasswordFieldTest extends SapphireTest { - public function testSetValue() { $field = new ConfirmedPasswordField('Test', 'Testing', 'valueA'); @@ -25,6 +25,9 @@ class ConfirmedPasswordFieldTest extends SapphireTest $this->assertEquals('valueB', $field->children->fieldByName($field->getName() . '[_ConfirmPassword]')->Value()); } + /** + * @useDatabase true + */ public function testHashHidden() { $field = new ConfirmedPasswordField('Password', 'Password', 'valueA'); @@ -202,6 +205,73 @@ class ConfirmedPasswordFieldTest extends SapphireTest ); } + public function testCurrentPasswordValidation() + { + $field = new ConfirmedPasswordField('Test', 'Testing', [ + '_Password' => 'abc', + '_ConfirmPassword' => 'abc', + ]); + $field->setRequireExistingPassword(true); + + $validator = new RequiredFields(); + $result = $field->validate($validator); + + $this->assertFalse($result, 'Validate method should return its result'); + $this->assertFalse($validator->getResult()->isValid()); + $this->assertContains( + 'You must enter your current password', + $validator->getResult()->serialize() + ); + } + + public function testMustBeLoggedInToChangePassword() + { + $field = new ConfirmedPasswordField('Test', 'Testing'); + $field->setRequireExistingPassword(true); + $field->setValue([ + '_CurrentPassword' => 'foo', + '_Password' => 'abc', + '_ConfirmPassword' => 'abc', + ]); + + $validator = new RequiredFields(); + $this->logOut(); + $result = $field->validate($validator); + + $this->assertFalse($result, 'Validate method should return its result'); + $this->assertFalse($validator->getResult()->isValid()); + $this->assertContains( + 'You must be logged in to change your password', + $validator->getResult()->serialize() + ); + } + + /** + * @useDatabase true + */ + public function testValidateCorrectPassword() + { + $this->logInWithPermission('ADMIN'); + + $field = new ConfirmedPasswordField('Test', 'Testing'); + $field->setRequireExistingPassword(true); + $field->setValue([ + '_CurrentPassword' => 'foo-not-going-to-be-the-correct-password', + '_Password' => 'abc', + '_ConfirmPassword' => 'abc', + ]); + + $validator = new RequiredFields(); + $result = $field->validate($validator); + + $this->assertFalse($result, 'Validate method should return its result'); + $this->assertFalse($validator->getResult()->isValid()); + $this->assertContains( + 'The current password you have entered is not correct', + $validator->getResult()->serialize() + ); + } + public function testTitle() { $this->assertNull(ConfirmedPasswordField::create('Test')->Title(), 'Should not have it\'s own title'); @@ -218,8 +288,7 @@ class ConfirmedPasswordFieldTest extends SapphireTest public function testSetRightTitlePropagatesToChildren() { - /** @var ConfirmedPasswordField $field */ - $field = ConfirmedPasswordField::create('Test'); + $field = new ConfirmedPasswordField('Test'); $this->assertCount(2, $field->getChildren()); foreach ($field->getChildren() as $child) { @@ -234,8 +303,7 @@ class ConfirmedPasswordFieldTest extends SapphireTest public function testSetChildrenTitles() { - /** @var ConfirmedPasswordField $field */ - $field = ConfirmedPasswordField::create('Test'); + $field = new ConfirmedPasswordField('Test'); $field->setRequireExistingPassword(true); $field->setChildrenTitles([ 'Current Password', @@ -247,4 +315,38 @@ class ConfirmedPasswordFieldTest extends SapphireTest $this->assertSame('Password', $field->getChildren()->shift()->Title()); $this->assertSame('Confirm Password', $field->getChildren()->shift()->Title()); } + + public function testPerformReadonlyTransformation() + { + $field = new ConfirmedPasswordField('Test', 'Change it'); + $result = $field->performReadonlyTransformation(); + + $this->assertInstanceOf(ReadonlyField::class, $result); + $this->assertSame('Change it', $result->Title()); + $this->assertContains('***', $result->Value()); + } + + public function testPerformDisabledTransformation() + { + $field = new ConfirmedPasswordField('Test', 'Change it'); + $result = $field->performDisabledTransformation(); + + $this->assertInstanceOf(ReadonlyField::class, $result); + } + + public function testSetRequireExistingPasswordOnlyRunsOnce() + { + $field = new ConfirmedPasswordField('Test', 'Change it'); + + $this->assertCount(2, $field->getChildren()); + + $field->setRequireExistingPassword(true); + $this->assertCount(3, $field->getChildren(), 'Current password field was not pushed'); + + $field->setRequireExistingPassword(true); + $this->assertCount(3, $field->getChildren(), 'Current password field should not be pushed again'); + + $field->setRequireExistingPassword(false); + $this->assertCount(2, $field->getChildren(), 'Current password field should not be removed'); + } } From d56bad7568df1b4da0e196c961b7d0d3b310f285 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 17:33:59 +0200 Subject: [PATCH 013/175] Add tests for edge cases in CurrencyField --- src/Forms/CurrencyField.php | 5 +-- tests/php/Forms/CurrencyFieldTest.php | 59 +++++++++++++++++++-------- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/Forms/CurrencyField.php b/src/Forms/CurrencyField.php index 80bf1a51e..5e2555c70 100644 --- a/src/Forms/CurrencyField.php +++ b/src/Forms/CurrencyField.php @@ -39,9 +39,8 @@ class CurrencyField extends TextField { if ($this->value) { return preg_replace('/[^0-9.\-]/', '', $this->value); - } else { - return 0.00; } + return 0.00; } public function Type() @@ -54,7 +53,7 @@ class CurrencyField extends TextField */ public function performReadonlyTransformation() { - return $this->castedCopy('SilverStripe\\Forms\\CurrencyField_Readonly'); + return $this->castedCopy(CurrencyField_Readonly::class); } public function validate($validator) diff --git a/tests/php/Forms/CurrencyFieldTest.php b/tests/php/Forms/CurrencyFieldTest.php index a985dffe1..dbb595996 100644 --- a/tests/php/Forms/CurrencyFieldTest.php +++ b/tests/php/Forms/CurrencyFieldTest.php @@ -5,6 +5,7 @@ namespace SilverStripe\Forms\Tests; use SilverStripe\Core\Config\Config; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\CurrencyField; +use SilverStripe\Forms\CurrencyField_Readonly; use SilverStripe\Forms\RequiredFields; use SilverStripe\ORM\FieldType\DBCurrency; @@ -124,56 +125,56 @@ class CurrencyFieldTest extends SapphireTest //tests with default currency symbol setting $f->setValue('123.45'); $this->assertEquals( - $f->value, + $f->Value(), '$123.45', 'Prepends dollar sign to positive decimal' ); $f->setValue('-123.45'); $this->assertEquals( - $f->value, + $f->Value(), '$-123.45', 'Prepends dollar sign to negative decimal' ); $f->setValue('$1'); $this->assertEquals( - $f->value, + $f->Value(), '$1.00', 'Formats small value' ); $f->setValue('$2.5'); $this->assertEquals( - $f->value, + $f->Value(), '$2.50', 'Formats small value' ); $f->setValue('$2500000.13'); $this->assertEquals( - $f->value, + $f->Value(), '$2,500,000.13', 'Formats large value' ); $f->setValue('$2.50000013'); $this->assertEquals( - $f->value, + $f->Value(), '$2.50', 'Truncates long decimal portions' ); $f->setValue('test123.00test'); $this->assertEquals( - $f->value, + $f->Value(), '$123.00', 'Strips alpha values' ); $f->setValue('test'); $this->assertEquals( - $f->value, + $f->Value(), '$0.00', 'Does not set alpha values' ); @@ -183,56 +184,56 @@ class CurrencyFieldTest extends SapphireTest $f->setValue('123.45'); $this->assertEquals( - $f->value, + $f->Value(), '€123.45', 'Prepends dollar sign to positive decimal' ); $f->setValue('-123.45'); $this->assertEquals( - $f->value, + $f->Value(), '€-123.45', 'Prepends dollar sign to negative decimal' ); $f->setValue('€1'); $this->assertEquals( - $f->value, + $f->Value(), '€1.00', 'Formats small value' ); $f->setValue('€2.5'); $this->assertEquals( - $f->value, + $f->Value(), '€2.50', 'Formats small value' ); $f->setValue('€2500000.13'); $this->assertEquals( - $f->value, + $f->Value(), '€2,500,000.13', 'Formats large value' ); $f->setValue('€2.50000013'); $this->assertEquals( - $f->value, + $f->Value(), '€2.50', 'Truncates long decimal portions' ); $f->setValue('test123.00test'); $this->assertEquals( - $f->value, + $f->Value(), '€123.00', 'Strips alpha values' ); $f->setValue('test'); $this->assertEquals( - $f->value, + $f->Value(), '€0.00', 'Does not set alpha values' ); @@ -282,4 +283,30 @@ class CurrencyFieldTest extends SapphireTest -123.45 ); } + + public function testDataValueReturnsEmptyFloat() + { + $field = new CurrencyField('Test', '', null); + $this->assertSame(0.00, $field->dataValue()); + } + + public function testPerformReadonlyTransformation() + { + $field = new CurrencyField('Test'); + $result = $field->performReadonlyTransformation(); + $this->assertInstanceOf(CurrencyField_Readonly::class, $result); + } + + public function testInvalidCurrencySymbol() + { + $field = new CurrencyField('Test', '', '$5.00'); + $validator = new RequiredFields(); + + DBCurrency::config()->update('currency_symbol', '€'); + $result = $field->validate($validator); + + $this->assertFalse($result, 'Validation should fail since wrong currency was used'); + $this->assertFalse($validator->getResult()->isValid(), 'Validator should receive failed state'); + $this->assertContains('Please enter a valid currency', $validator->getResult()->serialize()); + } } From c06cf4820e02a923a683419b03979bd3677f38db Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 17:41:41 +0200 Subject: [PATCH 014/175] BUG Readonly and disabled CurrencyFields no longer always returns dollar currency sign, now respect config --- src/Forms/CurrencyField_Disabled.php | 6 ++- src/Forms/CurrencyField_Readonly.php | 8 +-- .../php/Forms/CurrencyField_DisabledTest.php | 34 ++++++++++++ .../php/Forms/CurrencyField_ReadonlyTest.php | 53 +++++++++++++++++++ 4 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 tests/php/Forms/CurrencyField_DisabledTest.php create mode 100644 tests/php/Forms/CurrencyField_ReadonlyTest.php diff --git a/src/Forms/CurrencyField_Disabled.php b/src/Forms/CurrencyField_Disabled.php index 7e743755e..68145d41f 100644 --- a/src/Forms/CurrencyField_Disabled.php +++ b/src/Forms/CurrencyField_Disabled.php @@ -3,6 +3,7 @@ namespace SilverStripe\Forms; use SilverStripe\Core\Convert; +use SilverStripe\ORM\FieldType\DBCurrency; /** * Readonly version of a {@link CurrencyField}. @@ -13,7 +14,7 @@ class CurrencyField_Disabled extends CurrencyField protected $disabled = true; /** - * overloaded to display the correctly formated value for this datatype + * Overloaded to display the correctly formatted value for this data type * * @param array $properties * @return string @@ -22,7 +23,8 @@ class CurrencyField_Disabled extends CurrencyField { if ($this->value) { $val = Convert::raw2xml($this->value); - $val = _t('SilverStripe\\Forms\\CurrencyField.CURRENCYSYMBOL', '$') . number_format(preg_replace('/[^0-9.-]/', "", $val), 2); + $val = DBCurrency::config()->get('currency_symbol') + . number_format(preg_replace('/[^0-9.-]/', '', $val), 2); $valforInput = Convert::raw2att($val); } else { $valforInput = ''; diff --git a/src/Forms/CurrencyField_Readonly.php b/src/Forms/CurrencyField_Readonly.php index 775b9eb6b..292827600 100644 --- a/src/Forms/CurrencyField_Readonly.php +++ b/src/Forms/CurrencyField_Readonly.php @@ -3,6 +3,7 @@ namespace SilverStripe\Forms; use SilverStripe\Core\Convert; +use SilverStripe\ORM\FieldType\DBCurrency; /** * Readonly version of a {@link CurrencyField}. @@ -11,19 +12,20 @@ class CurrencyField_Readonly extends ReadonlyField { /** - * Overloaded to display the correctly formated value for this datatype + * Overloaded to display the correctly formatted value for this data type * * @param array $properties * @return string */ public function Field($properties = array()) { + $currencySymbol = DBCurrency::config()->get('currency_symbol'); if ($this->value) { $val = Convert::raw2xml($this->value); - $val = _t('SilverStripe\\Forms\\CurrencyField.CURRENCYSYMBOL', '$') . number_format(preg_replace('/[^0-9.-]/', "", $val), 2); + $val = $currencySymbol . number_format(preg_replace('/[^0-9.-]/', '', $val), 2); $valforInput = Convert::raw2att($val); } else { - $val = '' . _t('SilverStripe\\Forms\\CurrencyField.CURRENCYSYMBOL', '$') . '0.00'; + $val = '' . $currencySymbol . '0.00'; $valforInput = ''; } return "extraClass() . "\" id=\"" . $this->ID() . "\">$val" diff --git a/tests/php/Forms/CurrencyField_DisabledTest.php b/tests/php/Forms/CurrencyField_DisabledTest.php new file mode 100644 index 000000000..ed832521c --- /dev/null +++ b/tests/php/Forms/CurrencyField_DisabledTest.php @@ -0,0 +1,34 @@ +Field(); + + $this->assertContains('assertContains('disabled', $result, 'The input should be disabled'); + $this->assertContains('$5.00', $result, 'The value should be rendered'); + } + + /** + * @todo: Update the expectation when intl for currencies is implemented + */ + public function testFieldWithCustomisedCurrencySymbol() + { + DBCurrency::config()->update('currency_symbol', '€'); + $field = new CurrencyField_Disabled('Test', '', '€5.00'); + $result = $field->Field(); + + $this->assertContains('assertContains('disabled', $result, 'The input should be disabled'); + $this->assertContains('€5.00', $result, 'The value should be rendered'); + } +} diff --git a/tests/php/Forms/CurrencyField_ReadonlyTest.php b/tests/php/Forms/CurrencyField_ReadonlyTest.php new file mode 100644 index 000000000..289327cd1 --- /dev/null +++ b/tests/php/Forms/CurrencyField_ReadonlyTest.php @@ -0,0 +1,53 @@ +performReadonlyTransformation(); + $this->assertInstanceOf(CurrencyField_Readonly::class, $result); + $this->assertNotSame($result, $field, 'Should return a clone of the field'); + } + + public function testFieldWithValue() + { + $field = new CurrencyField_Readonly('Test', '', '$5.00'); + $result = $field->Field(); + + $this->assertContains('assertContains('readonly', $result, 'The input should be readonly'); + $this->assertContains('$5.00', $result, 'The value should be rendered'); + } + + public function testFieldWithOutValue() + { + DBCurrency::config()->update('currency_symbol', 'AUD'); + $field = new CurrencyField_Readonly('Test', '', null); + $result = $field->Field(); + + $this->assertContains('assertContains('readonly', $result, 'The input should be readonly'); + $this->assertContains('AUD0.00', $result, 'The value should be rendered'); + } + + /** + * @todo: Update the expectation when intl for currencies is implemented + */ + public function testFieldWithCustomisedCurrencySymbol() + { + DBCurrency::config()->update('currency_symbol', '€'); + $field = new CurrencyField_Readonly('Test', '', '€5.00'); + $result = $field->Field(); + + $this->assertContains('assertContains('readonly', $result, 'The input should be readonly'); + $this->assertContains('€5.00', $result, 'The value should be rendered'); + } +} From 0e2847e2899b6587e3195e2bb8dc064cecd0d0c4 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 17:59:55 +0200 Subject: [PATCH 015/175] Add tests for DatalessField --- tests/php/Forms/DatalessFieldTest.php | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/php/Forms/DatalessFieldTest.php diff --git a/tests/php/Forms/DatalessFieldTest.php b/tests/php/Forms/DatalessFieldTest.php new file mode 100644 index 000000000..a67cfc9a6 --- /dev/null +++ b/tests/php/Forms/DatalessFieldTest.php @@ -0,0 +1,45 @@ +getAttributes(); + $this->assertSame('hidden', $result['type']); + } + + public function testFieldHolderAndSmallFieldHolderReturnField() + { + /** @var DatalessField|PHPUnit_Framework_MockObject_MockObject $mock */ + $mock = $this->getMockBuilder(DatalessField::class) + ->disableOriginalConstructor() + ->setMethods(['Field']) + ->getMock(); + + $properties = [ + 'foo' => 'bar', + ]; + + $mock->expects($this->exactly(2))->method('Field')->with($properties)->willReturn('boo!'); + + $fieldHolder = $mock->FieldHolder($properties); + $smallFieldHolder = $mock->SmallFieldHolder($properties); + + $this->assertSame('boo!', $fieldHolder); + $this->assertSame('boo!', $smallFieldHolder); + } + + public function testPerformReadonlyTransformation() + { + $field = new DatalessField('Test'); + $result = $field->performReadonlyTransformation(); + $this->assertInstanceOf(DatalessField::class, $result); + $this->assertTrue($result->isReadonly()); + } +} From 97209bc919e5ce6f991f224fc9deadbbd58332f7 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 18:15:42 +0200 Subject: [PATCH 016/175] Add edge case unit tests for DateField --- src/Forms/DateField.php | 2 +- tests/php/Forms/DateFieldTest.php | 52 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/Forms/DateField.php b/src/Forms/DateField.php index a9c72e033..a58113b6f 100644 --- a/src/Forms/DateField.php +++ b/src/Forms/DateField.php @@ -10,7 +10,7 @@ use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\ValidationResult; /** - * Form used for editing a date stirng + * Form used for editing a date string * * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context, * since the required frontend dependencies are included through CMS bundling. diff --git a/tests/php/Forms/DateFieldTest.php b/tests/php/Forms/DateFieldTest.php index 5ea4d9a03..87065fc8a 100644 --- a/tests/php/Forms/DateFieldTest.php +++ b/tests/php/Forms/DateFieldTest.php @@ -5,8 +5,10 @@ namespace SilverStripe\Forms\Tests; use IntlDateFormatter; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\DateField; +use SilverStripe\Forms\DateField_Disabled; use SilverStripe\Forms\RequiredFields; use SilverStripe\i18n\i18n; +use SilverStripe\ORM\FieldType\DBDate; use SilverStripe\ORM\FieldType\DBDatetime; /** @@ -225,4 +227,54 @@ class DateFieldTest extends SapphireTest $dateField->setLocale('de_DE'); $dateField->Value(); } + + public function testGetDateFormatHTML5() + { + $field = new DateField('Date'); + $field->setHTML5(true); + $this->assertSame(DBDate::ISO_DATE, $field->getDateFormat()); + } + + public function testGetDateFormatViaSetter() + { + $field = new DateField('Date'); + $field->setHTML5(false); + $field->setDateFormat('d-m-Y'); + $this->assertSame('d-m-Y', $field->getDateFormat()); + } + + public function testGetAttributes() + { + $field = new DateField('Date'); + $field + ->setHTML5(true) + ->setMinDate('1980-05-10') + ->setMaxDate('1980-05-20'); + + $result = $field->getAttributes(); + $this->assertSame('1980-05-10', $result['min']); + $this->assertSame('1980-05-20', $result['max']); + } + + public function testSetSubmittedValueNull() + { + $field = new DateField('Date'); + $field->setSubmittedValue(false); + $this->assertNull($field->Value()); + } + + public function testPerformReadonlyTransformation() + { + $field = new DateField('Date'); + $result = $field->performReadonlyTransformation(); + $this->assertInstanceOf(DateField_Disabled::class, $result); + $this->assertTrue($result->isReadonly()); + } + + public function testValidateWithoutValueReturnsTrue() + { + $field = new DateField('Date'); + $validator = new RequiredFields(); + $this->assertTrue($field->validate($validator)); + } } From fd50ce629572673090c6a4fa64b718464b1a5a92 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 18:29:07 +0200 Subject: [PATCH 017/175] Add more edge case tests for disabled DateFields and DatetimeField --- tests/php/Forms/DateField_DisabledTest.php | 10 ++- tests/php/Forms/DatetimeFieldTest.php | 75 +++++++++++++++++++--- 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/tests/php/Forms/DateField_DisabledTest.php b/tests/php/Forms/DateField_DisabledTest.php index a8e5e3d1f..0108b751d 100644 --- a/tests/php/Forms/DateField_DisabledTest.php +++ b/tests/php/Forms/DateField_DisabledTest.php @@ -2,10 +2,8 @@ namespace SilverStripe\Forms\Tests; -use IntlDateFormatter; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\DateField_Disabled; -use SilverStripe\Forms\RequiredFields; use SilverStripe\i18n\i18n; use SilverStripe\ORM\FieldType\DBDatetime; @@ -76,4 +74,12 @@ class DateField_DisabledTest extends SapphireTest $actual = DateField_Disabled::create('Test')->setValue('This is not a date')->Field(); $this->assertEquals($expected, $actual); } + + public function testType() + { + $field = new DateField_Disabled('Test'); + $result = $field->Type(); + $this->assertContains('readonly', $result, 'Disabled field should be treated as readonly'); + $this->assertContains('date_disabled', $result, 'Field should contain date_disabled class'); + } } diff --git a/tests/php/Forms/DatetimeFieldTest.php b/tests/php/Forms/DatetimeFieldTest.php index d3ca4db24..37fe628e3 100644 --- a/tests/php/Forms/DatetimeFieldTest.php +++ b/tests/php/Forms/DatetimeFieldTest.php @@ -86,6 +86,13 @@ class DatetimeFieldTest extends SapphireTest $this->assertEquals('2003-01-30 11:59:38', $f->dataValue()); // server timezone (Berlin) } + public function testSetSubmittedValueNull() + { + $field = new DatetimeField('Datetime'); + $field->setSubmittedValue(false); + $this->assertNull($field->Value()); + } + public function testConstructorWithoutArgs() { $f = new DatetimeField('Datetime'); @@ -148,21 +155,24 @@ class DatetimeFieldTest extends SapphireTest public function testValidate() { - $f = new DatetimeField('Datetime', 'Datetime', '2003-03-29 23:59:38'); - $this->assertTrue($f->validate(new RequiredFields())); + $field = new DatetimeField('Datetime', 'Datetime', '2003-03-29 23:59:38'); + $this->assertTrue($field->validate(new RequiredFields())); - $f = new DatetimeField('Datetime', 'Datetime', '2003-03-29T23:59:38'); - $this->assertTrue($f->validate(new RequiredFields()), 'Normalised ISO'); + $field = new DatetimeField('Datetime', 'Datetime', '2003-03-29T23:59:38'); + $this->assertTrue($field->validate(new RequiredFields()), 'Normalised ISO'); - $f = new DatetimeField('Datetime', 'Datetime', '2003-03-29'); - $this->assertFalse($f->validate(new RequiredFields()), 'Leaving out time'); + $field = new DatetimeField('Datetime', 'Datetime', '2003-03-29'); + $this->assertFalse($field->validate(new RequiredFields()), 'Leaving out time'); - $f = (new DatetimeField('Datetime', 'Datetime')) + $field = (new DatetimeField('Datetime', 'Datetime')) ->setSubmittedValue('2003-03-29T00:00'); - $this->assertTrue($f->validate(new RequiredFields()), 'Leaving out seconds (like many browsers)'); + $this->assertTrue($field->validate(new RequiredFields()), 'Leaving out seconds (like many browsers)'); - $f = new DatetimeField('Datetime', 'Datetime', 'wrong'); - $this->assertFalse($f->validate(new RequiredFields())); + $field = new DatetimeField('Datetime', 'Datetime', 'wrong'); + $this->assertFalse($field->validate(new RequiredFields())); + + $field = new DatetimeField('Datetime', 'Datetime', false); + $this->assertTrue($field->validate(new RequiredFields())); } public function testSetMinDate() @@ -446,6 +456,51 @@ class DatetimeFieldTest extends SapphireTest $this->assertEquals($attrs['max'], '2010-01-31T23:00:00'); // frontend timezone } + public function testAttributesNonHTML5() + { + $field = new DatetimeField('Datetime'); + $field->setHTML5(false); + $result = $field->getAttributes(); + $this->assertSame('text', $result['type']); + } + + public function testFrontendToInternalEdgeCases() + { + $field = new DatetimeField('Datetime'); + + $this->assertNull($field->frontendToInternal(false)); + $this->assertNull($field->frontendToInternal('sdfsdfsfs$%^&*')); + } + + public function testInternalToFrontendEdgeCases() + { + $field = new DatetimeField('Datetime'); + + $this->assertNull($field->internalToFrontend(false)); + $this->assertNull($field->internalToFrontend('sdfsdfsfs$%^&*')); + } + + public function testPerformReadonlyTransformation() + { + $field = new DatetimeField('Datetime'); + + $result = $field->performReadonlyTransformation(); + $this->assertInstanceOf(DatetimeField::class, $result); + $this->assertNotSame($result, $field, 'Readonly field should be cloned'); + $this->assertTrue($result->isReadonly()); + } + + /** + * @expectedException \BadMethodCallException + * @expectedExceptionMessage Can't change timezone after setting a value + */ + public function testSetTimezoneThrowsExceptionWhenChangingTimezoneAfterSettingValue() + { + date_default_timezone_set('Europe/Berlin'); + $field = new DatetimeField('Datetime', 'Time', '2003-03-29 23:59:38'); + $field->setTimezone('Pacific/Auckland'); + } + protected function getMockForm() { /** @skipUpgrade */ From 449b2cf29187ca7e786c2bbcdd1eeebef1bee0cf Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 19:47:11 +0200 Subject: [PATCH 018/175] Add tests for DefaultFormFactory --- src/Forms/DefaultFormFactory.php | 1 + tests/php/Forms/DefaultFormFactoryTest.php | 38 ++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 tests/php/Forms/DefaultFormFactoryTest.php diff --git a/src/Forms/DefaultFormFactory.php b/src/Forms/DefaultFormFactory.php index 8aeaf3088..24c6e3feb 100644 --- a/src/Forms/DefaultFormFactory.php +++ b/src/Forms/DefaultFormFactory.php @@ -30,6 +30,7 @@ class DefaultFormFactory implements FormFactory * @param string $name * @param array $context * @return Form + * @throws InvalidArgumentException When required context is missing */ public function getForm(RequestHandler $controller = null, $name = FormFactory::DEFAULT_NAME, $context = []) { diff --git a/tests/php/Forms/DefaultFormFactoryTest.php b/tests/php/Forms/DefaultFormFactoryTest.php new file mode 100644 index 000000000..c8ab715f3 --- /dev/null +++ b/tests/php/Forms/DefaultFormFactoryTest.php @@ -0,0 +1,38 @@ +getForm(); + } + + public function testGetForm() + { + $record = new DataObject(); + $record->Title = 'Test'; + + $factory = new DefaultFormFactory(); + $form = $factory->getForm(null, null, ['Record' => $record]); + + $this->assertSame($record, $form->getRecord()); + } + + public function testGetRequiredContext() + { + $factory = new DefaultFormFactory(); + $this->assertContains('Record', $factory->getRequiredContext()); + } +} From bea4101e216c98da0c14df009ebd8be5341c56ed Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 20 Oct 2018 19:49:21 +0200 Subject: [PATCH 019/175] Add tests for DisabledTransformation, PrintableTransformation and PrintableTransformation_TabSet --- src/Forms/PrintableTransformation_TabSet.php | 4 +-- src/Forms/TabSet.php | 3 +- .../php/Forms/DisabledTransformationTest.php | 20 +++++++++++ .../php/Forms/PrintableTransformationTest.php | 25 +++++++++++++ .../PrintableTransformation_TabSetTest.php | 36 +++++++++++++++++++ 5 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 tests/php/Forms/DisabledTransformationTest.php create mode 100644 tests/php/Forms/PrintableTransformationTest.php create mode 100644 tests/php/Forms/PrintableTransformation_TabSetTest.php diff --git a/src/Forms/PrintableTransformation_TabSet.php b/src/Forms/PrintableTransformation_TabSet.php index f25598e5a..2be4f72d7 100644 --- a/src/Forms/PrintableTransformation_TabSet.php +++ b/src/Forms/PrintableTransformation_TabSet.php @@ -19,9 +19,9 @@ class PrintableTransformation_TabSet extends TabSet public function FieldHolder($properties = array()) { // This gives us support for sub-tabs. - $tag = ($this->tabSet) ? "h2>" : "h1>"; + $tag = $this->getTabSet() ? 'h2>' : 'h1>'; $retVal = ''; - foreach ($this->children as $tab) { + foreach ($this->getChildren() as $tab) { $retVal .= "<$tag" . $tab->Title() . "FieldHolder(); } diff --git a/src/Forms/TabSet.php b/src/Forms/TabSet.php index da18e9d48..dc0b60217 100644 --- a/src/Forms/TabSet.php +++ b/src/Forms/TabSet.php @@ -106,9 +106,8 @@ class TabSet extends CompositeField { if ($this->tabSet) { return $this->tabSet->ID() . '_' . $this->id . '_set'; - } else { - return $this->id; } + return $this->id; } /** diff --git a/tests/php/Forms/DisabledTransformationTest.php b/tests/php/Forms/DisabledTransformationTest.php new file mode 100644 index 000000000..74b460f39 --- /dev/null +++ b/tests/php/Forms/DisabledTransformationTest.php @@ -0,0 +1,20 @@ +transform($field); + + $this->assertTrue($newField->isDisabled(), 'Transformation failed to transform field to be disabled'); + } +} diff --git a/tests/php/Forms/PrintableTransformationTest.php b/tests/php/Forms/PrintableTransformationTest.php new file mode 100644 index 000000000..8a54d8d49 --- /dev/null +++ b/tests/php/Forms/PrintableTransformationTest.php @@ -0,0 +1,25 @@ +transformTabSet($tabSet); + + $this->assertInstanceOf(PrintableTransformation_TabSet::class, $result); + $this->assertSame('Root', $result->Title()); + } +} diff --git a/tests/php/Forms/PrintableTransformation_TabSetTest.php b/tests/php/Forms/PrintableTransformation_TabSetTest.php new file mode 100644 index 000000000..11ca4efd8 --- /dev/null +++ b/tests/php/Forms/PrintableTransformation_TabSetTest.php @@ -0,0 +1,36 @@ +FieldHolder(); + + $this->assertContains('

Main

', $result); + $this->assertContains('

Secondary

', $result); + + $transformationTabSet->setTabSet($optionsTabSet); + $result = $transformationTabSet->FieldHolder(); + + $this->assertContains('

Options

', $result); + } +} From af46381dca9ef11a407dbb7016a6ac1621a4b376 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Tue, 23 Oct 2018 11:52:25 +1300 Subject: [PATCH 020/175] MINOR Correct implementation of single lookup field when valueToLabel returns null --- src/Forms/SingleLookupField.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Forms/SingleLookupField.php b/src/Forms/SingleLookupField.php index a8dfc47bb..99b38f4c3 100644 --- a/src/Forms/SingleLookupField.php +++ b/src/Forms/SingleLookupField.php @@ -94,7 +94,7 @@ class SingleLookupField extends SingleSelectField return $label; } - return $value; + return parent::Value(); } /** From e9bffe40204af9aaee40d0d46f13900cb1573144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Federico=20Jaramillo=20Mart=C3=ADnez?= Date: Mon, 22 Oct 2018 22:33:16 -0500 Subject: [PATCH 021/175] Update comment about locks in MySQL MySQL 5.7.5 and newer allow for multiple lock form the same session. This information is now reflected in the comment. --- src/ORM/Connect/MySQLDatabase.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ORM/Connect/MySQLDatabase.php b/src/ORM/Connect/MySQLDatabase.php index 7a518ee70..74f155e30 100644 --- a/src/ORM/Connect/MySQLDatabase.php +++ b/src/ORM/Connect/MySQLDatabase.php @@ -495,8 +495,9 @@ class MySQLDatabase extends Database { $id = $this->getLockIdentifier($name); - // MySQL auto-releases existing locks on subsequent GET_LOCK() calls, - // in contrast to PostgreSQL and SQL Server who stack the locks. + // MySQL 5.7.4 and below auto-releases existing locks on subsequent GET_LOCK() calls. + // MySQL 5.7.5 and newer allow multiple locks per sessions even with the same name. + // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock return (bool) $this->query(sprintf("SELECT GET_LOCK('%s', %d)", $id, $timeout))->value(); } From 364eb417548ccaf614e77e2a46d1e461e39229e8 Mon Sep 17 00:00:00 2001 From: Luke Fromhold Date: Tue, 23 Oct 2018 19:06:55 +1100 Subject: [PATCH 022/175] Adding $Up. prefix to $ColumnCount references within $Fieldlist loops --- templates/SilverStripe/Forms/CompositeField.ss | 4 ++-- templates/SilverStripe/Forms/CompositeField_holder_small.ss | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/SilverStripe/Forms/CompositeField.ss b/templates/SilverStripe/Forms/CompositeField.ss index 41a3ea9b5..3a42cd042 100644 --- a/templates/SilverStripe/Forms/CompositeField.ss +++ b/templates/SilverStripe/Forms/CompositeField.ss @@ -1,6 +1,6 @@ <% loop $FieldList %> - <% if $ColumnCount %> -
+ <% if $Up.ColumnCount %> +
$FieldHolder
<% else %> diff --git a/templates/SilverStripe/Forms/CompositeField_holder_small.ss b/templates/SilverStripe/Forms/CompositeField_holder_small.ss index 081da5b4f..c7b433f13 100644 --- a/templates/SilverStripe/Forms/CompositeField_holder_small.ss +++ b/templates/SilverStripe/Forms/CompositeField_holder_small.ss @@ -4,8 +4,8 @@ <% end_if %> <% loop $FieldList %> - <% if $ColumnCount %> -
+ <% if $Up.ColumnCount %> +
$SmallFieldHolder
<% else %> From 66a404ad1a83ca2f26c5f968935503306eb54578 Mon Sep 17 00:00:00 2001 From: Will Rossiter Date: Wed, 24 Oct 2018 12:11:57 +1300 Subject: [PATCH 023/175] FIX use Injector for FormField::castedCopy Allows LookupField to be replaced with user specificed classes. --- src/Forms/FormField.php | 2 +- src/Forms/TreeDropdownField_Readonly.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Forms/FormField.php b/src/Forms/FormField.php index 632335391..5a9b65bef 100644 --- a/src/Forms/FormField.php +++ b/src/Forms/FormField.php @@ -1424,7 +1424,7 @@ class FormField extends RequestHandler $field = $classOrCopy; if (!is_object($field)) { - $field = new $classOrCopy($this->name); + $field = $classOrCopy::create($this->name); } $field diff --git a/src/Forms/TreeDropdownField_Readonly.php b/src/Forms/TreeDropdownField_Readonly.php index fed1a1765..c51867098 100644 --- a/src/Forms/TreeDropdownField_Readonly.php +++ b/src/Forms/TreeDropdownField_Readonly.php @@ -17,7 +17,7 @@ class TreeDropdownField_Readonly extends TreeDropdownField } $source = [ $this->value => $title ]; - $field = new LookupField($this->name, $this->title, $source); + $field = LookupField::create($this->name, $this->title, $source); $field->setValue($this->value); $field->setForm($this->form); return $field->Field(); From 4e62698391035e330815c83293a50bcc09f8413f Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Wed, 24 Oct 2018 15:27:15 +1300 Subject: [PATCH 024/175] Add entry to change log about CMSPageHistoryController deprecation. (#8508) * Add entry to change log about CMSPageHistoryController deprecation. * Correct typo #8508 * Tweaking CMS History controller changelog entry #8508 --- docs/en/04_Changelogs/4.3.0.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/en/04_Changelogs/4.3.0.md b/docs/en/04_Changelogs/4.3.0.md index 58f9a5b9d..9dba198d9 100644 --- a/docs/en/04_Changelogs/4.3.0.md +++ b/docs/en/04_Changelogs/4.3.0.md @@ -7,6 +7,7 @@ - Take care with `stageChildren()` overrides. `Hierarchy::numChildren() ` results will only make use of `stageChildren()` customisations that are applied to the base class and don't include record-specific behaviour. - New React-based search UI for the CMS, Asset-Admin, GridFields and ModelAdmins. - A new `GridFieldLazyLoader` component can be added to `GridField`. This will delay the fetching of data until the user access the container Tab of the GridField. + - `SilverStripe\VersionedAdmin\Controllers\CMSPageHistoryViewerController` is now the default CMS history controller and `SilverStripe\CMS\Controllers\CMSPageHistoryController` has been deprecated. ## Upgrading {#upgrading} @@ -41,3 +42,23 @@ public function getCMSFields() } ``` + +### Keep using the legacy `CMSPageHistoryController` + +To keep using the old CMS history controller for every page type, add the following entry to your YML config. + +```yml +SilverStripe\Core\Injector\Injector: + SilverStripe\CMS\Controllers\CMSPageHistoryController: + class: SilverStripe\CMS\Controllers\CMSPageHistoryController +``` + +If you want to use both CMS history controllers in different contexts, you can implement your own _Factory_ class. +```yml +SilverStripe\Core\Injector\Injector: + SilverStripe\CMS\Controllers\CMSPageHistoryController: + factory: + App\MySite\MyCustomControllerFactory +``` + +[Implementing a _Factory_ with the Injector](/developer_guides/extending/injector/#factories) From a3a4f48cb7385820b0a5ce97d717ffaadb614cea Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Wed, 24 Oct 2018 16:47:16 +1300 Subject: [PATCH 025/175] MINOR Update doc to explain how to use and build the pattern lib --- .../02_CMS_Architecture.md | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md b/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md index c98c7fdfe..f90a7fa4b 100644 --- a/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md +++ b/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md @@ -55,15 +55,35 @@ coding conventions. A pattern library is a collection of user interface design elements, this helps developers and designers collaborate and to provide a quick preview of elements as they were intended without the need to build an entire interface to see it. Components built in React and used by the CMS are actively being added to the pattern library. +The pattern library can be used to preview React components without including them in the SilverStripe CMS. -To access the pattern library, starting from your project root: +### Viewing the latest pattern library -``` -cd vendor/silverstripe/admin && yarn pattern-lib +The easiest way to access the pattern library is to view it online. The pattern library for the latest SilverStripe 4 development branch is automatically built and deployed. Note that this may include new components that are not yet available in a stable release. + +[Browse the SilverStripe pattern library online](https://silverstripe.github.io/silverstripe-admin). + +### Running the pattern library + +If you're developing a new React component, running the pattern library locally is a good way to interact with it. + +The pattern library is built from the `silverstripe/admin` module, but it also requires `silverstripe/asset-admin`, `silversrtipe/cms` and `silverstripe/campaign-admin`. + +To run the pattern library locally, you'll need a SilverStripe project based on `silverstripe/recipe-cms` and `yarn` installed locally. The pattern library requires the JS source files so you'll need to use the `--prefer-source` flag when installing your dependencies with Composer. + +```bash +composer install --prefer-source +(cd vendor/silverstripe/asset-admin && yarn install) +(cd vendor/silverstripe/campaign-admin && yarn install) +(cd vendor/silverstripe/cms && yarn install) +cd vendor/silverstripe/admin && yarn install && yarn pattern-lib ``` -Then browse to `http://localhost:6006/` +The pattern library will be available at [http://localhost:6006](http://localhost:6006). The JS source files will be watched, so every time you make a change to a JavaScript file, the pattern library will automatically update itself. +If you want to build a static version of the pattern library, you can replace `yarn pattern-lib` with `yarn build-storybook`. This will output the pattern library files to a `storybook-static` folder. + +The SilverStripe pattern library is built using the [StoryBook JS library](https://storybook.js.org/). You can read the StoryBook documentation to learn about more advanced features and customisation options. ## The Admin URL From e72fc9e3d0f35a1d43f55f83f9919f67d72fb7cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sam=20Minn=C3=A9e?= Date: Thu, 25 Oct 2018 11:42:45 +1300 Subject: [PATCH 026/175] FIX DataObject singleton creation (#8516) Ensure DataObject instances are aware they are singletons so functions like populateDefaults() can be skipped. (fixes #4878) Correctly applies https://github.com/silverstripe/silverstripe-framework/pull/7850 to the 4.x line This has already been fixed in 3.x --- src/Core/Injector/Injector.php | 9 +++++++ tests/php/ORM/DataObjectTest.php | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/Core/Injector/Injector.php b/src/Core/Injector/Injector.php index 6b0885132..6cf980cbf 100644 --- a/src/Core/Injector/Injector.php +++ b/src/Core/Injector/Injector.php @@ -13,6 +13,7 @@ use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Environment; use SilverStripe\Dev\Deprecation; +use SilverStripe\ORM\DataObject; /** * A simple injection manager that manages creating objects and injecting @@ -581,6 +582,14 @@ class Injector implements ContainerInterface $constructorParams = $spec['constructor']; } + // If we're dealing with a DataObject singleton without specific constructor params, pass through Singleton + // flag as second argument + if ((!$type || $type !== self::PROTOTYPE) + && empty($constructorParams) + && is_subclass_of($class, DataObject::class)) { + $constructorParams = array(null, true); + } + $factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator(); $object = $factory->create($class, $constructorParams); diff --git a/tests/php/ORM/DataObjectTest.php b/tests/php/ORM/DataObjectTest.php index 55c2ea659..3a2158c4c 100644 --- a/tests/php/ORM/DataObjectTest.php +++ b/tests/php/ORM/DataObjectTest.php @@ -66,6 +66,51 @@ class DataObjectTest extends SapphireTest ); } + /** + * @dataProvider provideSingletons + */ + public function testSingleton($inst, $defaultValue, $altDefaultValue) + { + $inst = $inst(); + // Test that populateDefaults() isn't called on singletons + // which can lead to SQL errors during build, and endless loops + if ($defaultValue) { + $this->assertEquals($defaultValue, $inst->MyFieldWithDefault); + } else { + $this->assertEmpty($inst->MyFieldWithDefault); + } + + if ($altDefaultValue) { + $this->assertEquals($altDefaultValue, $inst->MyFieldWithAltDefault); + } else { + $this->assertEmpty($inst->MyFieldWithAltDefault); + } + } + + public function provideSingletons() + { + // because PHPUnit evalutes test providers *before* setUp methods + // any extensions added in the setUp methods won't be available + // we must return closures to generate the arguments at run time + return array( + 'create() static method' => array(function () { + return DataObjectTest\Fixture::create(); + }, 'Default Value', 'Default Value'), + 'New object creation' => array(function () { + return new DataObjectTest\Fixture(); + }, 'Default Value', 'Default Value'), + 'singleton() function' => array(function () { + return singleton(DataObjectTest\Fixture::class); + }, null, null), + 'singleton() static method' => array(function () { + return DataObjectTest\Fixture::singleton(); + }, null, null), + 'Manual constructor args' => array(function () { + return new DataObjectTest\Fixture(null, true); + }, null, null), + ); + } + public function testDb() { $schema = DataObject::getSchema(); From d879148bffe31506345257b731d75756570b971a Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Fri, 26 Oct 2018 14:28:15 +1300 Subject: [PATCH 027/175] Add config to force legacy filter header globally --- docs/en/04_Changelogs/4.3.0.md | 6 ++++++ src/Forms/GridField/GridFieldFilterHeader.php | 15 +++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/en/04_Changelogs/4.3.0.md b/docs/en/04_Changelogs/4.3.0.md index 9dba198d9..3965ad378 100644 --- a/docs/en/04_Changelogs/4.3.0.md +++ b/docs/en/04_Changelogs/4.3.0.md @@ -26,6 +26,12 @@ To enable the legacy search API on a `GridFieldFilterHeader`, you can either: * set the `useLegacyFilterHeader` property to `true`, * or pass `true` to the first argument of its constructor. +To force the legacy search API on all instances of `GridFieldFilterHeader`, you can set it in your [configuration file](../../configuration): +```yml +SilverStripe\Forms\GridField\GridFieldFilterHeader: + force_legacy: true +``` + ```php public function getCMSFields() { diff --git a/src/Forms/GridField/GridFieldFilterHeader.php b/src/Forms/GridField/GridFieldFilterHeader.php index fd3dd8ea9..13ad12354 100755 --- a/src/Forms/GridField/GridFieldFilterHeader.php +++ b/src/Forms/GridField/GridFieldFilterHeader.php @@ -6,6 +6,7 @@ use LogicException; use SilverStripe\Admin\LeftAndMain; use SilverStripe\Control\Controller; use SilverStripe\Control\HTTPResponse; +use SilverStripe\Core\Config\Config; use SilverStripe\Core\Convert; use SilverStripe\Dev\Deprecation; use SilverStripe\Forms\FieldGroup; @@ -43,6 +44,16 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi */ public $useLegacyFilterHeader = false; + /** + * Forces all filter components to revert to displaying the legacy + * table header style rather than the react driven search box + * + * @deprecated 4.3.0:5.0.0 Will be removed in 5.0 + * @config + * @var bool + */ + private static $force_legacy = false; + /** * @var \SilverStripe\ORM\Search\SearchContext */ @@ -76,7 +87,7 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi } /** - * @param bool $useLegacy + * @param bool $useLegacy This will be removed in 5.0 * @param callable|null $updateSearchContext This will be removed in 5.0 * @param callable|null $updateSearchForm This will be removed in 5.0 */ @@ -85,7 +96,7 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi callable $updateSearchContext = null, callable $updateSearchForm = null ) { - $this->useLegacyFilterHeader = $useLegacy; + $this->useLegacyFilterHeader = Config::inst()->get(self::class, 'force_legacy') || $useLegacy; $this->updateSearchContextCallback = $updateSearchContext; $this->updateSearchFormCallback = $updateSearchForm; } From 3284bf48d6e3da8b2b1a7831e2d7fe4b401e2fd6 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Fri, 26 Oct 2018 14:43:56 +1300 Subject: [PATCH 028/175] Fix search filtering relations and clear filters (#8477) --- src/Forms/GridField/GridFieldFilterHeader.php | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Forms/GridField/GridFieldFilterHeader.php b/src/Forms/GridField/GridFieldFilterHeader.php index fd3dd8ea9..ab0dd58e0 100755 --- a/src/Forms/GridField/GridFieldFilterHeader.php +++ b/src/Forms/GridField/GridFieldFilterHeader.php @@ -154,7 +154,7 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi * If the GridField has a filterable datalist, return an array of actions * * @param GridField $gridField - * @return array + * @return void */ public function handleAction(GridField $gridField, $actionName, $arguments, $data) { @@ -163,14 +163,13 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi } $state = $gridField->State->GridFieldFilterHeader; + $state->Columns = null; if ($actionName === 'filter') { if (isset($data['filter'][$gridField->getName()])) { foreach ($data['filter'][$gridField->getName()] as $key => $filter) { $state->Columns->$key = $filter; } } - } elseif ($actionName === 'reset') { - $state->Columns = null; } } @@ -193,12 +192,10 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi $filterArguments = $columns->toArray(); $dataListClone = clone($dataList); - foreach ($filterArguments as $columnName => $value) { - if ($dataList->canFilterBy($columnName) && $value) { - $dataListClone = $dataListClone->filter($columnName . ':PartialMatch', $value); - } - } - return $dataListClone; + $results = $this->getSearchContext($gridField) + ->getQuery($filterArguments, false, false, $dataListClone); + + return $results; } /** @@ -337,9 +334,11 @@ class GridFieldFilterHeader implements GridField_URLHandler, GridField_HTMLProvi $field->addExtraClass('stacked'); } + $name = $gridField->Title ?: singleton($gridField->getModelClass())->i18n_plural_name(); + $this->searchForm = $form = new Form( $gridField, - "SearchForm", + $name . "SearchForm", $searchFields, new FieldList() ); From 1c6e222391d37f879c8575c94d125e99780cbbef Mon Sep 17 00:00:00 2001 From: Serge Latyntcev Date: Mon, 29 Oct 2018 15:46:54 +1300 Subject: [PATCH 029/175] Fix the GitHub issue template Add description of how to find the installed framework version in CMS for 4.3.0 and later. Fixes #8530 --- .github/ISSUE_TEMPLATE.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 778f3ce7f..9fcd614d2 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,9 @@ ## Affected Version Show version numbers by pasting the output of `composer info --direct`. -Alternatively, hover over the SilverStripe logo in the CMS to basic version information. +Alternatively, get the framework version information from the CMS. +In SilverStripe 4.3 and newer you may find the Help menu in the bottom left corner, unfold it and hover over the version number to get the information. +Otherwise, simply hover over the SilverStripe logo in the bottom left corner of the CMS. ## Description @@ -11,4 +13,4 @@ Please read https://docs.silverstripe.org/en/contributing/issues_and_bugs/ ## Steps to Reproduce -Help us with step-by-step instructions. \ No newline at end of file +Help us with step-by-step instructions. From c7b8b80e8b33a12667dfa9e5147bfbc583ccfc56 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Thu, 1 Nov 2018 11:12:52 +1300 Subject: [PATCH 030/175] Persist GridField readonly state, add view button (#8535) * Persist GridField readonly state, add view button * Minor clarity fixes --- src/Forms/GridField/GridField.php | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Forms/GridField/GridField.php b/src/Forms/GridField/GridField.php index 0585e3703..a180fa5dc 100644 --- a/src/Forms/GridField/GridField.php +++ b/src/Forms/GridField/GridField.php @@ -113,11 +113,13 @@ class GridField extends FormField protected $readonlyComponents = [ GridField_ActionMenu::class, GridFieldConfig_RecordViewer::class, + GridFieldButtonRow::class, GridFieldDataColumns::class, GridFieldDetailForm::class, GridFieldLazyLoader::class, GridFieldPageCount::class, GridFieldPaginator::class, + GridFieldFilterHeader::class, GridFieldSortableHeader::class, GridFieldToolbarHeader::class, GridFieldViewButton::class, @@ -241,16 +243,22 @@ class GridField extends FormField { $copy = clone $this; $copy->setReadonly(true); + $copyConfig = $copy->getConfig(); // get the whitelist for allowable readonly components $allowedComponents = $this->getReadonlyComponents(); foreach ($this->getConfig()->getComponents() as $component) { // if a component doesn't exist, remove it from the readonly version. if (!in_array(get_class($component), $allowedComponents)) { - $copy->getConfig()->removeComponent($component); + $copyConfig->removeComponent($component); } } + // As the edit button may have been removed, add a view button if it doesn't have one + if (!$copyConfig->getComponentByType(GridFieldViewButton::class)) { + $copyConfig->addComponent(new GridFieldViewButton); + } + return $copy; } @@ -290,6 +298,18 @@ class GridField extends FormField return $this; } + /** + * @param bool $readonly + * + * @return $this + */ + public function setReadonly($readonly) + { + parent::setReadonly($readonly); + $this->getState()->Readonly = $readonly; + return $this; + } + /** * @return ArrayList */ @@ -1009,6 +1029,9 @@ class GridField extends FormField } if ($request->getHeader('X-Pjax') === 'CurrentField') { + if ($this->getState()->Readonly) { + $this->performDisabledTransformation(); + } return $this->FieldHolder(); } From 7086f2ea3a86184c0d6aa3eb3ebe04fe3a9a9164 Mon Sep 17 00:00:00 2001 From: Michael Strong Date: Thu, 1 Nov 2018 13:42:27 +1300 Subject: [PATCH 031/175] BUGFIX many many through not sorting by join table (#8534) * BUGFIX many many through not sorting by join table * #8534 added docs to support many many sorting fix * #8534 added test cases for many_many default sorting --- .../00_Model/02_Relations.md | 15 +++++++ src/ORM/DataObject.php | 6 +++ src/ORM/ManyManyThroughQueryManipulator.php | 7 ---- tests/php/ORM/ManyManyListTest.php | 41 +++++++++++++++++++ tests/php/ORM/ManyManyThroughListTest.php | 33 +++++++++++++++ tests/php/ORM/ManyManyThroughListTest.yml | 25 +++++++++++ .../FallbackLocale.php | 22 ++++++++++ .../ORM/ManyManyThroughListTest/Locale.php | 36 ++++++++++++++++ 8 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 tests/php/ORM/ManyManyThroughListTest/FallbackLocale.php create mode 100644 tests/php/ORM/ManyManyThroughListTest/Locale.php diff --git a/docs/en/02_Developer_Guides/00_Model/02_Relations.md b/docs/en/02_Developer_Guides/00_Model/02_Relations.md index bc0876f4f..938de1a9d 100644 --- a/docs/en/02_Developer_Guides/00_Model/02_Relations.md +++ b/docs/en/02_Developer_Guides/00_Model/02_Relations.md @@ -295,6 +295,15 @@ class Supporter extends DataObject } ``` +To ensure this `many_many` is sorted by "Ranking" by default you can add this to your config: + +```yaml +Team_Supporters: + default_sort: '"Team_Supporter"."Ranking" ASC' +``` + +`Team_Supporters` is the table name automatically generated for the many_many relation in this case. + ### many_many through relationship joined on a separate DataObject If necessary, a third DataObject class can instead be specified as the joining table, @@ -312,6 +321,9 @@ This is declared via array syntax, with the following keys on the many_many: - `from` Name of the has_one relationship pointing back at the object declaring many_many - `to` Name of the has_one relationship pointing to the object declaring belongs_many_many. +Just like a any normal DataObject, you can apply a default sort which will be applied when +accessing many many through relations. + Note: The `through` class must not also be the name of any field or relation on the parent or child record. @@ -348,6 +360,8 @@ class TeamSupporter extends DataObject 'Team' => Team::class, 'Supporter' => Supporter::class, ]; + + private static $default_sort = '"TeamSupporter"."Ranking" ASC' } ``` @@ -468,6 +482,7 @@ the best way to think about it is that the object where the relationship will be Product => Categories, the `Product` should contain the `many_many`, because it is much more likely that the user will select Categories for a Product than vice-versa. + ## Cascading deletions Relationships between objects can cause cascading deletions, if necessary, through configuration of the diff --git a/src/ORM/DataObject.php b/src/ORM/DataObject.php index 1092afc32..a47034381 100644 --- a/src/ORM/DataObject.php +++ b/src/ORM/DataObject.php @@ -2051,6 +2051,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $query->setQueryParam('Component.ExtraFields', $extraFields); }); + // If we have a default sort set for our "join" then we should overwrite any default already set. + $joinSort = Config::inst()->get($manyManyComponent['join'], 'default_sort'); + if (!empty($joinSort)) { + $result = $result->sort($joinSort); + } + $this->extend('updateManyManyComponents', $result); // If this is called on a singleton, then we return an 'orphaned relation' that can have the diff --git a/src/ORM/ManyManyThroughQueryManipulator.php b/src/ORM/ManyManyThroughQueryManipulator.php index 2024a65aa..6fffbbe60 100644 --- a/src/ORM/ManyManyThroughQueryManipulator.php +++ b/src/ORM/ManyManyThroughQueryManipulator.php @@ -254,13 +254,6 @@ class ManyManyThroughQueryManipulator implements DataQueryManipulator ); } - // Set a default sort from the join model if available and nothing is already set - if (empty($sqlSelect->getOrderBy()) - && $sort = Config::inst()->get($this->getJoinClass(), 'default_sort') - ) { - $sqlSelect->setOrderBy($sort); - } - // Apply join and record sql for later insertion (at end of replacements) // By using a string placeholder $$_SUBQUERY_$$ we protect field/table rewrites from interfering twice // on the already-finalised inner list diff --git a/tests/php/ORM/ManyManyListTest.php b/tests/php/ORM/ManyManyListTest.php index aa6218474..564864f35 100644 --- a/tests/php/ORM/ManyManyListTest.php +++ b/tests/php/ORM/ManyManyListTest.php @@ -2,6 +2,7 @@ namespace SilverStripe\ORM\Tests; +use SilverStripe\Core\Config\Config; use SilverStripe\ORM\FieldType\DBMoney; use SilverStripe\ORM\ManyManyList; use SilverStripe\Core\Convert; @@ -369,6 +370,46 @@ class ManyManyListTest extends SapphireTest $this->assertSQLEquals($expected, $list->sql($parameters)); } + /** + * This tests that we can set a default sort on a join table, even though the class doesn't exist. + * + * @return void + */ + public function testSortByExtraFieldsDefaultSort() + { + $obj = new ManyManyListTest\ExtraFieldsObject(); + $obj->write(); + + $obj2 = new ManyManyListTest\ExtraFieldsObject(); + $obj2->write(); + + $money = new DBMoney(); + $money->setAmount(100); + $money->setCurrency('USD'); + + // Add two objects as relations (first is linking back to itself) + $obj->Clients()->add($obj, ['Worth' => $money, 'Reference' => 'A']); + $obj->Clients()->add($obj2, ['Worth' => $money, 'Reference' => 'B']); + + // Set the default sort for this relation + Config::inst()->update('ManyManyListTest_ExtraFields_Clients', 'default_sort', 'Reference ASC'); + $clients = $obj->Clients(); + $this->assertCount(2, $clients); + + list($first, $second) = $obj->Clients(); + $this->assertEquals('A', $first->Reference); + $this->assertEquals('B', $second->Reference); + + // Now we ensure the default sort is being respected by reversing its order + Config::inst()->update('ManyManyListTest_ExtraFields_Clients', 'default_sort', 'Reference DESC'); + $reverseClients = $obj->Clients(); + $this->assertCount(2, $reverseClients); + + list($reverseFirst, $reverseSecond) = $obj->Clients(); + $this->assertEquals('B', $reverseFirst->Reference); + $this->assertEquals('A', $reverseSecond->Reference); + } + public function testFilteringOnPreviouslyJoinedTable() { /** @var ManyManyListTest\Category $category */ diff --git a/tests/php/ORM/ManyManyThroughListTest.php b/tests/php/ORM/ManyManyThroughListTest.php index b85c6317b..0b18cbec5 100644 --- a/tests/php/ORM/ManyManyThroughListTest.php +++ b/tests/php/ORM/ManyManyThroughListTest.php @@ -2,11 +2,14 @@ namespace SilverStripe\ORM\Tests; +use SilverStripe\Core\Config\Config; use SilverStripe\Dev\SapphireTest; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\ManyManyThroughList; use SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyItem; use SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyJoinObject; +use SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale; +use SilverStripe\ORM\Tests\ManyManyThroughListTest\FallbackLocale; class ManyManyThroughListTest extends SapphireTest { @@ -20,6 +23,8 @@ class ManyManyThroughListTest extends SapphireTest ManyManyThroughListTest\PolyJoinObject::class, ManyManyThroughListTest\PolyObjectA::class, ManyManyThroughListTest\PolyObjectB::class, + ManyManyThroughListTest\Locale::class, + ManyManyThroughListTest\FallbackLocale::class, ]; protected function setUp() @@ -320,4 +325,32 @@ class ManyManyThroughListTest extends SapphireTest $this->assertEquals($joinTable, $objB1->Items()->getJoinTable()); $this->assertEquals($joinTable, $objB2->Items()->getJoinTable()); } + + /** + * This tests that default sort works when the join table has a default sort set, and the main + * dataobject has a default sort set. + * + * @return void + */ + public function testDefaultSortOnJoinAndMain() + { + // We have spanish mexico with two fall back locales; argentina and international sorted in that order. + $mexico = $this->objFromFixture(Locale::class, 'mexico'); + + $fallbacks = $mexico->Fallbacks(); + $this->assertCount(2, $fallbacks); + + // Ensure the default sort is is correct + list($first, $second) = $fallbacks; + $this->assertSame('Argentina', $first->Title); + $this->assertSame('International', $second->Title); + + // Ensure that we're respecting the default sort by reversing it + Config::inst()->update(FallbackLocale::class, 'default_sort', '"ManyManyThroughTest_FallbackLocale"."Sort" DESC'); + + $reverse = $mexico->Fallbacks(); + list($firstReverse, $secondReverse) = $reverse; + $this->assertSame('International', $firstReverse->Title); + $this->assertSame('Argentina', $secondReverse->Title); + } } diff --git a/tests/php/ORM/ManyManyThroughListTest.yml b/tests/php/ORM/ManyManyThroughListTest.yml index 88750a29d..c7c3c3c2a 100644 --- a/tests/php/ORM/ManyManyThroughListTest.yml +++ b/tests/php/ORM/ManyManyThroughListTest.yml @@ -51,3 +51,28 @@ SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyJoinObject: Sort: 2 Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyObjectB.objb2 Child: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyItem.child2 +SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale: + international: + Title: 'International' + Locale: 'en_NZ' + URLSegment: 'international' + IsGlobalDefault: 1 + mexico: + Title: 'Mexico' + Locale: 'es_MX' + URLSegment: 'mexico' + IsGlobalDefault: 0 + argentina: + Title: 'Argentina' + Locale: 'es_AR' + URLSegment: 'argentina' + IsGlobalDefault: 0 +SilverStripe\ORM\Tests\ManyManyThroughListTest\FallbackLocale: + mexico_international: + Sort: 2 + Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale.mexico + Locale: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale.international + mexico_argentina: + Sort: 1 + Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale.mexico + Locale: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale.argentina \ No newline at end of file diff --git a/tests/php/ORM/ManyManyThroughListTest/FallbackLocale.php b/tests/php/ORM/ManyManyThroughListTest/FallbackLocale.php new file mode 100644 index 000000000..cb865028f --- /dev/null +++ b/tests/php/ORM/ManyManyThroughListTest/FallbackLocale.php @@ -0,0 +1,22 @@ + 'Int', + ]; + + private static $has_one = [ + 'Parent' => Locale::class, + 'Locale' => Locale::class, + ]; + + private static $table_name = 'ManyManyThroughTest_FallbackLocale'; + + private static $default_sort = 'Sort'; +} diff --git a/tests/php/ORM/ManyManyThroughListTest/Locale.php b/tests/php/ORM/ManyManyThroughListTest/Locale.php new file mode 100644 index 000000000..37bff23d9 --- /dev/null +++ b/tests/php/ORM/ManyManyThroughListTest/Locale.php @@ -0,0 +1,36 @@ + 'Varchar(100)', + 'Locale' => 'Varchar(10)', + 'URLSegment' => 'Varchar(100)', + 'IsGlobalDefault' => 'Boolean', + ]; + + private static $has_many = [ + 'FallbackLocales' => FallbackLocale::class . '.Parent', + ]; + + private static $many_many = [ + 'Fallbacks' => [ + 'through' => FallbackLocale::class, + 'from' => 'Parent', + 'to' => 'Locale', + ], + ]; + + private static $default_sort = '"ManyManyThroughTest_Locale"."Locale" ASC'; +} From 55f95b7bc8f91384df459bd70c87cacf92225f68 Mon Sep 17 00:00:00 2001 From: Michael Strong Date: Thu, 1 Nov 2018 13:42:27 +1300 Subject: [PATCH 032/175] BUGFIX many many through not sorting by join table (#8534) * BUGFIX many many through not sorting by join table * #8534 added docs to support many many sorting fix * #8534 added test cases for many_many default sorting --- .../00_Model/02_Relations.md | 15 +++++++ src/ORM/DataObject.php | 6 +++ src/ORM/ManyManyThroughQueryManipulator.php | 7 ---- tests/php/ORM/ManyManyListTest.php | 41 +++++++++++++++++++ tests/php/ORM/ManyManyThroughListTest.php | 33 +++++++++++++++ tests/php/ORM/ManyManyThroughListTest.yml | 25 +++++++++++ .../FallbackLocale.php | 22 ++++++++++ .../ORM/ManyManyThroughListTest/Locale.php | 36 ++++++++++++++++ 8 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 tests/php/ORM/ManyManyThroughListTest/FallbackLocale.php create mode 100644 tests/php/ORM/ManyManyThroughListTest/Locale.php diff --git a/docs/en/02_Developer_Guides/00_Model/02_Relations.md b/docs/en/02_Developer_Guides/00_Model/02_Relations.md index bc0876f4f..938de1a9d 100644 --- a/docs/en/02_Developer_Guides/00_Model/02_Relations.md +++ b/docs/en/02_Developer_Guides/00_Model/02_Relations.md @@ -295,6 +295,15 @@ class Supporter extends DataObject } ``` +To ensure this `many_many` is sorted by "Ranking" by default you can add this to your config: + +```yaml +Team_Supporters: + default_sort: '"Team_Supporter"."Ranking" ASC' +``` + +`Team_Supporters` is the table name automatically generated for the many_many relation in this case. + ### many_many through relationship joined on a separate DataObject If necessary, a third DataObject class can instead be specified as the joining table, @@ -312,6 +321,9 @@ This is declared via array syntax, with the following keys on the many_many: - `from` Name of the has_one relationship pointing back at the object declaring many_many - `to` Name of the has_one relationship pointing to the object declaring belongs_many_many. +Just like a any normal DataObject, you can apply a default sort which will be applied when +accessing many many through relations. + Note: The `through` class must not also be the name of any field or relation on the parent or child record. @@ -348,6 +360,8 @@ class TeamSupporter extends DataObject 'Team' => Team::class, 'Supporter' => Supporter::class, ]; + + private static $default_sort = '"TeamSupporter"."Ranking" ASC' } ``` @@ -468,6 +482,7 @@ the best way to think about it is that the object where the relationship will be Product => Categories, the `Product` should contain the `many_many`, because it is much more likely that the user will select Categories for a Product than vice-versa. + ## Cascading deletions Relationships between objects can cause cascading deletions, if necessary, through configuration of the diff --git a/src/ORM/DataObject.php b/src/ORM/DataObject.php index 1092afc32..a47034381 100644 --- a/src/ORM/DataObject.php +++ b/src/ORM/DataObject.php @@ -2051,6 +2051,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $query->setQueryParam('Component.ExtraFields', $extraFields); }); + // If we have a default sort set for our "join" then we should overwrite any default already set. + $joinSort = Config::inst()->get($manyManyComponent['join'], 'default_sort'); + if (!empty($joinSort)) { + $result = $result->sort($joinSort); + } + $this->extend('updateManyManyComponents', $result); // If this is called on a singleton, then we return an 'orphaned relation' that can have the diff --git a/src/ORM/ManyManyThroughQueryManipulator.php b/src/ORM/ManyManyThroughQueryManipulator.php index 2024a65aa..6fffbbe60 100644 --- a/src/ORM/ManyManyThroughQueryManipulator.php +++ b/src/ORM/ManyManyThroughQueryManipulator.php @@ -254,13 +254,6 @@ class ManyManyThroughQueryManipulator implements DataQueryManipulator ); } - // Set a default sort from the join model if available and nothing is already set - if (empty($sqlSelect->getOrderBy()) - && $sort = Config::inst()->get($this->getJoinClass(), 'default_sort') - ) { - $sqlSelect->setOrderBy($sort); - } - // Apply join and record sql for later insertion (at end of replacements) // By using a string placeholder $$_SUBQUERY_$$ we protect field/table rewrites from interfering twice // on the already-finalised inner list diff --git a/tests/php/ORM/ManyManyListTest.php b/tests/php/ORM/ManyManyListTest.php index aa6218474..564864f35 100644 --- a/tests/php/ORM/ManyManyListTest.php +++ b/tests/php/ORM/ManyManyListTest.php @@ -2,6 +2,7 @@ namespace SilverStripe\ORM\Tests; +use SilverStripe\Core\Config\Config; use SilverStripe\ORM\FieldType\DBMoney; use SilverStripe\ORM\ManyManyList; use SilverStripe\Core\Convert; @@ -369,6 +370,46 @@ class ManyManyListTest extends SapphireTest $this->assertSQLEquals($expected, $list->sql($parameters)); } + /** + * This tests that we can set a default sort on a join table, even though the class doesn't exist. + * + * @return void + */ + public function testSortByExtraFieldsDefaultSort() + { + $obj = new ManyManyListTest\ExtraFieldsObject(); + $obj->write(); + + $obj2 = new ManyManyListTest\ExtraFieldsObject(); + $obj2->write(); + + $money = new DBMoney(); + $money->setAmount(100); + $money->setCurrency('USD'); + + // Add two objects as relations (first is linking back to itself) + $obj->Clients()->add($obj, ['Worth' => $money, 'Reference' => 'A']); + $obj->Clients()->add($obj2, ['Worth' => $money, 'Reference' => 'B']); + + // Set the default sort for this relation + Config::inst()->update('ManyManyListTest_ExtraFields_Clients', 'default_sort', 'Reference ASC'); + $clients = $obj->Clients(); + $this->assertCount(2, $clients); + + list($first, $second) = $obj->Clients(); + $this->assertEquals('A', $first->Reference); + $this->assertEquals('B', $second->Reference); + + // Now we ensure the default sort is being respected by reversing its order + Config::inst()->update('ManyManyListTest_ExtraFields_Clients', 'default_sort', 'Reference DESC'); + $reverseClients = $obj->Clients(); + $this->assertCount(2, $reverseClients); + + list($reverseFirst, $reverseSecond) = $obj->Clients(); + $this->assertEquals('B', $reverseFirst->Reference); + $this->assertEquals('A', $reverseSecond->Reference); + } + public function testFilteringOnPreviouslyJoinedTable() { /** @var ManyManyListTest\Category $category */ diff --git a/tests/php/ORM/ManyManyThroughListTest.php b/tests/php/ORM/ManyManyThroughListTest.php index b85c6317b..0b18cbec5 100644 --- a/tests/php/ORM/ManyManyThroughListTest.php +++ b/tests/php/ORM/ManyManyThroughListTest.php @@ -2,11 +2,14 @@ namespace SilverStripe\ORM\Tests; +use SilverStripe\Core\Config\Config; use SilverStripe\Dev\SapphireTest; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\ManyManyThroughList; use SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyItem; use SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyJoinObject; +use SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale; +use SilverStripe\ORM\Tests\ManyManyThroughListTest\FallbackLocale; class ManyManyThroughListTest extends SapphireTest { @@ -20,6 +23,8 @@ class ManyManyThroughListTest extends SapphireTest ManyManyThroughListTest\PolyJoinObject::class, ManyManyThroughListTest\PolyObjectA::class, ManyManyThroughListTest\PolyObjectB::class, + ManyManyThroughListTest\Locale::class, + ManyManyThroughListTest\FallbackLocale::class, ]; protected function setUp() @@ -320,4 +325,32 @@ class ManyManyThroughListTest extends SapphireTest $this->assertEquals($joinTable, $objB1->Items()->getJoinTable()); $this->assertEquals($joinTable, $objB2->Items()->getJoinTable()); } + + /** + * This tests that default sort works when the join table has a default sort set, and the main + * dataobject has a default sort set. + * + * @return void + */ + public function testDefaultSortOnJoinAndMain() + { + // We have spanish mexico with two fall back locales; argentina and international sorted in that order. + $mexico = $this->objFromFixture(Locale::class, 'mexico'); + + $fallbacks = $mexico->Fallbacks(); + $this->assertCount(2, $fallbacks); + + // Ensure the default sort is is correct + list($first, $second) = $fallbacks; + $this->assertSame('Argentina', $first->Title); + $this->assertSame('International', $second->Title); + + // Ensure that we're respecting the default sort by reversing it + Config::inst()->update(FallbackLocale::class, 'default_sort', '"ManyManyThroughTest_FallbackLocale"."Sort" DESC'); + + $reverse = $mexico->Fallbacks(); + list($firstReverse, $secondReverse) = $reverse; + $this->assertSame('International', $firstReverse->Title); + $this->assertSame('Argentina', $secondReverse->Title); + } } diff --git a/tests/php/ORM/ManyManyThroughListTest.yml b/tests/php/ORM/ManyManyThroughListTest.yml index 88750a29d..c7c3c3c2a 100644 --- a/tests/php/ORM/ManyManyThroughListTest.yml +++ b/tests/php/ORM/ManyManyThroughListTest.yml @@ -51,3 +51,28 @@ SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyJoinObject: Sort: 2 Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyObjectB.objb2 Child: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyItem.child2 +SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale: + international: + Title: 'International' + Locale: 'en_NZ' + URLSegment: 'international' + IsGlobalDefault: 1 + mexico: + Title: 'Mexico' + Locale: 'es_MX' + URLSegment: 'mexico' + IsGlobalDefault: 0 + argentina: + Title: 'Argentina' + Locale: 'es_AR' + URLSegment: 'argentina' + IsGlobalDefault: 0 +SilverStripe\ORM\Tests\ManyManyThroughListTest\FallbackLocale: + mexico_international: + Sort: 2 + Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale.mexico + Locale: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale.international + mexico_argentina: + Sort: 1 + Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale.mexico + Locale: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale.argentina \ No newline at end of file diff --git a/tests/php/ORM/ManyManyThroughListTest/FallbackLocale.php b/tests/php/ORM/ManyManyThroughListTest/FallbackLocale.php new file mode 100644 index 000000000..cb865028f --- /dev/null +++ b/tests/php/ORM/ManyManyThroughListTest/FallbackLocale.php @@ -0,0 +1,22 @@ + 'Int', + ]; + + private static $has_one = [ + 'Parent' => Locale::class, + 'Locale' => Locale::class, + ]; + + private static $table_name = 'ManyManyThroughTest_FallbackLocale'; + + private static $default_sort = 'Sort'; +} diff --git a/tests/php/ORM/ManyManyThroughListTest/Locale.php b/tests/php/ORM/ManyManyThroughListTest/Locale.php new file mode 100644 index 000000000..37bff23d9 --- /dev/null +++ b/tests/php/ORM/ManyManyThroughListTest/Locale.php @@ -0,0 +1,36 @@ + 'Varchar(100)', + 'Locale' => 'Varchar(10)', + 'URLSegment' => 'Varchar(100)', + 'IsGlobalDefault' => 'Boolean', + ]; + + private static $has_many = [ + 'FallbackLocales' => FallbackLocale::class . '.Parent', + ]; + + private static $many_many = [ + 'Fallbacks' => [ + 'through' => FallbackLocale::class, + 'from' => 'Parent', + 'to' => 'Locale', + ], + ]; + + private static $default_sort = '"ManyManyThroughTest_Locale"."Locale" ASC'; +} From 2ff7ee6752cc505fd538a12e1a0c1709231961a8 Mon Sep 17 00:00:00 2001 From: Guy Marriott Date: Thu, 1 Nov 2018 19:51:15 +1300 Subject: [PATCH 033/175] NEW Deprecate RandomGenerator::generateEntropy in favour of using random_bytes directly --- src/Security/RandomGenerator.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Security/RandomGenerator.php b/src/Security/RandomGenerator.php index b7279e0b6..89c223eb9 100644 --- a/src/Security/RandomGenerator.php +++ b/src/Security/RandomGenerator.php @@ -4,6 +4,7 @@ namespace SilverStripe\Security; use Error; use Exception; +use SilverStripe\Dev\Deprecation; /** * Convenience class for generating cryptographically secure pseudo-random strings/tokens @@ -13,9 +14,12 @@ class RandomGenerator /** * @return string A 128-character, randomly generated ASCII string * @throws Exception If no suitable CSPRNG is installed + * @deprecated 4.4:5.0 */ public function generateEntropy() { + Deprecation::notice('4.4', __METHOD__ . ' has been deprecated. Use random_bytes instead'); + try { return bin2hex(random_bytes(64)); } catch (Error $e) { @@ -38,9 +42,10 @@ class RandomGenerator * * @param string $algorithm Any identifier listed in hash_algos() (Default: whirlpool) * @return string Returned length will depend on the used $algorithm + * @throws Exception When there is no valid source of CSPRNG */ public function randomToken($algorithm = 'whirlpool') { - return hash($algorithm, $this->generateEntropy()); + return hash($algorithm, random_bytes(64)); } } From 46e4c19070688df74757bd9a44dc2f0b2fdcd6a9 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Thu, 1 Nov 2018 22:38:26 +1300 Subject: [PATCH 034/175] MINOR Add a reference to SS_Object in .upgrade.yml to allow upgrade from SS3.7 --- .upgrade.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.upgrade.yml b/.upgrade.yml index e4e2c392a..33d42e81e 100644 --- a/.upgrade.yml +++ b/.upgrade.yml @@ -958,6 +958,9 @@ warnings: 'Object': message: 'Replaced with traits' url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#object-replace' + 'SS_Object': + message: 'Replaced with traits' + url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#object-replace' 'SS_Log': message: 'Replaced with a PSR-3 logger' url: 'https://docs.silverstripe.org/en/4/changelogs/4.0.0#psr3-logging' From 8866e7674a1a9c2be48c8e9532cfcaa667cdf7b5 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Fri, 2 Nov 2018 12:26:53 +1300 Subject: [PATCH 035/175] BUG: Fix duplicate plugins on HTML editor fields (#8559) * BUG: Fix duplicate plugins on HTML editor fields * Add new test --- src/Forms/HTMLEditor/HTMLEditorConfig.php | 2 ++ src/Forms/HTMLEditor/TinyMCECombinedGenerator.php | 2 ++ tests/php/Forms/HTMLEditor/HTMLEditorConfigTest.php | 2 +- tests/php/Forms/HTMLEditor/TinyMCEConfigTest.php | 7 +++++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Forms/HTMLEditor/HTMLEditorConfig.php b/src/Forms/HTMLEditor/HTMLEditorConfig.php index 27a1527aa..6672971d8 100644 --- a/src/Forms/HTMLEditor/HTMLEditorConfig.php +++ b/src/Forms/HTMLEditor/HTMLEditorConfig.php @@ -83,6 +83,7 @@ abstract class HTMLEditorConfig // Create new instance if unconfigured if (!isset(self::$configs[$identifier])) { self::$configs[$identifier] = static::create(); + self::$configs[$identifier]->setOption('editorIdentifier', $identifier); } return self::$configs[$identifier]; } @@ -98,6 +99,7 @@ abstract class HTMLEditorConfig { if ($config) { self::$configs[$identifier] = $config; + self::$configs[$identifier]->setOption('editorIdentifier', $identifier); } else { unset(self::$configs[$identifier]); } diff --git a/src/Forms/HTMLEditor/TinyMCECombinedGenerator.php b/src/Forms/HTMLEditor/TinyMCECombinedGenerator.php index f2f3a3b09..a037cb355 100644 --- a/src/Forms/HTMLEditor/TinyMCECombinedGenerator.php +++ b/src/Forms/HTMLEditor/TinyMCECombinedGenerator.php @@ -130,11 +130,13 @@ class TinyMCECombinedGenerator implements TinyMCEScriptGenerator, Flushable // Register vars for config $baseDirJS = Convert::raw2js(Director::absoluteBaseURL()); + $name = Convert::raw2js($this->checkName($config)); $buffer = []; $buffer[] = << + +You are being redirected. If you are not redirected soon, click here to continue +HTML; + + // Build response + $result = new HTTPResponse($body); + $result->redirect($location); + return $result; + } + + /** + * Is this parameter requested without a valid token? + * + * @return bool True if the parameter is given without a valid token + */ + abstract public function reloadRequired(); + + /** + * Check if this token is provided either in the backurl, or directly, + * but without a token + * + * @return bool + */ + abstract public function reloadRequiredIfError(); + + /** + * Suppress the current parameter for the duration of this request + */ + abstract public function suppress(); + + /** + * Determine the querystring parameters to include + * + * @param bool $includeToken Include the token value? + * @return array List of querystring parameters, possibly including token parameter + */ + abstract public function params($includeToken = true); + + /** + * Get redirection URL + * + * @return string + */ + abstract protected function redirectURL(); +} diff --git a/src/Core/Startup/ErrorControlChain.php b/src/Core/Startup/ErrorControlChain.php index f34a2c802..e2d14db65 100644 --- a/src/Core/Startup/ErrorControlChain.php +++ b/src/Core/Startup/ErrorControlChain.php @@ -15,8 +15,7 @@ use Exception; * $chain = new ErrorControlChain(); * $chain->then($callback1)->then($callback2)->thenIfErrored($callback3)->execute(); * - * WARNING: This class is experimental and designed specifically for use pre-startup. - * It will likely be heavily refactored before the release of 3.2 + * @internal This class is designed specifically for use pre-startup and may change without warning */ class ErrorControlChain { diff --git a/src/Core/Startup/ErrorControlChainMiddleware.php b/src/Core/Startup/ErrorControlChainMiddleware.php index bdb5ff0a3..e81444629 100644 --- a/src/Core/Startup/ErrorControlChainMiddleware.php +++ b/src/Core/Startup/ErrorControlChainMiddleware.php @@ -12,6 +12,8 @@ use SilverStripe\Security\Security; /** * Decorates application bootstrapping with errorcontrolchain + * + * @internal This class is designed specifically for use pre-startup and may change without warning */ class ErrorControlChainMiddleware implements HTTPMiddleware { @@ -30,27 +32,42 @@ class ErrorControlChainMiddleware implements HTTPMiddleware $this->application = $application; } + /** + * @param HTTPRequest $request + * @return ConfirmationToken|null + */ + protected function prepareConfirmationTokenIfRequired(HTTPRequest $request) + { + $token = URLConfirmationToken::prepare_tokens(['dev/build'], $request); + + if (!$token) { + $token = ParameterConfirmationToken::prepare_tokens( + ['isTest', 'isDev', 'flush'], + $request + ); + } + + return $token; + } + public function process(HTTPRequest $request, callable $next) { $result = null; // Prepare tokens and execute chain - $reloadToken = ParameterConfirmationToken::prepare_tokens( - ['isTest', 'isDev', 'flush'], - $request - ); + $confirmationToken = $this->prepareConfirmationTokenIfRequired($request); $chain = new ErrorControlChain(); $chain - ->then(function () use ($request, $chain, $reloadToken, $next, &$result) { + ->then(function () use ($request, $chain, $confirmationToken, $next, &$result) { // If no redirection is necessary then we can disable error supression - if (!$reloadToken) { + if (!$confirmationToken) { $chain->setSuppression(false); } try { // Check if a token is requesting a redirect - if ($reloadToken && $reloadToken->reloadRequired()) { - $result = $this->safeReloadWithToken($request, $reloadToken); + if ($confirmationToken && $confirmationToken->reloadRequired()) { + $result = $this->safeReloadWithToken($request, $confirmationToken); } else { // If no reload necessary, process application $result = call_user_func($next, $request); @@ -60,9 +77,9 @@ class ErrorControlChainMiddleware implements HTTPMiddleware } }) // Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway - ->thenIfErrored(function () use ($reloadToken) { - if ($reloadToken && $reloadToken->reloadRequiredIfError()) { - $result = $reloadToken->reloadWithToken(); + ->thenIfErrored(function () use ($confirmationToken) { + if ($confirmationToken && $confirmationToken->reloadRequiredIfError()) { + $result = $confirmationToken->reloadWithToken(); $result->output(); } }) @@ -85,7 +102,7 @@ class ErrorControlChainMiddleware implements HTTPMiddleware // Ensure session is started $request->getSession()->init($request); - + // Request with ErrorDirector $result = ErrorDirector::singleton()->handleRequestWithToken( $request, @@ -98,7 +115,7 @@ class ErrorControlChainMiddleware implements HTTPMiddleware // Fail and redirect the user to the login page $params = array_merge($request->getVars(), $reloadToken->params(false)); - $backURL = $request->getURL() . '?' . http_build_query($params); + $backURL = $reloadToken->currentURL() . '?' . http_build_query($params); $loginPage = Director::absoluteURL(Security::config()->get('login_url')); $loginPage .= "?BackURL=" . urlencode($backURL); $result = new HTTPResponse(); diff --git a/src/Core/Startup/ErrorDirector.php b/src/Core/Startup/ErrorDirector.php index 3c994c6af..54001fd05 100644 --- a/src/Core/Startup/ErrorDirector.php +++ b/src/Core/Startup/ErrorDirector.php @@ -21,11 +21,11 @@ class ErrorDirector extends Director * Redirect with token if allowed, or null if not allowed * * @param HTTPRequest $request - * @param ParameterConfirmationToken $token + * @param ConfirmationToken $token * @param Kernel $kernel * @return null|HTTPResponse Redirection response, or null if not able to redirect */ - public function handleRequestWithToken(HTTPRequest $request, ParameterConfirmationToken $token, Kernel $kernel) + public function handleRequestWithToken(HTTPRequest $request, ConfirmationToken $token, Kernel $kernel) { Injector::inst()->registerService($request, HTTPRequest::class); diff --git a/src/Core/Startup/ParameterConfirmationToken.php b/src/Core/Startup/ParameterConfirmationToken.php index 1c80db1d0..4e90f1ef7 100644 --- a/src/Core/Startup/ParameterConfirmationToken.php +++ b/src/Core/Startup/ParameterConfirmationToken.php @@ -9,30 +9,21 @@ use SilverStripe\Core\Convert; use SilverStripe\Security\RandomGenerator; /** - * Class ParameterConfirmationToken + * This is used to protect dangerous GET parameters that need to be detected early in the request + * lifecycle by generating a one-time-use token & redirecting with that token included in the + * redirected URL * - * When you need to use a dangerous GET parameter that needs to be set before core/Core.php is - * established, this class takes care of allowing some other code of confirming the parameter, - * by generating a one-time-use token & redirecting with that token included in the redirected URL - * - * WARNING: This class is experimental and designed specifically for use pre-startup. - * It will likely be heavily refactored before the release of 3.2 + * @internal This class is designed specifically for use pre-startup and may change without warning */ -class ParameterConfirmationToken +class ParameterConfirmationToken extends ConfirmationToken { - /** * The name of the parameter * * @var string */ protected $parameterName = null; - - /** - * @var HTTPRequest - */ - protected $request = null; - + /** * The parameter given in the main request * @@ -48,60 +39,6 @@ class ParameterConfirmationToken protected $parameterBackURL = null; /** - * The validated and checked token for this parameter - * - * @var string|null A string value, or null if either not provided or invalid - */ - protected $token = null; - - protected function pathForToken($token) - { - return TEMP_PATH . DIRECTORY_SEPARATOR . 'token_' . preg_replace('/[^a-z0-9]+/', '', $token); - } - - /** - * Generate a new random token and store it - * - * @return string Token name - */ - protected function genToken() - { - // Generate a new random token (as random as possible) - $rg = new RandomGenerator(); - $token = $rg->randomToken('md5'); - - // Store a file in the session save path (safer than /tmp, as open_basedir might limit that) - file_put_contents($this->pathForToken($token), $token); - - return $token; - } - - /** - * Validate a token - * - * @param string $token - * @return boolean True if the token is valid - */ - protected function checkToken($token) - { - if (!$token) { - return false; - } - - $file = $this->pathForToken($token); - $content = null; - - if (file_exists($file)) { - $content = file_get_contents($file); - unlink($file); - } - - return $content == $token; - } - - /** - * Create a new ParameterConfirmationToken - * * @param string $parameterName Name of the querystring parameter to check * @param HTTPRequest $request */ @@ -176,54 +113,23 @@ class ParameterConfirmationToken return $this->parameterBackURL !== null; } - /** - * Is the necessary token provided for this parameter? - * A value must be provided for the token - * - * @return bool - */ - public function tokenProvided() - { - return !empty($this->token); - } - - /** - * Is this parameter requested without a valid token? - * - * @return bool True if the parameter is given without a valid token - */ public function reloadRequired() { return $this->parameterProvided() && !$this->tokenProvided(); } - /** - * Check if this token is provided either in the backurl, or directly, - * but without a token - * - * @return bool - */ public function reloadRequiredIfError() { // Don't reload if token exists return $this->reloadRequired() || $this->existsInReferer(); } - - /** - * Suppress the current parameter by unsetting it from $_GET - */ + public function suppress() { unset($_GET[$this->parameterName]); $this->request->offsetUnset($this->parameterName); } - /** - * Determine the querystring parameters to include - * - * @param bool $includeToken Include the token value as well? - * @return array List of querystring parameters with name and token parameters - */ public function params($includeToken = true) { $params = array( @@ -234,25 +140,7 @@ class ParameterConfirmationToken } return $params; } - - /** - * Get redirect url, excluding querystring - * - * @return string - */ - protected function currentURL() - { - return Controller::join_links( - BASE_URL ?: '/', - $this->request->getURL(false) - ); - } - - /** - * Get redirection URL - * - * @return string - */ + protected function redirectURL() { // If url is encoded via BackURL, defer to home page (prevent redirect to form action) @@ -267,48 +155,4 @@ class ParameterConfirmationToken // Merge get params with current url return Controller::join_links($url, '?' . http_build_query($params)); } - - /** - * Forces a reload of the request with the token included - * - * @return HTTPResponse - */ - public function reloadWithToken() - { - $location = $this->redirectURL(); - $locationJS = Convert::raw2js($location); - $locationATT = Convert::raw2att($location); - $body = <<location.href='$locationJS'; - -You are being redirected. If you are not redirected soon, click here to continue the flush -HTML; - - // Build response - $result = new HTTPResponse($body); - $result->redirect($location); - return $result; - } - - /** - * Given a list of token names, suppress all tokens that have not been validated, and - * return the non-validated token with the highest priority - * - * @param array $keys List of token keys in ascending priority (low to high) - * @param HTTPRequest $request - * @return ParameterConfirmationToken The token container for the unvalidated $key given with the highest priority - */ - public static function prepare_tokens($keys, HTTPRequest $request) - { - $target = null; - foreach ($keys as $key) { - $token = new ParameterConfirmationToken($key, $request); - // Validate this token - if ($token->reloadRequired() || $token->reloadRequiredIfError()) { - $token->suppress(); - $target = $token; - } - } - return $target; - } } diff --git a/src/Core/Startup/URLConfirmationToken.php b/src/Core/Startup/URLConfirmationToken.php new file mode 100644 index 000000000..8176a41b8 --- /dev/null +++ b/src/Core/Startup/URLConfirmationToken.php @@ -0,0 +1,136 @@ +urlToCheck = $urlToCheck; + $this->request = $request; + $this->currentURL = $request->getURL(false); + + $this->tokenParameterName = preg_replace('/[^a-z0-9]/i', '', $urlToCheck) . 'token'; + $this->urlExistsInBackURL = $this->getURLExistsInBackURL($request); + + // If the token provided is valid, mark it as such + $token = $request->getVar($this->tokenParameterName); + if ($this->checkToken($token)) { + $this->token = $token; + } + } + + /** + * @param HTTPRequest $request + * @return bool + */ + protected function getURLExistsInBackURL(HTTPRequest $request) + { + $backURL = $request->getVar('BackURL'); + return (strpos($backURL, $this->urlToCheck) === 0); + } + + /** + * @return bool + */ + protected function urlMatches() + { + return ($this->currentURL === $this->urlToCheck); + } + + /** + * @return string + */ + public function getURLToCheck() + { + return $this->urlToCheck; + } + + /** + * @return bool + */ + public function urlExistsInBackURL() + { + return $this->urlExistsInBackURL; + } + + public function reloadRequired() + { + return $this->urlMatches() && !$this->tokenProvided(); + } + + public function reloadRequiredIfError() + { + return $this->reloadRequired() || $this->urlExistsInBackURL(); + } + + public function suppress() + { + $_SERVER['REQUEST_URI'] = '/'; + $this->request->setURL('/'); + } + + public function params($includeToken = true) + { + $params = []; + if ($includeToken) { + $params[$this->tokenParameterName] = $this->genToken(); + } + + return $params; + } + + public function currentURL() + { + return Controller::join_links(Director::baseURL(), $this->currentURL); + } + + protected function redirectURL() + { + // If url is encoded via BackURL, defer to home page (prevent redirect to form action) + if ($this->urlExistsInBackURL && !$this->urlMatches()) { + $url = BASE_URL ?: '/'; + $params = $this->params(); + } else { + $url = $this->currentURL(); + $params = array_merge($this->request->getVars(), $this->params()); + } + + // Merge get params with current url + return Controller::join_links($url, '?' . http_build_query($params)); + } +} diff --git a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php index 5cce89d3e..7df6f6ebf 100644 --- a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php +++ b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php @@ -73,4 +73,52 @@ class ErrorControlChainMiddlewareTest extends SapphireTest $this->assertNotContains('?flush=1&flushtoken=', $location); $this->assertContains('Security/login', $location); } + + public function testLiveBuildAdmin() + { + // Mock admin + $adminID = $this->logInWithPermission('ADMIN'); + $this->logOut(); + + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as admin + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build/'); + $request->setSession(new Session(['loggedInAs' => $adminID])); + $result = $chain->process($request, function () { + return null; + }); + + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertContains('/dev/build', $location); + $this->assertContains('?devbuildtoken=', $location); + $this->assertNotContains('Security/login', $location); + } + + public function testLiveBuildUnauthenticated() + { + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as no one + Security::setCurrentUser(null); + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build'); + $request->setSession(new Session(['loggedInAs' => 0])); + $result = $chain->process($request, function () { + return null; + }); + + // Should be directed to login, not to flush + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertNotContains('/dev/build', $location); + $this->assertNotContains('?devbuildtoken=', $location); + $this->assertContains('Security/login', $location); + } } diff --git a/tests/php/Core/Startup/ParameterConfirmationTokenTest.php b/tests/php/Core/Startup/ParameterConfirmationTokenTest.php index 66616433f..e28af8ea5 100644 --- a/tests/php/Core/Startup/ParameterConfirmationTokenTest.php +++ b/tests/php/Core/Startup/ParameterConfirmationTokenTest.php @@ -149,14 +149,14 @@ class ParameterConfirmationTokenTest extends SapphireTest } /** - * currentAbsoluteURL needs to handle base or url being missing, or any combination of slashes. + * currentURL needs to handle base or url being missing, or any combination of slashes. * * There should always be exactly one slash between each part in the result, and any trailing slash * should be preserved. * * @dataProvider dataProviderURLs */ - public function testCurrentAbsoluteURLHandlesSlashes($url) + public function testCurrentURLHandlesSlashes($url) { $this->request->setUrl($url); diff --git a/tests/php/Core/Startup/URLConfirmationTokenTest.php b/tests/php/Core/Startup/URLConfirmationTokenTest.php new file mode 100644 index 000000000..73b07eb13 --- /dev/null +++ b/tests/php/Core/Startup/URLConfirmationTokenTest.php @@ -0,0 +1,148 @@ + 'value']); + $validToken = new StubValidToken('token/test/url', $request); + $this->assertTrue($validToken->urlMatches()); + $this->assertFalse($validToken->urlExistsInBackURL()); + $this->assertTrue($validToken->tokenProvided()); // Actually forced to true for this test + $this->assertFalse($validToken->reloadRequired()); + $this->assertFalse($validToken->reloadRequiredIfError()); + $this->assertStringStartsWith(Controller::join_links(BASE_URL, '/', 'token/test/url'), $validToken->redirectURL()); + } + + public function testTokenWithLeadingSlashInUrl() + { + $request = new HTTPRequest('GET', '/leading/slash/url', []); + $leadingSlash = new StubToken('leading/slash/url', $request); + $this->assertTrue($leadingSlash->urlMatches()); + $this->assertFalse($leadingSlash->urlExistsInBackURL()); + $this->assertFalse($leadingSlash->tokenProvided()); + $this->assertTrue($leadingSlash->reloadRequired()); + $this->assertTrue($leadingSlash->reloadRequiredIfError()); + $this->assertContains('leading/slash/url', $leadingSlash->redirectURL()); + $this->assertContains('leadingslashurltoken', $leadingSlash->redirectURL()); + } + + public function testTokenWithTrailingSlashInUrl() + { + $request = new HTTPRequest('GET', 'trailing/slash/url/', []); + $trailingSlash = new StubToken('trailing/slash/url', $request); + $this->assertTrue($trailingSlash->urlMatches()); + $this->assertFalse($trailingSlash->urlExistsInBackURL()); + $this->assertFalse($trailingSlash->tokenProvided()); + $this->assertTrue($trailingSlash->reloadRequired()); + $this->assertTrue($trailingSlash->reloadRequiredIfError()); + $this->assertContains('trailing/slash/url', $trailingSlash->redirectURL()); + $this->assertContains('trailingslashurltoken', $trailingSlash->redirectURL()); + } + + public function testTokenWithUrlMatchedInBackUrl() + { + $request = new HTTPRequest('GET', '/', ['BackURL' => 'back/url']); + $backUrl = new StubToken('back/url', $request); + $this->assertFalse($backUrl->urlMatches()); + $this->assertTrue($backUrl->urlExistsInBackURL()); + $this->assertFalse($backUrl->tokenProvided()); + $this->assertFalse($backUrl->reloadRequired()); + $this->assertTrue($backUrl->reloadRequiredIfError()); + $home = (BASE_URL ?: '/') . '?'; + $this->assertStringStartsWith($home, $backUrl->redirectURL()); + $this->assertContains('backurltoken', $backUrl->redirectURL()); + } + + public function testUrlSuppressionWhenTokenMissing() + { + // Check suppression + $request = new HTTPRequest('GET', 'test/url', []); + $token = new StubToken('test/url', $request); + $this->assertEquals('test/url', $request->getURL(false)); + $token->suppress(); + $this->assertEquals('', $request->getURL(false)); + } + + public function testPrepareTokens() + { + $request = new HTTPRequest('GET', 'test/url', []); + $token = URLConfirmationToken::prepare_tokens( + [ + 'test/url', + 'test', + 'url' + ], + $request + ); + // Test no invalid tokens + $this->assertEquals('test/url', $token->getURLToCheck()); + $this->assertNotEquals('test/url', $request->getURL(false), 'prepare_tokens() did not suppress URL'); + } + + public function testPrepareTokensDoesntSuppressWhenNotMatched() + { + $request = new HTTPRequest('GET', 'test/url', []); + $token = URLConfirmationToken::prepare_tokens( + ['another/url'], + $request + ); + $this->assertEmpty($token); + $this->assertEquals('test/url', $request->getURL(false), 'prepare_tokens() incorrectly suppressed URL'); + } + + public function testPrepareTokensWithUrlMatchedInBackUrl() + { + // Test backurl token + $request = new HTTPRequest('GET', '/', ['BackURL' => 'back/url']); + $token = URLConfirmationToken::prepare_tokens( + [ 'back/url' ], + $request + ); + $this->assertNotEmpty($token); + $this->assertEquals('back/url', $token->getURLToCheck()); + $this->assertNotEquals('back/url', $request->getURL(false), 'prepare_tokens() did not suppress URL'); + } + + public function dataProviderURLs() + { + return [ + [''], + ['/'], + ['bar'], + ['bar/'], + ['/bar'], + ['/bar/'], + ]; + } + + /** + * currentURL needs to handle base or url being missing, or any combination of slashes. + * + * There should always be exactly one slash between each part in the result, and any trailing slash + * should be preserved. + * + * @dataProvider dataProviderURLs + */ + public function testCurrentURLHandlesSlashes($url) + { + $request = new HTTPRequest('GET', $url, []); + + $token = new StubToken( + 'another/url', + $request + ); + $expected = rtrim(Controller::join_links(BASE_URL, '/', $url), '/') ?: '/'; + $this->assertEquals($expected, $token->currentURL(), "Invalid redirect for request url $url"); + } +} diff --git a/tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php b/tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php new file mode 100644 index 000000000..ca08d3e1f --- /dev/null +++ b/tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php @@ -0,0 +1,27 @@ + Date: Mon, 30 Jul 2018 11:50:11 +1200 Subject: [PATCH 044/175] [SS-2018-018] Ignore arguments in mysqli::real_connect backtrace calls --- src/Dev/Backtrace.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Dev/Backtrace.php b/src/Dev/Backtrace.php index daceca751..648cc883f 100644 --- a/src/Dev/Backtrace.php +++ b/src/Dev/Backtrace.php @@ -26,6 +26,7 @@ class Backtrace array('PDO', '__construct'), array('mysqli', 'mysqli'), array('mysqli', 'select_db'), + array('mysqli', 'real_connect'), array('SilverStripe\\ORM\\DB', 'connect'), array('SilverStripe\\Security\\Security', 'check_default_admin'), array('SilverStripe\\Security\\Security', 'encrypt_password'), From 0877442c6409ad94aa57d32debc875b636cf9be0 Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Fri, 24 Aug 2018 15:36:51 +0100 Subject: [PATCH 045/175] Implement ConfirmationTokenChain to handle multiple tokens at once --- ...oken.php => AbstractConfirmationToken.php} | 12 +- src/Core/Startup/ConfirmationTokenChain.php | 178 +++++++++++++++++ .../Startup/ErrorControlChainMiddleware.php | 62 +++--- src/Core/Startup/ErrorDirector.php | 13 +- .../Startup/ParameterConfirmationToken.php | 28 +-- src/Core/Startup/URLConfirmationToken.php | 29 +-- .../Startup/ConfirmationTokenChainTest.php | 185 ++++++++++++++++++ .../ErrorControlChainMiddlewareTest.php | 52 +++++ 8 files changed, 500 insertions(+), 59 deletions(-) rename src/Core/Startup/{ConfirmationToken.php => AbstractConfirmationToken.php} (95%) create mode 100644 src/Core/Startup/ConfirmationTokenChain.php create mode 100644 tests/php/Core/Startup/ConfirmationTokenChainTest.php diff --git a/src/Core/Startup/ConfirmationToken.php b/src/Core/Startup/AbstractConfirmationToken.php similarity index 95% rename from src/Core/Startup/ConfirmationToken.php rename to src/Core/Startup/AbstractConfirmationToken.php index d23563f46..11f78b48a 100644 --- a/src/Core/Startup/ConfirmationToken.php +++ b/src/Core/Startup/AbstractConfirmationToken.php @@ -15,7 +15,7 @@ use SilverStripe\Security\RandomGenerator; * * @internal This class is designed specifically for use pre-startup and may change without warning */ -abstract class ConfirmationToken +abstract class AbstractConfirmationToken { /** * @var HTTPRequest @@ -173,6 +173,16 @@ HTML; */ abstract public function params($includeToken = true); + /** + * @return string + */ + abstract public function getRedirectUrlBase(); + + /** + * @return array + */ + abstract public function getRedirectUrlParams(); + /** * Get redirection URL * diff --git a/src/Core/Startup/ConfirmationTokenChain.php b/src/Core/Startup/ConfirmationTokenChain.php new file mode 100644 index 000000000..a47f2c4c0 --- /dev/null +++ b/src/Core/Startup/ConfirmationTokenChain.php @@ -0,0 +1,178 @@ +tokens[] = $token; + } + + /** + * Collect all tokens that require a redirect + * + * @return \Generator + */ + protected function filteredTokens() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequired() || $token->reloadRequiredIfError()) { + yield $token; + } + } + } + + /** + * @return bool + */ + public function suppressionRequired() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequired()) { + return true; + } + } + + return false; + } + + /** + * Suppress URLs & GET vars from tokens that require a redirect + */ + public function suppressTokens() + { + foreach ($this->filteredTokens() as $token) { + $token->suppress(); + } + } + + /** + * @return bool + */ + public function reloadRequired() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequired()) { + return true; + } + } + + return false; + } + + /** + * @return bool + */ + public function reloadRequiredIfError() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequiredIfError()) { + return true; + } + } + + return false; + } + + /** + * @param bool $includeToken + * @return array + */ + public function params($includeToken = true) + { + $params = []; + foreach ($this->tokens as $token) { + $params = array_merge($params, $token->params($includeToken)); + } + + return $params; + } + + /** + * Fetch the URL we want to redirect to, excluding query string parameters. This may + * be the same URL (with a token to be added outside this method), or to a different + * URL if the current one has been suppressed + * + * @return string + */ + public function getRedirectUrlBase() + { + // URLConfirmationTokens may alter the URL to suppress the URL they're protecting, + // so we need to ensure they're inspected last and therefore take priority + $tokens = iterator_to_array($this->filteredTokens(), false); + usort($tokens, function ($a, $b) { + return ($a instanceof URLConfirmationToken) ? 1 : 0; + }); + + $urlBase = Director::baseURL(); + foreach ($tokens as $token) { + $urlBase = $token->getRedirectUrlBase(); + } + + return $urlBase; + } + + /** + * Collate GET vars from all token providers that need to apply a token + * + * @return array + */ + public function getRedirectUrlParams() + { + $params = []; + foreach ($this->filteredTokens() as $token) { + $params = array_merge($params, $token->getRedirectUrlParams()); + } + + return $params; + } + + /** + * @return string + */ + protected function redirectURL() + { + $params = http_build_query($this->getRedirectUrlParams()); + return Controller::join_links($this->getRedirectUrlBase(), '?' . $params); + } + + /** + * @return HTTPResponse + */ + public function reloadWithTokens() + { + $location = $this->redirectURL(); + $locationJS = Convert::raw2js($location); + $locationATT = Convert::raw2att($location); + $body = <<location.href='$locationJS'; + +You are being redirected. If you are not redirected soon, click here to continue +HTML; + + // Build response + $result = new HTTPResponse($body); + $result->redirect($location); + return $result; + } +} diff --git a/src/Core/Startup/ErrorControlChainMiddleware.php b/src/Core/Startup/ErrorControlChainMiddleware.php index e81444629..c29878e15 100644 --- a/src/Core/Startup/ErrorControlChainMiddleware.php +++ b/src/Core/Startup/ErrorControlChainMiddleware.php @@ -34,20 +34,18 @@ class ErrorControlChainMiddleware implements HTTPMiddleware /** * @param HTTPRequest $request - * @return ConfirmationToken|null + * @return ConfirmationTokenChain */ - protected function prepareConfirmationTokenIfRequired(HTTPRequest $request) + protected function prepareConfirmationTokenChain(HTTPRequest $request) { - $token = URLConfirmationToken::prepare_tokens(['dev/build'], $request); + $chain = new ConfirmationTokenChain(); + $chain->pushToken(new URLConfirmationToken('dev/build', $request)); - if (!$token) { - $token = ParameterConfirmationToken::prepare_tokens( - ['isTest', 'isDev', 'flush'], - $request - ); + foreach (['isTest', 'isDev', 'flush'] as $parameter) { + $chain->pushToken(new ParameterConfirmationToken($parameter, $request)); } - return $token; + return $chain; } public function process(HTTPRequest $request, callable $next) @@ -55,19 +53,21 @@ class ErrorControlChainMiddleware implements HTTPMiddleware $result = null; // Prepare tokens and execute chain - $confirmationToken = $this->prepareConfirmationTokenIfRequired($request); - $chain = new ErrorControlChain(); - $chain - ->then(function () use ($request, $chain, $confirmationToken, $next, &$result) { - // If no redirection is necessary then we can disable error supression - if (!$confirmationToken) { - $chain->setSuppression(false); + $confirmationTokenChain = $this->prepareConfirmationTokenChain($request); + $errorControlChain = new ErrorControlChain(); + $errorControlChain + ->then(function () use ($request, $errorControlChain, $confirmationTokenChain, $next, &$result) { + if ($confirmationTokenChain->suppressionRequired()) { + $confirmationTokenChain->suppressTokens(); + } else { + // If no redirection is necessary then we can disable error supression + $errorControlChain->setSuppression(false); } try { // Check if a token is requesting a redirect - if ($confirmationToken && $confirmationToken->reloadRequired()) { - $result = $this->safeReloadWithToken($request, $confirmationToken); + if ($confirmationTokenChain && $confirmationTokenChain->reloadRequired()) { + $result = $this->safeReloadWithTokens($request, $confirmationTokenChain); } else { // If no reload necessary, process application $result = call_user_func($next, $request); @@ -77,10 +77,16 @@ class ErrorControlChainMiddleware implements HTTPMiddleware } }) // Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway - ->thenIfErrored(function () use ($confirmationToken) { - if ($confirmationToken && $confirmationToken->reloadRequiredIfError()) { - $result = $confirmationToken->reloadWithToken(); - $result->output(); + ->thenIfErrored(function () use ($confirmationTokenChain) { + if ($confirmationTokenChain && $confirmationTokenChain->reloadRequiredIfError()) { + try { + // Reload requires manual boot + $this->getApplication()->getKernel()->boot(false); + } finally { + // Given we're in an error state here, try to continue even if the kernel boot fails + $result = $confirmationTokenChain->reloadWithTokens(); + $result->output(); + } } }) ->execute(); @@ -92,10 +98,10 @@ class ErrorControlChainMiddleware implements HTTPMiddleware * or authentication is impossible. * * @param HTTPRequest $request - * @param ParameterConfirmationToken $reloadToken + * @param ConfirmationTokenChain $confirmationTokenChain * @return HTTPResponse */ - protected function safeReloadWithToken(HTTPRequest $request, $reloadToken) + protected function safeReloadWithTokens(HTTPRequest $request, ConfirmationTokenChain $confirmationTokenChain) { // Safe reload requires manual boot $this->getApplication()->getKernel()->boot(false); @@ -104,9 +110,9 @@ class ErrorControlChainMiddleware implements HTTPMiddleware $request->getSession()->init($request); // Request with ErrorDirector - $result = ErrorDirector::singleton()->handleRequestWithToken( + $result = ErrorDirector::singleton()->handleRequestWithTokenChain( $request, - $reloadToken, + $confirmationTokenChain, $this->getApplication()->getKernel() ); if ($result) { @@ -114,8 +120,8 @@ class ErrorControlChainMiddleware implements HTTPMiddleware } // Fail and redirect the user to the login page - $params = array_merge($request->getVars(), $reloadToken->params(false)); - $backURL = $reloadToken->currentURL() . '?' . http_build_query($params); + $params = array_merge($request->getVars(), $confirmationTokenChain->params(false)); + $backURL = $confirmationTokenChain->getRedirectUrlBase() . '?' . http_build_query($params); $loginPage = Director::absoluteURL(Security::config()->get('login_url')); $loginPage .= "?BackURL=" . urlencode($backURL); $result = new HTTPResponse(); diff --git a/src/Core/Startup/ErrorDirector.php b/src/Core/Startup/ErrorDirector.php index 54001fd05..575df7183 100644 --- a/src/Core/Startup/ErrorDirector.php +++ b/src/Core/Startup/ErrorDirector.php @@ -21,18 +21,21 @@ class ErrorDirector extends Director * Redirect with token if allowed, or null if not allowed * * @param HTTPRequest $request - * @param ConfirmationToken $token + * @param ConfirmationTokenChain $confirmationTokenChain * @param Kernel $kernel * @return null|HTTPResponse Redirection response, or null if not able to redirect */ - public function handleRequestWithToken(HTTPRequest $request, ConfirmationToken $token, Kernel $kernel) - { + public function handleRequestWithTokenChain( + HTTPRequest $request, + ConfirmationTokenChain $confirmationTokenChain, + Kernel $kernel + ) { Injector::inst()->registerService($request, HTTPRequest::class); // Next, check if we're in dev mode, or the database doesn't have any security data, or we are admin - $reload = function (HTTPRequest $request) use ($token, $kernel) { + $reload = function (HTTPRequest $request) use ($confirmationTokenChain, $kernel) { if ($kernel->getEnvironment() === Kernel::DEV || !Security::database_is_ready() || Permission::check('ADMIN')) { - return $token->reloadWithToken(); + return $confirmationTokenChain->reloadWithTokens(); } return null; }; diff --git a/src/Core/Startup/ParameterConfirmationToken.php b/src/Core/Startup/ParameterConfirmationToken.php index 4e90f1ef7..bc751a2c3 100644 --- a/src/Core/Startup/ParameterConfirmationToken.php +++ b/src/Core/Startup/ParameterConfirmationToken.php @@ -3,6 +3,7 @@ namespace SilverStripe\Core\Startup; use SilverStripe\Control\Controller; +use SilverStripe\Control\Director; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; use SilverStripe\Core\Convert; @@ -15,7 +16,7 @@ use SilverStripe\Security\RandomGenerator; * * @internal This class is designed specifically for use pre-startup and may change without warning */ -class ParameterConfirmationToken extends ConfirmationToken +class ParameterConfirmationToken extends AbstractConfirmationToken { /** * The name of the parameter @@ -140,19 +141,22 @@ class ParameterConfirmationToken extends ConfirmationToken } return $params; } + + public function getRedirectUrlBase() + { + return ($this->existsInReferer() && !$this->parameterProvided()) ? Director::baseURL() : $this->currentURL(); + } + + public function getRedirectUrlParams() + { + return ($this->existsInReferer() && !$this->parameterProvided()) + ? $this->params() + : array_merge($this->request->getVars(), $this->params()); + } protected function redirectURL() { - // If url is encoded via BackURL, defer to home page (prevent redirect to form action) - if ($this->existsInReferer() && !$this->parameterProvided()) { - $url = BASE_URL ?: '/'; - $params = $this->params(); - } else { - $url = $this->currentURL(); - $params = array_merge($this->request->getVars(), $this->params()); - } - - // Merge get params with current url - return Controller::join_links($url, '?' . http_build_query($params)); + $query = http_build_query($this->getRedirectUrlParams()); + return Controller::join_links($this->getRedirectUrlBase(), '?' . $query); } } diff --git a/src/Core/Startup/URLConfirmationToken.php b/src/Core/Startup/URLConfirmationToken.php index 8176a41b8..bb509931e 100644 --- a/src/Core/Startup/URLConfirmationToken.php +++ b/src/Core/Startup/URLConfirmationToken.php @@ -12,7 +12,7 @@ use SilverStripe\Control\HTTPRequest; * * @internal This class is designed specifically for use pre-startup and may change without warning */ -class URLConfirmationToken extends ConfirmationToken +class URLConfirmationToken extends AbstractConfirmationToken { /** * @var string @@ -60,7 +60,7 @@ class URLConfirmationToken extends ConfirmationToken */ protected function getURLExistsInBackURL(HTTPRequest $request) { - $backURL = $request->getVar('BackURL'); + $backURL = ltrim($request->getVar('BackURL'), '/'); return (strpos($backURL, $this->urlToCheck) === 0); } @@ -119,18 +119,21 @@ class URLConfirmationToken extends ConfirmationToken return Controller::join_links(Director::baseURL(), $this->currentURL); } + public function getRedirectUrlBase() + { + return ($this->urlExistsInBackURL && !$this->urlMatches()) ? Director::baseURL() : $this->currentURL(); + } + + public function getRedirectUrlParams() + { + return ($this->urlExistsInBackURL && !$this->urlMatches()) + ? $this->params() + : array_merge($this->request->getVars(), $this->params()); + } + protected function redirectURL() { - // If url is encoded via BackURL, defer to home page (prevent redirect to form action) - if ($this->urlExistsInBackURL && !$this->urlMatches()) { - $url = BASE_URL ?: '/'; - $params = $this->params(); - } else { - $url = $this->currentURL(); - $params = array_merge($this->request->getVars(), $this->params()); - } - - // Merge get params with current url - return Controller::join_links($url, '?' . http_build_query($params)); + $query = http_build_query($this->getRedirectUrlParams()); + return Controller::join_links($this->getRedirectUrlBase(), '?' . $query); } } diff --git a/tests/php/Core/Startup/ConfirmationTokenChainTest.php b/tests/php/Core/Startup/ConfirmationTokenChainTest.php new file mode 100644 index 000000000..adb8fba36 --- /dev/null +++ b/tests/php/Core/Startup/ConfirmationTokenChainTest.php @@ -0,0 +1,185 @@ +createPartialMock(ParameterConfirmationToken::class, $methods); + $mock->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue($requiresReload)); + return $mock; + } + + protected function getTokenRequiringReloadIfError($requiresReload = true, $extraMethods = []) + { + $methods = array_merge(['reloadRequired', 'reloadRequiredIfError'], $extraMethods); + $mock = $this->createPartialMock(ParameterConfirmationToken::class, $methods); + $mock->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue(false)); + $mock->expects($this->any()) + ->method('reloadRequiredIfError') + ->will($this->returnValue($requiresReload)); + return $mock; + } + + public function testFilteredTokens() + { + $chain = new ConfirmationTokenChain(); + $chain->pushToken($tokenRequiringReload = $this->getTokenRequiringReload()); + $chain->pushToken($tokenNotRequiringReload = $this->getTokenRequiringReload(false)); + $chain->pushToken($tokenRequiringReloadIfError = $this->getTokenRequiringReloadIfError()); + $chain->pushToken($tokenNotRequiringReloadIfError = $this->getTokenRequiringReloadIfError(false)); + + $reflectionMethod = new \ReflectionMethod(ConfirmationTokenChain::class, 'filteredTokens'); + $reflectionMethod->setAccessible(true); + $tokens = iterator_to_array($reflectionMethod->invoke($chain)); + + $this->assertContains($tokenRequiringReload, $tokens, 'Token requiring a reload was not returned'); + $this->assertNotContains($tokenNotRequiringReload, $tokens, 'Token not requiring a reload was returned'); + $this->assertContains($tokenRequiringReloadIfError, $tokens, 'Token requiring a reload on error was not returned'); + $this->assertNotContains($tokenNotRequiringReloadIfError, $tokens, 'Token not requiring a reload on error was returned'); + } + + public function testSuppressionRequired() + { + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReload(false)); + $this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReloadIfError(false)); + $this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReload()); + $this->assertTrue($chain->suppressionRequired(), 'Suppression not marked as required'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReloadIfError()); + $this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required'); + } + + public function testSuppressTokens() + { + $mockToken = $this->getTokenRequiringReload(true, ['suppress']); + $mockToken->expects($this->once()) + ->method('suppress'); + $secondMockToken = $this->getTokenRequiringReloadIfError(true, ['suppress']); + $secondMockToken->expects($this->once()) + ->method('suppress'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $chain->suppressTokens(); + } + + public function testReloadRequired() + { + $mockToken = $this->getTokenRequiringReload(true); + $secondMockToken = $this->getTokenRequiringReload(false); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertTrue($chain->reloadRequired()); + } + + public function testReloadRequiredIfError() + { + $mockToken = $this->getTokenRequiringReloadIfError(true); + $secondMockToken = $this->getTokenRequiringReloadIfError(false); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertTrue($chain->reloadRequiredIfError()); + } + + public function testParams() + { + $mockToken = $this->getTokenRequiringReload(true, ['params']); + $mockToken->expects($this->once()) + ->method('params') + ->with($this->isTrue()) + ->will($this->returnValue(['mockTokenParam' => '1'])); + $secondMockToken = $this->getTokenRequiringReload(true, ['params']); + $secondMockToken->expects($this->once()) + ->method('params') + ->with($this->isTrue()) + ->will($this->returnValue(['secondMockTokenParam' => '2'])); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertEquals(['mockTokenParam' => '1', 'secondMockTokenParam' => '2'], $chain->params(true)); + + $mockToken = $this->getTokenRequiringReload(true, ['params']); + $mockToken->expects($this->once()) + ->method('params') + ->with($this->isFalse()) + ->will($this->returnValue(['mockTokenParam' => '1'])); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $this->assertEquals(['mockTokenParam' => '1'], $chain->params(false)); + } + + public function testGetRedirectUrlBase() + { + $mockUrlToken = $this->createPartialMock(URLConfirmationToken::class, ['reloadRequired', 'getRedirectUrlBase']); + $mockUrlToken->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue(true)); + $mockUrlToken->expects($this->any()) + ->method('getRedirectUrlBase') + ->will($this->returnValue('url-base')); + + $mockParameterToken = $this->createPartialMock(ParameterConfirmationToken::class, ['reloadRequired', 'getRedirectUrlBase']); + $mockParameterToken->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue(true)); + $mockParameterToken->expects($this->any()) + ->method('getRedirectUrlBase') + ->will($this->returnValue('parameter-base')); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockParameterToken); + $chain->pushToken($mockUrlToken); + $this->assertEquals('url-base', $chain->getRedirectUrlBase(), 'URLConfirmationToken url base should take priority'); + + // Push them in reverse order to check priority still correct + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockUrlToken); + $chain->pushToken($mockParameterToken); + $this->assertEquals('url-base', $chain->getRedirectUrlBase(), 'URLConfirmationToken url base should take priority'); + } + + public function testGetRedirectUrlParams() + { + $mockToken = $this->getTokenRequiringReload(true, ['getRedirectUrlParams']); + $mockToken->expects($this->once()) + ->method('getRedirectUrlParams') + ->will($this->returnValue(['mockTokenParam' => '1'])); + + $secondMockToken = $this->getTokenRequiringReload(true, ['getRedirectUrlParams']); + $secondMockToken->expects($this->once()) + ->method('getRedirectUrlParams') + ->will($this->returnValue(['secondMockTokenParam' => '2'])); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertEquals(['mockTokenParam' => '1', 'secondMockTokenParam' => '2'], $chain->getRedirectUrlParams()); + } +} diff --git a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php index 7df6f6ebf..a90799026 100644 --- a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php +++ b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php @@ -121,4 +121,56 @@ class ErrorControlChainMiddlewareTest extends SapphireTest $this->assertNotContains('?devbuildtoken=', $location); $this->assertContains('Security/login', $location); } + + public function testLiveBuildAndFlushAdmin() + { + // Mock admin + $adminID = $this->logInWithPermission('ADMIN'); + $this->logOut(); + + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as admin + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build/', ['flush' => '1']); + $request->setSession(new Session(['loggedInAs' => $adminID])); + $result = $chain->process($request, function () { + return null; + }); + + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertContains('/dev/build', $location); + $this->assertContains('flush=1', $location); + $this->assertContains('devbuildtoken=', $location); + $this->assertContains('flushtoken=', $location); + $this->assertNotContains('Security/login', $location); + } + + public function testLiveBuildAndFlushUnauthenticated() + { + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as no one + Security::setCurrentUser(null); + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build', ['flush' => '1']); + $request->setSession(new Session(['loggedInAs' => 0])); + $result = $chain->process($request, function () { + return null; + }); + + // Should be directed to login, not to flush + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertNotContains('/dev/build', $location); + $this->assertNotContains('flush=1', $location); + $this->assertNotContains('devbuildtoken=', $location); + $this->assertNotContains('flushtoken=', $location); + $this->assertContains('Security/login', $location); + } } From 0610f76da02ac53a1b51cdfe9eac34e943a66991 Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Tue, 21 Aug 2018 11:20:15 +0100 Subject: [PATCH 046/175] [SS-2018-019] Add confirmation token to dev/build --- src/Control/HTTPApplication.php | 2 +- src/Core/Startup/ConfirmationToken.php | 182 ++++++++++++++++++ src/Core/Startup/ErrorControlChain.php | 3 +- .../Startup/ErrorControlChainMiddleware.php | 43 +++-- src/Core/Startup/ErrorDirector.php | 4 +- .../Startup/ParameterConfirmationToken.php | 172 +---------------- src/Core/Startup/URLConfirmationToken.php | 136 +++++++++++++ .../ErrorControlChainMiddlewareTest.php | 48 +++++ .../ParameterConfirmationTokenTest.php | 4 +- .../Core/Startup/URLConfirmationTokenTest.php | 148 ++++++++++++++ .../URLConfirmationTokenTest/StubToken.php | 27 +++ .../StubValidToken.php | 15 ++ 12 files changed, 600 insertions(+), 184 deletions(-) create mode 100644 src/Core/Startup/ConfirmationToken.php create mode 100644 src/Core/Startup/URLConfirmationToken.php create mode 100644 tests/php/Core/Startup/URLConfirmationTokenTest.php create mode 100644 tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php create mode 100644 tests/php/Core/Startup/URLConfirmationTokenTest/StubValidToken.php diff --git a/src/Control/HTTPApplication.php b/src/Control/HTTPApplication.php index 4d0f7cb9d..35f453d6d 100644 --- a/src/Control/HTTPApplication.php +++ b/src/Control/HTTPApplication.php @@ -41,7 +41,7 @@ class HTTPApplication implements Application */ public function handle(HTTPRequest $request) { - $flush = array_key_exists('flush', $request->getVars()) || strpos($request->getURL(), 'dev/build') === 0; + $flush = array_key_exists('flush', $request->getVars()) || ($request->getURL() === 'dev/build'); // Ensure boot is invoked return $this->execute($request, function (HTTPRequest $request) { diff --git a/src/Core/Startup/ConfirmationToken.php b/src/Core/Startup/ConfirmationToken.php new file mode 100644 index 000000000..d23563f46 --- /dev/null +++ b/src/Core/Startup/ConfirmationToken.php @@ -0,0 +1,182 @@ +reloadRequired() || $token->reloadRequiredIfError()) { + $token->suppress(); + $target = $token; + } + } + return $target; + } + + /** + * Generate a local filesystem path to store a token + * + * @param $token + * @return string + */ + protected function pathForToken($token) + { + return TEMP_PATH . DIRECTORY_SEPARATOR . 'token_' . preg_replace('/[^a-z0-9]+/', '', $token); + } + + /** + * Generate a new random token and store it + * + * @return string Token name + */ + protected function genToken() + { + // Generate a new random token (as random as possible) + $rg = new RandomGenerator(); + $token = $rg->randomToken('md5'); + + // Store a file in the session save path (safer than /tmp, as open_basedir might limit that) + file_put_contents($this->pathForToken($token), $token); + + return $token; + } + + /** + * Is the necessary token provided for this parameter? + * A value must be provided for the token + * + * @return bool + */ + public function tokenProvided() + { + return !empty($this->token); + } + + /** + * Validate a token + * + * @param string $token + * @return boolean True if the token is valid + */ + protected function checkToken($token) + { + if (!$token) { + return false; + } + + $file = $this->pathForToken($token); + $content = null; + + if (file_exists($file)) { + $content = file_get_contents($file); + unlink($file); + } + + return $content === $token; + } + + /** + * Get redirect url, excluding querystring + * + * @return string + */ + public function currentURL() + { + return Controller::join_links(Director::baseURL(), $this->request->getURL(false)); + } + + /** + * Forces a reload of the request with the token included + * + * @return HTTPResponse + */ + public function reloadWithToken() + { + $location = $this->redirectURL(); + $locationJS = Convert::raw2js($location); + $locationATT = Convert::raw2att($location); + $body = <<location.href='$locationJS'; + +You are being redirected. If you are not redirected soon, click here to continue +HTML; + + // Build response + $result = new HTTPResponse($body); + $result->redirect($location); + return $result; + } + + /** + * Is this parameter requested without a valid token? + * + * @return bool True if the parameter is given without a valid token + */ + abstract public function reloadRequired(); + + /** + * Check if this token is provided either in the backurl, or directly, + * but without a token + * + * @return bool + */ + abstract public function reloadRequiredIfError(); + + /** + * Suppress the current parameter for the duration of this request + */ + abstract public function suppress(); + + /** + * Determine the querystring parameters to include + * + * @param bool $includeToken Include the token value? + * @return array List of querystring parameters, possibly including token parameter + */ + abstract public function params($includeToken = true); + + /** + * Get redirection URL + * + * @return string + */ + abstract protected function redirectURL(); +} diff --git a/src/Core/Startup/ErrorControlChain.php b/src/Core/Startup/ErrorControlChain.php index f34a2c802..e2d14db65 100644 --- a/src/Core/Startup/ErrorControlChain.php +++ b/src/Core/Startup/ErrorControlChain.php @@ -15,8 +15,7 @@ use Exception; * $chain = new ErrorControlChain(); * $chain->then($callback1)->then($callback2)->thenIfErrored($callback3)->execute(); * - * WARNING: This class is experimental and designed specifically for use pre-startup. - * It will likely be heavily refactored before the release of 3.2 + * @internal This class is designed specifically for use pre-startup and may change without warning */ class ErrorControlChain { diff --git a/src/Core/Startup/ErrorControlChainMiddleware.php b/src/Core/Startup/ErrorControlChainMiddleware.php index bdb5ff0a3..e81444629 100644 --- a/src/Core/Startup/ErrorControlChainMiddleware.php +++ b/src/Core/Startup/ErrorControlChainMiddleware.php @@ -12,6 +12,8 @@ use SilverStripe\Security\Security; /** * Decorates application bootstrapping with errorcontrolchain + * + * @internal This class is designed specifically for use pre-startup and may change without warning */ class ErrorControlChainMiddleware implements HTTPMiddleware { @@ -30,27 +32,42 @@ class ErrorControlChainMiddleware implements HTTPMiddleware $this->application = $application; } + /** + * @param HTTPRequest $request + * @return ConfirmationToken|null + */ + protected function prepareConfirmationTokenIfRequired(HTTPRequest $request) + { + $token = URLConfirmationToken::prepare_tokens(['dev/build'], $request); + + if (!$token) { + $token = ParameterConfirmationToken::prepare_tokens( + ['isTest', 'isDev', 'flush'], + $request + ); + } + + return $token; + } + public function process(HTTPRequest $request, callable $next) { $result = null; // Prepare tokens and execute chain - $reloadToken = ParameterConfirmationToken::prepare_tokens( - ['isTest', 'isDev', 'flush'], - $request - ); + $confirmationToken = $this->prepareConfirmationTokenIfRequired($request); $chain = new ErrorControlChain(); $chain - ->then(function () use ($request, $chain, $reloadToken, $next, &$result) { + ->then(function () use ($request, $chain, $confirmationToken, $next, &$result) { // If no redirection is necessary then we can disable error supression - if (!$reloadToken) { + if (!$confirmationToken) { $chain->setSuppression(false); } try { // Check if a token is requesting a redirect - if ($reloadToken && $reloadToken->reloadRequired()) { - $result = $this->safeReloadWithToken($request, $reloadToken); + if ($confirmationToken && $confirmationToken->reloadRequired()) { + $result = $this->safeReloadWithToken($request, $confirmationToken); } else { // If no reload necessary, process application $result = call_user_func($next, $request); @@ -60,9 +77,9 @@ class ErrorControlChainMiddleware implements HTTPMiddleware } }) // Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway - ->thenIfErrored(function () use ($reloadToken) { - if ($reloadToken && $reloadToken->reloadRequiredIfError()) { - $result = $reloadToken->reloadWithToken(); + ->thenIfErrored(function () use ($confirmationToken) { + if ($confirmationToken && $confirmationToken->reloadRequiredIfError()) { + $result = $confirmationToken->reloadWithToken(); $result->output(); } }) @@ -85,7 +102,7 @@ class ErrorControlChainMiddleware implements HTTPMiddleware // Ensure session is started $request->getSession()->init($request); - + // Request with ErrorDirector $result = ErrorDirector::singleton()->handleRequestWithToken( $request, @@ -98,7 +115,7 @@ class ErrorControlChainMiddleware implements HTTPMiddleware // Fail and redirect the user to the login page $params = array_merge($request->getVars(), $reloadToken->params(false)); - $backURL = $request->getURL() . '?' . http_build_query($params); + $backURL = $reloadToken->currentURL() . '?' . http_build_query($params); $loginPage = Director::absoluteURL(Security::config()->get('login_url')); $loginPage .= "?BackURL=" . urlencode($backURL); $result = new HTTPResponse(); diff --git a/src/Core/Startup/ErrorDirector.php b/src/Core/Startup/ErrorDirector.php index 3c994c6af..54001fd05 100644 --- a/src/Core/Startup/ErrorDirector.php +++ b/src/Core/Startup/ErrorDirector.php @@ -21,11 +21,11 @@ class ErrorDirector extends Director * Redirect with token if allowed, or null if not allowed * * @param HTTPRequest $request - * @param ParameterConfirmationToken $token + * @param ConfirmationToken $token * @param Kernel $kernel * @return null|HTTPResponse Redirection response, or null if not able to redirect */ - public function handleRequestWithToken(HTTPRequest $request, ParameterConfirmationToken $token, Kernel $kernel) + public function handleRequestWithToken(HTTPRequest $request, ConfirmationToken $token, Kernel $kernel) { Injector::inst()->registerService($request, HTTPRequest::class); diff --git a/src/Core/Startup/ParameterConfirmationToken.php b/src/Core/Startup/ParameterConfirmationToken.php index 1c80db1d0..4e90f1ef7 100644 --- a/src/Core/Startup/ParameterConfirmationToken.php +++ b/src/Core/Startup/ParameterConfirmationToken.php @@ -9,30 +9,21 @@ use SilverStripe\Core\Convert; use SilverStripe\Security\RandomGenerator; /** - * Class ParameterConfirmationToken + * This is used to protect dangerous GET parameters that need to be detected early in the request + * lifecycle by generating a one-time-use token & redirecting with that token included in the + * redirected URL * - * When you need to use a dangerous GET parameter that needs to be set before core/Core.php is - * established, this class takes care of allowing some other code of confirming the parameter, - * by generating a one-time-use token & redirecting with that token included in the redirected URL - * - * WARNING: This class is experimental and designed specifically for use pre-startup. - * It will likely be heavily refactored before the release of 3.2 + * @internal This class is designed specifically for use pre-startup and may change without warning */ -class ParameterConfirmationToken +class ParameterConfirmationToken extends ConfirmationToken { - /** * The name of the parameter * * @var string */ protected $parameterName = null; - - /** - * @var HTTPRequest - */ - protected $request = null; - + /** * The parameter given in the main request * @@ -48,60 +39,6 @@ class ParameterConfirmationToken protected $parameterBackURL = null; /** - * The validated and checked token for this parameter - * - * @var string|null A string value, or null if either not provided or invalid - */ - protected $token = null; - - protected function pathForToken($token) - { - return TEMP_PATH . DIRECTORY_SEPARATOR . 'token_' . preg_replace('/[^a-z0-9]+/', '', $token); - } - - /** - * Generate a new random token and store it - * - * @return string Token name - */ - protected function genToken() - { - // Generate a new random token (as random as possible) - $rg = new RandomGenerator(); - $token = $rg->randomToken('md5'); - - // Store a file in the session save path (safer than /tmp, as open_basedir might limit that) - file_put_contents($this->pathForToken($token), $token); - - return $token; - } - - /** - * Validate a token - * - * @param string $token - * @return boolean True if the token is valid - */ - protected function checkToken($token) - { - if (!$token) { - return false; - } - - $file = $this->pathForToken($token); - $content = null; - - if (file_exists($file)) { - $content = file_get_contents($file); - unlink($file); - } - - return $content == $token; - } - - /** - * Create a new ParameterConfirmationToken - * * @param string $parameterName Name of the querystring parameter to check * @param HTTPRequest $request */ @@ -176,54 +113,23 @@ class ParameterConfirmationToken return $this->parameterBackURL !== null; } - /** - * Is the necessary token provided for this parameter? - * A value must be provided for the token - * - * @return bool - */ - public function tokenProvided() - { - return !empty($this->token); - } - - /** - * Is this parameter requested without a valid token? - * - * @return bool True if the parameter is given without a valid token - */ public function reloadRequired() { return $this->parameterProvided() && !$this->tokenProvided(); } - /** - * Check if this token is provided either in the backurl, or directly, - * but without a token - * - * @return bool - */ public function reloadRequiredIfError() { // Don't reload if token exists return $this->reloadRequired() || $this->existsInReferer(); } - - /** - * Suppress the current parameter by unsetting it from $_GET - */ + public function suppress() { unset($_GET[$this->parameterName]); $this->request->offsetUnset($this->parameterName); } - /** - * Determine the querystring parameters to include - * - * @param bool $includeToken Include the token value as well? - * @return array List of querystring parameters with name and token parameters - */ public function params($includeToken = true) { $params = array( @@ -234,25 +140,7 @@ class ParameterConfirmationToken } return $params; } - - /** - * Get redirect url, excluding querystring - * - * @return string - */ - protected function currentURL() - { - return Controller::join_links( - BASE_URL ?: '/', - $this->request->getURL(false) - ); - } - - /** - * Get redirection URL - * - * @return string - */ + protected function redirectURL() { // If url is encoded via BackURL, defer to home page (prevent redirect to form action) @@ -267,48 +155,4 @@ class ParameterConfirmationToken // Merge get params with current url return Controller::join_links($url, '?' . http_build_query($params)); } - - /** - * Forces a reload of the request with the token included - * - * @return HTTPResponse - */ - public function reloadWithToken() - { - $location = $this->redirectURL(); - $locationJS = Convert::raw2js($location); - $locationATT = Convert::raw2att($location); - $body = <<location.href='$locationJS'; - -You are being redirected. If you are not redirected soon, click here to continue the flush -HTML; - - // Build response - $result = new HTTPResponse($body); - $result->redirect($location); - return $result; - } - - /** - * Given a list of token names, suppress all tokens that have not been validated, and - * return the non-validated token with the highest priority - * - * @param array $keys List of token keys in ascending priority (low to high) - * @param HTTPRequest $request - * @return ParameterConfirmationToken The token container for the unvalidated $key given with the highest priority - */ - public static function prepare_tokens($keys, HTTPRequest $request) - { - $target = null; - foreach ($keys as $key) { - $token = new ParameterConfirmationToken($key, $request); - // Validate this token - if ($token->reloadRequired() || $token->reloadRequiredIfError()) { - $token->suppress(); - $target = $token; - } - } - return $target; - } } diff --git a/src/Core/Startup/URLConfirmationToken.php b/src/Core/Startup/URLConfirmationToken.php new file mode 100644 index 000000000..8176a41b8 --- /dev/null +++ b/src/Core/Startup/URLConfirmationToken.php @@ -0,0 +1,136 @@ +urlToCheck = $urlToCheck; + $this->request = $request; + $this->currentURL = $request->getURL(false); + + $this->tokenParameterName = preg_replace('/[^a-z0-9]/i', '', $urlToCheck) . 'token'; + $this->urlExistsInBackURL = $this->getURLExistsInBackURL($request); + + // If the token provided is valid, mark it as such + $token = $request->getVar($this->tokenParameterName); + if ($this->checkToken($token)) { + $this->token = $token; + } + } + + /** + * @param HTTPRequest $request + * @return bool + */ + protected function getURLExistsInBackURL(HTTPRequest $request) + { + $backURL = $request->getVar('BackURL'); + return (strpos($backURL, $this->urlToCheck) === 0); + } + + /** + * @return bool + */ + protected function urlMatches() + { + return ($this->currentURL === $this->urlToCheck); + } + + /** + * @return string + */ + public function getURLToCheck() + { + return $this->urlToCheck; + } + + /** + * @return bool + */ + public function urlExistsInBackURL() + { + return $this->urlExistsInBackURL; + } + + public function reloadRequired() + { + return $this->urlMatches() && !$this->tokenProvided(); + } + + public function reloadRequiredIfError() + { + return $this->reloadRequired() || $this->urlExistsInBackURL(); + } + + public function suppress() + { + $_SERVER['REQUEST_URI'] = '/'; + $this->request->setURL('/'); + } + + public function params($includeToken = true) + { + $params = []; + if ($includeToken) { + $params[$this->tokenParameterName] = $this->genToken(); + } + + return $params; + } + + public function currentURL() + { + return Controller::join_links(Director::baseURL(), $this->currentURL); + } + + protected function redirectURL() + { + // If url is encoded via BackURL, defer to home page (prevent redirect to form action) + if ($this->urlExistsInBackURL && !$this->urlMatches()) { + $url = BASE_URL ?: '/'; + $params = $this->params(); + } else { + $url = $this->currentURL(); + $params = array_merge($this->request->getVars(), $this->params()); + } + + // Merge get params with current url + return Controller::join_links($url, '?' . http_build_query($params)); + } +} diff --git a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php index 5cce89d3e..7df6f6ebf 100644 --- a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php +++ b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php @@ -73,4 +73,52 @@ class ErrorControlChainMiddlewareTest extends SapphireTest $this->assertNotContains('?flush=1&flushtoken=', $location); $this->assertContains('Security/login', $location); } + + public function testLiveBuildAdmin() + { + // Mock admin + $adminID = $this->logInWithPermission('ADMIN'); + $this->logOut(); + + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as admin + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build/'); + $request->setSession(new Session(['loggedInAs' => $adminID])); + $result = $chain->process($request, function () { + return null; + }); + + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertContains('/dev/build', $location); + $this->assertContains('?devbuildtoken=', $location); + $this->assertNotContains('Security/login', $location); + } + + public function testLiveBuildUnauthenticated() + { + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as no one + Security::setCurrentUser(null); + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build'); + $request->setSession(new Session(['loggedInAs' => 0])); + $result = $chain->process($request, function () { + return null; + }); + + // Should be directed to login, not to flush + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertNotContains('/dev/build', $location); + $this->assertNotContains('?devbuildtoken=', $location); + $this->assertContains('Security/login', $location); + } } diff --git a/tests/php/Core/Startup/ParameterConfirmationTokenTest.php b/tests/php/Core/Startup/ParameterConfirmationTokenTest.php index 66616433f..e28af8ea5 100644 --- a/tests/php/Core/Startup/ParameterConfirmationTokenTest.php +++ b/tests/php/Core/Startup/ParameterConfirmationTokenTest.php @@ -149,14 +149,14 @@ class ParameterConfirmationTokenTest extends SapphireTest } /** - * currentAbsoluteURL needs to handle base or url being missing, or any combination of slashes. + * currentURL needs to handle base or url being missing, or any combination of slashes. * * There should always be exactly one slash between each part in the result, and any trailing slash * should be preserved. * * @dataProvider dataProviderURLs */ - public function testCurrentAbsoluteURLHandlesSlashes($url) + public function testCurrentURLHandlesSlashes($url) { $this->request->setUrl($url); diff --git a/tests/php/Core/Startup/URLConfirmationTokenTest.php b/tests/php/Core/Startup/URLConfirmationTokenTest.php new file mode 100644 index 000000000..73b07eb13 --- /dev/null +++ b/tests/php/Core/Startup/URLConfirmationTokenTest.php @@ -0,0 +1,148 @@ + 'value']); + $validToken = new StubValidToken('token/test/url', $request); + $this->assertTrue($validToken->urlMatches()); + $this->assertFalse($validToken->urlExistsInBackURL()); + $this->assertTrue($validToken->tokenProvided()); // Actually forced to true for this test + $this->assertFalse($validToken->reloadRequired()); + $this->assertFalse($validToken->reloadRequiredIfError()); + $this->assertStringStartsWith(Controller::join_links(BASE_URL, '/', 'token/test/url'), $validToken->redirectURL()); + } + + public function testTokenWithLeadingSlashInUrl() + { + $request = new HTTPRequest('GET', '/leading/slash/url', []); + $leadingSlash = new StubToken('leading/slash/url', $request); + $this->assertTrue($leadingSlash->urlMatches()); + $this->assertFalse($leadingSlash->urlExistsInBackURL()); + $this->assertFalse($leadingSlash->tokenProvided()); + $this->assertTrue($leadingSlash->reloadRequired()); + $this->assertTrue($leadingSlash->reloadRequiredIfError()); + $this->assertContains('leading/slash/url', $leadingSlash->redirectURL()); + $this->assertContains('leadingslashurltoken', $leadingSlash->redirectURL()); + } + + public function testTokenWithTrailingSlashInUrl() + { + $request = new HTTPRequest('GET', 'trailing/slash/url/', []); + $trailingSlash = new StubToken('trailing/slash/url', $request); + $this->assertTrue($trailingSlash->urlMatches()); + $this->assertFalse($trailingSlash->urlExistsInBackURL()); + $this->assertFalse($trailingSlash->tokenProvided()); + $this->assertTrue($trailingSlash->reloadRequired()); + $this->assertTrue($trailingSlash->reloadRequiredIfError()); + $this->assertContains('trailing/slash/url', $trailingSlash->redirectURL()); + $this->assertContains('trailingslashurltoken', $trailingSlash->redirectURL()); + } + + public function testTokenWithUrlMatchedInBackUrl() + { + $request = new HTTPRequest('GET', '/', ['BackURL' => 'back/url']); + $backUrl = new StubToken('back/url', $request); + $this->assertFalse($backUrl->urlMatches()); + $this->assertTrue($backUrl->urlExistsInBackURL()); + $this->assertFalse($backUrl->tokenProvided()); + $this->assertFalse($backUrl->reloadRequired()); + $this->assertTrue($backUrl->reloadRequiredIfError()); + $home = (BASE_URL ?: '/') . '?'; + $this->assertStringStartsWith($home, $backUrl->redirectURL()); + $this->assertContains('backurltoken', $backUrl->redirectURL()); + } + + public function testUrlSuppressionWhenTokenMissing() + { + // Check suppression + $request = new HTTPRequest('GET', 'test/url', []); + $token = new StubToken('test/url', $request); + $this->assertEquals('test/url', $request->getURL(false)); + $token->suppress(); + $this->assertEquals('', $request->getURL(false)); + } + + public function testPrepareTokens() + { + $request = new HTTPRequest('GET', 'test/url', []); + $token = URLConfirmationToken::prepare_tokens( + [ + 'test/url', + 'test', + 'url' + ], + $request + ); + // Test no invalid tokens + $this->assertEquals('test/url', $token->getURLToCheck()); + $this->assertNotEquals('test/url', $request->getURL(false), 'prepare_tokens() did not suppress URL'); + } + + public function testPrepareTokensDoesntSuppressWhenNotMatched() + { + $request = new HTTPRequest('GET', 'test/url', []); + $token = URLConfirmationToken::prepare_tokens( + ['another/url'], + $request + ); + $this->assertEmpty($token); + $this->assertEquals('test/url', $request->getURL(false), 'prepare_tokens() incorrectly suppressed URL'); + } + + public function testPrepareTokensWithUrlMatchedInBackUrl() + { + // Test backurl token + $request = new HTTPRequest('GET', '/', ['BackURL' => 'back/url']); + $token = URLConfirmationToken::prepare_tokens( + [ 'back/url' ], + $request + ); + $this->assertNotEmpty($token); + $this->assertEquals('back/url', $token->getURLToCheck()); + $this->assertNotEquals('back/url', $request->getURL(false), 'prepare_tokens() did not suppress URL'); + } + + public function dataProviderURLs() + { + return [ + [''], + ['/'], + ['bar'], + ['bar/'], + ['/bar'], + ['/bar/'], + ]; + } + + /** + * currentURL needs to handle base or url being missing, or any combination of slashes. + * + * There should always be exactly one slash between each part in the result, and any trailing slash + * should be preserved. + * + * @dataProvider dataProviderURLs + */ + public function testCurrentURLHandlesSlashes($url) + { + $request = new HTTPRequest('GET', $url, []); + + $token = new StubToken( + 'another/url', + $request + ); + $expected = rtrim(Controller::join_links(BASE_URL, '/', $url), '/') ?: '/'; + $this->assertEquals($expected, $token->currentURL(), "Invalid redirect for request url $url"); + } +} diff --git a/tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php b/tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php new file mode 100644 index 000000000..ca08d3e1f --- /dev/null +++ b/tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php @@ -0,0 +1,27 @@ + Date: Mon, 30 Jul 2018 11:50:11 +1200 Subject: [PATCH 047/175] [SS-2018-018] Ignore arguments in mysqli::real_connect backtrace calls --- src/Dev/Backtrace.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Dev/Backtrace.php b/src/Dev/Backtrace.php index daceca751..648cc883f 100644 --- a/src/Dev/Backtrace.php +++ b/src/Dev/Backtrace.php @@ -26,6 +26,7 @@ class Backtrace array('PDO', '__construct'), array('mysqli', 'mysqli'), array('mysqli', 'select_db'), + array('mysqli', 'real_connect'), array('SilverStripe\\ORM\\DB', 'connect'), array('SilverStripe\\Security\\Security', 'check_default_admin'), array('SilverStripe\\Security\\Security', 'encrypt_password'), From 5563537cc8c2383ba5fbe039886b10c4444f6434 Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Fri, 24 Aug 2018 15:36:51 +0100 Subject: [PATCH 048/175] Implement ConfirmationTokenChain to handle multiple tokens at once --- ...oken.php => AbstractConfirmationToken.php} | 12 +- src/Core/Startup/ConfirmationTokenChain.php | 178 +++++++++++++++++ .../Startup/ErrorControlChainMiddleware.php | 62 +++--- src/Core/Startup/ErrorDirector.php | 13 +- .../Startup/ParameterConfirmationToken.php | 28 +-- src/Core/Startup/URLConfirmationToken.php | 29 +-- .../Startup/ConfirmationTokenChainTest.php | 185 ++++++++++++++++++ .../ErrorControlChainMiddlewareTest.php | 52 +++++ 8 files changed, 500 insertions(+), 59 deletions(-) rename src/Core/Startup/{ConfirmationToken.php => AbstractConfirmationToken.php} (95%) create mode 100644 src/Core/Startup/ConfirmationTokenChain.php create mode 100644 tests/php/Core/Startup/ConfirmationTokenChainTest.php diff --git a/src/Core/Startup/ConfirmationToken.php b/src/Core/Startup/AbstractConfirmationToken.php similarity index 95% rename from src/Core/Startup/ConfirmationToken.php rename to src/Core/Startup/AbstractConfirmationToken.php index d23563f46..11f78b48a 100644 --- a/src/Core/Startup/ConfirmationToken.php +++ b/src/Core/Startup/AbstractConfirmationToken.php @@ -15,7 +15,7 @@ use SilverStripe\Security\RandomGenerator; * * @internal This class is designed specifically for use pre-startup and may change without warning */ -abstract class ConfirmationToken +abstract class AbstractConfirmationToken { /** * @var HTTPRequest @@ -173,6 +173,16 @@ HTML; */ abstract public function params($includeToken = true); + /** + * @return string + */ + abstract public function getRedirectUrlBase(); + + /** + * @return array + */ + abstract public function getRedirectUrlParams(); + /** * Get redirection URL * diff --git a/src/Core/Startup/ConfirmationTokenChain.php b/src/Core/Startup/ConfirmationTokenChain.php new file mode 100644 index 000000000..a47f2c4c0 --- /dev/null +++ b/src/Core/Startup/ConfirmationTokenChain.php @@ -0,0 +1,178 @@ +tokens[] = $token; + } + + /** + * Collect all tokens that require a redirect + * + * @return \Generator + */ + protected function filteredTokens() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequired() || $token->reloadRequiredIfError()) { + yield $token; + } + } + } + + /** + * @return bool + */ + public function suppressionRequired() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequired()) { + return true; + } + } + + return false; + } + + /** + * Suppress URLs & GET vars from tokens that require a redirect + */ + public function suppressTokens() + { + foreach ($this->filteredTokens() as $token) { + $token->suppress(); + } + } + + /** + * @return bool + */ + public function reloadRequired() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequired()) { + return true; + } + } + + return false; + } + + /** + * @return bool + */ + public function reloadRequiredIfError() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequiredIfError()) { + return true; + } + } + + return false; + } + + /** + * @param bool $includeToken + * @return array + */ + public function params($includeToken = true) + { + $params = []; + foreach ($this->tokens as $token) { + $params = array_merge($params, $token->params($includeToken)); + } + + return $params; + } + + /** + * Fetch the URL we want to redirect to, excluding query string parameters. This may + * be the same URL (with a token to be added outside this method), or to a different + * URL if the current one has been suppressed + * + * @return string + */ + public function getRedirectUrlBase() + { + // URLConfirmationTokens may alter the URL to suppress the URL they're protecting, + // so we need to ensure they're inspected last and therefore take priority + $tokens = iterator_to_array($this->filteredTokens(), false); + usort($tokens, function ($a, $b) { + return ($a instanceof URLConfirmationToken) ? 1 : 0; + }); + + $urlBase = Director::baseURL(); + foreach ($tokens as $token) { + $urlBase = $token->getRedirectUrlBase(); + } + + return $urlBase; + } + + /** + * Collate GET vars from all token providers that need to apply a token + * + * @return array + */ + public function getRedirectUrlParams() + { + $params = []; + foreach ($this->filteredTokens() as $token) { + $params = array_merge($params, $token->getRedirectUrlParams()); + } + + return $params; + } + + /** + * @return string + */ + protected function redirectURL() + { + $params = http_build_query($this->getRedirectUrlParams()); + return Controller::join_links($this->getRedirectUrlBase(), '?' . $params); + } + + /** + * @return HTTPResponse + */ + public function reloadWithTokens() + { + $location = $this->redirectURL(); + $locationJS = Convert::raw2js($location); + $locationATT = Convert::raw2att($location); + $body = <<location.href='$locationJS'; + +You are being redirected. If you are not redirected soon, click here to continue +HTML; + + // Build response + $result = new HTTPResponse($body); + $result->redirect($location); + return $result; + } +} diff --git a/src/Core/Startup/ErrorControlChainMiddleware.php b/src/Core/Startup/ErrorControlChainMiddleware.php index e81444629..c29878e15 100644 --- a/src/Core/Startup/ErrorControlChainMiddleware.php +++ b/src/Core/Startup/ErrorControlChainMiddleware.php @@ -34,20 +34,18 @@ class ErrorControlChainMiddleware implements HTTPMiddleware /** * @param HTTPRequest $request - * @return ConfirmationToken|null + * @return ConfirmationTokenChain */ - protected function prepareConfirmationTokenIfRequired(HTTPRequest $request) + protected function prepareConfirmationTokenChain(HTTPRequest $request) { - $token = URLConfirmationToken::prepare_tokens(['dev/build'], $request); + $chain = new ConfirmationTokenChain(); + $chain->pushToken(new URLConfirmationToken('dev/build', $request)); - if (!$token) { - $token = ParameterConfirmationToken::prepare_tokens( - ['isTest', 'isDev', 'flush'], - $request - ); + foreach (['isTest', 'isDev', 'flush'] as $parameter) { + $chain->pushToken(new ParameterConfirmationToken($parameter, $request)); } - return $token; + return $chain; } public function process(HTTPRequest $request, callable $next) @@ -55,19 +53,21 @@ class ErrorControlChainMiddleware implements HTTPMiddleware $result = null; // Prepare tokens and execute chain - $confirmationToken = $this->prepareConfirmationTokenIfRequired($request); - $chain = new ErrorControlChain(); - $chain - ->then(function () use ($request, $chain, $confirmationToken, $next, &$result) { - // If no redirection is necessary then we can disable error supression - if (!$confirmationToken) { - $chain->setSuppression(false); + $confirmationTokenChain = $this->prepareConfirmationTokenChain($request); + $errorControlChain = new ErrorControlChain(); + $errorControlChain + ->then(function () use ($request, $errorControlChain, $confirmationTokenChain, $next, &$result) { + if ($confirmationTokenChain->suppressionRequired()) { + $confirmationTokenChain->suppressTokens(); + } else { + // If no redirection is necessary then we can disable error supression + $errorControlChain->setSuppression(false); } try { // Check if a token is requesting a redirect - if ($confirmationToken && $confirmationToken->reloadRequired()) { - $result = $this->safeReloadWithToken($request, $confirmationToken); + if ($confirmationTokenChain && $confirmationTokenChain->reloadRequired()) { + $result = $this->safeReloadWithTokens($request, $confirmationTokenChain); } else { // If no reload necessary, process application $result = call_user_func($next, $request); @@ -77,10 +77,16 @@ class ErrorControlChainMiddleware implements HTTPMiddleware } }) // Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway - ->thenIfErrored(function () use ($confirmationToken) { - if ($confirmationToken && $confirmationToken->reloadRequiredIfError()) { - $result = $confirmationToken->reloadWithToken(); - $result->output(); + ->thenIfErrored(function () use ($confirmationTokenChain) { + if ($confirmationTokenChain && $confirmationTokenChain->reloadRequiredIfError()) { + try { + // Reload requires manual boot + $this->getApplication()->getKernel()->boot(false); + } finally { + // Given we're in an error state here, try to continue even if the kernel boot fails + $result = $confirmationTokenChain->reloadWithTokens(); + $result->output(); + } } }) ->execute(); @@ -92,10 +98,10 @@ class ErrorControlChainMiddleware implements HTTPMiddleware * or authentication is impossible. * * @param HTTPRequest $request - * @param ParameterConfirmationToken $reloadToken + * @param ConfirmationTokenChain $confirmationTokenChain * @return HTTPResponse */ - protected function safeReloadWithToken(HTTPRequest $request, $reloadToken) + protected function safeReloadWithTokens(HTTPRequest $request, ConfirmationTokenChain $confirmationTokenChain) { // Safe reload requires manual boot $this->getApplication()->getKernel()->boot(false); @@ -104,9 +110,9 @@ class ErrorControlChainMiddleware implements HTTPMiddleware $request->getSession()->init($request); // Request with ErrorDirector - $result = ErrorDirector::singleton()->handleRequestWithToken( + $result = ErrorDirector::singleton()->handleRequestWithTokenChain( $request, - $reloadToken, + $confirmationTokenChain, $this->getApplication()->getKernel() ); if ($result) { @@ -114,8 +120,8 @@ class ErrorControlChainMiddleware implements HTTPMiddleware } // Fail and redirect the user to the login page - $params = array_merge($request->getVars(), $reloadToken->params(false)); - $backURL = $reloadToken->currentURL() . '?' . http_build_query($params); + $params = array_merge($request->getVars(), $confirmationTokenChain->params(false)); + $backURL = $confirmationTokenChain->getRedirectUrlBase() . '?' . http_build_query($params); $loginPage = Director::absoluteURL(Security::config()->get('login_url')); $loginPage .= "?BackURL=" . urlencode($backURL); $result = new HTTPResponse(); diff --git a/src/Core/Startup/ErrorDirector.php b/src/Core/Startup/ErrorDirector.php index 54001fd05..575df7183 100644 --- a/src/Core/Startup/ErrorDirector.php +++ b/src/Core/Startup/ErrorDirector.php @@ -21,18 +21,21 @@ class ErrorDirector extends Director * Redirect with token if allowed, or null if not allowed * * @param HTTPRequest $request - * @param ConfirmationToken $token + * @param ConfirmationTokenChain $confirmationTokenChain * @param Kernel $kernel * @return null|HTTPResponse Redirection response, or null if not able to redirect */ - public function handleRequestWithToken(HTTPRequest $request, ConfirmationToken $token, Kernel $kernel) - { + public function handleRequestWithTokenChain( + HTTPRequest $request, + ConfirmationTokenChain $confirmationTokenChain, + Kernel $kernel + ) { Injector::inst()->registerService($request, HTTPRequest::class); // Next, check if we're in dev mode, or the database doesn't have any security data, or we are admin - $reload = function (HTTPRequest $request) use ($token, $kernel) { + $reload = function (HTTPRequest $request) use ($confirmationTokenChain, $kernel) { if ($kernel->getEnvironment() === Kernel::DEV || !Security::database_is_ready() || Permission::check('ADMIN')) { - return $token->reloadWithToken(); + return $confirmationTokenChain->reloadWithTokens(); } return null; }; diff --git a/src/Core/Startup/ParameterConfirmationToken.php b/src/Core/Startup/ParameterConfirmationToken.php index 4e90f1ef7..bc751a2c3 100644 --- a/src/Core/Startup/ParameterConfirmationToken.php +++ b/src/Core/Startup/ParameterConfirmationToken.php @@ -3,6 +3,7 @@ namespace SilverStripe\Core\Startup; use SilverStripe\Control\Controller; +use SilverStripe\Control\Director; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; use SilverStripe\Core\Convert; @@ -15,7 +16,7 @@ use SilverStripe\Security\RandomGenerator; * * @internal This class is designed specifically for use pre-startup and may change without warning */ -class ParameterConfirmationToken extends ConfirmationToken +class ParameterConfirmationToken extends AbstractConfirmationToken { /** * The name of the parameter @@ -140,19 +141,22 @@ class ParameterConfirmationToken extends ConfirmationToken } return $params; } + + public function getRedirectUrlBase() + { + return ($this->existsInReferer() && !$this->parameterProvided()) ? Director::baseURL() : $this->currentURL(); + } + + public function getRedirectUrlParams() + { + return ($this->existsInReferer() && !$this->parameterProvided()) + ? $this->params() + : array_merge($this->request->getVars(), $this->params()); + } protected function redirectURL() { - // If url is encoded via BackURL, defer to home page (prevent redirect to form action) - if ($this->existsInReferer() && !$this->parameterProvided()) { - $url = BASE_URL ?: '/'; - $params = $this->params(); - } else { - $url = $this->currentURL(); - $params = array_merge($this->request->getVars(), $this->params()); - } - - // Merge get params with current url - return Controller::join_links($url, '?' . http_build_query($params)); + $query = http_build_query($this->getRedirectUrlParams()); + return Controller::join_links($this->getRedirectUrlBase(), '?' . $query); } } diff --git a/src/Core/Startup/URLConfirmationToken.php b/src/Core/Startup/URLConfirmationToken.php index 8176a41b8..bb509931e 100644 --- a/src/Core/Startup/URLConfirmationToken.php +++ b/src/Core/Startup/URLConfirmationToken.php @@ -12,7 +12,7 @@ use SilverStripe\Control\HTTPRequest; * * @internal This class is designed specifically for use pre-startup and may change without warning */ -class URLConfirmationToken extends ConfirmationToken +class URLConfirmationToken extends AbstractConfirmationToken { /** * @var string @@ -60,7 +60,7 @@ class URLConfirmationToken extends ConfirmationToken */ protected function getURLExistsInBackURL(HTTPRequest $request) { - $backURL = $request->getVar('BackURL'); + $backURL = ltrim($request->getVar('BackURL'), '/'); return (strpos($backURL, $this->urlToCheck) === 0); } @@ -119,18 +119,21 @@ class URLConfirmationToken extends ConfirmationToken return Controller::join_links(Director::baseURL(), $this->currentURL); } + public function getRedirectUrlBase() + { + return ($this->urlExistsInBackURL && !$this->urlMatches()) ? Director::baseURL() : $this->currentURL(); + } + + public function getRedirectUrlParams() + { + return ($this->urlExistsInBackURL && !$this->urlMatches()) + ? $this->params() + : array_merge($this->request->getVars(), $this->params()); + } + protected function redirectURL() { - // If url is encoded via BackURL, defer to home page (prevent redirect to form action) - if ($this->urlExistsInBackURL && !$this->urlMatches()) { - $url = BASE_URL ?: '/'; - $params = $this->params(); - } else { - $url = $this->currentURL(); - $params = array_merge($this->request->getVars(), $this->params()); - } - - // Merge get params with current url - return Controller::join_links($url, '?' . http_build_query($params)); + $query = http_build_query($this->getRedirectUrlParams()); + return Controller::join_links($this->getRedirectUrlBase(), '?' . $query); } } diff --git a/tests/php/Core/Startup/ConfirmationTokenChainTest.php b/tests/php/Core/Startup/ConfirmationTokenChainTest.php new file mode 100644 index 000000000..adb8fba36 --- /dev/null +++ b/tests/php/Core/Startup/ConfirmationTokenChainTest.php @@ -0,0 +1,185 @@ +createPartialMock(ParameterConfirmationToken::class, $methods); + $mock->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue($requiresReload)); + return $mock; + } + + protected function getTokenRequiringReloadIfError($requiresReload = true, $extraMethods = []) + { + $methods = array_merge(['reloadRequired', 'reloadRequiredIfError'], $extraMethods); + $mock = $this->createPartialMock(ParameterConfirmationToken::class, $methods); + $mock->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue(false)); + $mock->expects($this->any()) + ->method('reloadRequiredIfError') + ->will($this->returnValue($requiresReload)); + return $mock; + } + + public function testFilteredTokens() + { + $chain = new ConfirmationTokenChain(); + $chain->pushToken($tokenRequiringReload = $this->getTokenRequiringReload()); + $chain->pushToken($tokenNotRequiringReload = $this->getTokenRequiringReload(false)); + $chain->pushToken($tokenRequiringReloadIfError = $this->getTokenRequiringReloadIfError()); + $chain->pushToken($tokenNotRequiringReloadIfError = $this->getTokenRequiringReloadIfError(false)); + + $reflectionMethod = new \ReflectionMethod(ConfirmationTokenChain::class, 'filteredTokens'); + $reflectionMethod->setAccessible(true); + $tokens = iterator_to_array($reflectionMethod->invoke($chain)); + + $this->assertContains($tokenRequiringReload, $tokens, 'Token requiring a reload was not returned'); + $this->assertNotContains($tokenNotRequiringReload, $tokens, 'Token not requiring a reload was returned'); + $this->assertContains($tokenRequiringReloadIfError, $tokens, 'Token requiring a reload on error was not returned'); + $this->assertNotContains($tokenNotRequiringReloadIfError, $tokens, 'Token not requiring a reload on error was returned'); + } + + public function testSuppressionRequired() + { + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReload(false)); + $this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReloadIfError(false)); + $this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReload()); + $this->assertTrue($chain->suppressionRequired(), 'Suppression not marked as required'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReloadIfError()); + $this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required'); + } + + public function testSuppressTokens() + { + $mockToken = $this->getTokenRequiringReload(true, ['suppress']); + $mockToken->expects($this->once()) + ->method('suppress'); + $secondMockToken = $this->getTokenRequiringReloadIfError(true, ['suppress']); + $secondMockToken->expects($this->once()) + ->method('suppress'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $chain->suppressTokens(); + } + + public function testReloadRequired() + { + $mockToken = $this->getTokenRequiringReload(true); + $secondMockToken = $this->getTokenRequiringReload(false); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertTrue($chain->reloadRequired()); + } + + public function testReloadRequiredIfError() + { + $mockToken = $this->getTokenRequiringReloadIfError(true); + $secondMockToken = $this->getTokenRequiringReloadIfError(false); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertTrue($chain->reloadRequiredIfError()); + } + + public function testParams() + { + $mockToken = $this->getTokenRequiringReload(true, ['params']); + $mockToken->expects($this->once()) + ->method('params') + ->with($this->isTrue()) + ->will($this->returnValue(['mockTokenParam' => '1'])); + $secondMockToken = $this->getTokenRequiringReload(true, ['params']); + $secondMockToken->expects($this->once()) + ->method('params') + ->with($this->isTrue()) + ->will($this->returnValue(['secondMockTokenParam' => '2'])); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertEquals(['mockTokenParam' => '1', 'secondMockTokenParam' => '2'], $chain->params(true)); + + $mockToken = $this->getTokenRequiringReload(true, ['params']); + $mockToken->expects($this->once()) + ->method('params') + ->with($this->isFalse()) + ->will($this->returnValue(['mockTokenParam' => '1'])); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $this->assertEquals(['mockTokenParam' => '1'], $chain->params(false)); + } + + public function testGetRedirectUrlBase() + { + $mockUrlToken = $this->createPartialMock(URLConfirmationToken::class, ['reloadRequired', 'getRedirectUrlBase']); + $mockUrlToken->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue(true)); + $mockUrlToken->expects($this->any()) + ->method('getRedirectUrlBase') + ->will($this->returnValue('url-base')); + + $mockParameterToken = $this->createPartialMock(ParameterConfirmationToken::class, ['reloadRequired', 'getRedirectUrlBase']); + $mockParameterToken->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue(true)); + $mockParameterToken->expects($this->any()) + ->method('getRedirectUrlBase') + ->will($this->returnValue('parameter-base')); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockParameterToken); + $chain->pushToken($mockUrlToken); + $this->assertEquals('url-base', $chain->getRedirectUrlBase(), 'URLConfirmationToken url base should take priority'); + + // Push them in reverse order to check priority still correct + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockUrlToken); + $chain->pushToken($mockParameterToken); + $this->assertEquals('url-base', $chain->getRedirectUrlBase(), 'URLConfirmationToken url base should take priority'); + } + + public function testGetRedirectUrlParams() + { + $mockToken = $this->getTokenRequiringReload(true, ['getRedirectUrlParams']); + $mockToken->expects($this->once()) + ->method('getRedirectUrlParams') + ->will($this->returnValue(['mockTokenParam' => '1'])); + + $secondMockToken = $this->getTokenRequiringReload(true, ['getRedirectUrlParams']); + $secondMockToken->expects($this->once()) + ->method('getRedirectUrlParams') + ->will($this->returnValue(['secondMockTokenParam' => '2'])); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertEquals(['mockTokenParam' => '1', 'secondMockTokenParam' => '2'], $chain->getRedirectUrlParams()); + } +} diff --git a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php index 7df6f6ebf..a90799026 100644 --- a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php +++ b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php @@ -121,4 +121,56 @@ class ErrorControlChainMiddlewareTest extends SapphireTest $this->assertNotContains('?devbuildtoken=', $location); $this->assertContains('Security/login', $location); } + + public function testLiveBuildAndFlushAdmin() + { + // Mock admin + $adminID = $this->logInWithPermission('ADMIN'); + $this->logOut(); + + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as admin + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build/', ['flush' => '1']); + $request->setSession(new Session(['loggedInAs' => $adminID])); + $result = $chain->process($request, function () { + return null; + }); + + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertContains('/dev/build', $location); + $this->assertContains('flush=1', $location); + $this->assertContains('devbuildtoken=', $location); + $this->assertContains('flushtoken=', $location); + $this->assertNotContains('Security/login', $location); + } + + public function testLiveBuildAndFlushUnauthenticated() + { + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as no one + Security::setCurrentUser(null); + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build', ['flush' => '1']); + $request->setSession(new Session(['loggedInAs' => 0])); + $result = $chain->process($request, function () { + return null; + }); + + // Should be directed to login, not to flush + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertNotContains('/dev/build', $location); + $this->assertNotContains('flush=1', $location); + $this->assertNotContains('devbuildtoken=', $location); + $this->assertNotContains('flushtoken=', $location); + $this->assertContains('Security/login', $location); + } } From af000bea9b16ea553cae7f7f662f74ab8dc343df Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Tue, 21 Aug 2018 11:20:15 +0100 Subject: [PATCH 049/175] [SS-2018-019] Add confirmation token to dev/build --- src/Control/HTTPApplication.php | 2 +- src/Core/Startup/ConfirmationToken.php | 182 ++++++++++++++++++ src/Core/Startup/ErrorControlChain.php | 3 +- .../Startup/ErrorControlChainMiddleware.php | 43 +++-- src/Core/Startup/ErrorDirector.php | 4 +- .../Startup/ParameterConfirmationToken.php | 172 +---------------- src/Core/Startup/URLConfirmationToken.php | 136 +++++++++++++ .../ErrorControlChainMiddlewareTest.php | 48 +++++ .../ParameterConfirmationTokenTest.php | 4 +- .../Core/Startup/URLConfirmationTokenTest.php | 148 ++++++++++++++ .../URLConfirmationTokenTest/StubToken.php | 27 +++ .../StubValidToken.php | 15 ++ 12 files changed, 600 insertions(+), 184 deletions(-) create mode 100644 src/Core/Startup/ConfirmationToken.php create mode 100644 src/Core/Startup/URLConfirmationToken.php create mode 100644 tests/php/Core/Startup/URLConfirmationTokenTest.php create mode 100644 tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php create mode 100644 tests/php/Core/Startup/URLConfirmationTokenTest/StubValidToken.php diff --git a/src/Control/HTTPApplication.php b/src/Control/HTTPApplication.php index 4d0f7cb9d..35f453d6d 100644 --- a/src/Control/HTTPApplication.php +++ b/src/Control/HTTPApplication.php @@ -41,7 +41,7 @@ class HTTPApplication implements Application */ public function handle(HTTPRequest $request) { - $flush = array_key_exists('flush', $request->getVars()) || strpos($request->getURL(), 'dev/build') === 0; + $flush = array_key_exists('flush', $request->getVars()) || ($request->getURL() === 'dev/build'); // Ensure boot is invoked return $this->execute($request, function (HTTPRequest $request) { diff --git a/src/Core/Startup/ConfirmationToken.php b/src/Core/Startup/ConfirmationToken.php new file mode 100644 index 000000000..d23563f46 --- /dev/null +++ b/src/Core/Startup/ConfirmationToken.php @@ -0,0 +1,182 @@ +reloadRequired() || $token->reloadRequiredIfError()) { + $token->suppress(); + $target = $token; + } + } + return $target; + } + + /** + * Generate a local filesystem path to store a token + * + * @param $token + * @return string + */ + protected function pathForToken($token) + { + return TEMP_PATH . DIRECTORY_SEPARATOR . 'token_' . preg_replace('/[^a-z0-9]+/', '', $token); + } + + /** + * Generate a new random token and store it + * + * @return string Token name + */ + protected function genToken() + { + // Generate a new random token (as random as possible) + $rg = new RandomGenerator(); + $token = $rg->randomToken('md5'); + + // Store a file in the session save path (safer than /tmp, as open_basedir might limit that) + file_put_contents($this->pathForToken($token), $token); + + return $token; + } + + /** + * Is the necessary token provided for this parameter? + * A value must be provided for the token + * + * @return bool + */ + public function tokenProvided() + { + return !empty($this->token); + } + + /** + * Validate a token + * + * @param string $token + * @return boolean True if the token is valid + */ + protected function checkToken($token) + { + if (!$token) { + return false; + } + + $file = $this->pathForToken($token); + $content = null; + + if (file_exists($file)) { + $content = file_get_contents($file); + unlink($file); + } + + return $content === $token; + } + + /** + * Get redirect url, excluding querystring + * + * @return string + */ + public function currentURL() + { + return Controller::join_links(Director::baseURL(), $this->request->getURL(false)); + } + + /** + * Forces a reload of the request with the token included + * + * @return HTTPResponse + */ + public function reloadWithToken() + { + $location = $this->redirectURL(); + $locationJS = Convert::raw2js($location); + $locationATT = Convert::raw2att($location); + $body = <<location.href='$locationJS'; + +You are being redirected. If you are not redirected soon, click here to continue +HTML; + + // Build response + $result = new HTTPResponse($body); + $result->redirect($location); + return $result; + } + + /** + * Is this parameter requested without a valid token? + * + * @return bool True if the parameter is given without a valid token + */ + abstract public function reloadRequired(); + + /** + * Check if this token is provided either in the backurl, or directly, + * but without a token + * + * @return bool + */ + abstract public function reloadRequiredIfError(); + + /** + * Suppress the current parameter for the duration of this request + */ + abstract public function suppress(); + + /** + * Determine the querystring parameters to include + * + * @param bool $includeToken Include the token value? + * @return array List of querystring parameters, possibly including token parameter + */ + abstract public function params($includeToken = true); + + /** + * Get redirection URL + * + * @return string + */ + abstract protected function redirectURL(); +} diff --git a/src/Core/Startup/ErrorControlChain.php b/src/Core/Startup/ErrorControlChain.php index f34a2c802..e2d14db65 100644 --- a/src/Core/Startup/ErrorControlChain.php +++ b/src/Core/Startup/ErrorControlChain.php @@ -15,8 +15,7 @@ use Exception; * $chain = new ErrorControlChain(); * $chain->then($callback1)->then($callback2)->thenIfErrored($callback3)->execute(); * - * WARNING: This class is experimental and designed specifically for use pre-startup. - * It will likely be heavily refactored before the release of 3.2 + * @internal This class is designed specifically for use pre-startup and may change without warning */ class ErrorControlChain { diff --git a/src/Core/Startup/ErrorControlChainMiddleware.php b/src/Core/Startup/ErrorControlChainMiddleware.php index bdb5ff0a3..e81444629 100644 --- a/src/Core/Startup/ErrorControlChainMiddleware.php +++ b/src/Core/Startup/ErrorControlChainMiddleware.php @@ -12,6 +12,8 @@ use SilverStripe\Security\Security; /** * Decorates application bootstrapping with errorcontrolchain + * + * @internal This class is designed specifically for use pre-startup and may change without warning */ class ErrorControlChainMiddleware implements HTTPMiddleware { @@ -30,27 +32,42 @@ class ErrorControlChainMiddleware implements HTTPMiddleware $this->application = $application; } + /** + * @param HTTPRequest $request + * @return ConfirmationToken|null + */ + protected function prepareConfirmationTokenIfRequired(HTTPRequest $request) + { + $token = URLConfirmationToken::prepare_tokens(['dev/build'], $request); + + if (!$token) { + $token = ParameterConfirmationToken::prepare_tokens( + ['isTest', 'isDev', 'flush'], + $request + ); + } + + return $token; + } + public function process(HTTPRequest $request, callable $next) { $result = null; // Prepare tokens and execute chain - $reloadToken = ParameterConfirmationToken::prepare_tokens( - ['isTest', 'isDev', 'flush'], - $request - ); + $confirmationToken = $this->prepareConfirmationTokenIfRequired($request); $chain = new ErrorControlChain(); $chain - ->then(function () use ($request, $chain, $reloadToken, $next, &$result) { + ->then(function () use ($request, $chain, $confirmationToken, $next, &$result) { // If no redirection is necessary then we can disable error supression - if (!$reloadToken) { + if (!$confirmationToken) { $chain->setSuppression(false); } try { // Check if a token is requesting a redirect - if ($reloadToken && $reloadToken->reloadRequired()) { - $result = $this->safeReloadWithToken($request, $reloadToken); + if ($confirmationToken && $confirmationToken->reloadRequired()) { + $result = $this->safeReloadWithToken($request, $confirmationToken); } else { // If no reload necessary, process application $result = call_user_func($next, $request); @@ -60,9 +77,9 @@ class ErrorControlChainMiddleware implements HTTPMiddleware } }) // Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway - ->thenIfErrored(function () use ($reloadToken) { - if ($reloadToken && $reloadToken->reloadRequiredIfError()) { - $result = $reloadToken->reloadWithToken(); + ->thenIfErrored(function () use ($confirmationToken) { + if ($confirmationToken && $confirmationToken->reloadRequiredIfError()) { + $result = $confirmationToken->reloadWithToken(); $result->output(); } }) @@ -85,7 +102,7 @@ class ErrorControlChainMiddleware implements HTTPMiddleware // Ensure session is started $request->getSession()->init($request); - + // Request with ErrorDirector $result = ErrorDirector::singleton()->handleRequestWithToken( $request, @@ -98,7 +115,7 @@ class ErrorControlChainMiddleware implements HTTPMiddleware // Fail and redirect the user to the login page $params = array_merge($request->getVars(), $reloadToken->params(false)); - $backURL = $request->getURL() . '?' . http_build_query($params); + $backURL = $reloadToken->currentURL() . '?' . http_build_query($params); $loginPage = Director::absoluteURL(Security::config()->get('login_url')); $loginPage .= "?BackURL=" . urlencode($backURL); $result = new HTTPResponse(); diff --git a/src/Core/Startup/ErrorDirector.php b/src/Core/Startup/ErrorDirector.php index 3c994c6af..54001fd05 100644 --- a/src/Core/Startup/ErrorDirector.php +++ b/src/Core/Startup/ErrorDirector.php @@ -21,11 +21,11 @@ class ErrorDirector extends Director * Redirect with token if allowed, or null if not allowed * * @param HTTPRequest $request - * @param ParameterConfirmationToken $token + * @param ConfirmationToken $token * @param Kernel $kernel * @return null|HTTPResponse Redirection response, or null if not able to redirect */ - public function handleRequestWithToken(HTTPRequest $request, ParameterConfirmationToken $token, Kernel $kernel) + public function handleRequestWithToken(HTTPRequest $request, ConfirmationToken $token, Kernel $kernel) { Injector::inst()->registerService($request, HTTPRequest::class); diff --git a/src/Core/Startup/ParameterConfirmationToken.php b/src/Core/Startup/ParameterConfirmationToken.php index 1c80db1d0..4e90f1ef7 100644 --- a/src/Core/Startup/ParameterConfirmationToken.php +++ b/src/Core/Startup/ParameterConfirmationToken.php @@ -9,30 +9,21 @@ use SilverStripe\Core\Convert; use SilverStripe\Security\RandomGenerator; /** - * Class ParameterConfirmationToken + * This is used to protect dangerous GET parameters that need to be detected early in the request + * lifecycle by generating a one-time-use token & redirecting with that token included in the + * redirected URL * - * When you need to use a dangerous GET parameter that needs to be set before core/Core.php is - * established, this class takes care of allowing some other code of confirming the parameter, - * by generating a one-time-use token & redirecting with that token included in the redirected URL - * - * WARNING: This class is experimental and designed specifically for use pre-startup. - * It will likely be heavily refactored before the release of 3.2 + * @internal This class is designed specifically for use pre-startup and may change without warning */ -class ParameterConfirmationToken +class ParameterConfirmationToken extends ConfirmationToken { - /** * The name of the parameter * * @var string */ protected $parameterName = null; - - /** - * @var HTTPRequest - */ - protected $request = null; - + /** * The parameter given in the main request * @@ -48,60 +39,6 @@ class ParameterConfirmationToken protected $parameterBackURL = null; /** - * The validated and checked token for this parameter - * - * @var string|null A string value, or null if either not provided or invalid - */ - protected $token = null; - - protected function pathForToken($token) - { - return TEMP_PATH . DIRECTORY_SEPARATOR . 'token_' . preg_replace('/[^a-z0-9]+/', '', $token); - } - - /** - * Generate a new random token and store it - * - * @return string Token name - */ - protected function genToken() - { - // Generate a new random token (as random as possible) - $rg = new RandomGenerator(); - $token = $rg->randomToken('md5'); - - // Store a file in the session save path (safer than /tmp, as open_basedir might limit that) - file_put_contents($this->pathForToken($token), $token); - - return $token; - } - - /** - * Validate a token - * - * @param string $token - * @return boolean True if the token is valid - */ - protected function checkToken($token) - { - if (!$token) { - return false; - } - - $file = $this->pathForToken($token); - $content = null; - - if (file_exists($file)) { - $content = file_get_contents($file); - unlink($file); - } - - return $content == $token; - } - - /** - * Create a new ParameterConfirmationToken - * * @param string $parameterName Name of the querystring parameter to check * @param HTTPRequest $request */ @@ -176,54 +113,23 @@ class ParameterConfirmationToken return $this->parameterBackURL !== null; } - /** - * Is the necessary token provided for this parameter? - * A value must be provided for the token - * - * @return bool - */ - public function tokenProvided() - { - return !empty($this->token); - } - - /** - * Is this parameter requested without a valid token? - * - * @return bool True if the parameter is given without a valid token - */ public function reloadRequired() { return $this->parameterProvided() && !$this->tokenProvided(); } - /** - * Check if this token is provided either in the backurl, or directly, - * but without a token - * - * @return bool - */ public function reloadRequiredIfError() { // Don't reload if token exists return $this->reloadRequired() || $this->existsInReferer(); } - - /** - * Suppress the current parameter by unsetting it from $_GET - */ + public function suppress() { unset($_GET[$this->parameterName]); $this->request->offsetUnset($this->parameterName); } - /** - * Determine the querystring parameters to include - * - * @param bool $includeToken Include the token value as well? - * @return array List of querystring parameters with name and token parameters - */ public function params($includeToken = true) { $params = array( @@ -234,25 +140,7 @@ class ParameterConfirmationToken } return $params; } - - /** - * Get redirect url, excluding querystring - * - * @return string - */ - protected function currentURL() - { - return Controller::join_links( - BASE_URL ?: '/', - $this->request->getURL(false) - ); - } - - /** - * Get redirection URL - * - * @return string - */ + protected function redirectURL() { // If url is encoded via BackURL, defer to home page (prevent redirect to form action) @@ -267,48 +155,4 @@ class ParameterConfirmationToken // Merge get params with current url return Controller::join_links($url, '?' . http_build_query($params)); } - - /** - * Forces a reload of the request with the token included - * - * @return HTTPResponse - */ - public function reloadWithToken() - { - $location = $this->redirectURL(); - $locationJS = Convert::raw2js($location); - $locationATT = Convert::raw2att($location); - $body = <<location.href='$locationJS'; - -You are being redirected. If you are not redirected soon, click here to continue the flush -HTML; - - // Build response - $result = new HTTPResponse($body); - $result->redirect($location); - return $result; - } - - /** - * Given a list of token names, suppress all tokens that have not been validated, and - * return the non-validated token with the highest priority - * - * @param array $keys List of token keys in ascending priority (low to high) - * @param HTTPRequest $request - * @return ParameterConfirmationToken The token container for the unvalidated $key given with the highest priority - */ - public static function prepare_tokens($keys, HTTPRequest $request) - { - $target = null; - foreach ($keys as $key) { - $token = new ParameterConfirmationToken($key, $request); - // Validate this token - if ($token->reloadRequired() || $token->reloadRequiredIfError()) { - $token->suppress(); - $target = $token; - } - } - return $target; - } } diff --git a/src/Core/Startup/URLConfirmationToken.php b/src/Core/Startup/URLConfirmationToken.php new file mode 100644 index 000000000..8176a41b8 --- /dev/null +++ b/src/Core/Startup/URLConfirmationToken.php @@ -0,0 +1,136 @@ +urlToCheck = $urlToCheck; + $this->request = $request; + $this->currentURL = $request->getURL(false); + + $this->tokenParameterName = preg_replace('/[^a-z0-9]/i', '', $urlToCheck) . 'token'; + $this->urlExistsInBackURL = $this->getURLExistsInBackURL($request); + + // If the token provided is valid, mark it as such + $token = $request->getVar($this->tokenParameterName); + if ($this->checkToken($token)) { + $this->token = $token; + } + } + + /** + * @param HTTPRequest $request + * @return bool + */ + protected function getURLExistsInBackURL(HTTPRequest $request) + { + $backURL = $request->getVar('BackURL'); + return (strpos($backURL, $this->urlToCheck) === 0); + } + + /** + * @return bool + */ + protected function urlMatches() + { + return ($this->currentURL === $this->urlToCheck); + } + + /** + * @return string + */ + public function getURLToCheck() + { + return $this->urlToCheck; + } + + /** + * @return bool + */ + public function urlExistsInBackURL() + { + return $this->urlExistsInBackURL; + } + + public function reloadRequired() + { + return $this->urlMatches() && !$this->tokenProvided(); + } + + public function reloadRequiredIfError() + { + return $this->reloadRequired() || $this->urlExistsInBackURL(); + } + + public function suppress() + { + $_SERVER['REQUEST_URI'] = '/'; + $this->request->setURL('/'); + } + + public function params($includeToken = true) + { + $params = []; + if ($includeToken) { + $params[$this->tokenParameterName] = $this->genToken(); + } + + return $params; + } + + public function currentURL() + { + return Controller::join_links(Director::baseURL(), $this->currentURL); + } + + protected function redirectURL() + { + // If url is encoded via BackURL, defer to home page (prevent redirect to form action) + if ($this->urlExistsInBackURL && !$this->urlMatches()) { + $url = BASE_URL ?: '/'; + $params = $this->params(); + } else { + $url = $this->currentURL(); + $params = array_merge($this->request->getVars(), $this->params()); + } + + // Merge get params with current url + return Controller::join_links($url, '?' . http_build_query($params)); + } +} diff --git a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php index d1e4175f8..27de7f930 100644 --- a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php +++ b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php @@ -74,4 +74,52 @@ class ErrorControlChainMiddlewareTest extends SapphireTest $this->assertNotContains('?flush=1&flushtoken=', $location); $this->assertContains('Security/login', $location); } + + public function testLiveBuildAdmin() + { + // Mock admin + $adminID = $this->logInWithPermission('ADMIN'); + $this->logOut(); + + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as admin + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build/'); + $request->setSession(new Session(['loggedInAs' => $adminID])); + $result = $chain->process($request, function () { + return null; + }); + + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertContains('/dev/build', $location); + $this->assertContains('?devbuildtoken=', $location); + $this->assertNotContains('Security/login', $location); + } + + public function testLiveBuildUnauthenticated() + { + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as no one + Security::setCurrentUser(null); + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build'); + $request->setSession(new Session(['loggedInAs' => 0])); + $result = $chain->process($request, function () { + return null; + }); + + // Should be directed to login, not to flush + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertNotContains('/dev/build', $location); + $this->assertNotContains('?devbuildtoken=', $location); + $this->assertContains('Security/login', $location); + } } diff --git a/tests/php/Core/Startup/ParameterConfirmationTokenTest.php b/tests/php/Core/Startup/ParameterConfirmationTokenTest.php index 66616433f..e28af8ea5 100644 --- a/tests/php/Core/Startup/ParameterConfirmationTokenTest.php +++ b/tests/php/Core/Startup/ParameterConfirmationTokenTest.php @@ -149,14 +149,14 @@ class ParameterConfirmationTokenTest extends SapphireTest } /** - * currentAbsoluteURL needs to handle base or url being missing, or any combination of slashes. + * currentURL needs to handle base or url being missing, or any combination of slashes. * * There should always be exactly one slash between each part in the result, and any trailing slash * should be preserved. * * @dataProvider dataProviderURLs */ - public function testCurrentAbsoluteURLHandlesSlashes($url) + public function testCurrentURLHandlesSlashes($url) { $this->request->setUrl($url); diff --git a/tests/php/Core/Startup/URLConfirmationTokenTest.php b/tests/php/Core/Startup/URLConfirmationTokenTest.php new file mode 100644 index 000000000..73b07eb13 --- /dev/null +++ b/tests/php/Core/Startup/URLConfirmationTokenTest.php @@ -0,0 +1,148 @@ + 'value']); + $validToken = new StubValidToken('token/test/url', $request); + $this->assertTrue($validToken->urlMatches()); + $this->assertFalse($validToken->urlExistsInBackURL()); + $this->assertTrue($validToken->tokenProvided()); // Actually forced to true for this test + $this->assertFalse($validToken->reloadRequired()); + $this->assertFalse($validToken->reloadRequiredIfError()); + $this->assertStringStartsWith(Controller::join_links(BASE_URL, '/', 'token/test/url'), $validToken->redirectURL()); + } + + public function testTokenWithLeadingSlashInUrl() + { + $request = new HTTPRequest('GET', '/leading/slash/url', []); + $leadingSlash = new StubToken('leading/slash/url', $request); + $this->assertTrue($leadingSlash->urlMatches()); + $this->assertFalse($leadingSlash->urlExistsInBackURL()); + $this->assertFalse($leadingSlash->tokenProvided()); + $this->assertTrue($leadingSlash->reloadRequired()); + $this->assertTrue($leadingSlash->reloadRequiredIfError()); + $this->assertContains('leading/slash/url', $leadingSlash->redirectURL()); + $this->assertContains('leadingslashurltoken', $leadingSlash->redirectURL()); + } + + public function testTokenWithTrailingSlashInUrl() + { + $request = new HTTPRequest('GET', 'trailing/slash/url/', []); + $trailingSlash = new StubToken('trailing/slash/url', $request); + $this->assertTrue($trailingSlash->urlMatches()); + $this->assertFalse($trailingSlash->urlExistsInBackURL()); + $this->assertFalse($trailingSlash->tokenProvided()); + $this->assertTrue($trailingSlash->reloadRequired()); + $this->assertTrue($trailingSlash->reloadRequiredIfError()); + $this->assertContains('trailing/slash/url', $trailingSlash->redirectURL()); + $this->assertContains('trailingslashurltoken', $trailingSlash->redirectURL()); + } + + public function testTokenWithUrlMatchedInBackUrl() + { + $request = new HTTPRequest('GET', '/', ['BackURL' => 'back/url']); + $backUrl = new StubToken('back/url', $request); + $this->assertFalse($backUrl->urlMatches()); + $this->assertTrue($backUrl->urlExistsInBackURL()); + $this->assertFalse($backUrl->tokenProvided()); + $this->assertFalse($backUrl->reloadRequired()); + $this->assertTrue($backUrl->reloadRequiredIfError()); + $home = (BASE_URL ?: '/') . '?'; + $this->assertStringStartsWith($home, $backUrl->redirectURL()); + $this->assertContains('backurltoken', $backUrl->redirectURL()); + } + + public function testUrlSuppressionWhenTokenMissing() + { + // Check suppression + $request = new HTTPRequest('GET', 'test/url', []); + $token = new StubToken('test/url', $request); + $this->assertEquals('test/url', $request->getURL(false)); + $token->suppress(); + $this->assertEquals('', $request->getURL(false)); + } + + public function testPrepareTokens() + { + $request = new HTTPRequest('GET', 'test/url', []); + $token = URLConfirmationToken::prepare_tokens( + [ + 'test/url', + 'test', + 'url' + ], + $request + ); + // Test no invalid tokens + $this->assertEquals('test/url', $token->getURLToCheck()); + $this->assertNotEquals('test/url', $request->getURL(false), 'prepare_tokens() did not suppress URL'); + } + + public function testPrepareTokensDoesntSuppressWhenNotMatched() + { + $request = new HTTPRequest('GET', 'test/url', []); + $token = URLConfirmationToken::prepare_tokens( + ['another/url'], + $request + ); + $this->assertEmpty($token); + $this->assertEquals('test/url', $request->getURL(false), 'prepare_tokens() incorrectly suppressed URL'); + } + + public function testPrepareTokensWithUrlMatchedInBackUrl() + { + // Test backurl token + $request = new HTTPRequest('GET', '/', ['BackURL' => 'back/url']); + $token = URLConfirmationToken::prepare_tokens( + [ 'back/url' ], + $request + ); + $this->assertNotEmpty($token); + $this->assertEquals('back/url', $token->getURLToCheck()); + $this->assertNotEquals('back/url', $request->getURL(false), 'prepare_tokens() did not suppress URL'); + } + + public function dataProviderURLs() + { + return [ + [''], + ['/'], + ['bar'], + ['bar/'], + ['/bar'], + ['/bar/'], + ]; + } + + /** + * currentURL needs to handle base or url being missing, or any combination of slashes. + * + * There should always be exactly one slash between each part in the result, and any trailing slash + * should be preserved. + * + * @dataProvider dataProviderURLs + */ + public function testCurrentURLHandlesSlashes($url) + { + $request = new HTTPRequest('GET', $url, []); + + $token = new StubToken( + 'another/url', + $request + ); + $expected = rtrim(Controller::join_links(BASE_URL, '/', $url), '/') ?: '/'; + $this->assertEquals($expected, $token->currentURL(), "Invalid redirect for request url $url"); + } +} diff --git a/tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php b/tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php new file mode 100644 index 000000000..ca08d3e1f --- /dev/null +++ b/tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php @@ -0,0 +1,27 @@ + Date: Mon, 30 Jul 2018 11:50:11 +1200 Subject: [PATCH 050/175] [SS-2018-018] Ignore arguments in mysqli::real_connect backtrace calls --- src/Dev/Backtrace.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Dev/Backtrace.php b/src/Dev/Backtrace.php index daceca751..648cc883f 100644 --- a/src/Dev/Backtrace.php +++ b/src/Dev/Backtrace.php @@ -26,6 +26,7 @@ class Backtrace array('PDO', '__construct'), array('mysqli', 'mysqli'), array('mysqli', 'select_db'), + array('mysqli', 'real_connect'), array('SilverStripe\\ORM\\DB', 'connect'), array('SilverStripe\\Security\\Security', 'check_default_admin'), array('SilverStripe\\Security\\Security', 'encrypt_password'), From 02ad0f44aae917283784d4e9594594285f45c27d Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Fri, 24 Aug 2018 15:36:51 +0100 Subject: [PATCH 051/175] Implement ConfirmationTokenChain to handle multiple tokens at once --- ...oken.php => AbstractConfirmationToken.php} | 12 +- src/Core/Startup/ConfirmationTokenChain.php | 178 +++++++++++++++++ .../Startup/ErrorControlChainMiddleware.php | 62 +++--- src/Core/Startup/ErrorDirector.php | 13 +- .../Startup/ParameterConfirmationToken.php | 28 +-- src/Core/Startup/URLConfirmationToken.php | 29 +-- .../Startup/ConfirmationTokenChainTest.php | 185 ++++++++++++++++++ .../ErrorControlChainMiddlewareTest.php | 52 +++++ 8 files changed, 500 insertions(+), 59 deletions(-) rename src/Core/Startup/{ConfirmationToken.php => AbstractConfirmationToken.php} (95%) create mode 100644 src/Core/Startup/ConfirmationTokenChain.php create mode 100644 tests/php/Core/Startup/ConfirmationTokenChainTest.php diff --git a/src/Core/Startup/ConfirmationToken.php b/src/Core/Startup/AbstractConfirmationToken.php similarity index 95% rename from src/Core/Startup/ConfirmationToken.php rename to src/Core/Startup/AbstractConfirmationToken.php index d23563f46..11f78b48a 100644 --- a/src/Core/Startup/ConfirmationToken.php +++ b/src/Core/Startup/AbstractConfirmationToken.php @@ -15,7 +15,7 @@ use SilverStripe\Security\RandomGenerator; * * @internal This class is designed specifically for use pre-startup and may change without warning */ -abstract class ConfirmationToken +abstract class AbstractConfirmationToken { /** * @var HTTPRequest @@ -173,6 +173,16 @@ HTML; */ abstract public function params($includeToken = true); + /** + * @return string + */ + abstract public function getRedirectUrlBase(); + + /** + * @return array + */ + abstract public function getRedirectUrlParams(); + /** * Get redirection URL * diff --git a/src/Core/Startup/ConfirmationTokenChain.php b/src/Core/Startup/ConfirmationTokenChain.php new file mode 100644 index 000000000..a47f2c4c0 --- /dev/null +++ b/src/Core/Startup/ConfirmationTokenChain.php @@ -0,0 +1,178 @@ +tokens[] = $token; + } + + /** + * Collect all tokens that require a redirect + * + * @return \Generator + */ + protected function filteredTokens() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequired() || $token->reloadRequiredIfError()) { + yield $token; + } + } + } + + /** + * @return bool + */ + public function suppressionRequired() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequired()) { + return true; + } + } + + return false; + } + + /** + * Suppress URLs & GET vars from tokens that require a redirect + */ + public function suppressTokens() + { + foreach ($this->filteredTokens() as $token) { + $token->suppress(); + } + } + + /** + * @return bool + */ + public function reloadRequired() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequired()) { + return true; + } + } + + return false; + } + + /** + * @return bool + */ + public function reloadRequiredIfError() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequiredIfError()) { + return true; + } + } + + return false; + } + + /** + * @param bool $includeToken + * @return array + */ + public function params($includeToken = true) + { + $params = []; + foreach ($this->tokens as $token) { + $params = array_merge($params, $token->params($includeToken)); + } + + return $params; + } + + /** + * Fetch the URL we want to redirect to, excluding query string parameters. This may + * be the same URL (with a token to be added outside this method), or to a different + * URL if the current one has been suppressed + * + * @return string + */ + public function getRedirectUrlBase() + { + // URLConfirmationTokens may alter the URL to suppress the URL they're protecting, + // so we need to ensure they're inspected last and therefore take priority + $tokens = iterator_to_array($this->filteredTokens(), false); + usort($tokens, function ($a, $b) { + return ($a instanceof URLConfirmationToken) ? 1 : 0; + }); + + $urlBase = Director::baseURL(); + foreach ($tokens as $token) { + $urlBase = $token->getRedirectUrlBase(); + } + + return $urlBase; + } + + /** + * Collate GET vars from all token providers that need to apply a token + * + * @return array + */ + public function getRedirectUrlParams() + { + $params = []; + foreach ($this->filteredTokens() as $token) { + $params = array_merge($params, $token->getRedirectUrlParams()); + } + + return $params; + } + + /** + * @return string + */ + protected function redirectURL() + { + $params = http_build_query($this->getRedirectUrlParams()); + return Controller::join_links($this->getRedirectUrlBase(), '?' . $params); + } + + /** + * @return HTTPResponse + */ + public function reloadWithTokens() + { + $location = $this->redirectURL(); + $locationJS = Convert::raw2js($location); + $locationATT = Convert::raw2att($location); + $body = <<location.href='$locationJS'; + +You are being redirected. If you are not redirected soon, click here to continue +HTML; + + // Build response + $result = new HTTPResponse($body); + $result->redirect($location); + return $result; + } +} diff --git a/src/Core/Startup/ErrorControlChainMiddleware.php b/src/Core/Startup/ErrorControlChainMiddleware.php index e81444629..c29878e15 100644 --- a/src/Core/Startup/ErrorControlChainMiddleware.php +++ b/src/Core/Startup/ErrorControlChainMiddleware.php @@ -34,20 +34,18 @@ class ErrorControlChainMiddleware implements HTTPMiddleware /** * @param HTTPRequest $request - * @return ConfirmationToken|null + * @return ConfirmationTokenChain */ - protected function prepareConfirmationTokenIfRequired(HTTPRequest $request) + protected function prepareConfirmationTokenChain(HTTPRequest $request) { - $token = URLConfirmationToken::prepare_tokens(['dev/build'], $request); + $chain = new ConfirmationTokenChain(); + $chain->pushToken(new URLConfirmationToken('dev/build', $request)); - if (!$token) { - $token = ParameterConfirmationToken::prepare_tokens( - ['isTest', 'isDev', 'flush'], - $request - ); + foreach (['isTest', 'isDev', 'flush'] as $parameter) { + $chain->pushToken(new ParameterConfirmationToken($parameter, $request)); } - return $token; + return $chain; } public function process(HTTPRequest $request, callable $next) @@ -55,19 +53,21 @@ class ErrorControlChainMiddleware implements HTTPMiddleware $result = null; // Prepare tokens and execute chain - $confirmationToken = $this->prepareConfirmationTokenIfRequired($request); - $chain = new ErrorControlChain(); - $chain - ->then(function () use ($request, $chain, $confirmationToken, $next, &$result) { - // If no redirection is necessary then we can disable error supression - if (!$confirmationToken) { - $chain->setSuppression(false); + $confirmationTokenChain = $this->prepareConfirmationTokenChain($request); + $errorControlChain = new ErrorControlChain(); + $errorControlChain + ->then(function () use ($request, $errorControlChain, $confirmationTokenChain, $next, &$result) { + if ($confirmationTokenChain->suppressionRequired()) { + $confirmationTokenChain->suppressTokens(); + } else { + // If no redirection is necessary then we can disable error supression + $errorControlChain->setSuppression(false); } try { // Check if a token is requesting a redirect - if ($confirmationToken && $confirmationToken->reloadRequired()) { - $result = $this->safeReloadWithToken($request, $confirmationToken); + if ($confirmationTokenChain && $confirmationTokenChain->reloadRequired()) { + $result = $this->safeReloadWithTokens($request, $confirmationTokenChain); } else { // If no reload necessary, process application $result = call_user_func($next, $request); @@ -77,10 +77,16 @@ class ErrorControlChainMiddleware implements HTTPMiddleware } }) // Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway - ->thenIfErrored(function () use ($confirmationToken) { - if ($confirmationToken && $confirmationToken->reloadRequiredIfError()) { - $result = $confirmationToken->reloadWithToken(); - $result->output(); + ->thenIfErrored(function () use ($confirmationTokenChain) { + if ($confirmationTokenChain && $confirmationTokenChain->reloadRequiredIfError()) { + try { + // Reload requires manual boot + $this->getApplication()->getKernel()->boot(false); + } finally { + // Given we're in an error state here, try to continue even if the kernel boot fails + $result = $confirmationTokenChain->reloadWithTokens(); + $result->output(); + } } }) ->execute(); @@ -92,10 +98,10 @@ class ErrorControlChainMiddleware implements HTTPMiddleware * or authentication is impossible. * * @param HTTPRequest $request - * @param ParameterConfirmationToken $reloadToken + * @param ConfirmationTokenChain $confirmationTokenChain * @return HTTPResponse */ - protected function safeReloadWithToken(HTTPRequest $request, $reloadToken) + protected function safeReloadWithTokens(HTTPRequest $request, ConfirmationTokenChain $confirmationTokenChain) { // Safe reload requires manual boot $this->getApplication()->getKernel()->boot(false); @@ -104,9 +110,9 @@ class ErrorControlChainMiddleware implements HTTPMiddleware $request->getSession()->init($request); // Request with ErrorDirector - $result = ErrorDirector::singleton()->handleRequestWithToken( + $result = ErrorDirector::singleton()->handleRequestWithTokenChain( $request, - $reloadToken, + $confirmationTokenChain, $this->getApplication()->getKernel() ); if ($result) { @@ -114,8 +120,8 @@ class ErrorControlChainMiddleware implements HTTPMiddleware } // Fail and redirect the user to the login page - $params = array_merge($request->getVars(), $reloadToken->params(false)); - $backURL = $reloadToken->currentURL() . '?' . http_build_query($params); + $params = array_merge($request->getVars(), $confirmationTokenChain->params(false)); + $backURL = $confirmationTokenChain->getRedirectUrlBase() . '?' . http_build_query($params); $loginPage = Director::absoluteURL(Security::config()->get('login_url')); $loginPage .= "?BackURL=" . urlencode($backURL); $result = new HTTPResponse(); diff --git a/src/Core/Startup/ErrorDirector.php b/src/Core/Startup/ErrorDirector.php index 54001fd05..575df7183 100644 --- a/src/Core/Startup/ErrorDirector.php +++ b/src/Core/Startup/ErrorDirector.php @@ -21,18 +21,21 @@ class ErrorDirector extends Director * Redirect with token if allowed, or null if not allowed * * @param HTTPRequest $request - * @param ConfirmationToken $token + * @param ConfirmationTokenChain $confirmationTokenChain * @param Kernel $kernel * @return null|HTTPResponse Redirection response, or null if not able to redirect */ - public function handleRequestWithToken(HTTPRequest $request, ConfirmationToken $token, Kernel $kernel) - { + public function handleRequestWithTokenChain( + HTTPRequest $request, + ConfirmationTokenChain $confirmationTokenChain, + Kernel $kernel + ) { Injector::inst()->registerService($request, HTTPRequest::class); // Next, check if we're in dev mode, or the database doesn't have any security data, or we are admin - $reload = function (HTTPRequest $request) use ($token, $kernel) { + $reload = function (HTTPRequest $request) use ($confirmationTokenChain, $kernel) { if ($kernel->getEnvironment() === Kernel::DEV || !Security::database_is_ready() || Permission::check('ADMIN')) { - return $token->reloadWithToken(); + return $confirmationTokenChain->reloadWithTokens(); } return null; }; diff --git a/src/Core/Startup/ParameterConfirmationToken.php b/src/Core/Startup/ParameterConfirmationToken.php index 4e90f1ef7..bc751a2c3 100644 --- a/src/Core/Startup/ParameterConfirmationToken.php +++ b/src/Core/Startup/ParameterConfirmationToken.php @@ -3,6 +3,7 @@ namespace SilverStripe\Core\Startup; use SilverStripe\Control\Controller; +use SilverStripe\Control\Director; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; use SilverStripe\Core\Convert; @@ -15,7 +16,7 @@ use SilverStripe\Security\RandomGenerator; * * @internal This class is designed specifically for use pre-startup and may change without warning */ -class ParameterConfirmationToken extends ConfirmationToken +class ParameterConfirmationToken extends AbstractConfirmationToken { /** * The name of the parameter @@ -140,19 +141,22 @@ class ParameterConfirmationToken extends ConfirmationToken } return $params; } + + public function getRedirectUrlBase() + { + return ($this->existsInReferer() && !$this->parameterProvided()) ? Director::baseURL() : $this->currentURL(); + } + + public function getRedirectUrlParams() + { + return ($this->existsInReferer() && !$this->parameterProvided()) + ? $this->params() + : array_merge($this->request->getVars(), $this->params()); + } protected function redirectURL() { - // If url is encoded via BackURL, defer to home page (prevent redirect to form action) - if ($this->existsInReferer() && !$this->parameterProvided()) { - $url = BASE_URL ?: '/'; - $params = $this->params(); - } else { - $url = $this->currentURL(); - $params = array_merge($this->request->getVars(), $this->params()); - } - - // Merge get params with current url - return Controller::join_links($url, '?' . http_build_query($params)); + $query = http_build_query($this->getRedirectUrlParams()); + return Controller::join_links($this->getRedirectUrlBase(), '?' . $query); } } diff --git a/src/Core/Startup/URLConfirmationToken.php b/src/Core/Startup/URLConfirmationToken.php index 8176a41b8..bb509931e 100644 --- a/src/Core/Startup/URLConfirmationToken.php +++ b/src/Core/Startup/URLConfirmationToken.php @@ -12,7 +12,7 @@ use SilverStripe\Control\HTTPRequest; * * @internal This class is designed specifically for use pre-startup and may change without warning */ -class URLConfirmationToken extends ConfirmationToken +class URLConfirmationToken extends AbstractConfirmationToken { /** * @var string @@ -60,7 +60,7 @@ class URLConfirmationToken extends ConfirmationToken */ protected function getURLExistsInBackURL(HTTPRequest $request) { - $backURL = $request->getVar('BackURL'); + $backURL = ltrim($request->getVar('BackURL'), '/'); return (strpos($backURL, $this->urlToCheck) === 0); } @@ -119,18 +119,21 @@ class URLConfirmationToken extends ConfirmationToken return Controller::join_links(Director::baseURL(), $this->currentURL); } + public function getRedirectUrlBase() + { + return ($this->urlExistsInBackURL && !$this->urlMatches()) ? Director::baseURL() : $this->currentURL(); + } + + public function getRedirectUrlParams() + { + return ($this->urlExistsInBackURL && !$this->urlMatches()) + ? $this->params() + : array_merge($this->request->getVars(), $this->params()); + } + protected function redirectURL() { - // If url is encoded via BackURL, defer to home page (prevent redirect to form action) - if ($this->urlExistsInBackURL && !$this->urlMatches()) { - $url = BASE_URL ?: '/'; - $params = $this->params(); - } else { - $url = $this->currentURL(); - $params = array_merge($this->request->getVars(), $this->params()); - } - - // Merge get params with current url - return Controller::join_links($url, '?' . http_build_query($params)); + $query = http_build_query($this->getRedirectUrlParams()); + return Controller::join_links($this->getRedirectUrlBase(), '?' . $query); } } diff --git a/tests/php/Core/Startup/ConfirmationTokenChainTest.php b/tests/php/Core/Startup/ConfirmationTokenChainTest.php new file mode 100644 index 000000000..adb8fba36 --- /dev/null +++ b/tests/php/Core/Startup/ConfirmationTokenChainTest.php @@ -0,0 +1,185 @@ +createPartialMock(ParameterConfirmationToken::class, $methods); + $mock->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue($requiresReload)); + return $mock; + } + + protected function getTokenRequiringReloadIfError($requiresReload = true, $extraMethods = []) + { + $methods = array_merge(['reloadRequired', 'reloadRequiredIfError'], $extraMethods); + $mock = $this->createPartialMock(ParameterConfirmationToken::class, $methods); + $mock->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue(false)); + $mock->expects($this->any()) + ->method('reloadRequiredIfError') + ->will($this->returnValue($requiresReload)); + return $mock; + } + + public function testFilteredTokens() + { + $chain = new ConfirmationTokenChain(); + $chain->pushToken($tokenRequiringReload = $this->getTokenRequiringReload()); + $chain->pushToken($tokenNotRequiringReload = $this->getTokenRequiringReload(false)); + $chain->pushToken($tokenRequiringReloadIfError = $this->getTokenRequiringReloadIfError()); + $chain->pushToken($tokenNotRequiringReloadIfError = $this->getTokenRequiringReloadIfError(false)); + + $reflectionMethod = new \ReflectionMethod(ConfirmationTokenChain::class, 'filteredTokens'); + $reflectionMethod->setAccessible(true); + $tokens = iterator_to_array($reflectionMethod->invoke($chain)); + + $this->assertContains($tokenRequiringReload, $tokens, 'Token requiring a reload was not returned'); + $this->assertNotContains($tokenNotRequiringReload, $tokens, 'Token not requiring a reload was returned'); + $this->assertContains($tokenRequiringReloadIfError, $tokens, 'Token requiring a reload on error was not returned'); + $this->assertNotContains($tokenNotRequiringReloadIfError, $tokens, 'Token not requiring a reload on error was returned'); + } + + public function testSuppressionRequired() + { + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReload(false)); + $this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReloadIfError(false)); + $this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReload()); + $this->assertTrue($chain->suppressionRequired(), 'Suppression not marked as required'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReloadIfError()); + $this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required'); + } + + public function testSuppressTokens() + { + $mockToken = $this->getTokenRequiringReload(true, ['suppress']); + $mockToken->expects($this->once()) + ->method('suppress'); + $secondMockToken = $this->getTokenRequiringReloadIfError(true, ['suppress']); + $secondMockToken->expects($this->once()) + ->method('suppress'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $chain->suppressTokens(); + } + + public function testReloadRequired() + { + $mockToken = $this->getTokenRequiringReload(true); + $secondMockToken = $this->getTokenRequiringReload(false); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertTrue($chain->reloadRequired()); + } + + public function testReloadRequiredIfError() + { + $mockToken = $this->getTokenRequiringReloadIfError(true); + $secondMockToken = $this->getTokenRequiringReloadIfError(false); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertTrue($chain->reloadRequiredIfError()); + } + + public function testParams() + { + $mockToken = $this->getTokenRequiringReload(true, ['params']); + $mockToken->expects($this->once()) + ->method('params') + ->with($this->isTrue()) + ->will($this->returnValue(['mockTokenParam' => '1'])); + $secondMockToken = $this->getTokenRequiringReload(true, ['params']); + $secondMockToken->expects($this->once()) + ->method('params') + ->with($this->isTrue()) + ->will($this->returnValue(['secondMockTokenParam' => '2'])); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertEquals(['mockTokenParam' => '1', 'secondMockTokenParam' => '2'], $chain->params(true)); + + $mockToken = $this->getTokenRequiringReload(true, ['params']); + $mockToken->expects($this->once()) + ->method('params') + ->with($this->isFalse()) + ->will($this->returnValue(['mockTokenParam' => '1'])); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $this->assertEquals(['mockTokenParam' => '1'], $chain->params(false)); + } + + public function testGetRedirectUrlBase() + { + $mockUrlToken = $this->createPartialMock(URLConfirmationToken::class, ['reloadRequired', 'getRedirectUrlBase']); + $mockUrlToken->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue(true)); + $mockUrlToken->expects($this->any()) + ->method('getRedirectUrlBase') + ->will($this->returnValue('url-base')); + + $mockParameterToken = $this->createPartialMock(ParameterConfirmationToken::class, ['reloadRequired', 'getRedirectUrlBase']); + $mockParameterToken->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue(true)); + $mockParameterToken->expects($this->any()) + ->method('getRedirectUrlBase') + ->will($this->returnValue('parameter-base')); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockParameterToken); + $chain->pushToken($mockUrlToken); + $this->assertEquals('url-base', $chain->getRedirectUrlBase(), 'URLConfirmationToken url base should take priority'); + + // Push them in reverse order to check priority still correct + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockUrlToken); + $chain->pushToken($mockParameterToken); + $this->assertEquals('url-base', $chain->getRedirectUrlBase(), 'URLConfirmationToken url base should take priority'); + } + + public function testGetRedirectUrlParams() + { + $mockToken = $this->getTokenRequiringReload(true, ['getRedirectUrlParams']); + $mockToken->expects($this->once()) + ->method('getRedirectUrlParams') + ->will($this->returnValue(['mockTokenParam' => '1'])); + + $secondMockToken = $this->getTokenRequiringReload(true, ['getRedirectUrlParams']); + $secondMockToken->expects($this->once()) + ->method('getRedirectUrlParams') + ->will($this->returnValue(['secondMockTokenParam' => '2'])); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertEquals(['mockTokenParam' => '1', 'secondMockTokenParam' => '2'], $chain->getRedirectUrlParams()); + } +} diff --git a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php index 27de7f930..7cf793c2f 100644 --- a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php +++ b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php @@ -122,4 +122,56 @@ class ErrorControlChainMiddlewareTest extends SapphireTest $this->assertNotContains('?devbuildtoken=', $location); $this->assertContains('Security/login', $location); } + + public function testLiveBuildAndFlushAdmin() + { + // Mock admin + $adminID = $this->logInWithPermission('ADMIN'); + $this->logOut(); + + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as admin + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build/', ['flush' => '1']); + $request->setSession(new Session(['loggedInAs' => $adminID])); + $result = $chain->process($request, function () { + return null; + }); + + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertContains('/dev/build', $location); + $this->assertContains('flush=1', $location); + $this->assertContains('devbuildtoken=', $location); + $this->assertContains('flushtoken=', $location); + $this->assertNotContains('Security/login', $location); + } + + public function testLiveBuildAndFlushUnauthenticated() + { + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as no one + Security::setCurrentUser(null); + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build', ['flush' => '1']); + $request->setSession(new Session(['loggedInAs' => 0])); + $result = $chain->process($request, function () { + return null; + }); + + // Should be directed to login, not to flush + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertNotContains('/dev/build', $location); + $this->assertNotContains('flush=1', $location); + $this->assertNotContains('devbuildtoken=', $location); + $this->assertNotContains('flushtoken=', $location); + $this->assertContains('Security/login', $location); + } } From 8d7c2dafabad505d769f3774c44e0595fb1a4cd9 Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Tue, 21 Aug 2018 11:20:15 +0100 Subject: [PATCH 052/175] [SS-2018-019] Add confirmation token to dev/build --- src/Control/HTTPApplication.php | 2 +- src/Core/Startup/ConfirmationToken.php | 182 ++++++++++++++++++ src/Core/Startup/ErrorControlChain.php | 3 +- .../Startup/ErrorControlChainMiddleware.php | 43 +++-- src/Core/Startup/ErrorDirector.php | 4 +- .../Startup/ParameterConfirmationToken.php | 172 +---------------- src/Core/Startup/URLConfirmationToken.php | 136 +++++++++++++ .../ErrorControlChainMiddlewareTest.php | 48 +++++ .../ParameterConfirmationTokenTest.php | 4 +- .../Core/Startup/URLConfirmationTokenTest.php | 148 ++++++++++++++ .../URLConfirmationTokenTest/StubToken.php | 27 +++ .../StubValidToken.php | 15 ++ 12 files changed, 600 insertions(+), 184 deletions(-) create mode 100644 src/Core/Startup/ConfirmationToken.php create mode 100644 src/Core/Startup/URLConfirmationToken.php create mode 100644 tests/php/Core/Startup/URLConfirmationTokenTest.php create mode 100644 tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php create mode 100644 tests/php/Core/Startup/URLConfirmationTokenTest/StubValidToken.php diff --git a/src/Control/HTTPApplication.php b/src/Control/HTTPApplication.php index 4d0f7cb9d..35f453d6d 100644 --- a/src/Control/HTTPApplication.php +++ b/src/Control/HTTPApplication.php @@ -41,7 +41,7 @@ class HTTPApplication implements Application */ public function handle(HTTPRequest $request) { - $flush = array_key_exists('flush', $request->getVars()) || strpos($request->getURL(), 'dev/build') === 0; + $flush = array_key_exists('flush', $request->getVars()) || ($request->getURL() === 'dev/build'); // Ensure boot is invoked return $this->execute($request, function (HTTPRequest $request) { diff --git a/src/Core/Startup/ConfirmationToken.php b/src/Core/Startup/ConfirmationToken.php new file mode 100644 index 000000000..d23563f46 --- /dev/null +++ b/src/Core/Startup/ConfirmationToken.php @@ -0,0 +1,182 @@ +reloadRequired() || $token->reloadRequiredIfError()) { + $token->suppress(); + $target = $token; + } + } + return $target; + } + + /** + * Generate a local filesystem path to store a token + * + * @param $token + * @return string + */ + protected function pathForToken($token) + { + return TEMP_PATH . DIRECTORY_SEPARATOR . 'token_' . preg_replace('/[^a-z0-9]+/', '', $token); + } + + /** + * Generate a new random token and store it + * + * @return string Token name + */ + protected function genToken() + { + // Generate a new random token (as random as possible) + $rg = new RandomGenerator(); + $token = $rg->randomToken('md5'); + + // Store a file in the session save path (safer than /tmp, as open_basedir might limit that) + file_put_contents($this->pathForToken($token), $token); + + return $token; + } + + /** + * Is the necessary token provided for this parameter? + * A value must be provided for the token + * + * @return bool + */ + public function tokenProvided() + { + return !empty($this->token); + } + + /** + * Validate a token + * + * @param string $token + * @return boolean True if the token is valid + */ + protected function checkToken($token) + { + if (!$token) { + return false; + } + + $file = $this->pathForToken($token); + $content = null; + + if (file_exists($file)) { + $content = file_get_contents($file); + unlink($file); + } + + return $content === $token; + } + + /** + * Get redirect url, excluding querystring + * + * @return string + */ + public function currentURL() + { + return Controller::join_links(Director::baseURL(), $this->request->getURL(false)); + } + + /** + * Forces a reload of the request with the token included + * + * @return HTTPResponse + */ + public function reloadWithToken() + { + $location = $this->redirectURL(); + $locationJS = Convert::raw2js($location); + $locationATT = Convert::raw2att($location); + $body = <<location.href='$locationJS'; + +You are being redirected. If you are not redirected soon, click here to continue +HTML; + + // Build response + $result = new HTTPResponse($body); + $result->redirect($location); + return $result; + } + + /** + * Is this parameter requested without a valid token? + * + * @return bool True if the parameter is given without a valid token + */ + abstract public function reloadRequired(); + + /** + * Check if this token is provided either in the backurl, or directly, + * but without a token + * + * @return bool + */ + abstract public function reloadRequiredIfError(); + + /** + * Suppress the current parameter for the duration of this request + */ + abstract public function suppress(); + + /** + * Determine the querystring parameters to include + * + * @param bool $includeToken Include the token value? + * @return array List of querystring parameters, possibly including token parameter + */ + abstract public function params($includeToken = true); + + /** + * Get redirection URL + * + * @return string + */ + abstract protected function redirectURL(); +} diff --git a/src/Core/Startup/ErrorControlChain.php b/src/Core/Startup/ErrorControlChain.php index f34a2c802..e2d14db65 100644 --- a/src/Core/Startup/ErrorControlChain.php +++ b/src/Core/Startup/ErrorControlChain.php @@ -15,8 +15,7 @@ use Exception; * $chain = new ErrorControlChain(); * $chain->then($callback1)->then($callback2)->thenIfErrored($callback3)->execute(); * - * WARNING: This class is experimental and designed specifically for use pre-startup. - * It will likely be heavily refactored before the release of 3.2 + * @internal This class is designed specifically for use pre-startup and may change without warning */ class ErrorControlChain { diff --git a/src/Core/Startup/ErrorControlChainMiddleware.php b/src/Core/Startup/ErrorControlChainMiddleware.php index bdb5ff0a3..e81444629 100644 --- a/src/Core/Startup/ErrorControlChainMiddleware.php +++ b/src/Core/Startup/ErrorControlChainMiddleware.php @@ -12,6 +12,8 @@ use SilverStripe\Security\Security; /** * Decorates application bootstrapping with errorcontrolchain + * + * @internal This class is designed specifically for use pre-startup and may change without warning */ class ErrorControlChainMiddleware implements HTTPMiddleware { @@ -30,27 +32,42 @@ class ErrorControlChainMiddleware implements HTTPMiddleware $this->application = $application; } + /** + * @param HTTPRequest $request + * @return ConfirmationToken|null + */ + protected function prepareConfirmationTokenIfRequired(HTTPRequest $request) + { + $token = URLConfirmationToken::prepare_tokens(['dev/build'], $request); + + if (!$token) { + $token = ParameterConfirmationToken::prepare_tokens( + ['isTest', 'isDev', 'flush'], + $request + ); + } + + return $token; + } + public function process(HTTPRequest $request, callable $next) { $result = null; // Prepare tokens and execute chain - $reloadToken = ParameterConfirmationToken::prepare_tokens( - ['isTest', 'isDev', 'flush'], - $request - ); + $confirmationToken = $this->prepareConfirmationTokenIfRequired($request); $chain = new ErrorControlChain(); $chain - ->then(function () use ($request, $chain, $reloadToken, $next, &$result) { + ->then(function () use ($request, $chain, $confirmationToken, $next, &$result) { // If no redirection is necessary then we can disable error supression - if (!$reloadToken) { + if (!$confirmationToken) { $chain->setSuppression(false); } try { // Check if a token is requesting a redirect - if ($reloadToken && $reloadToken->reloadRequired()) { - $result = $this->safeReloadWithToken($request, $reloadToken); + if ($confirmationToken && $confirmationToken->reloadRequired()) { + $result = $this->safeReloadWithToken($request, $confirmationToken); } else { // If no reload necessary, process application $result = call_user_func($next, $request); @@ -60,9 +77,9 @@ class ErrorControlChainMiddleware implements HTTPMiddleware } }) // Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway - ->thenIfErrored(function () use ($reloadToken) { - if ($reloadToken && $reloadToken->reloadRequiredIfError()) { - $result = $reloadToken->reloadWithToken(); + ->thenIfErrored(function () use ($confirmationToken) { + if ($confirmationToken && $confirmationToken->reloadRequiredIfError()) { + $result = $confirmationToken->reloadWithToken(); $result->output(); } }) @@ -85,7 +102,7 @@ class ErrorControlChainMiddleware implements HTTPMiddleware // Ensure session is started $request->getSession()->init($request); - + // Request with ErrorDirector $result = ErrorDirector::singleton()->handleRequestWithToken( $request, @@ -98,7 +115,7 @@ class ErrorControlChainMiddleware implements HTTPMiddleware // Fail and redirect the user to the login page $params = array_merge($request->getVars(), $reloadToken->params(false)); - $backURL = $request->getURL() . '?' . http_build_query($params); + $backURL = $reloadToken->currentURL() . '?' . http_build_query($params); $loginPage = Director::absoluteURL(Security::config()->get('login_url')); $loginPage .= "?BackURL=" . urlencode($backURL); $result = new HTTPResponse(); diff --git a/src/Core/Startup/ErrorDirector.php b/src/Core/Startup/ErrorDirector.php index 3c994c6af..54001fd05 100644 --- a/src/Core/Startup/ErrorDirector.php +++ b/src/Core/Startup/ErrorDirector.php @@ -21,11 +21,11 @@ class ErrorDirector extends Director * Redirect with token if allowed, or null if not allowed * * @param HTTPRequest $request - * @param ParameterConfirmationToken $token + * @param ConfirmationToken $token * @param Kernel $kernel * @return null|HTTPResponse Redirection response, or null if not able to redirect */ - public function handleRequestWithToken(HTTPRequest $request, ParameterConfirmationToken $token, Kernel $kernel) + public function handleRequestWithToken(HTTPRequest $request, ConfirmationToken $token, Kernel $kernel) { Injector::inst()->registerService($request, HTTPRequest::class); diff --git a/src/Core/Startup/ParameterConfirmationToken.php b/src/Core/Startup/ParameterConfirmationToken.php index 1c80db1d0..4e90f1ef7 100644 --- a/src/Core/Startup/ParameterConfirmationToken.php +++ b/src/Core/Startup/ParameterConfirmationToken.php @@ -9,30 +9,21 @@ use SilverStripe\Core\Convert; use SilverStripe\Security\RandomGenerator; /** - * Class ParameterConfirmationToken + * This is used to protect dangerous GET parameters that need to be detected early in the request + * lifecycle by generating a one-time-use token & redirecting with that token included in the + * redirected URL * - * When you need to use a dangerous GET parameter that needs to be set before core/Core.php is - * established, this class takes care of allowing some other code of confirming the parameter, - * by generating a one-time-use token & redirecting with that token included in the redirected URL - * - * WARNING: This class is experimental and designed specifically for use pre-startup. - * It will likely be heavily refactored before the release of 3.2 + * @internal This class is designed specifically for use pre-startup and may change without warning */ -class ParameterConfirmationToken +class ParameterConfirmationToken extends ConfirmationToken { - /** * The name of the parameter * * @var string */ protected $parameterName = null; - - /** - * @var HTTPRequest - */ - protected $request = null; - + /** * The parameter given in the main request * @@ -48,60 +39,6 @@ class ParameterConfirmationToken protected $parameterBackURL = null; /** - * The validated and checked token for this parameter - * - * @var string|null A string value, or null if either not provided or invalid - */ - protected $token = null; - - protected function pathForToken($token) - { - return TEMP_PATH . DIRECTORY_SEPARATOR . 'token_' . preg_replace('/[^a-z0-9]+/', '', $token); - } - - /** - * Generate a new random token and store it - * - * @return string Token name - */ - protected function genToken() - { - // Generate a new random token (as random as possible) - $rg = new RandomGenerator(); - $token = $rg->randomToken('md5'); - - // Store a file in the session save path (safer than /tmp, as open_basedir might limit that) - file_put_contents($this->pathForToken($token), $token); - - return $token; - } - - /** - * Validate a token - * - * @param string $token - * @return boolean True if the token is valid - */ - protected function checkToken($token) - { - if (!$token) { - return false; - } - - $file = $this->pathForToken($token); - $content = null; - - if (file_exists($file)) { - $content = file_get_contents($file); - unlink($file); - } - - return $content == $token; - } - - /** - * Create a new ParameterConfirmationToken - * * @param string $parameterName Name of the querystring parameter to check * @param HTTPRequest $request */ @@ -176,54 +113,23 @@ class ParameterConfirmationToken return $this->parameterBackURL !== null; } - /** - * Is the necessary token provided for this parameter? - * A value must be provided for the token - * - * @return bool - */ - public function tokenProvided() - { - return !empty($this->token); - } - - /** - * Is this parameter requested without a valid token? - * - * @return bool True if the parameter is given without a valid token - */ public function reloadRequired() { return $this->parameterProvided() && !$this->tokenProvided(); } - /** - * Check if this token is provided either in the backurl, or directly, - * but without a token - * - * @return bool - */ public function reloadRequiredIfError() { // Don't reload if token exists return $this->reloadRequired() || $this->existsInReferer(); } - - /** - * Suppress the current parameter by unsetting it from $_GET - */ + public function suppress() { unset($_GET[$this->parameterName]); $this->request->offsetUnset($this->parameterName); } - /** - * Determine the querystring parameters to include - * - * @param bool $includeToken Include the token value as well? - * @return array List of querystring parameters with name and token parameters - */ public function params($includeToken = true) { $params = array( @@ -234,25 +140,7 @@ class ParameterConfirmationToken } return $params; } - - /** - * Get redirect url, excluding querystring - * - * @return string - */ - protected function currentURL() - { - return Controller::join_links( - BASE_URL ?: '/', - $this->request->getURL(false) - ); - } - - /** - * Get redirection URL - * - * @return string - */ + protected function redirectURL() { // If url is encoded via BackURL, defer to home page (prevent redirect to form action) @@ -267,48 +155,4 @@ class ParameterConfirmationToken // Merge get params with current url return Controller::join_links($url, '?' . http_build_query($params)); } - - /** - * Forces a reload of the request with the token included - * - * @return HTTPResponse - */ - public function reloadWithToken() - { - $location = $this->redirectURL(); - $locationJS = Convert::raw2js($location); - $locationATT = Convert::raw2att($location); - $body = <<location.href='$locationJS'; - -You are being redirected. If you are not redirected soon, click here to continue the flush -HTML; - - // Build response - $result = new HTTPResponse($body); - $result->redirect($location); - return $result; - } - - /** - * Given a list of token names, suppress all tokens that have not been validated, and - * return the non-validated token with the highest priority - * - * @param array $keys List of token keys in ascending priority (low to high) - * @param HTTPRequest $request - * @return ParameterConfirmationToken The token container for the unvalidated $key given with the highest priority - */ - public static function prepare_tokens($keys, HTTPRequest $request) - { - $target = null; - foreach ($keys as $key) { - $token = new ParameterConfirmationToken($key, $request); - // Validate this token - if ($token->reloadRequired() || $token->reloadRequiredIfError()) { - $token->suppress(); - $target = $token; - } - } - return $target; - } } diff --git a/src/Core/Startup/URLConfirmationToken.php b/src/Core/Startup/URLConfirmationToken.php new file mode 100644 index 000000000..8176a41b8 --- /dev/null +++ b/src/Core/Startup/URLConfirmationToken.php @@ -0,0 +1,136 @@ +urlToCheck = $urlToCheck; + $this->request = $request; + $this->currentURL = $request->getURL(false); + + $this->tokenParameterName = preg_replace('/[^a-z0-9]/i', '', $urlToCheck) . 'token'; + $this->urlExistsInBackURL = $this->getURLExistsInBackURL($request); + + // If the token provided is valid, mark it as such + $token = $request->getVar($this->tokenParameterName); + if ($this->checkToken($token)) { + $this->token = $token; + } + } + + /** + * @param HTTPRequest $request + * @return bool + */ + protected function getURLExistsInBackURL(HTTPRequest $request) + { + $backURL = $request->getVar('BackURL'); + return (strpos($backURL, $this->urlToCheck) === 0); + } + + /** + * @return bool + */ + protected function urlMatches() + { + return ($this->currentURL === $this->urlToCheck); + } + + /** + * @return string + */ + public function getURLToCheck() + { + return $this->urlToCheck; + } + + /** + * @return bool + */ + public function urlExistsInBackURL() + { + return $this->urlExistsInBackURL; + } + + public function reloadRequired() + { + return $this->urlMatches() && !$this->tokenProvided(); + } + + public function reloadRequiredIfError() + { + return $this->reloadRequired() || $this->urlExistsInBackURL(); + } + + public function suppress() + { + $_SERVER['REQUEST_URI'] = '/'; + $this->request->setURL('/'); + } + + public function params($includeToken = true) + { + $params = []; + if ($includeToken) { + $params[$this->tokenParameterName] = $this->genToken(); + } + + return $params; + } + + public function currentURL() + { + return Controller::join_links(Director::baseURL(), $this->currentURL); + } + + protected function redirectURL() + { + // If url is encoded via BackURL, defer to home page (prevent redirect to form action) + if ($this->urlExistsInBackURL && !$this->urlMatches()) { + $url = BASE_URL ?: '/'; + $params = $this->params(); + } else { + $url = $this->currentURL(); + $params = array_merge($this->request->getVars(), $this->params()); + } + + // Merge get params with current url + return Controller::join_links($url, '?' . http_build_query($params)); + } +} diff --git a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php index d1e4175f8..27de7f930 100644 --- a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php +++ b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php @@ -74,4 +74,52 @@ class ErrorControlChainMiddlewareTest extends SapphireTest $this->assertNotContains('?flush=1&flushtoken=', $location); $this->assertContains('Security/login', $location); } + + public function testLiveBuildAdmin() + { + // Mock admin + $adminID = $this->logInWithPermission('ADMIN'); + $this->logOut(); + + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as admin + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build/'); + $request->setSession(new Session(['loggedInAs' => $adminID])); + $result = $chain->process($request, function () { + return null; + }); + + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertContains('/dev/build', $location); + $this->assertContains('?devbuildtoken=', $location); + $this->assertNotContains('Security/login', $location); + } + + public function testLiveBuildUnauthenticated() + { + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as no one + Security::setCurrentUser(null); + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build'); + $request->setSession(new Session(['loggedInAs' => 0])); + $result = $chain->process($request, function () { + return null; + }); + + // Should be directed to login, not to flush + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertNotContains('/dev/build', $location); + $this->assertNotContains('?devbuildtoken=', $location); + $this->assertContains('Security/login', $location); + } } diff --git a/tests/php/Core/Startup/ParameterConfirmationTokenTest.php b/tests/php/Core/Startup/ParameterConfirmationTokenTest.php index 66616433f..e28af8ea5 100644 --- a/tests/php/Core/Startup/ParameterConfirmationTokenTest.php +++ b/tests/php/Core/Startup/ParameterConfirmationTokenTest.php @@ -149,14 +149,14 @@ class ParameterConfirmationTokenTest extends SapphireTest } /** - * currentAbsoluteURL needs to handle base or url being missing, or any combination of slashes. + * currentURL needs to handle base or url being missing, or any combination of slashes. * * There should always be exactly one slash between each part in the result, and any trailing slash * should be preserved. * * @dataProvider dataProviderURLs */ - public function testCurrentAbsoluteURLHandlesSlashes($url) + public function testCurrentURLHandlesSlashes($url) { $this->request->setUrl($url); diff --git a/tests/php/Core/Startup/URLConfirmationTokenTest.php b/tests/php/Core/Startup/URLConfirmationTokenTest.php new file mode 100644 index 000000000..73b07eb13 --- /dev/null +++ b/tests/php/Core/Startup/URLConfirmationTokenTest.php @@ -0,0 +1,148 @@ + 'value']); + $validToken = new StubValidToken('token/test/url', $request); + $this->assertTrue($validToken->urlMatches()); + $this->assertFalse($validToken->urlExistsInBackURL()); + $this->assertTrue($validToken->tokenProvided()); // Actually forced to true for this test + $this->assertFalse($validToken->reloadRequired()); + $this->assertFalse($validToken->reloadRequiredIfError()); + $this->assertStringStartsWith(Controller::join_links(BASE_URL, '/', 'token/test/url'), $validToken->redirectURL()); + } + + public function testTokenWithLeadingSlashInUrl() + { + $request = new HTTPRequest('GET', '/leading/slash/url', []); + $leadingSlash = new StubToken('leading/slash/url', $request); + $this->assertTrue($leadingSlash->urlMatches()); + $this->assertFalse($leadingSlash->urlExistsInBackURL()); + $this->assertFalse($leadingSlash->tokenProvided()); + $this->assertTrue($leadingSlash->reloadRequired()); + $this->assertTrue($leadingSlash->reloadRequiredIfError()); + $this->assertContains('leading/slash/url', $leadingSlash->redirectURL()); + $this->assertContains('leadingslashurltoken', $leadingSlash->redirectURL()); + } + + public function testTokenWithTrailingSlashInUrl() + { + $request = new HTTPRequest('GET', 'trailing/slash/url/', []); + $trailingSlash = new StubToken('trailing/slash/url', $request); + $this->assertTrue($trailingSlash->urlMatches()); + $this->assertFalse($trailingSlash->urlExistsInBackURL()); + $this->assertFalse($trailingSlash->tokenProvided()); + $this->assertTrue($trailingSlash->reloadRequired()); + $this->assertTrue($trailingSlash->reloadRequiredIfError()); + $this->assertContains('trailing/slash/url', $trailingSlash->redirectURL()); + $this->assertContains('trailingslashurltoken', $trailingSlash->redirectURL()); + } + + public function testTokenWithUrlMatchedInBackUrl() + { + $request = new HTTPRequest('GET', '/', ['BackURL' => 'back/url']); + $backUrl = new StubToken('back/url', $request); + $this->assertFalse($backUrl->urlMatches()); + $this->assertTrue($backUrl->urlExistsInBackURL()); + $this->assertFalse($backUrl->tokenProvided()); + $this->assertFalse($backUrl->reloadRequired()); + $this->assertTrue($backUrl->reloadRequiredIfError()); + $home = (BASE_URL ?: '/') . '?'; + $this->assertStringStartsWith($home, $backUrl->redirectURL()); + $this->assertContains('backurltoken', $backUrl->redirectURL()); + } + + public function testUrlSuppressionWhenTokenMissing() + { + // Check suppression + $request = new HTTPRequest('GET', 'test/url', []); + $token = new StubToken('test/url', $request); + $this->assertEquals('test/url', $request->getURL(false)); + $token->suppress(); + $this->assertEquals('', $request->getURL(false)); + } + + public function testPrepareTokens() + { + $request = new HTTPRequest('GET', 'test/url', []); + $token = URLConfirmationToken::prepare_tokens( + [ + 'test/url', + 'test', + 'url' + ], + $request + ); + // Test no invalid tokens + $this->assertEquals('test/url', $token->getURLToCheck()); + $this->assertNotEquals('test/url', $request->getURL(false), 'prepare_tokens() did not suppress URL'); + } + + public function testPrepareTokensDoesntSuppressWhenNotMatched() + { + $request = new HTTPRequest('GET', 'test/url', []); + $token = URLConfirmationToken::prepare_tokens( + ['another/url'], + $request + ); + $this->assertEmpty($token); + $this->assertEquals('test/url', $request->getURL(false), 'prepare_tokens() incorrectly suppressed URL'); + } + + public function testPrepareTokensWithUrlMatchedInBackUrl() + { + // Test backurl token + $request = new HTTPRequest('GET', '/', ['BackURL' => 'back/url']); + $token = URLConfirmationToken::prepare_tokens( + [ 'back/url' ], + $request + ); + $this->assertNotEmpty($token); + $this->assertEquals('back/url', $token->getURLToCheck()); + $this->assertNotEquals('back/url', $request->getURL(false), 'prepare_tokens() did not suppress URL'); + } + + public function dataProviderURLs() + { + return [ + [''], + ['/'], + ['bar'], + ['bar/'], + ['/bar'], + ['/bar/'], + ]; + } + + /** + * currentURL needs to handle base or url being missing, or any combination of slashes. + * + * There should always be exactly one slash between each part in the result, and any trailing slash + * should be preserved. + * + * @dataProvider dataProviderURLs + */ + public function testCurrentURLHandlesSlashes($url) + { + $request = new HTTPRequest('GET', $url, []); + + $token = new StubToken( + 'another/url', + $request + ); + $expected = rtrim(Controller::join_links(BASE_URL, '/', $url), '/') ?: '/'; + $this->assertEquals($expected, $token->currentURL(), "Invalid redirect for request url $url"); + } +} diff --git a/tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php b/tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php new file mode 100644 index 000000000..ca08d3e1f --- /dev/null +++ b/tests/php/Core/Startup/URLConfirmationTokenTest/StubToken.php @@ -0,0 +1,27 @@ + Date: Mon, 30 Jul 2018 11:50:11 +1200 Subject: [PATCH 053/175] [SS-2018-018] Ignore arguments in mysqli::real_connect backtrace calls --- src/Dev/Backtrace.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Dev/Backtrace.php b/src/Dev/Backtrace.php index 756945c52..0b61f5a1b 100644 --- a/src/Dev/Backtrace.php +++ b/src/Dev/Backtrace.php @@ -26,6 +26,7 @@ class Backtrace array('PDO', '__construct'), array('mysqli', 'mysqli'), array('mysqli', 'select_db'), + array('mysqli', 'real_connect'), array('SilverStripe\\ORM\\DB', 'connect'), array('SilverStripe\\Security\\Security', 'check_default_admin'), array('SilverStripe\\Security\\Security', 'encrypt_password'), From 11fe5b3adffd36037ed62e490a4e8bedb7b76ac9 Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Fri, 24 Aug 2018 15:36:51 +0100 Subject: [PATCH 054/175] Implement ConfirmationTokenChain to handle multiple tokens at once --- ...oken.php => AbstractConfirmationToken.php} | 12 +- src/Core/Startup/ConfirmationTokenChain.php | 178 +++++++++++++++++ .../Startup/ErrorControlChainMiddleware.php | 62 +++--- src/Core/Startup/ErrorDirector.php | 13 +- .../Startup/ParameterConfirmationToken.php | 28 +-- src/Core/Startup/URLConfirmationToken.php | 29 +-- .../Startup/ConfirmationTokenChainTest.php | 185 ++++++++++++++++++ .../ErrorControlChainMiddlewareTest.php | 52 +++++ 8 files changed, 500 insertions(+), 59 deletions(-) rename src/Core/Startup/{ConfirmationToken.php => AbstractConfirmationToken.php} (95%) create mode 100644 src/Core/Startup/ConfirmationTokenChain.php create mode 100644 tests/php/Core/Startup/ConfirmationTokenChainTest.php diff --git a/src/Core/Startup/ConfirmationToken.php b/src/Core/Startup/AbstractConfirmationToken.php similarity index 95% rename from src/Core/Startup/ConfirmationToken.php rename to src/Core/Startup/AbstractConfirmationToken.php index d23563f46..11f78b48a 100644 --- a/src/Core/Startup/ConfirmationToken.php +++ b/src/Core/Startup/AbstractConfirmationToken.php @@ -15,7 +15,7 @@ use SilverStripe\Security\RandomGenerator; * * @internal This class is designed specifically for use pre-startup and may change without warning */ -abstract class ConfirmationToken +abstract class AbstractConfirmationToken { /** * @var HTTPRequest @@ -173,6 +173,16 @@ HTML; */ abstract public function params($includeToken = true); + /** + * @return string + */ + abstract public function getRedirectUrlBase(); + + /** + * @return array + */ + abstract public function getRedirectUrlParams(); + /** * Get redirection URL * diff --git a/src/Core/Startup/ConfirmationTokenChain.php b/src/Core/Startup/ConfirmationTokenChain.php new file mode 100644 index 000000000..a47f2c4c0 --- /dev/null +++ b/src/Core/Startup/ConfirmationTokenChain.php @@ -0,0 +1,178 @@ +tokens[] = $token; + } + + /** + * Collect all tokens that require a redirect + * + * @return \Generator + */ + protected function filteredTokens() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequired() || $token->reloadRequiredIfError()) { + yield $token; + } + } + } + + /** + * @return bool + */ + public function suppressionRequired() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequired()) { + return true; + } + } + + return false; + } + + /** + * Suppress URLs & GET vars from tokens that require a redirect + */ + public function suppressTokens() + { + foreach ($this->filteredTokens() as $token) { + $token->suppress(); + } + } + + /** + * @return bool + */ + public function reloadRequired() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequired()) { + return true; + } + } + + return false; + } + + /** + * @return bool + */ + public function reloadRequiredIfError() + { + foreach ($this->tokens as $token) { + if ($token->reloadRequiredIfError()) { + return true; + } + } + + return false; + } + + /** + * @param bool $includeToken + * @return array + */ + public function params($includeToken = true) + { + $params = []; + foreach ($this->tokens as $token) { + $params = array_merge($params, $token->params($includeToken)); + } + + return $params; + } + + /** + * Fetch the URL we want to redirect to, excluding query string parameters. This may + * be the same URL (with a token to be added outside this method), or to a different + * URL if the current one has been suppressed + * + * @return string + */ + public function getRedirectUrlBase() + { + // URLConfirmationTokens may alter the URL to suppress the URL they're protecting, + // so we need to ensure they're inspected last and therefore take priority + $tokens = iterator_to_array($this->filteredTokens(), false); + usort($tokens, function ($a, $b) { + return ($a instanceof URLConfirmationToken) ? 1 : 0; + }); + + $urlBase = Director::baseURL(); + foreach ($tokens as $token) { + $urlBase = $token->getRedirectUrlBase(); + } + + return $urlBase; + } + + /** + * Collate GET vars from all token providers that need to apply a token + * + * @return array + */ + public function getRedirectUrlParams() + { + $params = []; + foreach ($this->filteredTokens() as $token) { + $params = array_merge($params, $token->getRedirectUrlParams()); + } + + return $params; + } + + /** + * @return string + */ + protected function redirectURL() + { + $params = http_build_query($this->getRedirectUrlParams()); + return Controller::join_links($this->getRedirectUrlBase(), '?' . $params); + } + + /** + * @return HTTPResponse + */ + public function reloadWithTokens() + { + $location = $this->redirectURL(); + $locationJS = Convert::raw2js($location); + $locationATT = Convert::raw2att($location); + $body = <<location.href='$locationJS'; + +You are being redirected. If you are not redirected soon, click here to continue +HTML; + + // Build response + $result = new HTTPResponse($body); + $result->redirect($location); + return $result; + } +} diff --git a/src/Core/Startup/ErrorControlChainMiddleware.php b/src/Core/Startup/ErrorControlChainMiddleware.php index e81444629..c29878e15 100644 --- a/src/Core/Startup/ErrorControlChainMiddleware.php +++ b/src/Core/Startup/ErrorControlChainMiddleware.php @@ -34,20 +34,18 @@ class ErrorControlChainMiddleware implements HTTPMiddleware /** * @param HTTPRequest $request - * @return ConfirmationToken|null + * @return ConfirmationTokenChain */ - protected function prepareConfirmationTokenIfRequired(HTTPRequest $request) + protected function prepareConfirmationTokenChain(HTTPRequest $request) { - $token = URLConfirmationToken::prepare_tokens(['dev/build'], $request); + $chain = new ConfirmationTokenChain(); + $chain->pushToken(new URLConfirmationToken('dev/build', $request)); - if (!$token) { - $token = ParameterConfirmationToken::prepare_tokens( - ['isTest', 'isDev', 'flush'], - $request - ); + foreach (['isTest', 'isDev', 'flush'] as $parameter) { + $chain->pushToken(new ParameterConfirmationToken($parameter, $request)); } - return $token; + return $chain; } public function process(HTTPRequest $request, callable $next) @@ -55,19 +53,21 @@ class ErrorControlChainMiddleware implements HTTPMiddleware $result = null; // Prepare tokens and execute chain - $confirmationToken = $this->prepareConfirmationTokenIfRequired($request); - $chain = new ErrorControlChain(); - $chain - ->then(function () use ($request, $chain, $confirmationToken, $next, &$result) { - // If no redirection is necessary then we can disable error supression - if (!$confirmationToken) { - $chain->setSuppression(false); + $confirmationTokenChain = $this->prepareConfirmationTokenChain($request); + $errorControlChain = new ErrorControlChain(); + $errorControlChain + ->then(function () use ($request, $errorControlChain, $confirmationTokenChain, $next, &$result) { + if ($confirmationTokenChain->suppressionRequired()) { + $confirmationTokenChain->suppressTokens(); + } else { + // If no redirection is necessary then we can disable error supression + $errorControlChain->setSuppression(false); } try { // Check if a token is requesting a redirect - if ($confirmationToken && $confirmationToken->reloadRequired()) { - $result = $this->safeReloadWithToken($request, $confirmationToken); + if ($confirmationTokenChain && $confirmationTokenChain->reloadRequired()) { + $result = $this->safeReloadWithTokens($request, $confirmationTokenChain); } else { // If no reload necessary, process application $result = call_user_func($next, $request); @@ -77,10 +77,16 @@ class ErrorControlChainMiddleware implements HTTPMiddleware } }) // Finally if a token was requested but there was an error while figuring out if it's allowed, do it anyway - ->thenIfErrored(function () use ($confirmationToken) { - if ($confirmationToken && $confirmationToken->reloadRequiredIfError()) { - $result = $confirmationToken->reloadWithToken(); - $result->output(); + ->thenIfErrored(function () use ($confirmationTokenChain) { + if ($confirmationTokenChain && $confirmationTokenChain->reloadRequiredIfError()) { + try { + // Reload requires manual boot + $this->getApplication()->getKernel()->boot(false); + } finally { + // Given we're in an error state here, try to continue even if the kernel boot fails + $result = $confirmationTokenChain->reloadWithTokens(); + $result->output(); + } } }) ->execute(); @@ -92,10 +98,10 @@ class ErrorControlChainMiddleware implements HTTPMiddleware * or authentication is impossible. * * @param HTTPRequest $request - * @param ParameterConfirmationToken $reloadToken + * @param ConfirmationTokenChain $confirmationTokenChain * @return HTTPResponse */ - protected function safeReloadWithToken(HTTPRequest $request, $reloadToken) + protected function safeReloadWithTokens(HTTPRequest $request, ConfirmationTokenChain $confirmationTokenChain) { // Safe reload requires manual boot $this->getApplication()->getKernel()->boot(false); @@ -104,9 +110,9 @@ class ErrorControlChainMiddleware implements HTTPMiddleware $request->getSession()->init($request); // Request with ErrorDirector - $result = ErrorDirector::singleton()->handleRequestWithToken( + $result = ErrorDirector::singleton()->handleRequestWithTokenChain( $request, - $reloadToken, + $confirmationTokenChain, $this->getApplication()->getKernel() ); if ($result) { @@ -114,8 +120,8 @@ class ErrorControlChainMiddleware implements HTTPMiddleware } // Fail and redirect the user to the login page - $params = array_merge($request->getVars(), $reloadToken->params(false)); - $backURL = $reloadToken->currentURL() . '?' . http_build_query($params); + $params = array_merge($request->getVars(), $confirmationTokenChain->params(false)); + $backURL = $confirmationTokenChain->getRedirectUrlBase() . '?' . http_build_query($params); $loginPage = Director::absoluteURL(Security::config()->get('login_url')); $loginPage .= "?BackURL=" . urlencode($backURL); $result = new HTTPResponse(); diff --git a/src/Core/Startup/ErrorDirector.php b/src/Core/Startup/ErrorDirector.php index 54001fd05..575df7183 100644 --- a/src/Core/Startup/ErrorDirector.php +++ b/src/Core/Startup/ErrorDirector.php @@ -21,18 +21,21 @@ class ErrorDirector extends Director * Redirect with token if allowed, or null if not allowed * * @param HTTPRequest $request - * @param ConfirmationToken $token + * @param ConfirmationTokenChain $confirmationTokenChain * @param Kernel $kernel * @return null|HTTPResponse Redirection response, or null if not able to redirect */ - public function handleRequestWithToken(HTTPRequest $request, ConfirmationToken $token, Kernel $kernel) - { + public function handleRequestWithTokenChain( + HTTPRequest $request, + ConfirmationTokenChain $confirmationTokenChain, + Kernel $kernel + ) { Injector::inst()->registerService($request, HTTPRequest::class); // Next, check if we're in dev mode, or the database doesn't have any security data, or we are admin - $reload = function (HTTPRequest $request) use ($token, $kernel) { + $reload = function (HTTPRequest $request) use ($confirmationTokenChain, $kernel) { if ($kernel->getEnvironment() === Kernel::DEV || !Security::database_is_ready() || Permission::check('ADMIN')) { - return $token->reloadWithToken(); + return $confirmationTokenChain->reloadWithTokens(); } return null; }; diff --git a/src/Core/Startup/ParameterConfirmationToken.php b/src/Core/Startup/ParameterConfirmationToken.php index 4e90f1ef7..bc751a2c3 100644 --- a/src/Core/Startup/ParameterConfirmationToken.php +++ b/src/Core/Startup/ParameterConfirmationToken.php @@ -3,6 +3,7 @@ namespace SilverStripe\Core\Startup; use SilverStripe\Control\Controller; +use SilverStripe\Control\Director; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; use SilverStripe\Core\Convert; @@ -15,7 +16,7 @@ use SilverStripe\Security\RandomGenerator; * * @internal This class is designed specifically for use pre-startup and may change without warning */ -class ParameterConfirmationToken extends ConfirmationToken +class ParameterConfirmationToken extends AbstractConfirmationToken { /** * The name of the parameter @@ -140,19 +141,22 @@ class ParameterConfirmationToken extends ConfirmationToken } return $params; } + + public function getRedirectUrlBase() + { + return ($this->existsInReferer() && !$this->parameterProvided()) ? Director::baseURL() : $this->currentURL(); + } + + public function getRedirectUrlParams() + { + return ($this->existsInReferer() && !$this->parameterProvided()) + ? $this->params() + : array_merge($this->request->getVars(), $this->params()); + } protected function redirectURL() { - // If url is encoded via BackURL, defer to home page (prevent redirect to form action) - if ($this->existsInReferer() && !$this->parameterProvided()) { - $url = BASE_URL ?: '/'; - $params = $this->params(); - } else { - $url = $this->currentURL(); - $params = array_merge($this->request->getVars(), $this->params()); - } - - // Merge get params with current url - return Controller::join_links($url, '?' . http_build_query($params)); + $query = http_build_query($this->getRedirectUrlParams()); + return Controller::join_links($this->getRedirectUrlBase(), '?' . $query); } } diff --git a/src/Core/Startup/URLConfirmationToken.php b/src/Core/Startup/URLConfirmationToken.php index 8176a41b8..bb509931e 100644 --- a/src/Core/Startup/URLConfirmationToken.php +++ b/src/Core/Startup/URLConfirmationToken.php @@ -12,7 +12,7 @@ use SilverStripe\Control\HTTPRequest; * * @internal This class is designed specifically for use pre-startup and may change without warning */ -class URLConfirmationToken extends ConfirmationToken +class URLConfirmationToken extends AbstractConfirmationToken { /** * @var string @@ -60,7 +60,7 @@ class URLConfirmationToken extends ConfirmationToken */ protected function getURLExistsInBackURL(HTTPRequest $request) { - $backURL = $request->getVar('BackURL'); + $backURL = ltrim($request->getVar('BackURL'), '/'); return (strpos($backURL, $this->urlToCheck) === 0); } @@ -119,18 +119,21 @@ class URLConfirmationToken extends ConfirmationToken return Controller::join_links(Director::baseURL(), $this->currentURL); } + public function getRedirectUrlBase() + { + return ($this->urlExistsInBackURL && !$this->urlMatches()) ? Director::baseURL() : $this->currentURL(); + } + + public function getRedirectUrlParams() + { + return ($this->urlExistsInBackURL && !$this->urlMatches()) + ? $this->params() + : array_merge($this->request->getVars(), $this->params()); + } + protected function redirectURL() { - // If url is encoded via BackURL, defer to home page (prevent redirect to form action) - if ($this->urlExistsInBackURL && !$this->urlMatches()) { - $url = BASE_URL ?: '/'; - $params = $this->params(); - } else { - $url = $this->currentURL(); - $params = array_merge($this->request->getVars(), $this->params()); - } - - // Merge get params with current url - return Controller::join_links($url, '?' . http_build_query($params)); + $query = http_build_query($this->getRedirectUrlParams()); + return Controller::join_links($this->getRedirectUrlBase(), '?' . $query); } } diff --git a/tests/php/Core/Startup/ConfirmationTokenChainTest.php b/tests/php/Core/Startup/ConfirmationTokenChainTest.php new file mode 100644 index 000000000..adb8fba36 --- /dev/null +++ b/tests/php/Core/Startup/ConfirmationTokenChainTest.php @@ -0,0 +1,185 @@ +createPartialMock(ParameterConfirmationToken::class, $methods); + $mock->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue($requiresReload)); + return $mock; + } + + protected function getTokenRequiringReloadIfError($requiresReload = true, $extraMethods = []) + { + $methods = array_merge(['reloadRequired', 'reloadRequiredIfError'], $extraMethods); + $mock = $this->createPartialMock(ParameterConfirmationToken::class, $methods); + $mock->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue(false)); + $mock->expects($this->any()) + ->method('reloadRequiredIfError') + ->will($this->returnValue($requiresReload)); + return $mock; + } + + public function testFilteredTokens() + { + $chain = new ConfirmationTokenChain(); + $chain->pushToken($tokenRequiringReload = $this->getTokenRequiringReload()); + $chain->pushToken($tokenNotRequiringReload = $this->getTokenRequiringReload(false)); + $chain->pushToken($tokenRequiringReloadIfError = $this->getTokenRequiringReloadIfError()); + $chain->pushToken($tokenNotRequiringReloadIfError = $this->getTokenRequiringReloadIfError(false)); + + $reflectionMethod = new \ReflectionMethod(ConfirmationTokenChain::class, 'filteredTokens'); + $reflectionMethod->setAccessible(true); + $tokens = iterator_to_array($reflectionMethod->invoke($chain)); + + $this->assertContains($tokenRequiringReload, $tokens, 'Token requiring a reload was not returned'); + $this->assertNotContains($tokenNotRequiringReload, $tokens, 'Token not requiring a reload was returned'); + $this->assertContains($tokenRequiringReloadIfError, $tokens, 'Token requiring a reload on error was not returned'); + $this->assertNotContains($tokenNotRequiringReloadIfError, $tokens, 'Token not requiring a reload on error was returned'); + } + + public function testSuppressionRequired() + { + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReload(false)); + $this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReloadIfError(false)); + $this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReload()); + $this->assertTrue($chain->suppressionRequired(), 'Suppression not marked as required'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($this->getTokenRequiringReloadIfError()); + $this->assertFalse($chain->suppressionRequired(), 'Suppression incorrectly marked as required'); + } + + public function testSuppressTokens() + { + $mockToken = $this->getTokenRequiringReload(true, ['suppress']); + $mockToken->expects($this->once()) + ->method('suppress'); + $secondMockToken = $this->getTokenRequiringReloadIfError(true, ['suppress']); + $secondMockToken->expects($this->once()) + ->method('suppress'); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $chain->suppressTokens(); + } + + public function testReloadRequired() + { + $mockToken = $this->getTokenRequiringReload(true); + $secondMockToken = $this->getTokenRequiringReload(false); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertTrue($chain->reloadRequired()); + } + + public function testReloadRequiredIfError() + { + $mockToken = $this->getTokenRequiringReloadIfError(true); + $secondMockToken = $this->getTokenRequiringReloadIfError(false); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertTrue($chain->reloadRequiredIfError()); + } + + public function testParams() + { + $mockToken = $this->getTokenRequiringReload(true, ['params']); + $mockToken->expects($this->once()) + ->method('params') + ->with($this->isTrue()) + ->will($this->returnValue(['mockTokenParam' => '1'])); + $secondMockToken = $this->getTokenRequiringReload(true, ['params']); + $secondMockToken->expects($this->once()) + ->method('params') + ->with($this->isTrue()) + ->will($this->returnValue(['secondMockTokenParam' => '2'])); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertEquals(['mockTokenParam' => '1', 'secondMockTokenParam' => '2'], $chain->params(true)); + + $mockToken = $this->getTokenRequiringReload(true, ['params']); + $mockToken->expects($this->once()) + ->method('params') + ->with($this->isFalse()) + ->will($this->returnValue(['mockTokenParam' => '1'])); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $this->assertEquals(['mockTokenParam' => '1'], $chain->params(false)); + } + + public function testGetRedirectUrlBase() + { + $mockUrlToken = $this->createPartialMock(URLConfirmationToken::class, ['reloadRequired', 'getRedirectUrlBase']); + $mockUrlToken->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue(true)); + $mockUrlToken->expects($this->any()) + ->method('getRedirectUrlBase') + ->will($this->returnValue('url-base')); + + $mockParameterToken = $this->createPartialMock(ParameterConfirmationToken::class, ['reloadRequired', 'getRedirectUrlBase']); + $mockParameterToken->expects($this->any()) + ->method('reloadRequired') + ->will($this->returnValue(true)); + $mockParameterToken->expects($this->any()) + ->method('getRedirectUrlBase') + ->will($this->returnValue('parameter-base')); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockParameterToken); + $chain->pushToken($mockUrlToken); + $this->assertEquals('url-base', $chain->getRedirectUrlBase(), 'URLConfirmationToken url base should take priority'); + + // Push them in reverse order to check priority still correct + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockUrlToken); + $chain->pushToken($mockParameterToken); + $this->assertEquals('url-base', $chain->getRedirectUrlBase(), 'URLConfirmationToken url base should take priority'); + } + + public function testGetRedirectUrlParams() + { + $mockToken = $this->getTokenRequiringReload(true, ['getRedirectUrlParams']); + $mockToken->expects($this->once()) + ->method('getRedirectUrlParams') + ->will($this->returnValue(['mockTokenParam' => '1'])); + + $secondMockToken = $this->getTokenRequiringReload(true, ['getRedirectUrlParams']); + $secondMockToken->expects($this->once()) + ->method('getRedirectUrlParams') + ->will($this->returnValue(['secondMockTokenParam' => '2'])); + + $chain = new ConfirmationTokenChain(); + $chain->pushToken($mockToken); + $chain->pushToken($secondMockToken); + $this->assertEquals(['mockTokenParam' => '1', 'secondMockTokenParam' => '2'], $chain->getRedirectUrlParams()); + } +} diff --git a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php index 27de7f930..7cf793c2f 100644 --- a/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php +++ b/tests/php/Core/Startup/ErrorControlChainMiddlewareTest.php @@ -122,4 +122,56 @@ class ErrorControlChainMiddlewareTest extends SapphireTest $this->assertNotContains('?devbuildtoken=', $location); $this->assertContains('Security/login', $location); } + + public function testLiveBuildAndFlushAdmin() + { + // Mock admin + $adminID = $this->logInWithPermission('ADMIN'); + $this->logOut(); + + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as admin + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build/', ['flush' => '1']); + $request->setSession(new Session(['loggedInAs' => $adminID])); + $result = $chain->process($request, function () { + return null; + }); + + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertContains('/dev/build', $location); + $this->assertContains('flush=1', $location); + $this->assertContains('devbuildtoken=', $location); + $this->assertContains('flushtoken=', $location); + $this->assertNotContains('Security/login', $location); + } + + public function testLiveBuildAndFlushUnauthenticated() + { + // Mock app + $app = new HTTPApplication(new BlankKernel(BASE_PATH)); + $app->getKernel()->setEnvironment(Kernel::LIVE); + + // Test being logged in as no one + Security::setCurrentUser(null); + $chain = new ErrorControlChainMiddleware($app); + $request = new HTTPRequest('GET', '/dev/build', ['flush' => '1']); + $request->setSession(new Session(['loggedInAs' => 0])); + $result = $chain->process($request, function () { + return null; + }); + + // Should be directed to login, not to flush + $this->assertInstanceOf(HTTPResponse::class, $result); + $location = $result->getHeader('Location'); + $this->assertNotContains('/dev/build', $location); + $this->assertNotContains('flush=1', $location); + $this->assertNotContains('devbuildtoken=', $location); + $this->assertNotContains('flushtoken=', $location); + $this->assertContains('Security/login', $location); + } } From 1944a5a99b1c02c497676f6872882a2cf2250bea Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Wed, 7 Nov 2018 16:31:27 +1300 Subject: [PATCH 055/175] Update translations --- lang/ar.yml | 3 - lang/bg.yml | 3 - lang/cs.yml | 3 - lang/da.yml | 325 ++++++++++++++++++++++++++++++++++++++++++- lang/de.yml | 3 - lang/en.yml | 8 +- lang/eo.yml | 9 +- lang/es.yml | 3 - lang/et_EE.yml | 3 - lang/fa_IR.yml | 1 - lang/fi.yml | 17 ++- lang/fr.yml | 6 - lang/id.yml | 3 - lang/id_ID.yml | 3 - lang/it.yml | 15 +- lang/ja.yml | 3 - lang/lt.yml | 3 - lang/mi.yml | 3 - lang/nb.yml | 3 - lang/nl.yml | 160 ++++++++++++++++++++- lang/pl.yml | 6 - lang/ru.yml | 3 - lang/sk.yml | 3 - lang/sl.yml | 3 - lang/sr.yml | 3 - lang/sr@latin.yml | 3 - lang/sr_RS.yml | 3 - lang/sr_RS@latin.yml | 3 - lang/sv.yml | 19 ++- lang/zh.yml | 3 - 30 files changed, 534 insertions(+), 92 deletions(-) diff --git a/lang/ar.yml b/lang/ar.yml index 19381589f..518bf1344 100644 --- a/lang/ar.yml +++ b/lang/ar.yml @@ -150,7 +150,4 @@ ar: LOGIN: دخول LOSTPASSWORDHEADER: 'كلمة مرور مفقودة' NOTEPAGESECURED: 'هذه الصفحة محمية بكلمة مرور ، أدخل بيانات دخولك بالأسفل ليتم السماح لك بالوصول للصفحة' - NOTERESETLINKINVALID: "

رابط إعادة تعيين كلمة المرور غير صحيح أو نفذت صلاحيته.

\n

\nيمكنك طلب رابط جديد <\"{a href=\"{link1\"> هنا \n أو تغيير كلمة المرور الخاصة بك بعد <\"{a href=\"{link2\"> تسجيل دخولك.\n

" NOTERESETPASSWORD: 'أدخل بريدك الإلكتروني و سيتم إرسال رابط إعادة تهيئة كلمة المرور ' - PASSWORDSENTHEADER: 'رابط استعادة كلمة المرور تم إرساله إلى ''{بريدك}''' - PASSWORDSENTTEXT: 'شكرا لك! تم إرسال رابط إعادة تعيين إلى ''{بريدك}''، بشرط وجود حساب قائم بالنسبة لعنوان هذا البريد الإلكتروني .' diff --git a/lang/bg.yml b/lang/bg.yml index 3e1eba6a0..241841f1e 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -313,7 +313,4 @@ bg: LOGOUT: Изход LOSTPASSWORDHEADER: 'Забравена парола' NOTEPAGESECURED: 'Тази страница е защитена. Въведете вашите данни по-долу, за да продължите.' - NOTERESETLINKINVALID: '

Връзката за нулиране на парола не е вярна или е просрочена.

Можете да заявите нова тук или да промените паролата си след като влезете.

' NOTERESETPASSWORD: 'Въведете вашият email адрес и ще ви изпратим линк, с който ще можете да смените паролата си' - PASSWORDSENTHEADER: 'Връзка за нулиране на парола беше изпратена на ''{email}''' - PASSWORDSENTTEXT: 'Благодарим ви! Връзка за нулиране на паролата беше изпратен на ''{email}'', ако съществува акаунт с този имейл адрес.' diff --git a/lang/cs.yml b/lang/cs.yml index 4a3cae3ab..759f0268e 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -194,7 +194,4 @@ cs: LOGIN: Přihlásit LOSTPASSWORDHEADER: 'Zapomenuté heslo' NOTEPAGESECURED: 'Tato stránka je zabezpečená. Vložte své přihlašovací údaje a my Vám zároveň pošleme práva.' - NOTERESETLINKINVALID: '

Odkaz na resetování hesla není platný nebo je prošlý.

Můžete požádat o nový zde nebo změňte své heslo až se přihlásíte.

' NOTERESETPASSWORD: 'Zadejte svou e-mailovou adresu a bude vám zaslán nulovací odkaz pro Vaše heslo' - PASSWORDSENTHEADER: 'Odkaz na resetování hesla byl odeslán na ''{email}''' - PASSWORDSENTTEXT: 'Děkujeme! Resetovací odkaz byl odeslán na ''{email}'', pokud účet existuje pro tuto emailovou adresu.' diff --git a/lang/da.yml b/lang/da.yml index 8dc512b23..3fe62969a 100644 --- a/lang/da.yml +++ b/lang/da.yml @@ -1,5 +1,328 @@ da: + SilverStripe\Admin\LeftAndMain: + VersionUnknown: ukendt + SilverStripe\AssetAdmin\Forms\UploadField: + Dimensions: Dimensioner + EDIT: Rediger + EDITINFO: 'Rediger denne fil' + REMOVE: Fjern + SilverStripe\Control\ChangePasswordEmail_ss: + CHANGEPASSWORDFOREMAIL: 'Koden for kontoen med email addressen {email} er ændret. Hvis du ikke har skiftet din kode, så skift venligst din kode ved at klikke på linket herunder' + CHANGEPASSWORDTEXT1: 'Du skiftede dit kodeord for' + CHANGEPASSWORDTEXT3: 'Skift kodeord' + HELLO: Hej + SilverStripe\Control\Email\ForgotPasswordEmail_ss: + HELLO: Hej + TEXT1: 'Her er din' + TEXT2: 'link til at nulstille dit kodeord' + TEXT3: for + SilverStripe\Control\RequestProcessor: + INVALID_REQUEST: 'Ugyldig forespørgsel' + REQUEST_ABORTED: 'Forespørgsel annulleret' + SilverStripe\Core\Manifest\VersionProvider: + VERSIONUNKNOWN: Ukendt + SilverStripe\Forms\CheckboxField: + NOANSWER: Nej + YESANSWER: Ja + SilverStripe\Forms\CheckboxSetField_ss: + NOOPTIONSAVAILABLE: 'Ingen tilgængelige muligheder' + SilverStripe\Forms\ConfirmedPasswordField: + ATLEAST: 'Kodeord skal være mindst {min} tegn lang.' + BETWEEN: 'Kodeord skal være {min} til {max} karakterer lang.' + CURRENT_PASSWORD_ERROR: 'Det nuværende kodeord du har indtastet er ikke korrekt.' + CURRENT_PASSWORD_MISSING: 'Du skal indtaste dit nuværende kodeord.' + LOGGED_IN_ERROR: 'Du skal være logget ind for at skifte dit kodeord.' + MAXIMUM: 'Kodeord må maks være {max} tegn lang' + SHOWONCLICKTITLE: 'Skift kodeord' + SilverStripe\Forms\CurrencyField: + CURRENCYSYMBOL: DKK + SilverStripe\Forms\DateField: + VALIDDATEFORMAT2: 'Indtats venligst et gyldigt datoformat ({format})' + VALIDDATEMAXDATE: 'Din dato skal være ældre end eller matche den maksimalt tilladte dato ({date})' + VALIDDATEMINDATE: 'Din dato skal være yngre end eller matche den minimum tilladte dato ({date})' + SilverStripe\Forms\DatetimeField: + VALIDDATEMAXDATETIME: 'Din dato skal være ældre end eller matche den maksimalt tilladte dato ({datetime})' + VALIDDATETIMEFORMAT: 'Indtats venligst et gyldigt dato- og tidsformat ({format})' + VALIDDATETIMEMINDATE: 'Din dato skal være yngre end eller matche den minimum tilladte dato og tid ({datetime})' + SilverStripe\Forms\DropdownField: + CHOOSE: (Vælg) + CHOOSE_MODEL: '(Vælg {name})' + SOURCE_VALIDATION: 'Venligst vælg en eksisterende værdi fra listen. {value} er ikke en tilladt mulighed' + SilverStripe\Forms\EmailField: + VALIDATION: 'Indtast venligst en emailadresse' + SilverStripe\Forms\FileUploadReceiver: + FIELDNOTSET: 'Fil information ikke fundet' + SilverStripe\Forms\Form: + BAD_METHOD: 'Denne form kræver en {method} indsendelse' + CSRF_EXPIRED_MESSAGE: 'Din session er udløbet. Venligst gensend formularen.' + CSRF_FAILED_MESSAGE: 'Det ser ud til der har været et teknisk problem. Klik venligst på tilbageknappen, tryk opdater i din browser og prøv igen.' + VALIDATIONPASSWORDSDONTMATCH: 'Kodeordene er ikke identiske' + VALIDATIONPASSWORDSNOTEMPTY: 'Kodeord kan ikke være tomme' + VALIDATIONSTRONGPASSWORD: 'Kodeord skal mindst have et tal og et alfanumerisk tegn' + VALIDATOR: Validering + VALIDCURRENCY: 'Indtast venligst en gyldig valuta' + SilverStripe\Forms\FormField: + EXAMPLE: 'f.eks. {format}' + NONE: ingen + SilverStripe\Forms\FormScaffolder: + TABMAIN: Primær SilverStripe\Forms\GridField\GridField: - Filter: Filter + Add: 'Tilføj {name}' + CSVEXPORT: 'Eksporter til CSV' + CSVIMPORT: 'Importer CSV' + Filter: Filtrer + FilterBy: 'Filtrer på' + Find: Find + LinkExisting: 'Link eksisterende' + NewRecord: 'Ny {type}' + NoItemsFound: 'Ingen elementer fundet' + PRINTEDAT: 'Printet d.' + PRINTEDBY: 'Printet af' + PlaceHolder: 'Find {type}' + PlaceHolderWithLabels: 'Find {type} på {name}' + Print: Print + RelationSearch: Relationssøgning + ResetFilter: Nulstil + SilverStripe\Forms\GridField\GridFieldDeleteAction: + Delete: Slet + DeletePermissionsFailure: 'Ingen slette rettigheder' + EditPermissionsFailure: 'Ingen rettighed til at fjerne emnet' + UnlinkRelation: Fjern + SilverStripe\Forms\GridField\GridFieldDetailForm: + CancelBtn: Annuller + Create: Opret + Delete: Slet + DeletePermissionsFailure: 'Ingen slette rettigheder' + Deleted: 'Slet {type} {name}' + Save: Gem + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Rediger + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'Kan ikke fjerne dig selv fra denne gruppe, du vil miste administrator rettigheder' + SilverStripe\Forms\GridField\GridFieldPaginator: + OF: af + Page: Side + View: Vis + SilverStripe\Forms\MoneyField: + FIELDLABELAMOUNT: Beløb + FIELDLABELCURRENCY: Valuta + INVALID_CURRENCY: 'Valuta {currency} er ikke i listen over tilladte valutaer' + SilverStripe\Forms\MultiSelectField: + SOURCE_VALIDATION: 'Vælg venligst eksisterende værdier fra listen. Ugyldig mulighed(er) {value} valgt' + SilverStripe\Forms\NullableField: + IsNullLabel: 'Er Null' + SilverStripe\Forms\NumericField: + VALIDATION: '''{value}'' er ikke et tal, kun tal accepteres i dette felt' + SilverStripe\Forms\TimeField: + VALIDATEFORMAT: 'Indtats venligst et gyldigt tidsformat ({format})' + SilverStripe\ORM\DataObject: + PLURALNAME: Dataobjekter + PLURALS: + one: 'Et dataobjekt' + other: '{count} dataobjekter' + SINGULARNAME: Dataobjekt + SilverStripe\ORM\FieldType\DBBoolean: + ANY: Enhver + NOANSWER: Nej + YESANSWER: Ja + SilverStripe\ORM\FieldType\DBDate: + DAYS_SHORT_PLURALS: + one: '{count} dag' + other: '{count} dage' + HOURS_SHORT_PLURALS: + one: '{count} time' + other: '{count} timer' + LessThanMinuteAgo: 'mindre end et minut' + MINUTES_SHORT_PLURALS: + one: '{count} minut' + other: '{count} minutter' + MONTHS_SHORT_PLURALS: + one: '{count} måned' + other: '{count} måneder' + SECONDS_SHORT_PLURALS: + one: '{count} sekund' + other: '{count} sekunder' + TIMEDIFFAGO: '{difference} siden' + TIMEDIFFIN: 'i {difference}' + YEARS_SHORT_PLURALS: + one: '{count} år' + other: '{count} år' + SilverStripe\ORM\FieldType\DBEnum: + ANY: Enhver + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'For mange relaterede objekter; fallback felt i brug' + SilverStripe\ORM\Hierarchy: + LIMITED_TITLE: 'For mange underelementer ({count})' + SilverStripe\ORM\Hierarchy\Hierarchy: + InfiniteLoopNotAllowed: 'Uendeligt løkke fundet i "{type}" hierarkiet. Ændre venligst det overliggende element for at løse dette' + LIMITED_TITLE: 'For mange underelementer ({count})' + SilverStripe\ORM\ValidationException: + DEFAULT_ERROR: Valideringsfejl + SilverStripe\Security\BasicAuth: + ENTERINFO: 'Indtast venligst et brugernavn og kodeord.' + ERRORNOTADMIN: 'Den bruger er ikke en administrator.' + ERRORNOTREC: 'Brugernavn / kodeord kunne ikke genkendes' + SilverStripe\Security\CMSMemberLoginForm: + PASSWORDEXPIRED: '

Dit kodeord er udløbet. Vælg venligst et nyt.

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

Ugyldig bruger. Log venligst ind igen her for at fortsætte.

' + LOGIN_MESSAGE: '

Din session er løbet ud pga. inaktivitet

' + LOGIN_TITLE: 'Log ind igen, for at fortsætte hvor du slap.' + SUCCESS: Succes + SUCCESSCONTENT: '

Logget ind. Hvis du ikke automatisk viderestilles så klik her

' + SUCCESS_TITLE: 'Logget ind med sucess' + SilverStripe\Security\DefaultAdminService: + DefaultAdminFirstname: 'Standard admin' + SilverStripe\Security\Group: + AddRole: 'Tilføj en rolle for denne gruppe' + Code: 'Gruppe kode' + DefaultGroupTitleAdministrators: Administratorer + DefaultGroupTitleContentAuthors: Indholdsforfattere + Description: Beskrivelse + GROUPNAME: Gruppenavn + GroupReminder: 'Hvis du vælger en overliggende gruppe, får denne gruppe alle dens roller' + HierarchyPermsError: 'Kan ikke tildele overliggende gruppe "{group}" med fortrinsrettigheder (kræver ADMIN adgang)' + Locked: 'Låst?' + MEMBERS: Brugere + NEWGROUP: 'Ny gruppe' + NoRoles: 'Ingen roller fundet' + PERMISSIONS: Rettigheder + PLURALNAME: Grupper + PLURALS: + one: 'En gruppe' + other: '{count} grupper' + Parent: 'Overliggende gruppe' + ROLES: Roller + ROLESDESCRIPTION: 'Roller er et prædefineret sæt af rettigheder, som kan tildeles grupper.
De bliver nedarvet fra en overliggende grupper hvis krævet.' + RolesAddEditLink: 'Administrer roller' + SINGULARNAME: Gruppe + Sort: Sortering + has_many_Permissions: Rettigheder + many_many_Members: Brugere + SilverStripe\Security\LoginAttempt: + Email: 'Email adresse' + EmailHashed: 'Email adresse (hashed)' + IP: 'IP addresse' + PLURALNAME: Loginforsøg + PLURALS: + one: 'Et loginforsøg' + other: '{count} loginforsøg' + SINGULARNAME: 'Login forsøg' + Status: Status + SilverStripe\Security\Member: + ADDGROUP: 'Tilføj gruppe' + BUTTONCHANGEPASSWORD: 'Skift kodeord' + BUTTONLOGIN: 'Log ind' + BUTTONLOGINOTHER: 'Log ind med en anden bruger' + BUTTONLOGOUT: 'Log ud' + BUTTONLOSTPASSWORD: 'Jeg har glemt mit kodeord' + CONFIRMNEWPASSWORD: 'Bekræft nyt kodeord' + CONFIRMPASSWORD: 'Bekræft kodeord' + CURRENT_PASSWORD: 'Nuværende kodeord' + EDIT_PASSWORD: 'Nyt kodeord' + EMAIL: Email + EMPTYNEWPASSWORD: 'Det nye kodeord kan ikke være tom, prøv venligst igen' + ENTEREMAIL: 'Indtast venligst en email adresse for at få et nulstillingslink.' + ERRORLOCKEDOUT2: 'Din konto er blevet midlertidigt deaktiveret pga. for mange fejlslagne loginforsøg. Forsøg venligst igen om {count} minutter.' + ERRORNEWPASSWORD: 'Du har indtastet dit nye kodeord forskelligt, forsøg igen' + ERRORPASSWORDNOTMATCH: 'Dit nuværende kodeord matcher ikke, forsøg venligst igen' + ERRORWRONGCRED: 'De indtastede værdier ser ikke ud til at være korrekte. Forsøg venligst igen.' + FIRSTNAME: Fornavn + INTERFACELANG: 'Sprog i brugerfladen' + KEEPMESIGNEDIN: 'Hold mig logget ind' + LOGGEDINAS: 'Du er logget ind som {name}.' + NEWPASSWORD: 'Nyt kodeord' + PASSWORD: Kodeord + PASSWORDEXPIRED: 'Dit kodeord er udløbet. Vælg venligst et nyt.' + PLURALNAME: Brugere + PLURALS: + one: 'En bruger' + other: '{count} brugere' + REMEMBERME: 'Husk mig til næste gang? (i {count} dage på denne enhed)' + SINGULARNAME: Bruger + SUBJECTPASSWORDCHANGED: 'Dit kodeord er blevet ændret' + SUBJECTPASSWORDRESET: 'Link til at nulstille dit kodeord' + SURNAME: Efternavn + VALIDATIONADMINLOSTACCESS: 'Kan ikke fjerne alle admin grupper fra din profil' + ValidationIdentifierFailed: 'Kan ikke overskrive eksisterende bruger #{id} med identisk identifikator ({name} = {value}))' + WELCOMEBACK: 'Velkommen tilbage, {firstname}' + YOUROLDPASSWORD: 'Dit gamle kodeord' + belongs_many_many_Groups: Grupper + db_Locale: 'Sprog i brugerfladen' + db_LockedOutUntil: 'Låst ude indtil' + db_Password: Kodeord + db_PasswordExpiry: Kodeordsudløbsdato + SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm: + AUTHENTICATORNAME: 'CMS bruger loginform' + BUTTONFORGOTPASSWORD: 'Glemt kodeord' + BUTTONLOGIN: 'Log mig ind igen' + BUTTONLOGOUT: 'Log ud' + SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: + ERRORWRONGCRED: 'De indtastede værdier ser ikke ud til at være korrekte. Forsøg venligst igen.' + NoPassword: 'Der er ikke en kode på denne bruger.' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'Email og kodeord' + SilverStripe\Security\MemberPassword: + PLURALNAME: 'Bruger kodeord' + PLURALS: + one: 'Et bruger kodeord' + other: '{count} bruger kodeord' + SINGULARNAME: 'Bruger kodeord' + SilverStripe\Security\PasswordValidator: + LOWCHARSTRENGTH: 'Forøg venligst kodeordets styrke, ved at tilføje nogle af følgende tegn: {chars}' + PREVPASSWORD: 'Du har tidligere brugt dette kodeord, vælg venligst et nyt kodeord' + TOOSHORT: 'Kodeordet er for kort, det skal mindst være {minimum} eller flere tegn langt' SilverStripe\Security\Permission: + AdminGroup: Administrator + CMS_ACCESS_CATEGORY: 'CMS Adgang' CONTENT_CATEGORY: Indholdsrettigheder + FULLADMINRIGHTS: 'Fuld administrator rettighed' + FULLADMINRIGHTS_HELP: 'Indebærer og overskriver alle andre tildelte rettigheder.' + PERMISSIONS_CATEGORY: 'Roller og adgangsrettigheder' + PLURALNAME: Rettigheder + PLURALS: + one: 'En rettighed' + other: '{count} rettigheder' + SINGULARNAME: Rettighed + UserPermissionsIntro: 'Tildeling af grupper til denne bruger, ændrer de rettigheder brugeren har. Se gruppe området for rettigheds detaljer på de individuelle grupper.' + SilverStripe\Security\PermissionCheckboxSetField: + AssignedTo: 'tildelt til "{title}"' + FromGroup: 'nedarvet fra gruppen "{title}"' + FromRole: 'nedarvet fra rollen "{title}"' + FromRoleOnGroup: 'nedarvet fra rollen "{roletitle}" på gruppen "{grouptitle}"' + SilverStripe\Security\PermissionRole: + OnlyAdminCanApply: 'Kun administratorer kan tilføje' + PLURALNAME: Roller + PLURALS: + one: 'En rolle' + other: '{count} roller' + SINGULARNAME: Rolle + Title: Titel + SilverStripe\Security\PermissionRoleCode: + PLURALNAME: 'Rettigheds rolle koder' + PLURALS: + one: 'En rettigheds rolle kode' + other: '{count} rettigheds rolle koder' + PermsError: 'Kan ikke tildele koden "{code}" med fortrinsrettigheder (kræver ADMIN adgang)' + SINGULARNAME: 'Rettighed rolle kode' + SilverStripe\Security\RememberLoginHash: + PLURALNAME: 'Login hashes' + PLURALS: + one: 'Et login hash' + other: '{count} Login Hashes' + SINGULARNAME: 'Login hash' + SilverStripe\Security\Security: + ALREADYLOGGEDIN: 'Du har ikke adgang til denne side. Hvis du har en anden bruger der har adgang til denne side, kan du logge ind med denne herunder.' + BUTTONSEND: 'Send mig linket til at nulstille kodeordet' + CHANGEPASSWORDBELOW: 'Du kan ændre dit kodeord herunder.' + CHANGEPASSWORDHEADER: 'Skift dit kodeord' + CONFIRMLOGOUT: 'Klik venligst på knappen herunder, for at bekræfte at du vil logge ud.' + ENTERNEWPASSWORD: 'Indtast venligst et nyt kodeord.' + ERRORPASSWORDPERMISSION: 'Du skal være logget ind, for at kunne ændre dit kodeord!' + LOGIN: 'Log ind' + LOGOUT: 'Log ud' + LOSTPASSWORDHEADER: 'Glemt kodeord' + NOTEPAGESECURED: 'Denne side er beskyttet. Indtast dine loginoplysninger herunder for at få adgang.' + NOTERESETPASSWORD: 'Indtast din email adresse, så sender vi dig et link som du kan nulstille dit kodeord med' + PASSWORDRESETSENTHEADER: 'link til at nulstille kodeord afsendt' + PASSWORDRESETSENTTEXT: 'Tak for det. Et link til at nulstille dit kodeord er afsendt, hvis der findes en bruger med denne email adresse.' diff --git a/lang/de.yml b/lang/de.yml index 1e9f16e5e..77e2f1584 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -190,7 +190,4 @@ de: LOGIN: Anmelden LOSTPASSWORDHEADER: 'Passwort vergessen' NOTEPAGESECURED: 'Diese Seite ist geschützt. Bitte melden Sie sich an und Sie werden sofort weitergeleitet.' - NOTERESETLINKINVALID: '

Der Link zum Zurücksetzen des Passworts ist entweder nicht korrekt oder abgelaufen

Sie können einen neuen Link anfordern oder Ihr Passwort nach dem einloggen ändern.

' NOTERESETPASSWORD: 'Geben Sie Ihre E-Mail-Adresse ein und wir werden Ihnen einen Link zuschicken, mit dem Sie Ihr Passwort zurücksetzen können.' - PASSWORDSENTHEADER: 'Der Link zum Zurücksetzen des Passworts wurde an ''{email}'' gesendet' - PASSWORDSENTTEXT: 'Vielen Dank! Wenn ein Account zu der E-Mail Adresse ''{email}'' existiert, wurde eine E-Mail mit dem Link zum Zurücksetzen des Passworts verschickt.' diff --git a/lang/en.yml b/lang/en.yml index a81c56613..42a437d01 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -78,6 +78,7 @@ en: LinkExisting: 'Link Existing' NewRecord: 'New {type}' NoItemsFound: 'No items found' + OpenFilter: 'Open search and filter' PRINTEDAT: 'Printed at' PRINTEDBY: 'Printed by' PlaceHolder: 'Find {type}' @@ -85,10 +86,6 @@ en: Print: Print RelationSearch: 'Relation search' ResetFilter: Reset - OpenFilter: 'Open search and filter' - SilverStripe\Forms\GridField\GridFieldFilterHeader: - Search: 'Search "{name}"' - SearchFormFaliure: 'No search form could be generated' SilverStripe\Forms\GridField\GridFieldDeleteAction: Delete: Delete DeletePermissionsFailure: 'No delete permissions' @@ -103,6 +100,9 @@ en: Save: Save SilverStripe\Forms\GridField\GridFieldEditButton: EDIT: Edit + SilverStripe\Forms\GridField\GridFieldFilterHeader: + Search: 'Search "{name}"' + SearchFormFaliure: 'No search form could be generated' SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: UnlinkSelfFailure: 'Cannot remove yourself from this group, you will lose admin rights' SilverStripe\Forms\GridField\GridFieldPaginator: diff --git a/lang/eo.yml b/lang/eo.yml index 59806b953..fb6b74ca9 100644 --- a/lang/eo.yml +++ b/lang/eo.yml @@ -95,6 +95,8 @@ eo: DeletePermissionsFailure: 'Mankas permeso forigi' Deleted: 'Forigita {type} {name}' Save: Konservi + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Redakti SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: UnlinkSelfFailure: 'Ne povas forigi vin el ĉi tiu grupo; vi perdus administrajn rajtojn' SilverStripe\Forms\GridField\GridFieldPaginator: @@ -147,6 +149,8 @@ eo: other: '{count} jaroj' SilverStripe\ORM\FieldType\DBEnum: ANY: Ajna + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'Tro multaj objektoj; retropaŝa kampo uzata' SilverStripe\ORM\Hierarchy: LIMITED_TITLE: 'Tro da idoj ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: @@ -319,7 +323,6 @@ eo: LOGOUT: Elsaluti LOSTPASSWORDHEADER: 'Perdis pasvorton' NOTEPAGESECURED: 'Tiu paĝo estas sekurigita. Enigu viajn akreditaĵojn sube kaj vi aliros pluen.' - NOTERESETLINKINVALID: '

La pasvorta reagorda ligilo estas malvalida aŭ finiĝis.

Vi povas peti novan ĉi tie aŭ ŝanĝi vian pasvorton post vi ensalutis.

' NOTERESETPASSWORD: 'Enigu vian retpoŝtan adreson kaj ni sendos al vi ligilon per kiu vi povas reagordi vian pasvorton' - PASSWORDSENTHEADER: 'Pasvorta reagorda ligilo sendiĝis al ''{email}''' - PASSWORDSENTTEXT: 'Dankon! Reagordita ligilo sendiĝis al ''{email}'', kondiĉe ke konto ekzistas por tiu retadreso.' + PASSWORDRESETSENTHEADER: 'Pasvorta reagorda ligilo sendiĝis' + PASSWORDRESETSENTTEXT: 'Dankon. Reagorda ligilo sendiĝis, kondiĉe ke konto ekzistas por ĉi tiu retadreso.' diff --git a/lang/es.yml b/lang/es.yml index 0009c7a4a..debd4b4a7 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -249,7 +249,4 @@ es: LOGIN: Entrar LOSTPASSWORDHEADER: '¿Contraseña Perdida?' NOTEPAGESECURED: 'Esa página está protegida. Introduzca sus datos de acreditación a continuación y lo enviaremos a ella en un momento.' - NOTERESETLINKINVALID: '

El enlace para restablecer la contraseña es inválido o ha expirado.

Usted puede solicitar uno nuevo aqui o cambiar su contraseña después de que se haya conectado.

' NOTERESETPASSWORD: 'Introduzca su dirección de e-mail, y le enviaremos un enlace, con el cual podrá restaurar su contraseña' - PASSWORDSENTHEADER: 'Un enlace para restablecer la contraseña ha sido enviado a ''{email}''' - PASSWORDSENTTEXT: 'Gracias! Un enlace para restablecer la contraseña ha sido enviado a ''{email}'', siempre que una cuenta exista para la dirección de email indicada.' diff --git a/lang/et_EE.yml b/lang/et_EE.yml index 63fc4cd5d..a8fb3b686 100644 --- a/lang/et_EE.yml +++ b/lang/et_EE.yml @@ -139,7 +139,4 @@ et_EE: ERRORPASSWORDPERMISSION: 'Pead olema sisseloginud, et parooli muuta!' LOGIN: 'Logi sisse' NOTEPAGESECURED: 'See leht on turvatud. Sisesta enda andmed allpool ja me saadame sind otse edasi' - NOTERESETLINKINVALID: '

Parooli lähtestamise link on kehtetu või aegunud.

Saate taotleda uut linki siin või muuta parooli pärast sisselogimist.

' NOTERESETPASSWORD: 'Sisesta oma email ja me saadame sulle lingi kus saad oma parooli tühistada.' - PASSWORDSENTHEADER: 'Parooli lähtestamise link saadeti aadressile ''{email}''' - PASSWORDSENTTEXT: 'Aitäh! Lähtestamislink saadeti aadressile ''{email}'' eeldusel, et selle e-posti aadressiga seotud konto on olemas.' diff --git a/lang/fa_IR.yml b/lang/fa_IR.yml index b292b25d7..b9aa0ebea 100644 --- a/lang/fa_IR.yml +++ b/lang/fa_IR.yml @@ -168,4 +168,3 @@ fa_IR: ERRORPASSWORDPERMISSION: 'جهت تغییر گذرواژه خود باید وارد شده باشید!' LOGIN: ورود LOSTPASSWORDHEADER: 'فراموشی گذرواژه' - PASSWORDSENTHEADER: 'لینک ازنوسازی گذرواژه به ''{email}'' ارسال شد' diff --git a/lang/fi.yml b/lang/fi.yml index 364762d3a..b19807225 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -76,6 +76,7 @@ fi: LinkExisting: 'Linkitä olemassaoleva' NewRecord: 'Uusi {type}' NoItemsFound: 'Ei kohteita' + OpenFilter: 'Avaa haku ja suodatus' PRINTEDAT: Tulostettu PRINTEDBY: Tulostaja PlaceHolder: 'Etsi {type}' @@ -95,12 +96,19 @@ fi: DeletePermissionsFailure: 'Ei oikeuksia poistamiseen' Deleted: 'Poistettiin {type} {name}' Save: Tallenna + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Muokkaa + SilverStripe\Forms\GridField\GridFieldFilterHeader: + Search: 'Haku "{name}"' + SearchFormFaliure: 'Hakulomaketta ei pystytty luomaan.' 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: OF: / Page: Sivu View: Näytä + SilverStripe\Forms\GridField\GridFieldViewButton: + VIEW: Avaa SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Määrä FIELDLABELCURRENCY: Valuutta @@ -147,6 +155,8 @@ fi: other: '{count} vuotta' SilverStripe\ORM\FieldType\DBEnum: ANY: Yhtään + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'Liian monta samaan liittyvää objektia: oletuskenttä käytössä' SilverStripe\ORM\Hierarchy: LIMITED_TITLE: 'Liian monta lapsiobjektia ({count}}' SilverStripe\ORM\Hierarchy\Hierarchy: @@ -197,6 +207,7 @@ fi: many_many_Members: Jäsenet SilverStripe\Security\LoginAttempt: Email: Sähköpostiosoite + EmailHashed: 'Sähköpostiosoite (tiivistetty)' IP: IP-osoite PLURALNAME: Kirjautumisyritykset PLURALS: @@ -255,6 +266,8 @@ fi: SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: ERRORWRONGCRED: 'Antamasi tiedot eivät näytä oikeilta. Yritä uudelleen.' NoPassword: 'Tällä käyttäjällä ei ole salasanaa' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'Sähköpostiosoite & salasana' SilverStripe\Security\MemberPassword: PLURALNAME: 'Käyttäjän salasanat' PLURALS: @@ -318,5 +331,5 @@ fi: NOTEPAGESECURED: 'Tämä sivu on suojattu. Syötä tunnistetietosi alle niin pääset eteenpäin.' NOTERESETLINKINVALID: '

Salasanan palautuslinkki on virheellinen tai vanhentunut.

Voit pyytää uuden napsauttamalla tästä tai vaihtaa salasanasi kirjautumisen jälkeen.

' NOTERESETPASSWORD: 'Syötä sähköpostiosoitteesi ja lähetämme sinulle linkin, jonka avulla saat palautettua salasanasi' - PASSWORDSENTHEADER: 'Salasanan palautuslinkki lähetettiin osoitteeseen ''{email}''' - PASSWORDSENTTEXT: 'Kiitos! Salasanan palautuslinkki lähetettiin osoitteeseen ''{email}'', joka on liitettynä tähän käyttäjätiliin.' + PASSWORDRESETSENTHEADER: 'Salasanan palautuslinkki lähetetty' + PASSWORDRESETSENTTEXT: 'Kiitos, palautuslinkki on lähetetty käyttäjätilille asetettuun sähköpostiosoitteeseen.' diff --git a/lang/fr.yml b/lang/fr.yml index 5a18188da..450cfdb87 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -84,7 +84,6 @@ fr: RelationSearch: 'Rechercher relations' ResetFilter: Réinitialiser SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Supprimer Delete: Supprimer DeletePermissionsFailure: 'Vous n’avez pas les autorisations pour supprimer' EditPermissionsFailure: 'Pas de permissions pour délier l''enregistrement' @@ -96,8 +95,6 @@ fr: DeletePermissionsFailure: 'Vous n’avez pas les autorisations pour supprimer' Deleted: '{type} {name} supprimés' Save: Enregistrer - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - 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: @@ -322,7 +319,4 @@ fr: LOGOUT: 'Se déconnecter' LOSTPASSWORDHEADER: 'Mot de passe oublié' NOTEPAGESECURED: 'Cette page est sécurisée. Entrez vos identifiants ci-dessous et vous pourrez y avoir accès.' - NOTERESETLINKINVALID: '

Le lien de réinitialisation du mot de passe n’est pas valide ou a expiré.

Vous pouvez en demander un nouveau en suivant ce lien ou changer de mot de passe après connexion.

' NOTERESETPASSWORD: 'Entrez votre adresse email et nous vous enverrons un lien pour modifier votre mot de passe' - PASSWORDSENTHEADER: "Lien de réinitialisation de mot de passe envoyé à «\_{email}\_»" - PASSWORDSENTTEXT: "Merci\_! Un lien de réinitialisation vient d’être envoyé à «\_{email}\_», à condition que cette adresse existe." diff --git a/lang/id.yml b/lang/id.yml index d17eac338..c9c06bbff 100644 --- a/lang/id.yml +++ b/lang/id.yml @@ -167,7 +167,4 @@ id: LOGIN: Masuk LOSTPASSWORDHEADER: 'Kata Kunci yang Terlupa' NOTEPAGESECURED: 'Laman ini diamankan. Isikan data berikut untuk dikirimkan hak akses Anda.' - NOTERESETLINKINVALID: '

Tautan penggantian kata kunci tidak valid atau sudah kadaluarsa.

Anda dapat meminta yang baru di sini atau mengganti kata kunci setelah Anda masuk.

' NOTERESETPASSWORD: 'Isikan alamat email Anda untuk mendapatkan tautan penggantian kata kunci' - PASSWORDSENTHEADER: 'Tautan penggantian kata kunci dikirimkan ke ''{email}''' - PASSWORDSENTTEXT: 'Terimakasih! Tautan reset telah dikirim ke ''{email}'', berisi informasi akun untuk alamat email ini.' diff --git a/lang/id_ID.yml b/lang/id_ID.yml index 22ebcb2e5..11d1f2bb3 100644 --- a/lang/id_ID.yml +++ b/lang/id_ID.yml @@ -166,7 +166,4 @@ id_ID: LOGIN: Masuk LOSTPASSWORDHEADER: 'Kata Kunci yang Terlupa' NOTEPAGESECURED: 'Laman ini diamankan. Isikan data berikut untuk dikirimkan hak akses Anda.' - NOTERESETLINKINVALID: '

Tautan penggantian kata kunci tidak valid atau sudah kadaluarsa.

Anda dapat meminta yang baru di sini atau mengganti kata kunci setelah Anda masuk.

' NOTERESETPASSWORD: 'Isikan alamat email Anda untuk mendapatkan tautan penggantian kata kunci' - PASSWORDSENTHEADER: 'Tautan penggantian kata kunci dikirimkan ke ''{email}''' - PASSWORDSENTTEXT: 'Terimakasih! Tautan reset telah dikirim ke ''{email}'', berisi informasi akun untuk alamat email ini.' diff --git a/lang/it.yml b/lang/it.yml index 05bf5dc83..4089faf9b 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -95,6 +95,10 @@ it: DeletePermissionsFailure: 'Non hai i permessi per eliminare' Deleted: 'Eliminato {type} {name}' Save: Salva + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Modifica + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'Non è possibile rimuovere te stesso da questo gruppo, perderesti i diritti di admin' SilverStripe\Forms\GridField\GridFieldPaginator: OF: di Page: Pagina @@ -145,6 +149,8 @@ it: other: '{count} anni' SilverStripe\ORM\FieldType\DBEnum: ANY: Qualsiasi + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'Troppi oggetti correlati; campo di fallback in uso' SilverStripe\ORM\Hierarchy: LIMITED_TITLE: 'Troppi figli ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: @@ -195,6 +201,7 @@ it: many_many_Members: Membri SilverStripe\Security\LoginAttempt: Email: 'Indirizzo e-mail' + EmailHashed: 'Indirizzo email (hash)' IP: 'Indirizzo IP' PLURALNAME: 'Tentativi d''accesso' PLURALS: @@ -236,6 +243,7 @@ it: SUBJECTPASSWORDCHANGED: 'La tua password è stata cambiata' SUBJECTPASSWORDRESET: 'Link per azzerare la tua password' SURNAME: Cognome + VALIDATIONADMINLOSTACCESS: 'Non è possibile rimuovere tutti i gruppi admin dal tuo profilo' ValidationIdentifierFailed: 'Non posso sovrascrivere l''utente esistente #{id} con identificatore identico ({name} = {value}))' WELCOMEBACK: 'Bentornato, {firstname}' YOUROLDPASSWORD: 'La tua vecchia password' @@ -252,6 +260,8 @@ it: SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: ERRORWRONGCRED: 'I dettagli forniti non sembrano corretti. Per favore riprovare.' NoPassword: 'Manca la password per questo utente.' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'E-mail & Password' SilverStripe\Security\MemberPassword: PLURALNAME: 'Password utenti' PLURALS: @@ -313,7 +323,6 @@ it: LOGOUT: Scollegati LOSTPASSWORDHEADER: 'Password smarrita' NOTEPAGESECURED: 'La pagina è protetta. Inserisci le credenziali qui sotto per poter andare avanti.' - NOTERESETLINKINVALID: '

Il link per azzerare la password non è valido o è scaduto.

Puoi richiederne uno nuovo qui o cambiare la tua password dopo che ti sei connesso.

' NOTERESETPASSWORD: 'Inserisci il tuo indirizzo e-mail e ti verrà inviato un link per poter azzerare la tua password.' - PASSWORDSENTHEADER: 'Link per azzeramento della password inviato a ''{email}''' - PASSWORDSENTTEXT: 'Grazie! Un link di azzeramento è stato inviato a ''{email}'', fornito un account esistente per questo indirizzo e-mail.' + PASSWORDRESETSENTHEADER: 'Link di azzeramento password inviato' + PASSWORDRESETSENTTEXT: 'Grazie! Un link di azzeramento è stato inviato, supponendo un account esista a quell''indirizzo e-mail.' diff --git a/lang/ja.yml b/lang/ja.yml index b2f92eb1e..433a832e2 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -146,7 +146,4 @@ ja: ERRORPASSWORDPERMISSION: パスワードを変更する為に、ログインしなければなりません! LOGIN: ログイン NOTEPAGESECURED: このページはセキュリティで保護されております証明書キーを下記に入力してください。こちらからすぐに送信します - NOTERESETLINKINVALID: '

パスワードのリセットリンクは有効でないか期限切れです。

新しいパスワードを要求することができます ここ もしくはパスワードを変更することができます ログインした後 .

' NOTERESETPASSWORD: メールアドレスを入力してください、パスワードをリセットするURLを送信致します - PASSWORDSENTHEADER: 'パスワードリセットリンクは ''{email}'' に送信されました' - PASSWORDSENTTEXT: 'ありがとうございました! リセットリンクは、''{email}'' に、このアカウントが存在することを前提として送信されました。' diff --git a/lang/lt.yml b/lang/lt.yml index abdbaea25..3ca7adbff 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -167,7 +167,4 @@ lt: LOGIN: Prisijungti LOSTPASSWORDHEADER: 'Slaptažodžio atstatymas' NOTEPAGESECURED: 'Šis puslapis yra apsaugotas. Įveskite savo duomenis į žemiau esančius laukelius.' - NOTERESETLINKINVALID: '

Neteisinga arba negaliojanti slaptažodžio atstatymo nuoroda.

Galite atsisiųsti naują čia arba pasikeisti slaptažodį po to, kai prisijungsite.

' NOTERESETPASSWORD: 'Įveskite savo e. pašto adresą ir atsiųsime slaptažodžio atstatymui skirtą nuorodą' - PASSWORDSENTHEADER: 'Slaptažodžio atstatymo nuoroda nusiųsta į ''{email}''' - PASSWORDSENTTEXT: 'Atstatymo nuoroda nusiųsta į ''{email}''' diff --git a/lang/mi.yml b/lang/mi.yml index 7a2601ed5..e9fe2308e 100644 --- a/lang/mi.yml +++ b/lang/mi.yml @@ -149,7 +149,4 @@ mi: LOGIN: Takiuru LOSTPASSWORDHEADER: 'Kupuhipa Ngaro' NOTEPAGESECURED: 'Kua ngita tēnā whārangi. Tāurua ō taipitoptio tuakiri ki raro, ā, mā mātou koe e tuku kia haere tonu.' - NOTERESETLINKINVALID: '

He muhu, kua mōnehu rānei te hono tautuhi kupuhipa anō.

Ka taea te tono i te mea hōui konei ka huri rānei i tō kupuhipa ā muri i tōtakiuru.

' NOTERESETPASSWORD: 'Tāurua tō wāhitau īmēra, mā mātou e tuku tētahi hono ki a koe e taea ai te tautuhi anō i tō kupuhipa' - PASSWORDSENTHEADER: 'I tukuna he hono tautuhi kupuhipa anō ki ''{email}''' - PASSWORDSENTTEXT: 'Kia ora! Kua tukuna he hono tautuhi anō ki ''{email}'',engari rā kei te tīariari he pūkete mō taua wāhitau īmēra.' diff --git a/lang/nb.yml b/lang/nb.yml index 91dd9f758..f1dc4c74d 100644 --- a/lang/nb.yml +++ b/lang/nb.yml @@ -152,7 +152,4 @@ nb: LOGIN: 'Logg inn' LOSTPASSWORDHEADER: 'Mistet passord' NOTEPAGESECURED: 'Den siden er sikret. Skriv inn gyldig innloggingsinfo så kommer du inn.' - NOTERESETLINKINVALID: '

Lenken for å nullstille passordet er ugyldig eller utgått.

Du kan kreve en ny her eller endre passordet etter at du har logget inn.

' NOTERESETPASSWORD: 'Skriv inn epostadressen din og vi vil sende deg en lenke som nullstiller passordet.' - PASSWORDSENTHEADER: 'Lenke for nullstilling av passord ble sendt til ''{email}''' - PASSWORDSENTTEXT: 'Takk! En lenke for å lage nytt passord er sendt til ''{email}'', forutsatt at det eksisterer en konto for denne epostadressen.' diff --git a/lang/nl.yml b/lang/nl.yml index 64f191213..f6de04299 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -1,4 +1,26 @@ nl: + SilverStripe\Admin\LeftAndMain: + VersionUnknown: onbekend + SilverStripe\AssetAdmin\Forms\UploadField: + Dimensions: Afmetingen + EDIT: Bewerken + EDITINFO: 'Bewerk dit bestand' + REMOVE: Verwijder + SilverStripe\Control\ChangePasswordEmail_ss: + CHANGEPASSWORDFOREMAIL: 'Het wachtwoord voor het account met e-mailadres {email} is aangepast. Indien u uw wachtwoord niet heeft aangepast kunt u dat doen met onderstaande link.' + CHANGEPASSWORDTEXT1: 'U heeft het wachtwoord veranderd voor' + CHANGEPASSWORDTEXT3: 'Wachtwoord veranderen' + HELLO: Hallo + SilverStripe\Control\Email\ForgotPasswordEmail_ss: + HELLO: Hallo + TEXT1: 'Hier is uw' + TEXT2: 'link om uw wachtwoord opnieuw aan te maken' + TEXT3: voor + SilverStripe\Control\RequestProcessor: + INVALID_REQUEST: 'Fout bij verwerken' + REQUEST_ABORTED: 'Fout bij verwerken (geannuleerd)' + SilverStripe\Core\Manifest\VersionProvider: + VERSIONUNKNOWN: Onbekend SilverStripe\Forms\CheckboxField: NOANSWER: Nee YESANSWER: Ja @@ -8,6 +30,8 @@ nl: ATLEAST: 'Een wachtwoord moet tenminste {min} karakters hebben.' BETWEEN: 'Een wachtwoord moet tussen de {min} en {max} karakters hebben' CURRENT_PASSWORD_ERROR: 'Het wachtwoord dat u heeft ingevoerd is niet juist.' + CURRENT_PASSWORD_MISSING: 'Voer uw huidige wachtwoord in.' + LOGGED_IN_ERROR: 'U moet ingelogd zijn om uw wachtwoord te kunnen veranderen!' MAXIMUM: 'Een wachtwoord mag maximaal {max} karakters hebben.' SHOWONCLICKTITLE: 'Verander wachtwoord' SilverStripe\Forms\CurrencyField: @@ -16,12 +40,20 @@ nl: 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})' + SilverStripe\Forms\DatetimeField: + VALIDDATEMAXDATETIME: 'De datum moet ouder of gelijk zijn aan de maximale datum ({datetime})' + VALIDDATETIMEFORMAT: 'Vul een geldige datum in ({format})' + VALIDDATETIMEMINDATE: 'De datum moet nieuwer of gelijk zijn aan de minimale datum ({datetime})' SilverStripe\Forms\DropdownField: CHOOSE: (Kies) + CHOOSE_MODEL: '(Selecteer {name})' SOURCE_VALIDATION: 'Selecteer een optie uit de lijst. {value} is geen geldige keuze.' SilverStripe\Forms\EmailField: VALIDATION: 'Gelieve een e-mailadres in te voeren.' + SilverStripe\Forms\FileUploadReceiver: + FIELDNOTSET: 'Bestandsinformatie niet gevonden' SilverStripe\Forms\Form: + BAD_METHOD: 'Dit formulier moet middels {method} verzonden worden' CSRF_EXPIRED_MESSAGE: 'Uw sessie is verlopen. Verzend het formulier opnieuw.' CSRF_FAILED_MESSAGE: 'Er lijkt een technisch probleem te zijn. Klik op de knop terug, vernieuw uw browser, en probeer het opnieuw.' VALIDATIONPASSWORDSDONTMATCH: 'Wachtwoorden komen niet overeen' @@ -30,7 +62,10 @@ nl: VALIDATOR: Validator VALIDCURRENCY: 'Vul een geldige munteenheid in' SilverStripe\Forms\FormField: + EXAMPLE: 'bijv. {format}' NONE: geen + SilverStripe\Forms\FormScaffolder: + TABMAIN: Hoofdgedeelte SilverStripe\Forms\GridField\GridField: Add: '{name} toevoegen' CSVEXPORT: 'Exporteer naar CSV' @@ -41,6 +76,7 @@ nl: LinkExisting: 'Koppel een bestaand item' NewRecord: 'Nieuw {type}' NoItemsFound: 'Geen items gevonden.' + OpenFilter: 'Zoeken en filteren openen' PRINTEDAT: 'Geprint op' PRINTEDBY: 'Geprint door' PlaceHolder: 'Zoek {type}' @@ -60,27 +96,72 @@ nl: DeletePermissionsFailure: 'Onvoldoende rechten om te verwijderen' Deleted: '{type} {name} verwijderd' Save: Opslaan + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Bewerken + SilverStripe\Forms\GridField\GridFieldFilterHeader: + Search: 'Zoek naar "{name}"' + SearchFormFaliure: 'Er kon geen zoekformulier worden aangemaakt' + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'U kunt uzelf niet verwijderen van deze groep, omdat u dan geen admin-rechten meer heeft.' + SilverStripe\Forms\GridField\GridFieldPaginator: + OF: van + Page: Pagina + View: Bekijk + SilverStripe\Forms\GridField\GridFieldViewButton: + VIEW: Bekijk SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Aantal FIELDLABELCURRENCY: Munteenheid + INVALID_CURRENCY: 'Valuta {currency} is niet toegestaan' + SilverStripe\Forms\MultiSelectField: + SOURCE_VALIDATION: 'Selecteer een optie uit de lijst. {value} is geen geldige keuze.' SilverStripe\Forms\NullableField: IsNullLabel: 'Is null' SilverStripe\Forms\NumericField: VALIDATION: '''{value}'' is geen getal, enkel getallen worden door dit veld geaccepteerd' SilverStripe\Forms\TimeField: VALIDATEFORMAT: 'Vul een geldig datumformaat in ({format})' + SilverStripe\ORM\DataObject: + PLURALNAME: 'Data objecten' + PLURALS: + one: 'Data object' + other: '{count} Data objecten' + SINGULARNAME: 'Data object' SilverStripe\ORM\FieldType\DBBoolean: ANY: Elke NOANSWER: Nee YESANSWER: Ja SilverStripe\ORM\FieldType\DBDate: + DAYS_SHORT_PLURALS: + one: '{count} dag' + other: '{count} dagen' + HOURS_SHORT_PLURALS: + one: '{count} uur' + other: '{count} uren' LessThanMinuteAgo: 'minder dan één minuut' + MINUTES_SHORT_PLURALS: + one: '{count} minuut' + other: '{count} minuten' + MONTHS_SHORT_PLURALS: + one: '{count} maand' + other: '{count} maanden' + SECONDS_SHORT_PLURALS: + one: '{count} seconde' + other: '{count} seconden' TIMEDIFFAGO: '{difference} geleden' TIMEDIFFIN: 'in {difference}' + YEARS_SHORT_PLURALS: + one: '{count} jaar' + other: '{count} jaren' SilverStripe\ORM\FieldType\DBEnum: ANY: Elke + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'Teveel keuzes in de lijst; een alternatief veld wordt getoond.' + SilverStripe\ORM\Hierarchy: + LIMITED_TITLE: 'Teveel onderliggende items ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: InfiniteLoopNotAllowed: 'Oneindige lus gevonden in "{type}" hiërarchie. Wijzig het hogere niveau om dit op te lossen' + LIMITED_TITLE: 'Teveel onderliggende items ({count})' SilverStripe\ORM\ValidationException: DEFAULT_ERROR: Validatiefout SilverStripe\Security\BasicAuth: @@ -91,34 +172,60 @@ nl: PASSWORDEXPIRED: '

Uw wachtwoord is verlopen. Kies een nieuw wachtwoord.

' SilverStripe\Security\CMSSecurity: INVALIDUSER: '

Ongeldige gebruiker Log hier opnieuw in om verder te gaan.

' + LOGIN_MESSAGE: 'Sessie is verlopen' + LOGIN_TITLE: '

U kunt verder met wat u aan het doen was, door opnieuw in te loggen.

' SUCCESS: Succes SUCCESSCONTENT: '

U bent ingelogd. Klik hier als u niet automatisch wordt doorgestuurd.

' + SUCCESS_TITLE: 'Inloggen is gelukt' + SilverStripe\Security\DefaultAdminService: + DefaultAdminFirstname: 'Standaard Beheerder' SilverStripe\Security\Group: AddRole: 'Voeg een rol toe aan deze groep' Code: 'Groep code' DefaultGroupTitleAdministrators: Beheerders DefaultGroupTitleContentAuthors: 'Inhoud Auteurs' Description: 'Omschrijving ' + GROUPNAME: 'Groep naam' GroupReminder: 'Als u de bovenliggende groep selecteert, neemt deze groep alle rollen over' HierarchyPermsError: 'U moet (ADMIN) rechten hebben om de bovenliggende groep "{group}" toe te kennen' Locked: 'Gesloten?' + MEMBERS: Leden + NEWGROUP: 'Nieuwe groep' NoRoles: 'Geen rollen gevonden' + PERMISSIONS: Rechten + PLURALNAME: Groepen + PLURALS: + one: 'Een groep' + other: '{count} groepen' Parent: 'Bovenliggende groep' + ROLES: Rollen + ROLESDESCRIPTION: 'Rollen zijn logische groeperingen van rechten die in het Rollen tabblad gewijzigd kunnen worden.
Rollen worden automatisch overgenomen van bovenliggende groepen.' RolesAddEditLink: 'Rollen beheren' + SINGULARNAME: Groep Sort: Sorteer-richting has_many_Permissions: Rechten many_many_Members: Leden SilverStripe\Security\LoginAttempt: + Email: 'E-mailadres ' + EmailHashed: 'E-mailadres (versleuteld)' IP: 'IP adres' + PLURALNAME: Inlogpogingen + PLURALS: + one: 'Een inlogpoging' + other: '{count} inlogpogingen' + SINGULARNAME: Inlogpogingen Status: Status SilverStripe\Security\Member: ADDGROUP: 'Groep toevoegen' BUTTONCHANGEPASSWORD: 'Wachtwoord veranderen' BUTTONLOGIN: Inloggen BUTTONLOGINOTHER: 'Als iemand anders inloggen' + BUTTONLOGOUT: Uitloggen BUTTONLOSTPASSWORD: 'Ik ben mijn wachtwoord vergeten' CONFIRMNEWPASSWORD: 'Bevestig het nieuwe wachtwoord' CONFIRMPASSWORD: 'Bevestig wachtwoord' + CURRENT_PASSWORD: 'Huidige wachtwoord' + EDIT_PASSWORD: 'Nieuw wachtwoord' EMAIL: E-mail EMPTYNEWPASSWORD: 'Het nieuwe wachtwoord mag niet leeg zijn, probeer opnieuw' ENTEREMAIL: 'Typ uw e-mailadres om een link te ontvangen waarmee u uw wachtwoord kunt resetten.' @@ -128,13 +235,21 @@ nl: ERRORWRONGCRED: 'De ingevulde gegevens lijken niet correct. Probeer het nog een keer.' FIRSTNAME: Voornaam INTERFACELANG: 'Interface taal' + KEEPMESIGNEDIN: 'Houd mij ingelogd' LOGGEDINAS: 'U bent ingelogd als {name}.' NEWPASSWORD: 'Nieuw wachtwoord' PASSWORD: Wachtwoord PASSWORDEXPIRED: 'Uw wachtwoord is verlopen. Kies een nieuw wachtwoord.' + PLURALNAME: Leden + PLURALS: + one: 'Een lid' + other: '{count} leden' + REMEMBERME: 'Onthoud mij voor volgende keer? (voor {count} dagen op dit apparaat)' + SINGULARNAME: Lid SUBJECTPASSWORDCHANGED: 'Uw wachtwoord is veranderd' SUBJECTPASSWORDRESET: 'Link om uw wachtwoord opnieuw aan te maken' SURNAME: Achternaam + VALIDATIONADMINLOSTACCESS: 'Niet mogelijk om alle admin-groepen te verwijderen van uw profiel' ValidationIdentifierFailed: 'Een bestaande gebruiker #{id} kan niet dezelfde unieke velden hebben ({name} = {value}))' WELCOMEBACK: 'Welkom terug, {firstname}' YOUROLDPASSWORD: 'Uw oude wachtwoord' @@ -143,15 +258,38 @@ nl: db_LockedOutUntil: 'Gesloten tot' db_Password: Wachtwoord db_PasswordExpiry: 'Wachtwoord vervaldatum' + SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm: + AUTHENTICATORNAME: Inlogformulier + BUTTONFORGOTPASSWORD: 'Wachtwoord vergeten' + BUTTONLOGIN: 'Opnieuw inloggen' + BUTTONLOGOUT: Uitloggen + SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: + ERRORWRONGCRED: 'De ingevulde gegevens lijken niet correct. Probeer het nog een keer.' + NoPassword: 'Er is geen wachtwoord voor deze gebruiker.' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'E-mail & wachtwoord' + SilverStripe\Security\MemberPassword: + PLURALNAME: Gebruikerswachtwoorden + PLURALS: + one: 'Een gebruikerswachtwoord' + other: '{count} Gebruikerswachtwoorden' + SINGULARNAME: Gebruikerswachtwoord SilverStripe\Security\PasswordValidator: LOWCHARSTRENGTH: 'Maak a.u.b. uw wachtwoord sterker door enkele van de volgende karakters te gebruiken: {chars}' PREVPASSWORD: 'U heeft dit wachtwoord in het verleden al gebruikt, kies a.u.b. een nieuw wachtwoord.' TOOSHORT: 'Het wachtwoord is te kort, het moet minimaal {minimum} karakters hebben' SilverStripe\Security\Permission: AdminGroup: Beheerder + CMS_ACCESS_CATEGORY: 'CMS toegang' CONTENT_CATEGORY: Inhoudsrechten FULLADMINRIGHTS: 'Volledige admin rechten' FULLADMINRIGHTS_HELP: 'Impliceert en overstemt alle andere toegewezen rechten.' + PERMISSIONS_CATEGORY: 'Rollen en toegangsrechten' + PLURALNAME: Rechten + PLURALS: + one: Machtiging + other: '{count} rechten' + SINGULARNAME: Machtiging UserPermissionsIntro: 'Groepen aan deze gebruiker toewijzen zullen diens permissies aanpassen. Zie de sectie Groepen voor meer informatie over machtigingen voor afzonderlijke groepen.' SilverStripe\Security\PermissionCheckboxSetField: AssignedTo: 'toegewezen aan "{title}"' @@ -161,21 +299,37 @@ nl: SilverStripe\Security\PermissionRole: OnlyAdminCanApply: 'Alleen admin kan doorvoeren' PLURALNAME: Rollen + PLURALS: + one: 'Een rol' + other: '{count} rollen' SINGULARNAME: Rol Title: Titel SilverStripe\Security\PermissionRoleCode: + PLURALNAME: 'Permissie codes' + PLURALS: + one: 'Een permissiecode' + other: '{count} permissiecodes' PermsError: 'U moet (ADMIN) rechten hebben om de code "{code}" toe te kennen' + SINGULARNAME: Permissiecode + SilverStripe\Security\RememberLoginHash: + PLURALNAME: 'Versleutelde logins' + PLURALS: + one: 'Een versleutelde login' + other: '{count} versleutelde logins' + SINGULARNAME: 'Versleutelde login' SilverStripe\Security\Security: ALREADYLOGGEDIN: 'U hebt geen toegang tot deze pagina. Als u een andere account met de nodige rechten hebt, kan u hieronder opnieuw inloggen.' BUTTONSEND: 'Nieuw wachtwoord aanmaken' CHANGEPASSWORDBELOW: 'U kunt uw wachtwoord hieronder veranderen.' CHANGEPASSWORDHEADER: 'Verander uw wachtwoord' + CONFIRMLOGOUT: 'Klik op onderstaande knop om uit te loggen.' ENTERNEWPASSWORD: 'Voer een nieuw wachtwoord in.' ERRORPASSWORDPERMISSION: 'U moet ingelogd zijn om uw wachtwoord te kunnen veranderen!' LOGIN: 'Meld aan' + LOGOUT: Uitloggen LOSTPASSWORDHEADER: 'Wachtwoord vergeten' NOTEPAGESECURED: 'Deze pagina is beveiligd. Voer uw gegevens in en u wordt automatisch doorgestuurd.' - NOTERESETLINKINVALID: '

De link om uw wachtwoord te kunnen wijzigen is niet meer geldig.

U kunt een nieuwe link aanvragen of uw wachtwoord aanpassen door in te loggen.

' + NOTERESETLINKINVALID: '

De reset link is ongeldig of komen te vervallen.

Je kan hier een nieuwe link aanvragen of het wachtwoord veranderen nadat je bent ingelogd.

' NOTERESETPASSWORD: 'Voer uw e-mailadres in en we sturen een link waarmee u een nieuw wachtwoord kunt instellen.' - PASSWORDSENTHEADER: 'Wachtwoord herstel link verzonden naar {email}' - PASSWORDSENTTEXT: 'Bedankt! Er is een link verstuurd naar {email} om uw wachtwoord opnieuw in te stellen, in de veronderstelling dat er een account bestaat voor dit e-mailadres.' + PASSWORDRESETSENTHEADER: 'link om uw wachtwoord opnieuw aan te maken' + PASSWORDRESETSENTTEXT: 'Bedankt! Er is een link verstuurd om uw wachtwoord opnieuw in te stellen (mits het mailadres reeds bekend is bij ons).' diff --git a/lang/pl.yml b/lang/pl.yml index 3774d27f3..1905acb38 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -84,7 +84,6 @@ pl: RelationSearch: 'Wyszukiwanie powiązań' ResetFilter: Resetuj SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Usuń Delete: Usuń DeletePermissionsFailure: 'Brak uprawnień do usuwania' EditPermissionsFailure: 'Nie masz uprawnień, aby odłączyć rekord' @@ -96,8 +95,6 @@ pl: DeletePermissionsFailure: 'Brak uprawnień do usuwania' Deleted: 'Usunięto {type} {name}' 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: @@ -352,7 +349,4 @@ pl: 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ę.

' NOTERESETPASSWORD: 'Wpisz adres e-mail, na który mamy wysłać link gdzie możesz zresetować swoje hasło' - PASSWORDSENTHEADER: 'Link resetujący hasła został wysłany do ''{email}''' - PASSWORDSENTTEXT: 'Dziękujemy! Link resetujący hasło został wysłany do ''{email}'', o ile konto użytkownika dla takiego e-maila istnieje.' diff --git a/lang/ru.yml b/lang/ru.yml index 73602e013..add72be7f 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -339,7 +339,4 @@ ru: LOGOUT: Выйти LOSTPASSWORDHEADER: 'Восстановление пароля' NOTEPAGESECURED: 'Эта страница защищена. Пожалуйста, введите свои учетные данные для входа.' - NOTERESETLINKINVALID: '

Неверная ссылка переустановки пароля или время действия ссылки истекло.

Вы можете повторно запросить ссылку, щелкнув здесь, или поменять пароль, войдя в систему.

' NOTERESETPASSWORD: 'Введите Ваш адрес email, и Вам будет отправлена ссылка, по которой Вы сможете переустановить свой пароль' - PASSWORDSENTHEADER: 'Ссылка для переустановки пароля выслана на ''{email}''' - PASSWORDSENTTEXT: 'Ссылка переустановки пароля была выслана на адрес ''{email}'' (письмо дойдет до получателя только в том случае, если аккаунт с таким электронным адресом действительно зарегистрирован).' diff --git a/lang/sk.yml b/lang/sk.yml index 861045fc6..88f4bf260 100644 --- a/lang/sk.yml +++ b/lang/sk.yml @@ -228,7 +228,4 @@ sk: LOGIN: Prihlásiť LOSTPASSWORDHEADER: 'Zabudnuté heslo' NOTEPAGESECURED: 'Táto stránka je zabezpečená. Zadajte svoje prihlasovacie údaje a my Vám zároveň pošleme práva.' - NOTERESETLINKINVALID: '

Odkaz na resetovanie hesla nie je platný alebo je vypršala jeho platnosť.

Môžete požiadať o nový tu alebo zmeňte svoje heslo po prihlásení.

' NOTERESETPASSWORD: 'Zadajte svoju e-mailovú adresu a my Vám pošleme odkaz na resetovanie hesla' - PASSWORDSENTHEADER: 'Odkaz na resetovanie hesla bol odoslaný na ''{email}''' - PASSWORDSENTTEXT: 'Ďakujeme! Resetovací odkaz bol odoslaný na ''''{email}'''', pokiaľ účet existuje pre túto emailovú adresu.' diff --git a/lang/sl.yml b/lang/sl.yml index bccd51826..5496aff5f 100644 --- a/lang/sl.yml +++ b/lang/sl.yml @@ -135,7 +135,4 @@ sl: LOGIN: Prijava LOSTPASSWORDHEADER: 'Izgubljeno geslo' NOTEPAGESECURED: 'Stran je zaščitena. Da bi lahko nadaljevali, vpišite svoje podatke.' - NOTERESETLINKINVALID: '

Povezava za ponastavitev gesla je napačna ali pa je njena veljavnost potekla.

Tukaj lahko zaprosite za novo povezavo or pa zamenjate geslo, ko se prijavite v sistem.

' NOTERESETPASSWORD: 'Vpišite e-naslov, na katerega vam bomo poslali povezavo za ponastavitev gesla' - PASSWORDSENTHEADER: 'Povezava za ponastavitev gesla je bila poslana na e-naslov ''{email}''.' - PASSWORDSENTTEXT: 'Hvala! Povezava za ponastavitev gesla je bila poslana na e-naslov ''{email}'', ki je naveden kot e-naslov vašega računa. ' diff --git a/lang/sr.yml b/lang/sr.yml index 07f642278..622b657b3 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -151,7 +151,4 @@ sr: ERRORPASSWORDPERMISSION: 'Морате да будете пријављени да бисте променили своју лозинку!' LOGIN: Пријављивање NOTEPAGESECURED: 'Ова страна је обезбеђена. Унесите своје податке и ми ћемо вам послати садржај.' - NOTERESETLINKINVALID: '

Линк за ресетовање лозинке је погрешан или је истекло време за његово коришћење.

Можете да захтевате нови овде или да промените Вашу лозинку након што се пријавите.

' NOTERESETPASSWORD: 'Унесите своју адресу е-поште и ми ћемо вам послати линк помоћу којег можете да промените своју лозинку' - PASSWORDSENTHEADER: 'Линк за ресетовање лозинке послат је на адресу е-поште: ''{email}''' - PASSWORDSENTTEXT: 'Хвала Вам! Линк за ресетовање лозинке је послат не адресу е-поште ''{email}''. Порука ће стићи примаоцу само ако постоји регистрован налог са том адресом е-поште.' diff --git a/lang/sr@latin.yml b/lang/sr@latin.yml index 1b210ae51..f91aa9337 100644 --- a/lang/sr@latin.yml +++ b/lang/sr@latin.yml @@ -150,7 +150,4 @@ sr@latin: ERRORPASSWORDPERMISSION: 'Morate da budete prijavljeni da biste promenili svoju lozinku!' LOGIN: Prijavljivanje NOTEPAGESECURED: 'Ova strana je obezbeđena. Unesite svoje podatke i mi ćemo vam poslati sadržaj.' - NOTERESETLINKINVALID: '

Link za resetovanje lozinke je pogrešan ili je isteklo vreme za njegovo korišćenje.

Možete da zahtevate novi ovde ili da promenite Vašu lozinku nakon što se prijavite.

' NOTERESETPASSWORD: 'Unesite svoju adresu e-pošte i mi ćemo vam poslati link pomoću kojeg možete da promenite svoju lozinku' - PASSWORDSENTHEADER: 'Link za resetovanje lozinke poslat je na adresu e-pošte: ''{email}''' - PASSWORDSENTTEXT: 'Hvala Vam! Link za resetovanje lozinke je poslat ne adresu e-pošte ''{email}''. Poruka će stići primaocu samo ako postoji registrovan nalog sa tom adresom e-pošte.' diff --git a/lang/sr_RS.yml b/lang/sr_RS.yml index fc79812da..298049430 100644 --- a/lang/sr_RS.yml +++ b/lang/sr_RS.yml @@ -150,7 +150,4 @@ sr_RS: ERRORPASSWORDPERMISSION: 'Морате да будете пријављени да бисте променили своју лозинку!' LOGIN: Пријављивање NOTEPAGESECURED: 'Ова страна је обезбеђена. Унесите своје податке и ми ћемо вам послати садржај.' - NOTERESETLINKINVALID: '

Линк за ресетовање лозинке је погрешан или је истекло време за његово коришћење.

Можете да захтевате нови овде или да промените Вашу лозинку након што се пријавите.

' NOTERESETPASSWORD: 'Унесите своју адресу е-поште и ми ћемо вам послати линк помоћу којег можете да промените своју лозинку' - PASSWORDSENTHEADER: 'Линк за ресетовање лозинке послат је на адресу е-поште: ''{email}''' - PASSWORDSENTTEXT: 'Хвала Вам! Линк за ресетовање лозинке је послат не адресу е-поште ''{email}''. Порука ће стићи примаоцу само ако постоји регистрован налог са том адресом е-поште.' diff --git a/lang/sr_RS@latin.yml b/lang/sr_RS@latin.yml index f39b36058..453ffa17d 100644 --- a/lang/sr_RS@latin.yml +++ b/lang/sr_RS@latin.yml @@ -151,7 +151,4 @@ sr_RS@latin: ERRORPASSWORDPERMISSION: 'Morate da budete prijavljeni da biste promenili svoju lozinku!' LOGIN: Prijavljivanje NOTEPAGESECURED: 'Ova strana je obezbeđena. Unesite svoje podatke i mi ćemo vam poslati sadržaj.' - NOTERESETLINKINVALID: '

Link za resetovanje lozinke je pogrešan ili je isteklo vreme za njegovo korišćenje.

Možete da zahtevate novi ovde ili da promenite Vašu lozinku nakon što se prijavite.

' NOTERESETPASSWORD: 'Unesite svoju adresu e-pošte i mi ćemo vam poslati link pomoću kojeg možete da promenite svoju lozinku' - PASSWORDSENTHEADER: 'Link za resetovanje lozinke poslat je na adresu e-pošte: ''{email}''' - PASSWORDSENTTEXT: 'Hvala Vam! Link za resetovanje lozinke je poslat ne adresu e-pošte ''{email}''. Poruka će stići primaocu samo ako postoji registrovan nalog sa tom adresom e-pošte.' diff --git a/lang/sv.yml b/lang/sv.yml index f35c2ee99..f2a18d659 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -93,7 +93,12 @@ sv: DeletePermissionsFailure: 'Rättighet för att radera saknas' Deleted: 'Raderade {type} {name}' Save: Spara + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Ändra + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'Du kan inte radera dig själv från den här gruppen, då du då kommer att förlora dina admin-rättigheter' SilverStripe\Forms\GridField\GridFieldPaginator: + OF: av Page: Sida View: Visa SilverStripe\Forms\MoneyField: @@ -108,6 +113,12 @@ sv: VALIDATION: '''{value}'' är inget nummer, bara siffror (utan mellanslag) kan accepteras för det här fältet' SilverStripe\Forms\TimeField: VALIDATEFORMAT: 'Var god att ange tid i ett giltigt format ({format})' + SilverStripe\ORM\DataObject: + PLURALNAME: Dataobjekt + PLURALS: + one: 'Ett dataobjekt' + other: '{count} Dataobjekt' + SINGULARNAME: Dataobjekt SilverStripe\ORM\FieldType\DBBoolean: ANY: 'Vilken som helst' NOANSWER: Nej @@ -136,6 +147,8 @@ sv: other: '{count} år' SilverStripe\ORM\FieldType\DBEnum: ANY: 'Vilken som helst' + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'För många relaterade objekt; använder fallback-fält' SilverStripe\ORM\Hierarchy: LIMITED_TITLE: 'För många barn ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: @@ -221,6 +234,7 @@ sv: PLURALS: one: 'En medlem' other: '{count} medlemmar' + REMEMBERME: 'Kom ihåg mig nästa gång? (i {count} dagar på denna enhet)' SINGULARNAME: Medlem SUBJECTPASSWORDCHANGED: 'Ditt lösenord har ändrats' SUBJECTPASSWORDRESET: 'Din återställningslänk' @@ -288,7 +302,6 @@ sv: LOGOUT: 'Logga ut' LOSTPASSWORDHEADER: 'Bortglömt lösenord' NOTEPAGESECURED: 'Den här sidan är låst. Fyll i dina uppgifter nedan så skickar vi dig vidare.' - NOTERESETLINKINVALID: '

Återställningslänk för lösenord är felaktig eller för gammal.

Du kan begära en ny här eller ändra ditt lösenord när du loggat in.

' NOTERESETPASSWORD: 'Ange din e-postadress så skickar vi en länk med vilken du kan återställa ditt lösenord' - PASSWORDSENTHEADER: 'Återställningslänk för lösenord har skickats till ''{email}''' - PASSWORDSENTTEXT: 'Tack en återställningslänk har skickats till ''{email}'', förutsatt att ett konto med den addressen finns.' + PASSWORDRESETSENTHEADER: 'Återställningslänk för lösenord skickad' + PASSWORDRESETSENTTEXT: 'Tack. En återställningslänk har skickats, förutsatt att ett konto med denna adress existerar.' diff --git a/lang/zh.yml b/lang/zh.yml index 26c8e2893..748ee671c 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -166,7 +166,4 @@ zh: LOGIN: 登录 LOSTPASSWORDHEADER: 忘记密码 NOTEPAGESECURED: 该页面受安全保护。请在下面输入您的证书,然后我们会立即将您引导至该页面。 - NOTERESETLINKINVALID: '

密码重设链接无效或已过期。

您可以在这里 要求一个新的或在登录后更改您的密码。

' NOTERESETPASSWORD: 请输入您的电子邮件地址,然后我们会将一个链接发送给您,您可以用它来重设您的密码 - PASSWORDSENTHEADER: '密码重设链接已发送至''{email}''' - PASSWORDSENTTEXT: '谢谢!复位链接已发送到 ''{email}'',假定此电子邮件地址存在一个帐户。' From 74de2ce3b7d6132043ffb7238761a5fe2016f507 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Wed, 7 Nov 2018 17:57:45 +1300 Subject: [PATCH 056/175] Update translations --- lang/ar.yml | 3 - lang/bg.yml | 3 - lang/cs.yml | 3 - lang/da.yml | 325 ++++++++++++++++++++++++++++++++++++++++++- lang/de.yml | 3 - lang/eo.yml | 9 +- lang/es.yml | 3 - lang/et_EE.yml | 3 - lang/fa_IR.yml | 1 - lang/fi.yml | 17 ++- lang/fr.yml | 6 - lang/id.yml | 3 - lang/id_ID.yml | 3 - lang/it.yml | 15 +- lang/ja.yml | 3 - lang/lt.yml | 3 - lang/mi.yml | 3 - lang/nb.yml | 3 - lang/nl.yml | 160 ++++++++++++++++++++- lang/pl.yml | 6 - lang/ru.yml | 3 - lang/sk.yml | 3 - lang/sl.yml | 3 - lang/sr.yml | 3 - lang/sr@latin.yml | 3 - lang/sr_RS.yml | 3 - lang/sr_RS@latin.yml | 3 - lang/sv.yml | 19 ++- lang/zh.yml | 3 - 29 files changed, 530 insertions(+), 88 deletions(-) diff --git a/lang/ar.yml b/lang/ar.yml index 19381589f..518bf1344 100644 --- a/lang/ar.yml +++ b/lang/ar.yml @@ -150,7 +150,4 @@ ar: LOGIN: دخول LOSTPASSWORDHEADER: 'كلمة مرور مفقودة' NOTEPAGESECURED: 'هذه الصفحة محمية بكلمة مرور ، أدخل بيانات دخولك بالأسفل ليتم السماح لك بالوصول للصفحة' - NOTERESETLINKINVALID: "

رابط إعادة تعيين كلمة المرور غير صحيح أو نفذت صلاحيته.

\n

\nيمكنك طلب رابط جديد <\"{a href=\"{link1\"> هنا \n أو تغيير كلمة المرور الخاصة بك بعد <\"{a href=\"{link2\"> تسجيل دخولك.\n

" NOTERESETPASSWORD: 'أدخل بريدك الإلكتروني و سيتم إرسال رابط إعادة تهيئة كلمة المرور ' - PASSWORDSENTHEADER: 'رابط استعادة كلمة المرور تم إرساله إلى ''{بريدك}''' - PASSWORDSENTTEXT: 'شكرا لك! تم إرسال رابط إعادة تعيين إلى ''{بريدك}''، بشرط وجود حساب قائم بالنسبة لعنوان هذا البريد الإلكتروني .' diff --git a/lang/bg.yml b/lang/bg.yml index 3e1eba6a0..241841f1e 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -313,7 +313,4 @@ bg: LOGOUT: Изход LOSTPASSWORDHEADER: 'Забравена парола' NOTEPAGESECURED: 'Тази страница е защитена. Въведете вашите данни по-долу, за да продължите.' - NOTERESETLINKINVALID: '

Връзката за нулиране на парола не е вярна или е просрочена.

Можете да заявите нова тук или да промените паролата си след като влезете.

' NOTERESETPASSWORD: 'Въведете вашият email адрес и ще ви изпратим линк, с който ще можете да смените паролата си' - PASSWORDSENTHEADER: 'Връзка за нулиране на парола беше изпратена на ''{email}''' - PASSWORDSENTTEXT: 'Благодарим ви! Връзка за нулиране на паролата беше изпратен на ''{email}'', ако съществува акаунт с този имейл адрес.' diff --git a/lang/cs.yml b/lang/cs.yml index 4a3cae3ab..759f0268e 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -194,7 +194,4 @@ cs: LOGIN: Přihlásit LOSTPASSWORDHEADER: 'Zapomenuté heslo' NOTEPAGESECURED: 'Tato stránka je zabezpečená. Vložte své přihlašovací údaje a my Vám zároveň pošleme práva.' - NOTERESETLINKINVALID: '

Odkaz na resetování hesla není platný nebo je prošlý.

Můžete požádat o nový zde nebo změňte své heslo až se přihlásíte.

' NOTERESETPASSWORD: 'Zadejte svou e-mailovou adresu a bude vám zaslán nulovací odkaz pro Vaše heslo' - PASSWORDSENTHEADER: 'Odkaz na resetování hesla byl odeslán na ''{email}''' - PASSWORDSENTTEXT: 'Děkujeme! Resetovací odkaz byl odeslán na ''{email}'', pokud účet existuje pro tuto emailovou adresu.' diff --git a/lang/da.yml b/lang/da.yml index 8dc512b23..3fe62969a 100644 --- a/lang/da.yml +++ b/lang/da.yml @@ -1,5 +1,328 @@ da: + SilverStripe\Admin\LeftAndMain: + VersionUnknown: ukendt + SilverStripe\AssetAdmin\Forms\UploadField: + Dimensions: Dimensioner + EDIT: Rediger + EDITINFO: 'Rediger denne fil' + REMOVE: Fjern + SilverStripe\Control\ChangePasswordEmail_ss: + CHANGEPASSWORDFOREMAIL: 'Koden for kontoen med email addressen {email} er ændret. Hvis du ikke har skiftet din kode, så skift venligst din kode ved at klikke på linket herunder' + CHANGEPASSWORDTEXT1: 'Du skiftede dit kodeord for' + CHANGEPASSWORDTEXT3: 'Skift kodeord' + HELLO: Hej + SilverStripe\Control\Email\ForgotPasswordEmail_ss: + HELLO: Hej + TEXT1: 'Her er din' + TEXT2: 'link til at nulstille dit kodeord' + TEXT3: for + SilverStripe\Control\RequestProcessor: + INVALID_REQUEST: 'Ugyldig forespørgsel' + REQUEST_ABORTED: 'Forespørgsel annulleret' + SilverStripe\Core\Manifest\VersionProvider: + VERSIONUNKNOWN: Ukendt + SilverStripe\Forms\CheckboxField: + NOANSWER: Nej + YESANSWER: Ja + SilverStripe\Forms\CheckboxSetField_ss: + NOOPTIONSAVAILABLE: 'Ingen tilgængelige muligheder' + SilverStripe\Forms\ConfirmedPasswordField: + ATLEAST: 'Kodeord skal være mindst {min} tegn lang.' + BETWEEN: 'Kodeord skal være {min} til {max} karakterer lang.' + CURRENT_PASSWORD_ERROR: 'Det nuværende kodeord du har indtastet er ikke korrekt.' + CURRENT_PASSWORD_MISSING: 'Du skal indtaste dit nuværende kodeord.' + LOGGED_IN_ERROR: 'Du skal være logget ind for at skifte dit kodeord.' + MAXIMUM: 'Kodeord må maks være {max} tegn lang' + SHOWONCLICKTITLE: 'Skift kodeord' + SilverStripe\Forms\CurrencyField: + CURRENCYSYMBOL: DKK + SilverStripe\Forms\DateField: + VALIDDATEFORMAT2: 'Indtats venligst et gyldigt datoformat ({format})' + VALIDDATEMAXDATE: 'Din dato skal være ældre end eller matche den maksimalt tilladte dato ({date})' + VALIDDATEMINDATE: 'Din dato skal være yngre end eller matche den minimum tilladte dato ({date})' + SilverStripe\Forms\DatetimeField: + VALIDDATEMAXDATETIME: 'Din dato skal være ældre end eller matche den maksimalt tilladte dato ({datetime})' + VALIDDATETIMEFORMAT: 'Indtats venligst et gyldigt dato- og tidsformat ({format})' + VALIDDATETIMEMINDATE: 'Din dato skal være yngre end eller matche den minimum tilladte dato og tid ({datetime})' + SilverStripe\Forms\DropdownField: + CHOOSE: (Vælg) + CHOOSE_MODEL: '(Vælg {name})' + SOURCE_VALIDATION: 'Venligst vælg en eksisterende værdi fra listen. {value} er ikke en tilladt mulighed' + SilverStripe\Forms\EmailField: + VALIDATION: 'Indtast venligst en emailadresse' + SilverStripe\Forms\FileUploadReceiver: + FIELDNOTSET: 'Fil information ikke fundet' + SilverStripe\Forms\Form: + BAD_METHOD: 'Denne form kræver en {method} indsendelse' + CSRF_EXPIRED_MESSAGE: 'Din session er udløbet. Venligst gensend formularen.' + CSRF_FAILED_MESSAGE: 'Det ser ud til der har været et teknisk problem. Klik venligst på tilbageknappen, tryk opdater i din browser og prøv igen.' + VALIDATIONPASSWORDSDONTMATCH: 'Kodeordene er ikke identiske' + VALIDATIONPASSWORDSNOTEMPTY: 'Kodeord kan ikke være tomme' + VALIDATIONSTRONGPASSWORD: 'Kodeord skal mindst have et tal og et alfanumerisk tegn' + VALIDATOR: Validering + VALIDCURRENCY: 'Indtast venligst en gyldig valuta' + SilverStripe\Forms\FormField: + EXAMPLE: 'f.eks. {format}' + NONE: ingen + SilverStripe\Forms\FormScaffolder: + TABMAIN: Primær SilverStripe\Forms\GridField\GridField: - Filter: Filter + Add: 'Tilføj {name}' + CSVEXPORT: 'Eksporter til CSV' + CSVIMPORT: 'Importer CSV' + Filter: Filtrer + FilterBy: 'Filtrer på' + Find: Find + LinkExisting: 'Link eksisterende' + NewRecord: 'Ny {type}' + NoItemsFound: 'Ingen elementer fundet' + PRINTEDAT: 'Printet d.' + PRINTEDBY: 'Printet af' + PlaceHolder: 'Find {type}' + PlaceHolderWithLabels: 'Find {type} på {name}' + Print: Print + RelationSearch: Relationssøgning + ResetFilter: Nulstil + SilverStripe\Forms\GridField\GridFieldDeleteAction: + Delete: Slet + DeletePermissionsFailure: 'Ingen slette rettigheder' + EditPermissionsFailure: 'Ingen rettighed til at fjerne emnet' + UnlinkRelation: Fjern + SilverStripe\Forms\GridField\GridFieldDetailForm: + CancelBtn: Annuller + Create: Opret + Delete: Slet + DeletePermissionsFailure: 'Ingen slette rettigheder' + Deleted: 'Slet {type} {name}' + Save: Gem + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Rediger + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'Kan ikke fjerne dig selv fra denne gruppe, du vil miste administrator rettigheder' + SilverStripe\Forms\GridField\GridFieldPaginator: + OF: af + Page: Side + View: Vis + SilverStripe\Forms\MoneyField: + FIELDLABELAMOUNT: Beløb + FIELDLABELCURRENCY: Valuta + INVALID_CURRENCY: 'Valuta {currency} er ikke i listen over tilladte valutaer' + SilverStripe\Forms\MultiSelectField: + SOURCE_VALIDATION: 'Vælg venligst eksisterende værdier fra listen. Ugyldig mulighed(er) {value} valgt' + SilverStripe\Forms\NullableField: + IsNullLabel: 'Er Null' + SilverStripe\Forms\NumericField: + VALIDATION: '''{value}'' er ikke et tal, kun tal accepteres i dette felt' + SilverStripe\Forms\TimeField: + VALIDATEFORMAT: 'Indtats venligst et gyldigt tidsformat ({format})' + SilverStripe\ORM\DataObject: + PLURALNAME: Dataobjekter + PLURALS: + one: 'Et dataobjekt' + other: '{count} dataobjekter' + SINGULARNAME: Dataobjekt + SilverStripe\ORM\FieldType\DBBoolean: + ANY: Enhver + NOANSWER: Nej + YESANSWER: Ja + SilverStripe\ORM\FieldType\DBDate: + DAYS_SHORT_PLURALS: + one: '{count} dag' + other: '{count} dage' + HOURS_SHORT_PLURALS: + one: '{count} time' + other: '{count} timer' + LessThanMinuteAgo: 'mindre end et minut' + MINUTES_SHORT_PLURALS: + one: '{count} minut' + other: '{count} minutter' + MONTHS_SHORT_PLURALS: + one: '{count} måned' + other: '{count} måneder' + SECONDS_SHORT_PLURALS: + one: '{count} sekund' + other: '{count} sekunder' + TIMEDIFFAGO: '{difference} siden' + TIMEDIFFIN: 'i {difference}' + YEARS_SHORT_PLURALS: + one: '{count} år' + other: '{count} år' + SilverStripe\ORM\FieldType\DBEnum: + ANY: Enhver + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'For mange relaterede objekter; fallback felt i brug' + SilverStripe\ORM\Hierarchy: + LIMITED_TITLE: 'For mange underelementer ({count})' + SilverStripe\ORM\Hierarchy\Hierarchy: + InfiniteLoopNotAllowed: 'Uendeligt løkke fundet i "{type}" hierarkiet. Ændre venligst det overliggende element for at løse dette' + LIMITED_TITLE: 'For mange underelementer ({count})' + SilverStripe\ORM\ValidationException: + DEFAULT_ERROR: Valideringsfejl + SilverStripe\Security\BasicAuth: + ENTERINFO: 'Indtast venligst et brugernavn og kodeord.' + ERRORNOTADMIN: 'Den bruger er ikke en administrator.' + ERRORNOTREC: 'Brugernavn / kodeord kunne ikke genkendes' + SilverStripe\Security\CMSMemberLoginForm: + PASSWORDEXPIRED: '

Dit kodeord er udløbet. Vælg venligst et nyt.

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

Ugyldig bruger. Log venligst ind igen her for at fortsætte.

' + LOGIN_MESSAGE: '

Din session er løbet ud pga. inaktivitet

' + LOGIN_TITLE: 'Log ind igen, for at fortsætte hvor du slap.' + SUCCESS: Succes + SUCCESSCONTENT: '

Logget ind. Hvis du ikke automatisk viderestilles så klik her

' + SUCCESS_TITLE: 'Logget ind med sucess' + SilverStripe\Security\DefaultAdminService: + DefaultAdminFirstname: 'Standard admin' + SilverStripe\Security\Group: + AddRole: 'Tilføj en rolle for denne gruppe' + Code: 'Gruppe kode' + DefaultGroupTitleAdministrators: Administratorer + DefaultGroupTitleContentAuthors: Indholdsforfattere + Description: Beskrivelse + GROUPNAME: Gruppenavn + GroupReminder: 'Hvis du vælger en overliggende gruppe, får denne gruppe alle dens roller' + HierarchyPermsError: 'Kan ikke tildele overliggende gruppe "{group}" med fortrinsrettigheder (kræver ADMIN adgang)' + Locked: 'Låst?' + MEMBERS: Brugere + NEWGROUP: 'Ny gruppe' + NoRoles: 'Ingen roller fundet' + PERMISSIONS: Rettigheder + PLURALNAME: Grupper + PLURALS: + one: 'En gruppe' + other: '{count} grupper' + Parent: 'Overliggende gruppe' + ROLES: Roller + ROLESDESCRIPTION: 'Roller er et prædefineret sæt af rettigheder, som kan tildeles grupper.
De bliver nedarvet fra en overliggende grupper hvis krævet.' + RolesAddEditLink: 'Administrer roller' + SINGULARNAME: Gruppe + Sort: Sortering + has_many_Permissions: Rettigheder + many_many_Members: Brugere + SilverStripe\Security\LoginAttempt: + Email: 'Email adresse' + EmailHashed: 'Email adresse (hashed)' + IP: 'IP addresse' + PLURALNAME: Loginforsøg + PLURALS: + one: 'Et loginforsøg' + other: '{count} loginforsøg' + SINGULARNAME: 'Login forsøg' + Status: Status + SilverStripe\Security\Member: + ADDGROUP: 'Tilføj gruppe' + BUTTONCHANGEPASSWORD: 'Skift kodeord' + BUTTONLOGIN: 'Log ind' + BUTTONLOGINOTHER: 'Log ind med en anden bruger' + BUTTONLOGOUT: 'Log ud' + BUTTONLOSTPASSWORD: 'Jeg har glemt mit kodeord' + CONFIRMNEWPASSWORD: 'Bekræft nyt kodeord' + CONFIRMPASSWORD: 'Bekræft kodeord' + CURRENT_PASSWORD: 'Nuværende kodeord' + EDIT_PASSWORD: 'Nyt kodeord' + EMAIL: Email + EMPTYNEWPASSWORD: 'Det nye kodeord kan ikke være tom, prøv venligst igen' + ENTEREMAIL: 'Indtast venligst en email adresse for at få et nulstillingslink.' + ERRORLOCKEDOUT2: 'Din konto er blevet midlertidigt deaktiveret pga. for mange fejlslagne loginforsøg. Forsøg venligst igen om {count} minutter.' + ERRORNEWPASSWORD: 'Du har indtastet dit nye kodeord forskelligt, forsøg igen' + ERRORPASSWORDNOTMATCH: 'Dit nuværende kodeord matcher ikke, forsøg venligst igen' + ERRORWRONGCRED: 'De indtastede værdier ser ikke ud til at være korrekte. Forsøg venligst igen.' + FIRSTNAME: Fornavn + INTERFACELANG: 'Sprog i brugerfladen' + KEEPMESIGNEDIN: 'Hold mig logget ind' + LOGGEDINAS: 'Du er logget ind som {name}.' + NEWPASSWORD: 'Nyt kodeord' + PASSWORD: Kodeord + PASSWORDEXPIRED: 'Dit kodeord er udløbet. Vælg venligst et nyt.' + PLURALNAME: Brugere + PLURALS: + one: 'En bruger' + other: '{count} brugere' + REMEMBERME: 'Husk mig til næste gang? (i {count} dage på denne enhed)' + SINGULARNAME: Bruger + SUBJECTPASSWORDCHANGED: 'Dit kodeord er blevet ændret' + SUBJECTPASSWORDRESET: 'Link til at nulstille dit kodeord' + SURNAME: Efternavn + VALIDATIONADMINLOSTACCESS: 'Kan ikke fjerne alle admin grupper fra din profil' + ValidationIdentifierFailed: 'Kan ikke overskrive eksisterende bruger #{id} med identisk identifikator ({name} = {value}))' + WELCOMEBACK: 'Velkommen tilbage, {firstname}' + YOUROLDPASSWORD: 'Dit gamle kodeord' + belongs_many_many_Groups: Grupper + db_Locale: 'Sprog i brugerfladen' + db_LockedOutUntil: 'Låst ude indtil' + db_Password: Kodeord + db_PasswordExpiry: Kodeordsudløbsdato + SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm: + AUTHENTICATORNAME: 'CMS bruger loginform' + BUTTONFORGOTPASSWORD: 'Glemt kodeord' + BUTTONLOGIN: 'Log mig ind igen' + BUTTONLOGOUT: 'Log ud' + SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: + ERRORWRONGCRED: 'De indtastede værdier ser ikke ud til at være korrekte. Forsøg venligst igen.' + NoPassword: 'Der er ikke en kode på denne bruger.' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'Email og kodeord' + SilverStripe\Security\MemberPassword: + PLURALNAME: 'Bruger kodeord' + PLURALS: + one: 'Et bruger kodeord' + other: '{count} bruger kodeord' + SINGULARNAME: 'Bruger kodeord' + SilverStripe\Security\PasswordValidator: + LOWCHARSTRENGTH: 'Forøg venligst kodeordets styrke, ved at tilføje nogle af følgende tegn: {chars}' + PREVPASSWORD: 'Du har tidligere brugt dette kodeord, vælg venligst et nyt kodeord' + TOOSHORT: 'Kodeordet er for kort, det skal mindst være {minimum} eller flere tegn langt' SilverStripe\Security\Permission: + AdminGroup: Administrator + CMS_ACCESS_CATEGORY: 'CMS Adgang' CONTENT_CATEGORY: Indholdsrettigheder + FULLADMINRIGHTS: 'Fuld administrator rettighed' + FULLADMINRIGHTS_HELP: 'Indebærer og overskriver alle andre tildelte rettigheder.' + PERMISSIONS_CATEGORY: 'Roller og adgangsrettigheder' + PLURALNAME: Rettigheder + PLURALS: + one: 'En rettighed' + other: '{count} rettigheder' + SINGULARNAME: Rettighed + UserPermissionsIntro: 'Tildeling af grupper til denne bruger, ændrer de rettigheder brugeren har. Se gruppe området for rettigheds detaljer på de individuelle grupper.' + SilverStripe\Security\PermissionCheckboxSetField: + AssignedTo: 'tildelt til "{title}"' + FromGroup: 'nedarvet fra gruppen "{title}"' + FromRole: 'nedarvet fra rollen "{title}"' + FromRoleOnGroup: 'nedarvet fra rollen "{roletitle}" på gruppen "{grouptitle}"' + SilverStripe\Security\PermissionRole: + OnlyAdminCanApply: 'Kun administratorer kan tilføje' + PLURALNAME: Roller + PLURALS: + one: 'En rolle' + other: '{count} roller' + SINGULARNAME: Rolle + Title: Titel + SilverStripe\Security\PermissionRoleCode: + PLURALNAME: 'Rettigheds rolle koder' + PLURALS: + one: 'En rettigheds rolle kode' + other: '{count} rettigheds rolle koder' + PermsError: 'Kan ikke tildele koden "{code}" med fortrinsrettigheder (kræver ADMIN adgang)' + SINGULARNAME: 'Rettighed rolle kode' + SilverStripe\Security\RememberLoginHash: + PLURALNAME: 'Login hashes' + PLURALS: + one: 'Et login hash' + other: '{count} Login Hashes' + SINGULARNAME: 'Login hash' + SilverStripe\Security\Security: + ALREADYLOGGEDIN: 'Du har ikke adgang til denne side. Hvis du har en anden bruger der har adgang til denne side, kan du logge ind med denne herunder.' + BUTTONSEND: 'Send mig linket til at nulstille kodeordet' + CHANGEPASSWORDBELOW: 'Du kan ændre dit kodeord herunder.' + CHANGEPASSWORDHEADER: 'Skift dit kodeord' + CONFIRMLOGOUT: 'Klik venligst på knappen herunder, for at bekræfte at du vil logge ud.' + ENTERNEWPASSWORD: 'Indtast venligst et nyt kodeord.' + ERRORPASSWORDPERMISSION: 'Du skal være logget ind, for at kunne ændre dit kodeord!' + LOGIN: 'Log ind' + LOGOUT: 'Log ud' + LOSTPASSWORDHEADER: 'Glemt kodeord' + NOTEPAGESECURED: 'Denne side er beskyttet. Indtast dine loginoplysninger herunder for at få adgang.' + NOTERESETPASSWORD: 'Indtast din email adresse, så sender vi dig et link som du kan nulstille dit kodeord med' + PASSWORDRESETSENTHEADER: 'link til at nulstille kodeord afsendt' + PASSWORDRESETSENTTEXT: 'Tak for det. Et link til at nulstille dit kodeord er afsendt, hvis der findes en bruger med denne email adresse.' diff --git a/lang/de.yml b/lang/de.yml index 1e9f16e5e..77e2f1584 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -190,7 +190,4 @@ de: LOGIN: Anmelden LOSTPASSWORDHEADER: 'Passwort vergessen' NOTEPAGESECURED: 'Diese Seite ist geschützt. Bitte melden Sie sich an und Sie werden sofort weitergeleitet.' - NOTERESETLINKINVALID: '

Der Link zum Zurücksetzen des Passworts ist entweder nicht korrekt oder abgelaufen

Sie können einen neuen Link anfordern oder Ihr Passwort nach dem einloggen ändern.

' NOTERESETPASSWORD: 'Geben Sie Ihre E-Mail-Adresse ein und wir werden Ihnen einen Link zuschicken, mit dem Sie Ihr Passwort zurücksetzen können.' - PASSWORDSENTHEADER: 'Der Link zum Zurücksetzen des Passworts wurde an ''{email}'' gesendet' - PASSWORDSENTTEXT: 'Vielen Dank! Wenn ein Account zu der E-Mail Adresse ''{email}'' existiert, wurde eine E-Mail mit dem Link zum Zurücksetzen des Passworts verschickt.' diff --git a/lang/eo.yml b/lang/eo.yml index 59806b953..fb6b74ca9 100644 --- a/lang/eo.yml +++ b/lang/eo.yml @@ -95,6 +95,8 @@ eo: DeletePermissionsFailure: 'Mankas permeso forigi' Deleted: 'Forigita {type} {name}' Save: Konservi + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Redakti SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: UnlinkSelfFailure: 'Ne povas forigi vin el ĉi tiu grupo; vi perdus administrajn rajtojn' SilverStripe\Forms\GridField\GridFieldPaginator: @@ -147,6 +149,8 @@ eo: other: '{count} jaroj' SilverStripe\ORM\FieldType\DBEnum: ANY: Ajna + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'Tro multaj objektoj; retropaŝa kampo uzata' SilverStripe\ORM\Hierarchy: LIMITED_TITLE: 'Tro da idoj ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: @@ -319,7 +323,6 @@ eo: LOGOUT: Elsaluti LOSTPASSWORDHEADER: 'Perdis pasvorton' NOTEPAGESECURED: 'Tiu paĝo estas sekurigita. Enigu viajn akreditaĵojn sube kaj vi aliros pluen.' - NOTERESETLINKINVALID: '

La pasvorta reagorda ligilo estas malvalida aŭ finiĝis.

Vi povas peti novan ĉi tie aŭ ŝanĝi vian pasvorton post vi ensalutis.

' NOTERESETPASSWORD: 'Enigu vian retpoŝtan adreson kaj ni sendos al vi ligilon per kiu vi povas reagordi vian pasvorton' - PASSWORDSENTHEADER: 'Pasvorta reagorda ligilo sendiĝis al ''{email}''' - PASSWORDSENTTEXT: 'Dankon! Reagordita ligilo sendiĝis al ''{email}'', kondiĉe ke konto ekzistas por tiu retadreso.' + PASSWORDRESETSENTHEADER: 'Pasvorta reagorda ligilo sendiĝis' + PASSWORDRESETSENTTEXT: 'Dankon. Reagorda ligilo sendiĝis, kondiĉe ke konto ekzistas por ĉi tiu retadreso.' diff --git a/lang/es.yml b/lang/es.yml index 0009c7a4a..debd4b4a7 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -249,7 +249,4 @@ es: LOGIN: Entrar LOSTPASSWORDHEADER: '¿Contraseña Perdida?' NOTEPAGESECURED: 'Esa página está protegida. Introduzca sus datos de acreditación a continuación y lo enviaremos a ella en un momento.' - NOTERESETLINKINVALID: '

El enlace para restablecer la contraseña es inválido o ha expirado.

Usted puede solicitar uno nuevo aqui o cambiar su contraseña después de que se haya conectado.

' NOTERESETPASSWORD: 'Introduzca su dirección de e-mail, y le enviaremos un enlace, con el cual podrá restaurar su contraseña' - PASSWORDSENTHEADER: 'Un enlace para restablecer la contraseña ha sido enviado a ''{email}''' - PASSWORDSENTTEXT: 'Gracias! Un enlace para restablecer la contraseña ha sido enviado a ''{email}'', siempre que una cuenta exista para la dirección de email indicada.' diff --git a/lang/et_EE.yml b/lang/et_EE.yml index 63fc4cd5d..a8fb3b686 100644 --- a/lang/et_EE.yml +++ b/lang/et_EE.yml @@ -139,7 +139,4 @@ et_EE: ERRORPASSWORDPERMISSION: 'Pead olema sisseloginud, et parooli muuta!' LOGIN: 'Logi sisse' NOTEPAGESECURED: 'See leht on turvatud. Sisesta enda andmed allpool ja me saadame sind otse edasi' - NOTERESETLINKINVALID: '

Parooli lähtestamise link on kehtetu või aegunud.

Saate taotleda uut linki siin või muuta parooli pärast sisselogimist.

' NOTERESETPASSWORD: 'Sisesta oma email ja me saadame sulle lingi kus saad oma parooli tühistada.' - PASSWORDSENTHEADER: 'Parooli lähtestamise link saadeti aadressile ''{email}''' - PASSWORDSENTTEXT: 'Aitäh! Lähtestamislink saadeti aadressile ''{email}'' eeldusel, et selle e-posti aadressiga seotud konto on olemas.' diff --git a/lang/fa_IR.yml b/lang/fa_IR.yml index b292b25d7..b9aa0ebea 100644 --- a/lang/fa_IR.yml +++ b/lang/fa_IR.yml @@ -168,4 +168,3 @@ fa_IR: ERRORPASSWORDPERMISSION: 'جهت تغییر گذرواژه خود باید وارد شده باشید!' LOGIN: ورود LOSTPASSWORDHEADER: 'فراموشی گذرواژه' - PASSWORDSENTHEADER: 'لینک ازنوسازی گذرواژه به ''{email}'' ارسال شد' diff --git a/lang/fi.yml b/lang/fi.yml index 364762d3a..b19807225 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -76,6 +76,7 @@ fi: LinkExisting: 'Linkitä olemassaoleva' NewRecord: 'Uusi {type}' NoItemsFound: 'Ei kohteita' + OpenFilter: 'Avaa haku ja suodatus' PRINTEDAT: Tulostettu PRINTEDBY: Tulostaja PlaceHolder: 'Etsi {type}' @@ -95,12 +96,19 @@ fi: DeletePermissionsFailure: 'Ei oikeuksia poistamiseen' Deleted: 'Poistettiin {type} {name}' Save: Tallenna + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Muokkaa + SilverStripe\Forms\GridField\GridFieldFilterHeader: + Search: 'Haku "{name}"' + SearchFormFaliure: 'Hakulomaketta ei pystytty luomaan.' 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: OF: / Page: Sivu View: Näytä + SilverStripe\Forms\GridField\GridFieldViewButton: + VIEW: Avaa SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Määrä FIELDLABELCURRENCY: Valuutta @@ -147,6 +155,8 @@ fi: other: '{count} vuotta' SilverStripe\ORM\FieldType\DBEnum: ANY: Yhtään + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'Liian monta samaan liittyvää objektia: oletuskenttä käytössä' SilverStripe\ORM\Hierarchy: LIMITED_TITLE: 'Liian monta lapsiobjektia ({count}}' SilverStripe\ORM\Hierarchy\Hierarchy: @@ -197,6 +207,7 @@ fi: many_many_Members: Jäsenet SilverStripe\Security\LoginAttempt: Email: Sähköpostiosoite + EmailHashed: 'Sähköpostiosoite (tiivistetty)' IP: IP-osoite PLURALNAME: Kirjautumisyritykset PLURALS: @@ -255,6 +266,8 @@ fi: SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: ERRORWRONGCRED: 'Antamasi tiedot eivät näytä oikeilta. Yritä uudelleen.' NoPassword: 'Tällä käyttäjällä ei ole salasanaa' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'Sähköpostiosoite & salasana' SilverStripe\Security\MemberPassword: PLURALNAME: 'Käyttäjän salasanat' PLURALS: @@ -318,5 +331,5 @@ fi: NOTEPAGESECURED: 'Tämä sivu on suojattu. Syötä tunnistetietosi alle niin pääset eteenpäin.' NOTERESETLINKINVALID: '

Salasanan palautuslinkki on virheellinen tai vanhentunut.

Voit pyytää uuden napsauttamalla tästä tai vaihtaa salasanasi kirjautumisen jälkeen.

' NOTERESETPASSWORD: 'Syötä sähköpostiosoitteesi ja lähetämme sinulle linkin, jonka avulla saat palautettua salasanasi' - PASSWORDSENTHEADER: 'Salasanan palautuslinkki lähetettiin osoitteeseen ''{email}''' - PASSWORDSENTTEXT: 'Kiitos! Salasanan palautuslinkki lähetettiin osoitteeseen ''{email}'', joka on liitettynä tähän käyttäjätiliin.' + PASSWORDRESETSENTHEADER: 'Salasanan palautuslinkki lähetetty' + PASSWORDRESETSENTTEXT: 'Kiitos, palautuslinkki on lähetetty käyttäjätilille asetettuun sähköpostiosoitteeseen.' diff --git a/lang/fr.yml b/lang/fr.yml index 5a18188da..450cfdb87 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -84,7 +84,6 @@ fr: RelationSearch: 'Rechercher relations' ResetFilter: Réinitialiser SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Supprimer Delete: Supprimer DeletePermissionsFailure: 'Vous n’avez pas les autorisations pour supprimer' EditPermissionsFailure: 'Pas de permissions pour délier l''enregistrement' @@ -96,8 +95,6 @@ fr: DeletePermissionsFailure: 'Vous n’avez pas les autorisations pour supprimer' Deleted: '{type} {name} supprimés' Save: Enregistrer - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - 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: @@ -322,7 +319,4 @@ fr: LOGOUT: 'Se déconnecter' LOSTPASSWORDHEADER: 'Mot de passe oublié' NOTEPAGESECURED: 'Cette page est sécurisée. Entrez vos identifiants ci-dessous et vous pourrez y avoir accès.' - NOTERESETLINKINVALID: '

Le lien de réinitialisation du mot de passe n’est pas valide ou a expiré.

Vous pouvez en demander un nouveau en suivant ce lien ou changer de mot de passe après connexion.

' NOTERESETPASSWORD: 'Entrez votre adresse email et nous vous enverrons un lien pour modifier votre mot de passe' - PASSWORDSENTHEADER: "Lien de réinitialisation de mot de passe envoyé à «\_{email}\_»" - PASSWORDSENTTEXT: "Merci\_! Un lien de réinitialisation vient d’être envoyé à «\_{email}\_», à condition que cette adresse existe." diff --git a/lang/id.yml b/lang/id.yml index d17eac338..c9c06bbff 100644 --- a/lang/id.yml +++ b/lang/id.yml @@ -167,7 +167,4 @@ id: LOGIN: Masuk LOSTPASSWORDHEADER: 'Kata Kunci yang Terlupa' NOTEPAGESECURED: 'Laman ini diamankan. Isikan data berikut untuk dikirimkan hak akses Anda.' - NOTERESETLINKINVALID: '

Tautan penggantian kata kunci tidak valid atau sudah kadaluarsa.

Anda dapat meminta yang baru di sini atau mengganti kata kunci setelah Anda masuk.

' NOTERESETPASSWORD: 'Isikan alamat email Anda untuk mendapatkan tautan penggantian kata kunci' - PASSWORDSENTHEADER: 'Tautan penggantian kata kunci dikirimkan ke ''{email}''' - PASSWORDSENTTEXT: 'Terimakasih! Tautan reset telah dikirim ke ''{email}'', berisi informasi akun untuk alamat email ini.' diff --git a/lang/id_ID.yml b/lang/id_ID.yml index 22ebcb2e5..11d1f2bb3 100644 --- a/lang/id_ID.yml +++ b/lang/id_ID.yml @@ -166,7 +166,4 @@ id_ID: LOGIN: Masuk LOSTPASSWORDHEADER: 'Kata Kunci yang Terlupa' NOTEPAGESECURED: 'Laman ini diamankan. Isikan data berikut untuk dikirimkan hak akses Anda.' - NOTERESETLINKINVALID: '

Tautan penggantian kata kunci tidak valid atau sudah kadaluarsa.

Anda dapat meminta yang baru di sini atau mengganti kata kunci setelah Anda masuk.

' NOTERESETPASSWORD: 'Isikan alamat email Anda untuk mendapatkan tautan penggantian kata kunci' - PASSWORDSENTHEADER: 'Tautan penggantian kata kunci dikirimkan ke ''{email}''' - PASSWORDSENTTEXT: 'Terimakasih! Tautan reset telah dikirim ke ''{email}'', berisi informasi akun untuk alamat email ini.' diff --git a/lang/it.yml b/lang/it.yml index 05bf5dc83..4089faf9b 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -95,6 +95,10 @@ it: DeletePermissionsFailure: 'Non hai i permessi per eliminare' Deleted: 'Eliminato {type} {name}' Save: Salva + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Modifica + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'Non è possibile rimuovere te stesso da questo gruppo, perderesti i diritti di admin' SilverStripe\Forms\GridField\GridFieldPaginator: OF: di Page: Pagina @@ -145,6 +149,8 @@ it: other: '{count} anni' SilverStripe\ORM\FieldType\DBEnum: ANY: Qualsiasi + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'Troppi oggetti correlati; campo di fallback in uso' SilverStripe\ORM\Hierarchy: LIMITED_TITLE: 'Troppi figli ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: @@ -195,6 +201,7 @@ it: many_many_Members: Membri SilverStripe\Security\LoginAttempt: Email: 'Indirizzo e-mail' + EmailHashed: 'Indirizzo email (hash)' IP: 'Indirizzo IP' PLURALNAME: 'Tentativi d''accesso' PLURALS: @@ -236,6 +243,7 @@ it: SUBJECTPASSWORDCHANGED: 'La tua password è stata cambiata' SUBJECTPASSWORDRESET: 'Link per azzerare la tua password' SURNAME: Cognome + VALIDATIONADMINLOSTACCESS: 'Non è possibile rimuovere tutti i gruppi admin dal tuo profilo' ValidationIdentifierFailed: 'Non posso sovrascrivere l''utente esistente #{id} con identificatore identico ({name} = {value}))' WELCOMEBACK: 'Bentornato, {firstname}' YOUROLDPASSWORD: 'La tua vecchia password' @@ -252,6 +260,8 @@ it: SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: ERRORWRONGCRED: 'I dettagli forniti non sembrano corretti. Per favore riprovare.' NoPassword: 'Manca la password per questo utente.' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'E-mail & Password' SilverStripe\Security\MemberPassword: PLURALNAME: 'Password utenti' PLURALS: @@ -313,7 +323,6 @@ it: LOGOUT: Scollegati LOSTPASSWORDHEADER: 'Password smarrita' NOTEPAGESECURED: 'La pagina è protetta. Inserisci le credenziali qui sotto per poter andare avanti.' - NOTERESETLINKINVALID: '

Il link per azzerare la password non è valido o è scaduto.

Puoi richiederne uno nuovo qui o cambiare la tua password dopo che ti sei connesso.

' NOTERESETPASSWORD: 'Inserisci il tuo indirizzo e-mail e ti verrà inviato un link per poter azzerare la tua password.' - PASSWORDSENTHEADER: 'Link per azzeramento della password inviato a ''{email}''' - PASSWORDSENTTEXT: 'Grazie! Un link di azzeramento è stato inviato a ''{email}'', fornito un account esistente per questo indirizzo e-mail.' + PASSWORDRESETSENTHEADER: 'Link di azzeramento password inviato' + PASSWORDRESETSENTTEXT: 'Grazie! Un link di azzeramento è stato inviato, supponendo un account esista a quell''indirizzo e-mail.' diff --git a/lang/ja.yml b/lang/ja.yml index b2f92eb1e..433a832e2 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -146,7 +146,4 @@ ja: ERRORPASSWORDPERMISSION: パスワードを変更する為に、ログインしなければなりません! LOGIN: ログイン NOTEPAGESECURED: このページはセキュリティで保護されております証明書キーを下記に入力してください。こちらからすぐに送信します - NOTERESETLINKINVALID: '

パスワードのリセットリンクは有効でないか期限切れです。

新しいパスワードを要求することができます ここ もしくはパスワードを変更することができます ログインした後 .

' NOTERESETPASSWORD: メールアドレスを入力してください、パスワードをリセットするURLを送信致します - PASSWORDSENTHEADER: 'パスワードリセットリンクは ''{email}'' に送信されました' - PASSWORDSENTTEXT: 'ありがとうございました! リセットリンクは、''{email}'' に、このアカウントが存在することを前提として送信されました。' diff --git a/lang/lt.yml b/lang/lt.yml index abdbaea25..3ca7adbff 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -167,7 +167,4 @@ lt: LOGIN: Prisijungti LOSTPASSWORDHEADER: 'Slaptažodžio atstatymas' NOTEPAGESECURED: 'Šis puslapis yra apsaugotas. Įveskite savo duomenis į žemiau esančius laukelius.' - NOTERESETLINKINVALID: '

Neteisinga arba negaliojanti slaptažodžio atstatymo nuoroda.

Galite atsisiųsti naują čia arba pasikeisti slaptažodį po to, kai prisijungsite.

' NOTERESETPASSWORD: 'Įveskite savo e. pašto adresą ir atsiųsime slaptažodžio atstatymui skirtą nuorodą' - PASSWORDSENTHEADER: 'Slaptažodžio atstatymo nuoroda nusiųsta į ''{email}''' - PASSWORDSENTTEXT: 'Atstatymo nuoroda nusiųsta į ''{email}''' diff --git a/lang/mi.yml b/lang/mi.yml index 7a2601ed5..e9fe2308e 100644 --- a/lang/mi.yml +++ b/lang/mi.yml @@ -149,7 +149,4 @@ mi: LOGIN: Takiuru LOSTPASSWORDHEADER: 'Kupuhipa Ngaro' NOTEPAGESECURED: 'Kua ngita tēnā whārangi. Tāurua ō taipitoptio tuakiri ki raro, ā, mā mātou koe e tuku kia haere tonu.' - NOTERESETLINKINVALID: '

He muhu, kua mōnehu rānei te hono tautuhi kupuhipa anō.

Ka taea te tono i te mea hōui konei ka huri rānei i tō kupuhipa ā muri i tōtakiuru.

' NOTERESETPASSWORD: 'Tāurua tō wāhitau īmēra, mā mātou e tuku tētahi hono ki a koe e taea ai te tautuhi anō i tō kupuhipa' - PASSWORDSENTHEADER: 'I tukuna he hono tautuhi kupuhipa anō ki ''{email}''' - PASSWORDSENTTEXT: 'Kia ora! Kua tukuna he hono tautuhi anō ki ''{email}'',engari rā kei te tīariari he pūkete mō taua wāhitau īmēra.' diff --git a/lang/nb.yml b/lang/nb.yml index 91dd9f758..f1dc4c74d 100644 --- a/lang/nb.yml +++ b/lang/nb.yml @@ -152,7 +152,4 @@ nb: LOGIN: 'Logg inn' LOSTPASSWORDHEADER: 'Mistet passord' NOTEPAGESECURED: 'Den siden er sikret. Skriv inn gyldig innloggingsinfo så kommer du inn.' - NOTERESETLINKINVALID: '

Lenken for å nullstille passordet er ugyldig eller utgått.

Du kan kreve en ny her eller endre passordet etter at du har logget inn.

' NOTERESETPASSWORD: 'Skriv inn epostadressen din og vi vil sende deg en lenke som nullstiller passordet.' - PASSWORDSENTHEADER: 'Lenke for nullstilling av passord ble sendt til ''{email}''' - PASSWORDSENTTEXT: 'Takk! En lenke for å lage nytt passord er sendt til ''{email}'', forutsatt at det eksisterer en konto for denne epostadressen.' diff --git a/lang/nl.yml b/lang/nl.yml index 64f191213..f6de04299 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -1,4 +1,26 @@ nl: + SilverStripe\Admin\LeftAndMain: + VersionUnknown: onbekend + SilverStripe\AssetAdmin\Forms\UploadField: + Dimensions: Afmetingen + EDIT: Bewerken + EDITINFO: 'Bewerk dit bestand' + REMOVE: Verwijder + SilverStripe\Control\ChangePasswordEmail_ss: + CHANGEPASSWORDFOREMAIL: 'Het wachtwoord voor het account met e-mailadres {email} is aangepast. Indien u uw wachtwoord niet heeft aangepast kunt u dat doen met onderstaande link.' + CHANGEPASSWORDTEXT1: 'U heeft het wachtwoord veranderd voor' + CHANGEPASSWORDTEXT3: 'Wachtwoord veranderen' + HELLO: Hallo + SilverStripe\Control\Email\ForgotPasswordEmail_ss: + HELLO: Hallo + TEXT1: 'Hier is uw' + TEXT2: 'link om uw wachtwoord opnieuw aan te maken' + TEXT3: voor + SilverStripe\Control\RequestProcessor: + INVALID_REQUEST: 'Fout bij verwerken' + REQUEST_ABORTED: 'Fout bij verwerken (geannuleerd)' + SilverStripe\Core\Manifest\VersionProvider: + VERSIONUNKNOWN: Onbekend SilverStripe\Forms\CheckboxField: NOANSWER: Nee YESANSWER: Ja @@ -8,6 +30,8 @@ nl: ATLEAST: 'Een wachtwoord moet tenminste {min} karakters hebben.' BETWEEN: 'Een wachtwoord moet tussen de {min} en {max} karakters hebben' CURRENT_PASSWORD_ERROR: 'Het wachtwoord dat u heeft ingevoerd is niet juist.' + CURRENT_PASSWORD_MISSING: 'Voer uw huidige wachtwoord in.' + LOGGED_IN_ERROR: 'U moet ingelogd zijn om uw wachtwoord te kunnen veranderen!' MAXIMUM: 'Een wachtwoord mag maximaal {max} karakters hebben.' SHOWONCLICKTITLE: 'Verander wachtwoord' SilverStripe\Forms\CurrencyField: @@ -16,12 +40,20 @@ nl: 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})' + SilverStripe\Forms\DatetimeField: + VALIDDATEMAXDATETIME: 'De datum moet ouder of gelijk zijn aan de maximale datum ({datetime})' + VALIDDATETIMEFORMAT: 'Vul een geldige datum in ({format})' + VALIDDATETIMEMINDATE: 'De datum moet nieuwer of gelijk zijn aan de minimale datum ({datetime})' SilverStripe\Forms\DropdownField: CHOOSE: (Kies) + CHOOSE_MODEL: '(Selecteer {name})' SOURCE_VALIDATION: 'Selecteer een optie uit de lijst. {value} is geen geldige keuze.' SilverStripe\Forms\EmailField: VALIDATION: 'Gelieve een e-mailadres in te voeren.' + SilverStripe\Forms\FileUploadReceiver: + FIELDNOTSET: 'Bestandsinformatie niet gevonden' SilverStripe\Forms\Form: + BAD_METHOD: 'Dit formulier moet middels {method} verzonden worden' CSRF_EXPIRED_MESSAGE: 'Uw sessie is verlopen. Verzend het formulier opnieuw.' CSRF_FAILED_MESSAGE: 'Er lijkt een technisch probleem te zijn. Klik op de knop terug, vernieuw uw browser, en probeer het opnieuw.' VALIDATIONPASSWORDSDONTMATCH: 'Wachtwoorden komen niet overeen' @@ -30,7 +62,10 @@ nl: VALIDATOR: Validator VALIDCURRENCY: 'Vul een geldige munteenheid in' SilverStripe\Forms\FormField: + EXAMPLE: 'bijv. {format}' NONE: geen + SilverStripe\Forms\FormScaffolder: + TABMAIN: Hoofdgedeelte SilverStripe\Forms\GridField\GridField: Add: '{name} toevoegen' CSVEXPORT: 'Exporteer naar CSV' @@ -41,6 +76,7 @@ nl: LinkExisting: 'Koppel een bestaand item' NewRecord: 'Nieuw {type}' NoItemsFound: 'Geen items gevonden.' + OpenFilter: 'Zoeken en filteren openen' PRINTEDAT: 'Geprint op' PRINTEDBY: 'Geprint door' PlaceHolder: 'Zoek {type}' @@ -60,27 +96,72 @@ nl: DeletePermissionsFailure: 'Onvoldoende rechten om te verwijderen' Deleted: '{type} {name} verwijderd' Save: Opslaan + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Bewerken + SilverStripe\Forms\GridField\GridFieldFilterHeader: + Search: 'Zoek naar "{name}"' + SearchFormFaliure: 'Er kon geen zoekformulier worden aangemaakt' + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'U kunt uzelf niet verwijderen van deze groep, omdat u dan geen admin-rechten meer heeft.' + SilverStripe\Forms\GridField\GridFieldPaginator: + OF: van + Page: Pagina + View: Bekijk + SilverStripe\Forms\GridField\GridFieldViewButton: + VIEW: Bekijk SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Aantal FIELDLABELCURRENCY: Munteenheid + INVALID_CURRENCY: 'Valuta {currency} is niet toegestaan' + SilverStripe\Forms\MultiSelectField: + SOURCE_VALIDATION: 'Selecteer een optie uit de lijst. {value} is geen geldige keuze.' SilverStripe\Forms\NullableField: IsNullLabel: 'Is null' SilverStripe\Forms\NumericField: VALIDATION: '''{value}'' is geen getal, enkel getallen worden door dit veld geaccepteerd' SilverStripe\Forms\TimeField: VALIDATEFORMAT: 'Vul een geldig datumformaat in ({format})' + SilverStripe\ORM\DataObject: + PLURALNAME: 'Data objecten' + PLURALS: + one: 'Data object' + other: '{count} Data objecten' + SINGULARNAME: 'Data object' SilverStripe\ORM\FieldType\DBBoolean: ANY: Elke NOANSWER: Nee YESANSWER: Ja SilverStripe\ORM\FieldType\DBDate: + DAYS_SHORT_PLURALS: + one: '{count} dag' + other: '{count} dagen' + HOURS_SHORT_PLURALS: + one: '{count} uur' + other: '{count} uren' LessThanMinuteAgo: 'minder dan één minuut' + MINUTES_SHORT_PLURALS: + one: '{count} minuut' + other: '{count} minuten' + MONTHS_SHORT_PLURALS: + one: '{count} maand' + other: '{count} maanden' + SECONDS_SHORT_PLURALS: + one: '{count} seconde' + other: '{count} seconden' TIMEDIFFAGO: '{difference} geleden' TIMEDIFFIN: 'in {difference}' + YEARS_SHORT_PLURALS: + one: '{count} jaar' + other: '{count} jaren' SilverStripe\ORM\FieldType\DBEnum: ANY: Elke + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'Teveel keuzes in de lijst; een alternatief veld wordt getoond.' + SilverStripe\ORM\Hierarchy: + LIMITED_TITLE: 'Teveel onderliggende items ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: InfiniteLoopNotAllowed: 'Oneindige lus gevonden in "{type}" hiërarchie. Wijzig het hogere niveau om dit op te lossen' + LIMITED_TITLE: 'Teveel onderliggende items ({count})' SilverStripe\ORM\ValidationException: DEFAULT_ERROR: Validatiefout SilverStripe\Security\BasicAuth: @@ -91,34 +172,60 @@ nl: PASSWORDEXPIRED: '

Uw wachtwoord is verlopen. Kies een nieuw wachtwoord.

' SilverStripe\Security\CMSSecurity: INVALIDUSER: '

Ongeldige gebruiker Log hier opnieuw in om verder te gaan.

' + LOGIN_MESSAGE: 'Sessie is verlopen' + LOGIN_TITLE: '

U kunt verder met wat u aan het doen was, door opnieuw in te loggen.

' SUCCESS: Succes SUCCESSCONTENT: '

U bent ingelogd. Klik hier als u niet automatisch wordt doorgestuurd.

' + SUCCESS_TITLE: 'Inloggen is gelukt' + SilverStripe\Security\DefaultAdminService: + DefaultAdminFirstname: 'Standaard Beheerder' SilverStripe\Security\Group: AddRole: 'Voeg een rol toe aan deze groep' Code: 'Groep code' DefaultGroupTitleAdministrators: Beheerders DefaultGroupTitleContentAuthors: 'Inhoud Auteurs' Description: 'Omschrijving ' + GROUPNAME: 'Groep naam' GroupReminder: 'Als u de bovenliggende groep selecteert, neemt deze groep alle rollen over' HierarchyPermsError: 'U moet (ADMIN) rechten hebben om de bovenliggende groep "{group}" toe te kennen' Locked: 'Gesloten?' + MEMBERS: Leden + NEWGROUP: 'Nieuwe groep' NoRoles: 'Geen rollen gevonden' + PERMISSIONS: Rechten + PLURALNAME: Groepen + PLURALS: + one: 'Een groep' + other: '{count} groepen' Parent: 'Bovenliggende groep' + ROLES: Rollen + ROLESDESCRIPTION: 'Rollen zijn logische groeperingen van rechten die in het Rollen tabblad gewijzigd kunnen worden.
Rollen worden automatisch overgenomen van bovenliggende groepen.' RolesAddEditLink: 'Rollen beheren' + SINGULARNAME: Groep Sort: Sorteer-richting has_many_Permissions: Rechten many_many_Members: Leden SilverStripe\Security\LoginAttempt: + Email: 'E-mailadres ' + EmailHashed: 'E-mailadres (versleuteld)' IP: 'IP adres' + PLURALNAME: Inlogpogingen + PLURALS: + one: 'Een inlogpoging' + other: '{count} inlogpogingen' + SINGULARNAME: Inlogpogingen Status: Status SilverStripe\Security\Member: ADDGROUP: 'Groep toevoegen' BUTTONCHANGEPASSWORD: 'Wachtwoord veranderen' BUTTONLOGIN: Inloggen BUTTONLOGINOTHER: 'Als iemand anders inloggen' + BUTTONLOGOUT: Uitloggen BUTTONLOSTPASSWORD: 'Ik ben mijn wachtwoord vergeten' CONFIRMNEWPASSWORD: 'Bevestig het nieuwe wachtwoord' CONFIRMPASSWORD: 'Bevestig wachtwoord' + CURRENT_PASSWORD: 'Huidige wachtwoord' + EDIT_PASSWORD: 'Nieuw wachtwoord' EMAIL: E-mail EMPTYNEWPASSWORD: 'Het nieuwe wachtwoord mag niet leeg zijn, probeer opnieuw' ENTEREMAIL: 'Typ uw e-mailadres om een link te ontvangen waarmee u uw wachtwoord kunt resetten.' @@ -128,13 +235,21 @@ nl: ERRORWRONGCRED: 'De ingevulde gegevens lijken niet correct. Probeer het nog een keer.' FIRSTNAME: Voornaam INTERFACELANG: 'Interface taal' + KEEPMESIGNEDIN: 'Houd mij ingelogd' LOGGEDINAS: 'U bent ingelogd als {name}.' NEWPASSWORD: 'Nieuw wachtwoord' PASSWORD: Wachtwoord PASSWORDEXPIRED: 'Uw wachtwoord is verlopen. Kies een nieuw wachtwoord.' + PLURALNAME: Leden + PLURALS: + one: 'Een lid' + other: '{count} leden' + REMEMBERME: 'Onthoud mij voor volgende keer? (voor {count} dagen op dit apparaat)' + SINGULARNAME: Lid SUBJECTPASSWORDCHANGED: 'Uw wachtwoord is veranderd' SUBJECTPASSWORDRESET: 'Link om uw wachtwoord opnieuw aan te maken' SURNAME: Achternaam + VALIDATIONADMINLOSTACCESS: 'Niet mogelijk om alle admin-groepen te verwijderen van uw profiel' ValidationIdentifierFailed: 'Een bestaande gebruiker #{id} kan niet dezelfde unieke velden hebben ({name} = {value}))' WELCOMEBACK: 'Welkom terug, {firstname}' YOUROLDPASSWORD: 'Uw oude wachtwoord' @@ -143,15 +258,38 @@ nl: db_LockedOutUntil: 'Gesloten tot' db_Password: Wachtwoord db_PasswordExpiry: 'Wachtwoord vervaldatum' + SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm: + AUTHENTICATORNAME: Inlogformulier + BUTTONFORGOTPASSWORD: 'Wachtwoord vergeten' + BUTTONLOGIN: 'Opnieuw inloggen' + BUTTONLOGOUT: Uitloggen + SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: + ERRORWRONGCRED: 'De ingevulde gegevens lijken niet correct. Probeer het nog een keer.' + NoPassword: 'Er is geen wachtwoord voor deze gebruiker.' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'E-mail & wachtwoord' + SilverStripe\Security\MemberPassword: + PLURALNAME: Gebruikerswachtwoorden + PLURALS: + one: 'Een gebruikerswachtwoord' + other: '{count} Gebruikerswachtwoorden' + SINGULARNAME: Gebruikerswachtwoord SilverStripe\Security\PasswordValidator: LOWCHARSTRENGTH: 'Maak a.u.b. uw wachtwoord sterker door enkele van de volgende karakters te gebruiken: {chars}' PREVPASSWORD: 'U heeft dit wachtwoord in het verleden al gebruikt, kies a.u.b. een nieuw wachtwoord.' TOOSHORT: 'Het wachtwoord is te kort, het moet minimaal {minimum} karakters hebben' SilverStripe\Security\Permission: AdminGroup: Beheerder + CMS_ACCESS_CATEGORY: 'CMS toegang' CONTENT_CATEGORY: Inhoudsrechten FULLADMINRIGHTS: 'Volledige admin rechten' FULLADMINRIGHTS_HELP: 'Impliceert en overstemt alle andere toegewezen rechten.' + PERMISSIONS_CATEGORY: 'Rollen en toegangsrechten' + PLURALNAME: Rechten + PLURALS: + one: Machtiging + other: '{count} rechten' + SINGULARNAME: Machtiging UserPermissionsIntro: 'Groepen aan deze gebruiker toewijzen zullen diens permissies aanpassen. Zie de sectie Groepen voor meer informatie over machtigingen voor afzonderlijke groepen.' SilverStripe\Security\PermissionCheckboxSetField: AssignedTo: 'toegewezen aan "{title}"' @@ -161,21 +299,37 @@ nl: SilverStripe\Security\PermissionRole: OnlyAdminCanApply: 'Alleen admin kan doorvoeren' PLURALNAME: Rollen + PLURALS: + one: 'Een rol' + other: '{count} rollen' SINGULARNAME: Rol Title: Titel SilverStripe\Security\PermissionRoleCode: + PLURALNAME: 'Permissie codes' + PLURALS: + one: 'Een permissiecode' + other: '{count} permissiecodes' PermsError: 'U moet (ADMIN) rechten hebben om de code "{code}" toe te kennen' + SINGULARNAME: Permissiecode + SilverStripe\Security\RememberLoginHash: + PLURALNAME: 'Versleutelde logins' + PLURALS: + one: 'Een versleutelde login' + other: '{count} versleutelde logins' + SINGULARNAME: 'Versleutelde login' SilverStripe\Security\Security: ALREADYLOGGEDIN: 'U hebt geen toegang tot deze pagina. Als u een andere account met de nodige rechten hebt, kan u hieronder opnieuw inloggen.' BUTTONSEND: 'Nieuw wachtwoord aanmaken' CHANGEPASSWORDBELOW: 'U kunt uw wachtwoord hieronder veranderen.' CHANGEPASSWORDHEADER: 'Verander uw wachtwoord' + CONFIRMLOGOUT: 'Klik op onderstaande knop om uit te loggen.' ENTERNEWPASSWORD: 'Voer een nieuw wachtwoord in.' ERRORPASSWORDPERMISSION: 'U moet ingelogd zijn om uw wachtwoord te kunnen veranderen!' LOGIN: 'Meld aan' + LOGOUT: Uitloggen LOSTPASSWORDHEADER: 'Wachtwoord vergeten' NOTEPAGESECURED: 'Deze pagina is beveiligd. Voer uw gegevens in en u wordt automatisch doorgestuurd.' - NOTERESETLINKINVALID: '

De link om uw wachtwoord te kunnen wijzigen is niet meer geldig.

U kunt een nieuwe link aanvragen of uw wachtwoord aanpassen door in te loggen.

' + NOTERESETLINKINVALID: '

De reset link is ongeldig of komen te vervallen.

Je kan hier een nieuwe link aanvragen of het wachtwoord veranderen nadat je bent ingelogd.

' NOTERESETPASSWORD: 'Voer uw e-mailadres in en we sturen een link waarmee u een nieuw wachtwoord kunt instellen.' - PASSWORDSENTHEADER: 'Wachtwoord herstel link verzonden naar {email}' - PASSWORDSENTTEXT: 'Bedankt! Er is een link verstuurd naar {email} om uw wachtwoord opnieuw in te stellen, in de veronderstelling dat er een account bestaat voor dit e-mailadres.' + PASSWORDRESETSENTHEADER: 'link om uw wachtwoord opnieuw aan te maken' + PASSWORDRESETSENTTEXT: 'Bedankt! Er is een link verstuurd om uw wachtwoord opnieuw in te stellen (mits het mailadres reeds bekend is bij ons).' diff --git a/lang/pl.yml b/lang/pl.yml index 3774d27f3..1905acb38 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -84,7 +84,6 @@ pl: RelationSearch: 'Wyszukiwanie powiązań' ResetFilter: Resetuj SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Usuń Delete: Usuń DeletePermissionsFailure: 'Brak uprawnień do usuwania' EditPermissionsFailure: 'Nie masz uprawnień, aby odłączyć rekord' @@ -96,8 +95,6 @@ pl: DeletePermissionsFailure: 'Brak uprawnień do usuwania' Deleted: 'Usunięto {type} {name}' 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: @@ -352,7 +349,4 @@ pl: 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ę.

' NOTERESETPASSWORD: 'Wpisz adres e-mail, na który mamy wysłać link gdzie możesz zresetować swoje hasło' - PASSWORDSENTHEADER: 'Link resetujący hasła został wysłany do ''{email}''' - PASSWORDSENTTEXT: 'Dziękujemy! Link resetujący hasło został wysłany do ''{email}'', o ile konto użytkownika dla takiego e-maila istnieje.' diff --git a/lang/ru.yml b/lang/ru.yml index 73602e013..add72be7f 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -339,7 +339,4 @@ ru: LOGOUT: Выйти LOSTPASSWORDHEADER: 'Восстановление пароля' NOTEPAGESECURED: 'Эта страница защищена. Пожалуйста, введите свои учетные данные для входа.' - NOTERESETLINKINVALID: '

Неверная ссылка переустановки пароля или время действия ссылки истекло.

Вы можете повторно запросить ссылку, щелкнув здесь, или поменять пароль, войдя в систему.

' NOTERESETPASSWORD: 'Введите Ваш адрес email, и Вам будет отправлена ссылка, по которой Вы сможете переустановить свой пароль' - PASSWORDSENTHEADER: 'Ссылка для переустановки пароля выслана на ''{email}''' - PASSWORDSENTTEXT: 'Ссылка переустановки пароля была выслана на адрес ''{email}'' (письмо дойдет до получателя только в том случае, если аккаунт с таким электронным адресом действительно зарегистрирован).' diff --git a/lang/sk.yml b/lang/sk.yml index 861045fc6..88f4bf260 100644 --- a/lang/sk.yml +++ b/lang/sk.yml @@ -228,7 +228,4 @@ sk: LOGIN: Prihlásiť LOSTPASSWORDHEADER: 'Zabudnuté heslo' NOTEPAGESECURED: 'Táto stránka je zabezpečená. Zadajte svoje prihlasovacie údaje a my Vám zároveň pošleme práva.' - NOTERESETLINKINVALID: '

Odkaz na resetovanie hesla nie je platný alebo je vypršala jeho platnosť.

Môžete požiadať o nový tu alebo zmeňte svoje heslo po prihlásení.

' NOTERESETPASSWORD: 'Zadajte svoju e-mailovú adresu a my Vám pošleme odkaz na resetovanie hesla' - PASSWORDSENTHEADER: 'Odkaz na resetovanie hesla bol odoslaný na ''{email}''' - PASSWORDSENTTEXT: 'Ďakujeme! Resetovací odkaz bol odoslaný na ''''{email}'''', pokiaľ účet existuje pre túto emailovú adresu.' diff --git a/lang/sl.yml b/lang/sl.yml index bccd51826..5496aff5f 100644 --- a/lang/sl.yml +++ b/lang/sl.yml @@ -135,7 +135,4 @@ sl: LOGIN: Prijava LOSTPASSWORDHEADER: 'Izgubljeno geslo' NOTEPAGESECURED: 'Stran je zaščitena. Da bi lahko nadaljevali, vpišite svoje podatke.' - NOTERESETLINKINVALID: '

Povezava za ponastavitev gesla je napačna ali pa je njena veljavnost potekla.

Tukaj lahko zaprosite za novo povezavo or pa zamenjate geslo, ko se prijavite v sistem.

' NOTERESETPASSWORD: 'Vpišite e-naslov, na katerega vam bomo poslali povezavo za ponastavitev gesla' - PASSWORDSENTHEADER: 'Povezava za ponastavitev gesla je bila poslana na e-naslov ''{email}''.' - PASSWORDSENTTEXT: 'Hvala! Povezava za ponastavitev gesla je bila poslana na e-naslov ''{email}'', ki je naveden kot e-naslov vašega računa. ' diff --git a/lang/sr.yml b/lang/sr.yml index 07f642278..622b657b3 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -151,7 +151,4 @@ sr: ERRORPASSWORDPERMISSION: 'Морате да будете пријављени да бисте променили своју лозинку!' LOGIN: Пријављивање NOTEPAGESECURED: 'Ова страна је обезбеђена. Унесите своје податке и ми ћемо вам послати садржај.' - NOTERESETLINKINVALID: '

Линк за ресетовање лозинке је погрешан или је истекло време за његово коришћење.

Можете да захтевате нови овде или да промените Вашу лозинку након што се пријавите.

' NOTERESETPASSWORD: 'Унесите своју адресу е-поште и ми ћемо вам послати линк помоћу којег можете да промените своју лозинку' - PASSWORDSENTHEADER: 'Линк за ресетовање лозинке послат је на адресу е-поште: ''{email}''' - PASSWORDSENTTEXT: 'Хвала Вам! Линк за ресетовање лозинке је послат не адресу е-поште ''{email}''. Порука ће стићи примаоцу само ако постоји регистрован налог са том адресом е-поште.' diff --git a/lang/sr@latin.yml b/lang/sr@latin.yml index 1b210ae51..f91aa9337 100644 --- a/lang/sr@latin.yml +++ b/lang/sr@latin.yml @@ -150,7 +150,4 @@ sr@latin: ERRORPASSWORDPERMISSION: 'Morate da budete prijavljeni da biste promenili svoju lozinku!' LOGIN: Prijavljivanje NOTEPAGESECURED: 'Ova strana je obezbeđena. Unesite svoje podatke i mi ćemo vam poslati sadržaj.' - NOTERESETLINKINVALID: '

Link za resetovanje lozinke je pogrešan ili je isteklo vreme za njegovo korišćenje.

Možete da zahtevate novi ovde ili da promenite Vašu lozinku nakon što se prijavite.

' NOTERESETPASSWORD: 'Unesite svoju adresu e-pošte i mi ćemo vam poslati link pomoću kojeg možete da promenite svoju lozinku' - PASSWORDSENTHEADER: 'Link za resetovanje lozinke poslat je na adresu e-pošte: ''{email}''' - PASSWORDSENTTEXT: 'Hvala Vam! Link za resetovanje lozinke je poslat ne adresu e-pošte ''{email}''. Poruka će stići primaocu samo ako postoji registrovan nalog sa tom adresom e-pošte.' diff --git a/lang/sr_RS.yml b/lang/sr_RS.yml index fc79812da..298049430 100644 --- a/lang/sr_RS.yml +++ b/lang/sr_RS.yml @@ -150,7 +150,4 @@ sr_RS: ERRORPASSWORDPERMISSION: 'Морате да будете пријављени да бисте променили своју лозинку!' LOGIN: Пријављивање NOTEPAGESECURED: 'Ова страна је обезбеђена. Унесите своје податке и ми ћемо вам послати садржај.' - NOTERESETLINKINVALID: '

Линк за ресетовање лозинке је погрешан или је истекло време за његово коришћење.

Можете да захтевате нови овде или да промените Вашу лозинку након што се пријавите.

' NOTERESETPASSWORD: 'Унесите своју адресу е-поште и ми ћемо вам послати линк помоћу којег можете да промените своју лозинку' - PASSWORDSENTHEADER: 'Линк за ресетовање лозинке послат је на адресу е-поште: ''{email}''' - PASSWORDSENTTEXT: 'Хвала Вам! Линк за ресетовање лозинке је послат не адресу е-поште ''{email}''. Порука ће стићи примаоцу само ако постоји регистрован налог са том адресом е-поште.' diff --git a/lang/sr_RS@latin.yml b/lang/sr_RS@latin.yml index f39b36058..453ffa17d 100644 --- a/lang/sr_RS@latin.yml +++ b/lang/sr_RS@latin.yml @@ -151,7 +151,4 @@ sr_RS@latin: ERRORPASSWORDPERMISSION: 'Morate da budete prijavljeni da biste promenili svoju lozinku!' LOGIN: Prijavljivanje NOTEPAGESECURED: 'Ova strana je obezbeđena. Unesite svoje podatke i mi ćemo vam poslati sadržaj.' - NOTERESETLINKINVALID: '

Link za resetovanje lozinke je pogrešan ili je isteklo vreme za njegovo korišćenje.

Možete da zahtevate novi ovde ili da promenite Vašu lozinku nakon što se prijavite.

' NOTERESETPASSWORD: 'Unesite svoju adresu e-pošte i mi ćemo vam poslati link pomoću kojeg možete da promenite svoju lozinku' - PASSWORDSENTHEADER: 'Link za resetovanje lozinke poslat je na adresu e-pošte: ''{email}''' - PASSWORDSENTTEXT: 'Hvala Vam! Link za resetovanje lozinke je poslat ne adresu e-pošte ''{email}''. Poruka će stići primaocu samo ako postoji registrovan nalog sa tom adresom e-pošte.' diff --git a/lang/sv.yml b/lang/sv.yml index f35c2ee99..f2a18d659 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -93,7 +93,12 @@ sv: DeletePermissionsFailure: 'Rättighet för att radera saknas' Deleted: 'Raderade {type} {name}' Save: Spara + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Ändra + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'Du kan inte radera dig själv från den här gruppen, då du då kommer att förlora dina admin-rättigheter' SilverStripe\Forms\GridField\GridFieldPaginator: + OF: av Page: Sida View: Visa SilverStripe\Forms\MoneyField: @@ -108,6 +113,12 @@ sv: VALIDATION: '''{value}'' är inget nummer, bara siffror (utan mellanslag) kan accepteras för det här fältet' SilverStripe\Forms\TimeField: VALIDATEFORMAT: 'Var god att ange tid i ett giltigt format ({format})' + SilverStripe\ORM\DataObject: + PLURALNAME: Dataobjekt + PLURALS: + one: 'Ett dataobjekt' + other: '{count} Dataobjekt' + SINGULARNAME: Dataobjekt SilverStripe\ORM\FieldType\DBBoolean: ANY: 'Vilken som helst' NOANSWER: Nej @@ -136,6 +147,8 @@ sv: other: '{count} år' SilverStripe\ORM\FieldType\DBEnum: ANY: 'Vilken som helst' + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'För många relaterade objekt; använder fallback-fält' SilverStripe\ORM\Hierarchy: LIMITED_TITLE: 'För många barn ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: @@ -221,6 +234,7 @@ sv: PLURALS: one: 'En medlem' other: '{count} medlemmar' + REMEMBERME: 'Kom ihåg mig nästa gång? (i {count} dagar på denna enhet)' SINGULARNAME: Medlem SUBJECTPASSWORDCHANGED: 'Ditt lösenord har ändrats' SUBJECTPASSWORDRESET: 'Din återställningslänk' @@ -288,7 +302,6 @@ sv: LOGOUT: 'Logga ut' LOSTPASSWORDHEADER: 'Bortglömt lösenord' NOTEPAGESECURED: 'Den här sidan är låst. Fyll i dina uppgifter nedan så skickar vi dig vidare.' - NOTERESETLINKINVALID: '

Återställningslänk för lösenord är felaktig eller för gammal.

Du kan begära en ny här eller ändra ditt lösenord när du loggat in.

' NOTERESETPASSWORD: 'Ange din e-postadress så skickar vi en länk med vilken du kan återställa ditt lösenord' - PASSWORDSENTHEADER: 'Återställningslänk för lösenord har skickats till ''{email}''' - PASSWORDSENTTEXT: 'Tack en återställningslänk har skickats till ''{email}'', förutsatt att ett konto med den addressen finns.' + PASSWORDRESETSENTHEADER: 'Återställningslänk för lösenord skickad' + PASSWORDRESETSENTTEXT: 'Tack. En återställningslänk har skickats, förutsatt att ett konto med denna adress existerar.' diff --git a/lang/zh.yml b/lang/zh.yml index 26c8e2893..748ee671c 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -166,7 +166,4 @@ zh: LOGIN: 登录 LOSTPASSWORDHEADER: 忘记密码 NOTEPAGESECURED: 该页面受安全保护。请在下面输入您的证书,然后我们会立即将您引导至该页面。 - NOTERESETLINKINVALID: '

密码重设链接无效或已过期。

您可以在这里 要求一个新的或在登录后更改您的密码。

' NOTERESETPASSWORD: 请输入您的电子邮件地址,然后我们会将一个链接发送给您,您可以用它来重设您的密码 - PASSWORDSENTHEADER: '密码重设链接已发送至''{email}''' - PASSWORDSENTTEXT: '谢谢!复位链接已发送到 ''{email}'',假定此电子邮件地址存在一个帐户。' From a65d313fd79dd5078b26d496f889e31da9054df2 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Wed, 7 Nov 2018 19:12:38 +1300 Subject: [PATCH 057/175] Added 4.2.2 changelog --- docs/en/04_Changelogs/4.2.2.md | 61 ++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/en/04_Changelogs/4.2.2.md diff --git a/docs/en/04_Changelogs/4.2.2.md b/docs/en/04_Changelogs/4.2.2.md new file mode 100644 index 000000000..f0df5cec5 --- /dev/null +++ b/docs/en/04_Changelogs/4.2.2.md @@ -0,0 +1,61 @@ +# 4.2.2 + + + +## Change Log + +### Security + + * 2018-08-21 [0610f76da](https://github.com/silverstripe/silverstripe-framework/commit/0610f76da02ac53a1b51cdfe9eac34e943a66991) Add confirmation token to dev/build (Loz Calver) - See [ss-2018-019](https://www.silverstripe.org/download/security-releases/ss-2018-019) + * 2018-08-12 [909ab03](https://github.com/silverstripe/silverstripe-admin/commit/909ab03fc4e742a05a06c33c5233691fd7466836) Add CSRF to Apollo (Aaron Carlino) - See [ss-2018-007](https://www.silverstripe.org/download/security-releases/ss-2018-007) + * 2018-07-29 [214e28127](https://github.com/silverstripe/silverstripe-framework/commit/214e28127f5425b61c15b69f884afdbad31133c2) Ignore arguments in mysqli::real_connect backtrace calls (Robbie Averill) - See [ss-2018-018](https://www.silverstripe.org/download/security-releases/ss-2018-018) + +### Features and Enhancements + + * 2018-08-24 [2b335b4](https://github.com/silverstripe/silverstripe-graphql/commit/2b335b4239946f9a6fb1d525452cf1fe6d22a9ce) Proof of concept of cached graphql queries (#166) (Damian Mooyman) + * 2018-07-01 [73d3da2](https://github.com/silverstripe/silverstripe-admin/commit/73d3da2bc8566cb1cb5da0124b7deb513728b5ab) Pattern library now has FormAction examples (Robbie Averill) + +### Bugfixes + + * 2018-10-24 [e72fc9e3d](https://github.com/silverstripe/silverstripe-framework/commit/e72fc9e3d0f35a1d43f55f83f9919f67d72fb7cb) DataObject singleton creation (#8516) (Sam Minnée) + * 2018-10-19 [7c65916](https://github.com/silverstripe/silverstripe-asset-admin/commit/7c659167f2eda63d882a097f2f413b9f3cb79e31) Use fixtured file title in test assertion (Robbie Averill) + * 2018-10-17 [d71ee0c](https://github.com/silverstripe/silverstripe-admin/commit/d71ee0ce9898e73c9a7d913356fc6bfe6c2b42fc) Fixes #674 TinyMCE width - this should match form field widths at lower width resolutions but expand up to the max width on wider resolutions (bergice) + * 2018-10-16 [a6a174399](https://github.com/silverstripe/silverstripe-framework/commit/a6a17439976710b2311558d363b5467fa429dcca) Fix `ENTER` not triggering form save button as `GridField`s used `submit` type buttons (bergice) + * 2018-10-14 [c0c446a](https://github.com/silverstripe/silverstripe-versioned/commit/c0c446ad8f29dd66398feb38f5d92fa4f60a4a8b) Fix relations between staged/unstaged objects (Harsh Chokshi) + * 2018-10-09 [f710c5c](https://github.com/silverstripe/silverstripe-admin/commit/f710c5cdcd2cf95fdaa738f55c0f2529fcbe826d) Only hide overflow from inactive chosen fields (Robbie Averill) + * 2018-10-01 [5422e28](https://github.com/silverstripe/silverstripe-asset-admin/commit/5422e28635cec8f285eb422fa85f57f4418c09b8) Folder sort incorrect (Luke Edwards) + * 2018-09-28 [231d6d9a9](https://github.com/silverstripe/silverstripe-framework/commit/231d6d9a9f388e10cf77149aec22e947db648644) New members now receive the configured default locale, not the current locale (Robbie Averill) + * 2018-09-21 [1d5ecd342](https://github.com/silverstripe/silverstripe-framework/commit/1d5ecd342e417b4707a3bbc34e97949bffd14afb) Prevent error on valid response status codes (Damian Mooyman) + * 2018-09-18 [bbe7c66](https://github.com/silverstripe/silverstripe-asset-admin/commit/bbe7c660cf40d4c942eaf6e76755eeaf46c63471) Add `AssetAdmin::getMinimalistObjectFromData()` to build file metadata for UploadField (#829) (Maxime Rainville) + * 2018-09-18 [db63f55fb](https://github.com/silverstripe/silverstripe-framework/commit/db63f55fbb8e635e4e7215b7b7eff4e1f1cb7b22) Changes being detected on TreeMulti as values not sorted (Luke Edwards) + * 2018-09-13 [5c102dec](https://github.com/silverstripe/silverstripe-cms/commit/5c102decbde43395e14aeff83a20c4c6f1d048ae) Improve performance of CMSMain::getArchiveWarningMessage (#2231) (Maxime Rainville) + * 2018-09-10 [8ae0ef0](https://github.com/silverstripe/silverstripe-versioned/commit/8ae0ef0002a229d233f7395cfed15c979c3f1698) Do not update LeftAndMain link with Stage param (#173) (Maxime Rainville) + * 2018-09-03 [1c4311d](https://github.com/silverstripe/silverstripe-asset-admin/commit/1c4311d4e6548600272daa0ce83afa12cf7e99c3) fix description for docs.silverstripe.org (wernerkrauss) + * 2018-09-03 [b922c0d73](https://github.com/silverstripe/silverstripe-framework/commit/b922c0d7327b5d0222dd280afcb64f83a09ea859) Check scheme is truthy before setting it to the request (Robbie Averill) + * 2018-08-31 [68c2c976d](https://github.com/silverstripe/silverstripe-framework/commit/68c2c976d4813607a420ac4cda7b01f0a7aee8c7) Fix alignment test step definition (#8354) (Luke Edwards) + * 2018-08-30 [234b795f8](https://github.com/silverstripe/silverstripe-framework/commit/234b795f89657c6b25da6101a9fc878e3297c301) Use classes for TinyMCE alignment buttons (Luke Edwards) + * 2018-08-28 [d651d0fbf](https://github.com/silverstripe/silverstripe-framework/commit/d651d0fbfcababeaf317b27cb00b4f33b9d99eab) Use base class (not remapping target class) when looking up whether object is versioned (Robbie Averill) + * 2018-08-27 [4da556923](https://github.com/silverstripe/silverstripe-framework/commit/4da5569232505ee574e0b5106ff2116611393aa4) ensure createFromVariables takes correct params on CLIRequestBuilder (Scott Hutchinson) + * 2018-08-27 [f3230c78](https://github.com/silverstripe/silverstripe-reports/commit/f3230c78d4e3731a10a5f4c508bc68c6a8534866) Use requestVar() to include post vars as well as get vars (Robbie Averill) + * 2018-08-23 [f37dd74](https://github.com/silverstripe/silverstripe-admin/commit/f37dd74be7afae5e40e85ce2a90a4d92bf7e80bb) Site tree items do not disappear on save with source file comments enabled (Robbie Averill) + * 2018-08-20 [dbab69669](https://github.com/silverstripe/silverstripe-framework/commit/dbab6966908f0a293ee6d469cec6b4650dc5a0f1) Message when changing password with invalid token now contains correct links to login (Robbie Averill) + * 2018-08-20 [9da7f99](https://github.com/silverstripe/silverstripe-versioned/commit/9da7f991f33ac16070b2e47b764b216a87f96622) Draft content requiring login message now correctly renders HTML link (Robbie Averill) + * 2018-08-17 [c361b09](https://github.com/silverstripe/silverstripe-admin/commit/c361b091b1640c25f1d23914489212fce1e29377) overflow of chosen dropdowns when inactive (Scott Hutchinson) + * 2018-08-16 [66cd3af](https://github.com/silverstripe/silverstripe-admin/commit/66cd3af09fcf68bf177a46ac57434442642d1b7c) Filtering or paginating a gridfield causing a change event (Luke Edwards) + * 2018-08-15 [0db594b2d](https://github.com/silverstripe/silverstripe-framework/commit/0db594b2d39c93dd2e911414bee5520c84048906) Remove double escaping of HTML values in print views (Robbie Averill) + * 2018-08-15 [0c713b5](https://github.com/silverstripe/silverstripe-assets/commit/0c713b5b1eb6a08ac00dcadb187b8b3ef7115fc4) Fix routing for files with dots in filename (Damian Mooyman) + * 2018-08-14 [873873dc3](https://github.com/silverstripe/silverstripe-framework/commit/873873dc303ce2041aa23e365464133a359e1561) Pass request to dummy controller before calling init (Robbie Averill) + * 2018-08-14 [27ac001d5](https://github.com/silverstripe/silverstripe-framework/commit/27ac001d5b27cce4f80ce4b3335c14708b116830) email rendering should not include requirements (Thomas Portelange) + * 2018-08-14 [8ec551e5](https://github.com/silverstripe/silverstripe-cms/commit/8ec551e57b04d00d6897d06c2779557f0ec8109d) Broken "show as list" (#2232) (Maxime Rainville) + * 2018-08-12 [9f5b0086c](https://github.com/silverstripe/silverstripe-framework/commit/9f5b0086cb1a0259c5c87ea205390c5e69dcae90) Paginating a gridfield causing a change event (Luke Edwards) + * 2018-08-10 [d4995f52](https://github.com/silverstripe/silverstripe-cms/commit/d4995f5204f020f75fbddb3e49b944a54be5c6c2) Separating ModelAsController catch-all route to apply after all other configuration (Guy Marriott) + * 2018-08-08 [e14ab99](https://github.com/silverstripe/silverstripe-graphql/commit/e14ab991f5c99cee6b1bdfa18ab07a1e4b40961e) Don't rely on return value of GraphQL scaffolding providers (#171) (Guy Marriott) + * 2018-08-06 [df7396e8](https://github.com/silverstripe/silverstripe-cms/commit/df7396e8845eea7a75e73237de9ee7e4cb6568f6) CMS routes are now run after #coreroutes without re-including itself (Robbie Averill) + * 2018-07-27 [85b4b48fb](https://github.com/silverstripe/silverstripe-framework/commit/85b4b48fb5489cdba4b18cbf510d883986dd61c1) Restore default delete action on GridFieldConfig_RecordEditor (Maxime Rainville) + * 2018-07-27 [0d90cdb05](https://github.com/silverstripe/silverstripe-framework/commit/0d90cdb05d058763e5e52720ab653c5cc391dc3b) Altering ID of authenticator tabs to resolve ID conflict (Guy Marriott) + * 2018-07-26 [fea9ef7](https://github.com/silverstripe/silverstripe-admin/commit/fea9ef7d2a53904086f9fad6eedba7bb307c8578) #579 BUG Ambiguous column RecordID when doing batch actions (Ed Linklater) + * 2018-07-23 [a0487e5](https://github.com/silverstripe/silverstripe-admin/commit/a0487e59fc04af0d15e66d4c2874051288b4e63e) Treat readonly as disabled and fix handling for ui-constructive class (Robbie Averill) + * 2018-07-16 [e1296d48](https://github.com/silverstripe/silverstripe-reports/commit/e1296d4813ac1b677aa7a612ba0ad3b2ba62ccae) Filter var can be returned correctly from get variables as a fallback (Robbie Averill) + * 2018-06-27 [8ccebf8](https://github.com/silverstripe/silverstripe-admin/commit/8ccebf813e95980363a92ec37332d2241327441f) Stop sslink from hijacking anchor plugin (Will Rossiter) + * 2018-05-18 [953153500](https://github.com/silverstripe/silverstripe-framework/commit/953153500d490f5b5abf7283c34242c3b22a855a) Polymorphic relationship class columns have obsolete class names remapped (Robbie Averill) + * 2018-05-08 [97a8f56](https://github.com/silverstripe/silverstripe-admin/commit/97a8f56c43ddb3c77a5bbc452755d44afb9a9472) Add missing focus styles for preview options (fixes silverstripe/silverstripe-framework #2101) (Loz Calver) From 26d404ca4ce1ca3f09d3334d4091995525b6b7f4 Mon Sep 17 00:00:00 2001 From: Vagrant Default User Date: Wed, 7 Nov 2018 21:37:35 +1300 Subject: [PATCH 058/175] Update translations --- lang/ar.yml | 3 - lang/bg.yml | 3 - lang/cs.yml | 3 - lang/da.yml | 325 ++++++++++++++++++++++++++++++++++++++++++- lang/de.yml | 3 - lang/eo.yml | 9 +- lang/es.yml | 3 - lang/et_EE.yml | 3 - lang/fa_IR.yml | 1 - lang/fi.yml | 11 +- lang/fr.yml | 3 - lang/id.yml | 3 - lang/id_ID.yml | 3 - lang/it.yml | 15 +- lang/ja.yml | 3 - lang/lt.yml | 3 - lang/mi.yml | 3 - lang/nb.yml | 3 - lang/nl.yml | 154 +++++++++++++++++++- lang/pl.yml | 202 ++++++++++++++++++++++++++- lang/ru.yml | 3 - lang/sk.yml | 3 - lang/sl.yml | 3 - lang/sr.yml | 3 - lang/sr@latin.yml | 3 - lang/sr_RS.yml | 3 - lang/sr_RS@latin.yml | 3 - lang/sv.yml | 19 ++- lang/zh.yml | 3 - 29 files changed, 716 insertions(+), 83 deletions(-) diff --git a/lang/ar.yml b/lang/ar.yml index 19381589f..518bf1344 100644 --- a/lang/ar.yml +++ b/lang/ar.yml @@ -150,7 +150,4 @@ ar: LOGIN: دخول LOSTPASSWORDHEADER: 'كلمة مرور مفقودة' NOTEPAGESECURED: 'هذه الصفحة محمية بكلمة مرور ، أدخل بيانات دخولك بالأسفل ليتم السماح لك بالوصول للصفحة' - NOTERESETLINKINVALID: "

رابط إعادة تعيين كلمة المرور غير صحيح أو نفذت صلاحيته.

\n

\nيمكنك طلب رابط جديد <\"{a href=\"{link1\"> هنا \n أو تغيير كلمة المرور الخاصة بك بعد <\"{a href=\"{link2\"> تسجيل دخولك.\n

" NOTERESETPASSWORD: 'أدخل بريدك الإلكتروني و سيتم إرسال رابط إعادة تهيئة كلمة المرور ' - PASSWORDSENTHEADER: 'رابط استعادة كلمة المرور تم إرساله إلى ''{بريدك}''' - PASSWORDSENTTEXT: 'شكرا لك! تم إرسال رابط إعادة تعيين إلى ''{بريدك}''، بشرط وجود حساب قائم بالنسبة لعنوان هذا البريد الإلكتروني .' diff --git a/lang/bg.yml b/lang/bg.yml index 3e1eba6a0..241841f1e 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -313,7 +313,4 @@ bg: LOGOUT: Изход LOSTPASSWORDHEADER: 'Забравена парола' NOTEPAGESECURED: 'Тази страница е защитена. Въведете вашите данни по-долу, за да продължите.' - NOTERESETLINKINVALID: '

Връзката за нулиране на парола не е вярна или е просрочена.

Можете да заявите нова тук или да промените паролата си след като влезете.

' NOTERESETPASSWORD: 'Въведете вашият email адрес и ще ви изпратим линк, с който ще можете да смените паролата си' - PASSWORDSENTHEADER: 'Връзка за нулиране на парола беше изпратена на ''{email}''' - PASSWORDSENTTEXT: 'Благодарим ви! Връзка за нулиране на паролата беше изпратен на ''{email}'', ако съществува акаунт с този имейл адрес.' diff --git a/lang/cs.yml b/lang/cs.yml index 4a3cae3ab..759f0268e 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -194,7 +194,4 @@ cs: LOGIN: Přihlásit LOSTPASSWORDHEADER: 'Zapomenuté heslo' NOTEPAGESECURED: 'Tato stránka je zabezpečená. Vložte své přihlašovací údaje a my Vám zároveň pošleme práva.' - NOTERESETLINKINVALID: '

Odkaz na resetování hesla není platný nebo je prošlý.

Můžete požádat o nový zde nebo změňte své heslo až se přihlásíte.

' NOTERESETPASSWORD: 'Zadejte svou e-mailovou adresu a bude vám zaslán nulovací odkaz pro Vaše heslo' - PASSWORDSENTHEADER: 'Odkaz na resetování hesla byl odeslán na ''{email}''' - PASSWORDSENTTEXT: 'Děkujeme! Resetovací odkaz byl odeslán na ''{email}'', pokud účet existuje pro tuto emailovou adresu.' diff --git a/lang/da.yml b/lang/da.yml index 8dc512b23..3fe62969a 100644 --- a/lang/da.yml +++ b/lang/da.yml @@ -1,5 +1,328 @@ da: + SilverStripe\Admin\LeftAndMain: + VersionUnknown: ukendt + SilverStripe\AssetAdmin\Forms\UploadField: + Dimensions: Dimensioner + EDIT: Rediger + EDITINFO: 'Rediger denne fil' + REMOVE: Fjern + SilverStripe\Control\ChangePasswordEmail_ss: + CHANGEPASSWORDFOREMAIL: 'Koden for kontoen med email addressen {email} er ændret. Hvis du ikke har skiftet din kode, så skift venligst din kode ved at klikke på linket herunder' + CHANGEPASSWORDTEXT1: 'Du skiftede dit kodeord for' + CHANGEPASSWORDTEXT3: 'Skift kodeord' + HELLO: Hej + SilverStripe\Control\Email\ForgotPasswordEmail_ss: + HELLO: Hej + TEXT1: 'Her er din' + TEXT2: 'link til at nulstille dit kodeord' + TEXT3: for + SilverStripe\Control\RequestProcessor: + INVALID_REQUEST: 'Ugyldig forespørgsel' + REQUEST_ABORTED: 'Forespørgsel annulleret' + SilverStripe\Core\Manifest\VersionProvider: + VERSIONUNKNOWN: Ukendt + SilverStripe\Forms\CheckboxField: + NOANSWER: Nej + YESANSWER: Ja + SilverStripe\Forms\CheckboxSetField_ss: + NOOPTIONSAVAILABLE: 'Ingen tilgængelige muligheder' + SilverStripe\Forms\ConfirmedPasswordField: + ATLEAST: 'Kodeord skal være mindst {min} tegn lang.' + BETWEEN: 'Kodeord skal være {min} til {max} karakterer lang.' + CURRENT_PASSWORD_ERROR: 'Det nuværende kodeord du har indtastet er ikke korrekt.' + CURRENT_PASSWORD_MISSING: 'Du skal indtaste dit nuværende kodeord.' + LOGGED_IN_ERROR: 'Du skal være logget ind for at skifte dit kodeord.' + MAXIMUM: 'Kodeord må maks være {max} tegn lang' + SHOWONCLICKTITLE: 'Skift kodeord' + SilverStripe\Forms\CurrencyField: + CURRENCYSYMBOL: DKK + SilverStripe\Forms\DateField: + VALIDDATEFORMAT2: 'Indtats venligst et gyldigt datoformat ({format})' + VALIDDATEMAXDATE: 'Din dato skal være ældre end eller matche den maksimalt tilladte dato ({date})' + VALIDDATEMINDATE: 'Din dato skal være yngre end eller matche den minimum tilladte dato ({date})' + SilverStripe\Forms\DatetimeField: + VALIDDATEMAXDATETIME: 'Din dato skal være ældre end eller matche den maksimalt tilladte dato ({datetime})' + VALIDDATETIMEFORMAT: 'Indtats venligst et gyldigt dato- og tidsformat ({format})' + VALIDDATETIMEMINDATE: 'Din dato skal være yngre end eller matche den minimum tilladte dato og tid ({datetime})' + SilverStripe\Forms\DropdownField: + CHOOSE: (Vælg) + CHOOSE_MODEL: '(Vælg {name})' + SOURCE_VALIDATION: 'Venligst vælg en eksisterende værdi fra listen. {value} er ikke en tilladt mulighed' + SilverStripe\Forms\EmailField: + VALIDATION: 'Indtast venligst en emailadresse' + SilverStripe\Forms\FileUploadReceiver: + FIELDNOTSET: 'Fil information ikke fundet' + SilverStripe\Forms\Form: + BAD_METHOD: 'Denne form kræver en {method} indsendelse' + CSRF_EXPIRED_MESSAGE: 'Din session er udløbet. Venligst gensend formularen.' + CSRF_FAILED_MESSAGE: 'Det ser ud til der har været et teknisk problem. Klik venligst på tilbageknappen, tryk opdater i din browser og prøv igen.' + VALIDATIONPASSWORDSDONTMATCH: 'Kodeordene er ikke identiske' + VALIDATIONPASSWORDSNOTEMPTY: 'Kodeord kan ikke være tomme' + VALIDATIONSTRONGPASSWORD: 'Kodeord skal mindst have et tal og et alfanumerisk tegn' + VALIDATOR: Validering + VALIDCURRENCY: 'Indtast venligst en gyldig valuta' + SilverStripe\Forms\FormField: + EXAMPLE: 'f.eks. {format}' + NONE: ingen + SilverStripe\Forms\FormScaffolder: + TABMAIN: Primær SilverStripe\Forms\GridField\GridField: - Filter: Filter + Add: 'Tilføj {name}' + CSVEXPORT: 'Eksporter til CSV' + CSVIMPORT: 'Importer CSV' + Filter: Filtrer + FilterBy: 'Filtrer på' + Find: Find + LinkExisting: 'Link eksisterende' + NewRecord: 'Ny {type}' + NoItemsFound: 'Ingen elementer fundet' + PRINTEDAT: 'Printet d.' + PRINTEDBY: 'Printet af' + PlaceHolder: 'Find {type}' + PlaceHolderWithLabels: 'Find {type} på {name}' + Print: Print + RelationSearch: Relationssøgning + ResetFilter: Nulstil + SilverStripe\Forms\GridField\GridFieldDeleteAction: + Delete: Slet + DeletePermissionsFailure: 'Ingen slette rettigheder' + EditPermissionsFailure: 'Ingen rettighed til at fjerne emnet' + UnlinkRelation: Fjern + SilverStripe\Forms\GridField\GridFieldDetailForm: + CancelBtn: Annuller + Create: Opret + Delete: Slet + DeletePermissionsFailure: 'Ingen slette rettigheder' + Deleted: 'Slet {type} {name}' + Save: Gem + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Rediger + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'Kan ikke fjerne dig selv fra denne gruppe, du vil miste administrator rettigheder' + SilverStripe\Forms\GridField\GridFieldPaginator: + OF: af + Page: Side + View: Vis + SilverStripe\Forms\MoneyField: + FIELDLABELAMOUNT: Beløb + FIELDLABELCURRENCY: Valuta + INVALID_CURRENCY: 'Valuta {currency} er ikke i listen over tilladte valutaer' + SilverStripe\Forms\MultiSelectField: + SOURCE_VALIDATION: 'Vælg venligst eksisterende værdier fra listen. Ugyldig mulighed(er) {value} valgt' + SilverStripe\Forms\NullableField: + IsNullLabel: 'Er Null' + SilverStripe\Forms\NumericField: + VALIDATION: '''{value}'' er ikke et tal, kun tal accepteres i dette felt' + SilverStripe\Forms\TimeField: + VALIDATEFORMAT: 'Indtats venligst et gyldigt tidsformat ({format})' + SilverStripe\ORM\DataObject: + PLURALNAME: Dataobjekter + PLURALS: + one: 'Et dataobjekt' + other: '{count} dataobjekter' + SINGULARNAME: Dataobjekt + SilverStripe\ORM\FieldType\DBBoolean: + ANY: Enhver + NOANSWER: Nej + YESANSWER: Ja + SilverStripe\ORM\FieldType\DBDate: + DAYS_SHORT_PLURALS: + one: '{count} dag' + other: '{count} dage' + HOURS_SHORT_PLURALS: + one: '{count} time' + other: '{count} timer' + LessThanMinuteAgo: 'mindre end et minut' + MINUTES_SHORT_PLURALS: + one: '{count} minut' + other: '{count} minutter' + MONTHS_SHORT_PLURALS: + one: '{count} måned' + other: '{count} måneder' + SECONDS_SHORT_PLURALS: + one: '{count} sekund' + other: '{count} sekunder' + TIMEDIFFAGO: '{difference} siden' + TIMEDIFFIN: 'i {difference}' + YEARS_SHORT_PLURALS: + one: '{count} år' + other: '{count} år' + SilverStripe\ORM\FieldType\DBEnum: + ANY: Enhver + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'For mange relaterede objekter; fallback felt i brug' + SilverStripe\ORM\Hierarchy: + LIMITED_TITLE: 'For mange underelementer ({count})' + SilverStripe\ORM\Hierarchy\Hierarchy: + InfiniteLoopNotAllowed: 'Uendeligt løkke fundet i "{type}" hierarkiet. Ændre venligst det overliggende element for at løse dette' + LIMITED_TITLE: 'For mange underelementer ({count})' + SilverStripe\ORM\ValidationException: + DEFAULT_ERROR: Valideringsfejl + SilverStripe\Security\BasicAuth: + ENTERINFO: 'Indtast venligst et brugernavn og kodeord.' + ERRORNOTADMIN: 'Den bruger er ikke en administrator.' + ERRORNOTREC: 'Brugernavn / kodeord kunne ikke genkendes' + SilverStripe\Security\CMSMemberLoginForm: + PASSWORDEXPIRED: '

Dit kodeord er udløbet. Vælg venligst et nyt.

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

Ugyldig bruger. Log venligst ind igen her for at fortsætte.

' + LOGIN_MESSAGE: '

Din session er løbet ud pga. inaktivitet

' + LOGIN_TITLE: 'Log ind igen, for at fortsætte hvor du slap.' + SUCCESS: Succes + SUCCESSCONTENT: '

Logget ind. Hvis du ikke automatisk viderestilles så klik her

' + SUCCESS_TITLE: 'Logget ind med sucess' + SilverStripe\Security\DefaultAdminService: + DefaultAdminFirstname: 'Standard admin' + SilverStripe\Security\Group: + AddRole: 'Tilføj en rolle for denne gruppe' + Code: 'Gruppe kode' + DefaultGroupTitleAdministrators: Administratorer + DefaultGroupTitleContentAuthors: Indholdsforfattere + Description: Beskrivelse + GROUPNAME: Gruppenavn + GroupReminder: 'Hvis du vælger en overliggende gruppe, får denne gruppe alle dens roller' + HierarchyPermsError: 'Kan ikke tildele overliggende gruppe "{group}" med fortrinsrettigheder (kræver ADMIN adgang)' + Locked: 'Låst?' + MEMBERS: Brugere + NEWGROUP: 'Ny gruppe' + NoRoles: 'Ingen roller fundet' + PERMISSIONS: Rettigheder + PLURALNAME: Grupper + PLURALS: + one: 'En gruppe' + other: '{count} grupper' + Parent: 'Overliggende gruppe' + ROLES: Roller + ROLESDESCRIPTION: 'Roller er et prædefineret sæt af rettigheder, som kan tildeles grupper.
De bliver nedarvet fra en overliggende grupper hvis krævet.' + RolesAddEditLink: 'Administrer roller' + SINGULARNAME: Gruppe + Sort: Sortering + has_many_Permissions: Rettigheder + many_many_Members: Brugere + SilverStripe\Security\LoginAttempt: + Email: 'Email adresse' + EmailHashed: 'Email adresse (hashed)' + IP: 'IP addresse' + PLURALNAME: Loginforsøg + PLURALS: + one: 'Et loginforsøg' + other: '{count} loginforsøg' + SINGULARNAME: 'Login forsøg' + Status: Status + SilverStripe\Security\Member: + ADDGROUP: 'Tilføj gruppe' + BUTTONCHANGEPASSWORD: 'Skift kodeord' + BUTTONLOGIN: 'Log ind' + BUTTONLOGINOTHER: 'Log ind med en anden bruger' + BUTTONLOGOUT: 'Log ud' + BUTTONLOSTPASSWORD: 'Jeg har glemt mit kodeord' + CONFIRMNEWPASSWORD: 'Bekræft nyt kodeord' + CONFIRMPASSWORD: 'Bekræft kodeord' + CURRENT_PASSWORD: 'Nuværende kodeord' + EDIT_PASSWORD: 'Nyt kodeord' + EMAIL: Email + EMPTYNEWPASSWORD: 'Det nye kodeord kan ikke være tom, prøv venligst igen' + ENTEREMAIL: 'Indtast venligst en email adresse for at få et nulstillingslink.' + ERRORLOCKEDOUT2: 'Din konto er blevet midlertidigt deaktiveret pga. for mange fejlslagne loginforsøg. Forsøg venligst igen om {count} minutter.' + ERRORNEWPASSWORD: 'Du har indtastet dit nye kodeord forskelligt, forsøg igen' + ERRORPASSWORDNOTMATCH: 'Dit nuværende kodeord matcher ikke, forsøg venligst igen' + ERRORWRONGCRED: 'De indtastede værdier ser ikke ud til at være korrekte. Forsøg venligst igen.' + FIRSTNAME: Fornavn + INTERFACELANG: 'Sprog i brugerfladen' + KEEPMESIGNEDIN: 'Hold mig logget ind' + LOGGEDINAS: 'Du er logget ind som {name}.' + NEWPASSWORD: 'Nyt kodeord' + PASSWORD: Kodeord + PASSWORDEXPIRED: 'Dit kodeord er udløbet. Vælg venligst et nyt.' + PLURALNAME: Brugere + PLURALS: + one: 'En bruger' + other: '{count} brugere' + REMEMBERME: 'Husk mig til næste gang? (i {count} dage på denne enhed)' + SINGULARNAME: Bruger + SUBJECTPASSWORDCHANGED: 'Dit kodeord er blevet ændret' + SUBJECTPASSWORDRESET: 'Link til at nulstille dit kodeord' + SURNAME: Efternavn + VALIDATIONADMINLOSTACCESS: 'Kan ikke fjerne alle admin grupper fra din profil' + ValidationIdentifierFailed: 'Kan ikke overskrive eksisterende bruger #{id} med identisk identifikator ({name} = {value}))' + WELCOMEBACK: 'Velkommen tilbage, {firstname}' + YOUROLDPASSWORD: 'Dit gamle kodeord' + belongs_many_many_Groups: Grupper + db_Locale: 'Sprog i brugerfladen' + db_LockedOutUntil: 'Låst ude indtil' + db_Password: Kodeord + db_PasswordExpiry: Kodeordsudløbsdato + SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm: + AUTHENTICATORNAME: 'CMS bruger loginform' + BUTTONFORGOTPASSWORD: 'Glemt kodeord' + BUTTONLOGIN: 'Log mig ind igen' + BUTTONLOGOUT: 'Log ud' + SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: + ERRORWRONGCRED: 'De indtastede værdier ser ikke ud til at være korrekte. Forsøg venligst igen.' + NoPassword: 'Der er ikke en kode på denne bruger.' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'Email og kodeord' + SilverStripe\Security\MemberPassword: + PLURALNAME: 'Bruger kodeord' + PLURALS: + one: 'Et bruger kodeord' + other: '{count} bruger kodeord' + SINGULARNAME: 'Bruger kodeord' + SilverStripe\Security\PasswordValidator: + LOWCHARSTRENGTH: 'Forøg venligst kodeordets styrke, ved at tilføje nogle af følgende tegn: {chars}' + PREVPASSWORD: 'Du har tidligere brugt dette kodeord, vælg venligst et nyt kodeord' + TOOSHORT: 'Kodeordet er for kort, det skal mindst være {minimum} eller flere tegn langt' SilverStripe\Security\Permission: + AdminGroup: Administrator + CMS_ACCESS_CATEGORY: 'CMS Adgang' CONTENT_CATEGORY: Indholdsrettigheder + FULLADMINRIGHTS: 'Fuld administrator rettighed' + FULLADMINRIGHTS_HELP: 'Indebærer og overskriver alle andre tildelte rettigheder.' + PERMISSIONS_CATEGORY: 'Roller og adgangsrettigheder' + PLURALNAME: Rettigheder + PLURALS: + one: 'En rettighed' + other: '{count} rettigheder' + SINGULARNAME: Rettighed + UserPermissionsIntro: 'Tildeling af grupper til denne bruger, ændrer de rettigheder brugeren har. Se gruppe området for rettigheds detaljer på de individuelle grupper.' + SilverStripe\Security\PermissionCheckboxSetField: + AssignedTo: 'tildelt til "{title}"' + FromGroup: 'nedarvet fra gruppen "{title}"' + FromRole: 'nedarvet fra rollen "{title}"' + FromRoleOnGroup: 'nedarvet fra rollen "{roletitle}" på gruppen "{grouptitle}"' + SilverStripe\Security\PermissionRole: + OnlyAdminCanApply: 'Kun administratorer kan tilføje' + PLURALNAME: Roller + PLURALS: + one: 'En rolle' + other: '{count} roller' + SINGULARNAME: Rolle + Title: Titel + SilverStripe\Security\PermissionRoleCode: + PLURALNAME: 'Rettigheds rolle koder' + PLURALS: + one: 'En rettigheds rolle kode' + other: '{count} rettigheds rolle koder' + PermsError: 'Kan ikke tildele koden "{code}" med fortrinsrettigheder (kræver ADMIN adgang)' + SINGULARNAME: 'Rettighed rolle kode' + SilverStripe\Security\RememberLoginHash: + PLURALNAME: 'Login hashes' + PLURALS: + one: 'Et login hash' + other: '{count} Login Hashes' + SINGULARNAME: 'Login hash' + SilverStripe\Security\Security: + ALREADYLOGGEDIN: 'Du har ikke adgang til denne side. Hvis du har en anden bruger der har adgang til denne side, kan du logge ind med denne herunder.' + BUTTONSEND: 'Send mig linket til at nulstille kodeordet' + CHANGEPASSWORDBELOW: 'Du kan ændre dit kodeord herunder.' + CHANGEPASSWORDHEADER: 'Skift dit kodeord' + CONFIRMLOGOUT: 'Klik venligst på knappen herunder, for at bekræfte at du vil logge ud.' + ENTERNEWPASSWORD: 'Indtast venligst et nyt kodeord.' + ERRORPASSWORDPERMISSION: 'Du skal være logget ind, for at kunne ændre dit kodeord!' + LOGIN: 'Log ind' + LOGOUT: 'Log ud' + LOSTPASSWORDHEADER: 'Glemt kodeord' + NOTEPAGESECURED: 'Denne side er beskyttet. Indtast dine loginoplysninger herunder for at få adgang.' + NOTERESETPASSWORD: 'Indtast din email adresse, så sender vi dig et link som du kan nulstille dit kodeord med' + PASSWORDRESETSENTHEADER: 'link til at nulstille kodeord afsendt' + PASSWORDRESETSENTTEXT: 'Tak for det. Et link til at nulstille dit kodeord er afsendt, hvis der findes en bruger med denne email adresse.' diff --git a/lang/de.yml b/lang/de.yml index 1e9f16e5e..77e2f1584 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -190,7 +190,4 @@ de: LOGIN: Anmelden LOSTPASSWORDHEADER: 'Passwort vergessen' NOTEPAGESECURED: 'Diese Seite ist geschützt. Bitte melden Sie sich an und Sie werden sofort weitergeleitet.' - NOTERESETLINKINVALID: '

Der Link zum Zurücksetzen des Passworts ist entweder nicht korrekt oder abgelaufen

Sie können einen neuen Link anfordern oder Ihr Passwort nach dem einloggen ändern.

' NOTERESETPASSWORD: 'Geben Sie Ihre E-Mail-Adresse ein und wir werden Ihnen einen Link zuschicken, mit dem Sie Ihr Passwort zurücksetzen können.' - PASSWORDSENTHEADER: 'Der Link zum Zurücksetzen des Passworts wurde an ''{email}'' gesendet' - PASSWORDSENTTEXT: 'Vielen Dank! Wenn ein Account zu der E-Mail Adresse ''{email}'' existiert, wurde eine E-Mail mit dem Link zum Zurücksetzen des Passworts verschickt.' diff --git a/lang/eo.yml b/lang/eo.yml index 59806b953..fb6b74ca9 100644 --- a/lang/eo.yml +++ b/lang/eo.yml @@ -95,6 +95,8 @@ eo: DeletePermissionsFailure: 'Mankas permeso forigi' Deleted: 'Forigita {type} {name}' Save: Konservi + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Redakti SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: UnlinkSelfFailure: 'Ne povas forigi vin el ĉi tiu grupo; vi perdus administrajn rajtojn' SilverStripe\Forms\GridField\GridFieldPaginator: @@ -147,6 +149,8 @@ eo: other: '{count} jaroj' SilverStripe\ORM\FieldType\DBEnum: ANY: Ajna + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'Tro multaj objektoj; retropaŝa kampo uzata' SilverStripe\ORM\Hierarchy: LIMITED_TITLE: 'Tro da idoj ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: @@ -319,7 +323,6 @@ eo: LOGOUT: Elsaluti LOSTPASSWORDHEADER: 'Perdis pasvorton' NOTEPAGESECURED: 'Tiu paĝo estas sekurigita. Enigu viajn akreditaĵojn sube kaj vi aliros pluen.' - NOTERESETLINKINVALID: '

La pasvorta reagorda ligilo estas malvalida aŭ finiĝis.

Vi povas peti novan ĉi tie aŭ ŝanĝi vian pasvorton post vi ensalutis.

' NOTERESETPASSWORD: 'Enigu vian retpoŝtan adreson kaj ni sendos al vi ligilon per kiu vi povas reagordi vian pasvorton' - PASSWORDSENTHEADER: 'Pasvorta reagorda ligilo sendiĝis al ''{email}''' - PASSWORDSENTTEXT: 'Dankon! Reagordita ligilo sendiĝis al ''{email}'', kondiĉe ke konto ekzistas por tiu retadreso.' + PASSWORDRESETSENTHEADER: 'Pasvorta reagorda ligilo sendiĝis' + PASSWORDRESETSENTTEXT: 'Dankon. Reagorda ligilo sendiĝis, kondiĉe ke konto ekzistas por ĉi tiu retadreso.' diff --git a/lang/es.yml b/lang/es.yml index 0009c7a4a..debd4b4a7 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -249,7 +249,4 @@ es: LOGIN: Entrar LOSTPASSWORDHEADER: '¿Contraseña Perdida?' NOTEPAGESECURED: 'Esa página está protegida. Introduzca sus datos de acreditación a continuación y lo enviaremos a ella en un momento.' - NOTERESETLINKINVALID: '

El enlace para restablecer la contraseña es inválido o ha expirado.

Usted puede solicitar uno nuevo aqui o cambiar su contraseña después de que se haya conectado.

' NOTERESETPASSWORD: 'Introduzca su dirección de e-mail, y le enviaremos un enlace, con el cual podrá restaurar su contraseña' - PASSWORDSENTHEADER: 'Un enlace para restablecer la contraseña ha sido enviado a ''{email}''' - PASSWORDSENTTEXT: 'Gracias! Un enlace para restablecer la contraseña ha sido enviado a ''{email}'', siempre que una cuenta exista para la dirección de email indicada.' diff --git a/lang/et_EE.yml b/lang/et_EE.yml index 63fc4cd5d..a8fb3b686 100644 --- a/lang/et_EE.yml +++ b/lang/et_EE.yml @@ -139,7 +139,4 @@ et_EE: ERRORPASSWORDPERMISSION: 'Pead olema sisseloginud, et parooli muuta!' LOGIN: 'Logi sisse' NOTEPAGESECURED: 'See leht on turvatud. Sisesta enda andmed allpool ja me saadame sind otse edasi' - NOTERESETLINKINVALID: '

Parooli lähtestamise link on kehtetu või aegunud.

Saate taotleda uut linki siin või muuta parooli pärast sisselogimist.

' NOTERESETPASSWORD: 'Sisesta oma email ja me saadame sulle lingi kus saad oma parooli tühistada.' - PASSWORDSENTHEADER: 'Parooli lähtestamise link saadeti aadressile ''{email}''' - PASSWORDSENTTEXT: 'Aitäh! Lähtestamislink saadeti aadressile ''{email}'' eeldusel, et selle e-posti aadressiga seotud konto on olemas.' diff --git a/lang/fa_IR.yml b/lang/fa_IR.yml index b292b25d7..b9aa0ebea 100644 --- a/lang/fa_IR.yml +++ b/lang/fa_IR.yml @@ -168,4 +168,3 @@ fa_IR: ERRORPASSWORDPERMISSION: 'جهت تغییر گذرواژه خود باید وارد شده باشید!' LOGIN: ورود LOSTPASSWORDHEADER: 'فراموشی گذرواژه' - PASSWORDSENTHEADER: 'لینک ازنوسازی گذرواژه به ''{email}'' ارسال شد' diff --git a/lang/fi.yml b/lang/fi.yml index 364762d3a..7f8579e7f 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -95,6 +95,8 @@ fi: DeletePermissionsFailure: 'Ei oikeuksia poistamiseen' Deleted: 'Poistettiin {type} {name}' Save: Tallenna + SilverStripe\Forms\GridField\GridFieldEditButton: + 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: @@ -147,6 +149,8 @@ fi: other: '{count} vuotta' SilverStripe\ORM\FieldType\DBEnum: ANY: Yhtään + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'Liian monta samaan liittyvää objektia: oletuskenttä käytössä' SilverStripe\ORM\Hierarchy: LIMITED_TITLE: 'Liian monta lapsiobjektia ({count}}' SilverStripe\ORM\Hierarchy\Hierarchy: @@ -197,6 +201,7 @@ fi: many_many_Members: Jäsenet SilverStripe\Security\LoginAttempt: Email: Sähköpostiosoite + EmailHashed: 'Sähköpostiosoite (tiivistetty)' IP: IP-osoite PLURALNAME: Kirjautumisyritykset PLURALS: @@ -255,6 +260,8 @@ fi: SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: ERRORWRONGCRED: 'Antamasi tiedot eivät näytä oikeilta. Yritä uudelleen.' NoPassword: 'Tällä käyttäjällä ei ole salasanaa' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'Sähköpostiosoite & salasana' SilverStripe\Security\MemberPassword: PLURALNAME: 'Käyttäjän salasanat' PLURALS: @@ -318,5 +325,5 @@ fi: NOTEPAGESECURED: 'Tämä sivu on suojattu. Syötä tunnistetietosi alle niin pääset eteenpäin.' NOTERESETLINKINVALID: '

Salasanan palautuslinkki on virheellinen tai vanhentunut.

Voit pyytää uuden napsauttamalla tästä tai vaihtaa salasanasi kirjautumisen jälkeen.

' NOTERESETPASSWORD: 'Syötä sähköpostiosoitteesi ja lähetämme sinulle linkin, jonka avulla saat palautettua salasanasi' - PASSWORDSENTHEADER: 'Salasanan palautuslinkki lähetettiin osoitteeseen ''{email}''' - PASSWORDSENTTEXT: 'Kiitos! Salasanan palautuslinkki lähetettiin osoitteeseen ''{email}'', joka on liitettynä tähän käyttäjätiliin.' + PASSWORDRESETSENTHEADER: 'Salasanan palautuslinkki lähetetty' + PASSWORDRESETSENTTEXT: 'Kiitos, palautuslinkki on lähetetty käyttäjätilille asetettuun sähköpostiosoitteeseen.' diff --git a/lang/fr.yml b/lang/fr.yml index 15b6dab42..450cfdb87 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -319,7 +319,4 @@ fr: LOGOUT: 'Se déconnecter' LOSTPASSWORDHEADER: 'Mot de passe oublié' NOTEPAGESECURED: 'Cette page est sécurisée. Entrez vos identifiants ci-dessous et vous pourrez y avoir accès.' - NOTERESETLINKINVALID: '

Le lien de réinitialisation du mot de passe n’est pas valide ou a expiré.

Vous pouvez en demander un nouveau en suivant ce lien ou changer de mot de passe après connexion.

' NOTERESETPASSWORD: 'Entrez votre adresse email et nous vous enverrons un lien pour modifier votre mot de passe' - PASSWORDSENTHEADER: "Lien de réinitialisation de mot de passe envoyé à «\_{email}\_»" - PASSWORDSENTTEXT: "Merci\_! Un lien de réinitialisation vient d’être envoyé à «\_{email}\_», à condition que cette adresse existe." diff --git a/lang/id.yml b/lang/id.yml index d17eac338..c9c06bbff 100644 --- a/lang/id.yml +++ b/lang/id.yml @@ -167,7 +167,4 @@ id: LOGIN: Masuk LOSTPASSWORDHEADER: 'Kata Kunci yang Terlupa' NOTEPAGESECURED: 'Laman ini diamankan. Isikan data berikut untuk dikirimkan hak akses Anda.' - NOTERESETLINKINVALID: '

Tautan penggantian kata kunci tidak valid atau sudah kadaluarsa.

Anda dapat meminta yang baru di sini atau mengganti kata kunci setelah Anda masuk.

' NOTERESETPASSWORD: 'Isikan alamat email Anda untuk mendapatkan tautan penggantian kata kunci' - PASSWORDSENTHEADER: 'Tautan penggantian kata kunci dikirimkan ke ''{email}''' - PASSWORDSENTTEXT: 'Terimakasih! Tautan reset telah dikirim ke ''{email}'', berisi informasi akun untuk alamat email ini.' diff --git a/lang/id_ID.yml b/lang/id_ID.yml index 22ebcb2e5..11d1f2bb3 100644 --- a/lang/id_ID.yml +++ b/lang/id_ID.yml @@ -166,7 +166,4 @@ id_ID: LOGIN: Masuk LOSTPASSWORDHEADER: 'Kata Kunci yang Terlupa' NOTEPAGESECURED: 'Laman ini diamankan. Isikan data berikut untuk dikirimkan hak akses Anda.' - NOTERESETLINKINVALID: '

Tautan penggantian kata kunci tidak valid atau sudah kadaluarsa.

Anda dapat meminta yang baru di sini atau mengganti kata kunci setelah Anda masuk.

' NOTERESETPASSWORD: 'Isikan alamat email Anda untuk mendapatkan tautan penggantian kata kunci' - PASSWORDSENTHEADER: 'Tautan penggantian kata kunci dikirimkan ke ''{email}''' - PASSWORDSENTTEXT: 'Terimakasih! Tautan reset telah dikirim ke ''{email}'', berisi informasi akun untuk alamat email ini.' diff --git a/lang/it.yml b/lang/it.yml index 05bf5dc83..4089faf9b 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -95,6 +95,10 @@ it: DeletePermissionsFailure: 'Non hai i permessi per eliminare' Deleted: 'Eliminato {type} {name}' Save: Salva + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Modifica + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'Non è possibile rimuovere te stesso da questo gruppo, perderesti i diritti di admin' SilverStripe\Forms\GridField\GridFieldPaginator: OF: di Page: Pagina @@ -145,6 +149,8 @@ it: other: '{count} anni' SilverStripe\ORM\FieldType\DBEnum: ANY: Qualsiasi + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'Troppi oggetti correlati; campo di fallback in uso' SilverStripe\ORM\Hierarchy: LIMITED_TITLE: 'Troppi figli ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: @@ -195,6 +201,7 @@ it: many_many_Members: Membri SilverStripe\Security\LoginAttempt: Email: 'Indirizzo e-mail' + EmailHashed: 'Indirizzo email (hash)' IP: 'Indirizzo IP' PLURALNAME: 'Tentativi d''accesso' PLURALS: @@ -236,6 +243,7 @@ it: SUBJECTPASSWORDCHANGED: 'La tua password è stata cambiata' SUBJECTPASSWORDRESET: 'Link per azzerare la tua password' SURNAME: Cognome + VALIDATIONADMINLOSTACCESS: 'Non è possibile rimuovere tutti i gruppi admin dal tuo profilo' ValidationIdentifierFailed: 'Non posso sovrascrivere l''utente esistente #{id} con identificatore identico ({name} = {value}))' WELCOMEBACK: 'Bentornato, {firstname}' YOUROLDPASSWORD: 'La tua vecchia password' @@ -252,6 +260,8 @@ it: SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: ERRORWRONGCRED: 'I dettagli forniti non sembrano corretti. Per favore riprovare.' NoPassword: 'Manca la password per questo utente.' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'E-mail & Password' SilverStripe\Security\MemberPassword: PLURALNAME: 'Password utenti' PLURALS: @@ -313,7 +323,6 @@ it: LOGOUT: Scollegati LOSTPASSWORDHEADER: 'Password smarrita' NOTEPAGESECURED: 'La pagina è protetta. Inserisci le credenziali qui sotto per poter andare avanti.' - NOTERESETLINKINVALID: '

Il link per azzerare la password non è valido o è scaduto.

Puoi richiederne uno nuovo qui o cambiare la tua password dopo che ti sei connesso.

' NOTERESETPASSWORD: 'Inserisci il tuo indirizzo e-mail e ti verrà inviato un link per poter azzerare la tua password.' - PASSWORDSENTHEADER: 'Link per azzeramento della password inviato a ''{email}''' - PASSWORDSENTTEXT: 'Grazie! Un link di azzeramento è stato inviato a ''{email}'', fornito un account esistente per questo indirizzo e-mail.' + PASSWORDRESETSENTHEADER: 'Link di azzeramento password inviato' + PASSWORDRESETSENTTEXT: 'Grazie! Un link di azzeramento è stato inviato, supponendo un account esista a quell''indirizzo e-mail.' diff --git a/lang/ja.yml b/lang/ja.yml index b2f92eb1e..433a832e2 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -146,7 +146,4 @@ ja: ERRORPASSWORDPERMISSION: パスワードを変更する為に、ログインしなければなりません! LOGIN: ログイン NOTEPAGESECURED: このページはセキュリティで保護されております証明書キーを下記に入力してください。こちらからすぐに送信します - NOTERESETLINKINVALID: '

パスワードのリセットリンクは有効でないか期限切れです。

新しいパスワードを要求することができます ここ もしくはパスワードを変更することができます ログインした後 .

' NOTERESETPASSWORD: メールアドレスを入力してください、パスワードをリセットするURLを送信致します - PASSWORDSENTHEADER: 'パスワードリセットリンクは ''{email}'' に送信されました' - PASSWORDSENTTEXT: 'ありがとうございました! リセットリンクは、''{email}'' に、このアカウントが存在することを前提として送信されました。' diff --git a/lang/lt.yml b/lang/lt.yml index abdbaea25..3ca7adbff 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -167,7 +167,4 @@ lt: LOGIN: Prisijungti LOSTPASSWORDHEADER: 'Slaptažodžio atstatymas' NOTEPAGESECURED: 'Šis puslapis yra apsaugotas. Įveskite savo duomenis į žemiau esančius laukelius.' - NOTERESETLINKINVALID: '

Neteisinga arba negaliojanti slaptažodžio atstatymo nuoroda.

Galite atsisiųsti naują čia arba pasikeisti slaptažodį po to, kai prisijungsite.

' NOTERESETPASSWORD: 'Įveskite savo e. pašto adresą ir atsiųsime slaptažodžio atstatymui skirtą nuorodą' - PASSWORDSENTHEADER: 'Slaptažodžio atstatymo nuoroda nusiųsta į ''{email}''' - PASSWORDSENTTEXT: 'Atstatymo nuoroda nusiųsta į ''{email}''' diff --git a/lang/mi.yml b/lang/mi.yml index 7a2601ed5..e9fe2308e 100644 --- a/lang/mi.yml +++ b/lang/mi.yml @@ -149,7 +149,4 @@ mi: LOGIN: Takiuru LOSTPASSWORDHEADER: 'Kupuhipa Ngaro' NOTEPAGESECURED: 'Kua ngita tēnā whārangi. Tāurua ō taipitoptio tuakiri ki raro, ā, mā mātou koe e tuku kia haere tonu.' - NOTERESETLINKINVALID: '

He muhu, kua mōnehu rānei te hono tautuhi kupuhipa anō.

Ka taea te tono i te mea hōui konei ka huri rānei i tō kupuhipa ā muri i tōtakiuru.

' NOTERESETPASSWORD: 'Tāurua tō wāhitau īmēra, mā mātou e tuku tētahi hono ki a koe e taea ai te tautuhi anō i tō kupuhipa' - PASSWORDSENTHEADER: 'I tukuna he hono tautuhi kupuhipa anō ki ''{email}''' - PASSWORDSENTTEXT: 'Kia ora! Kua tukuna he hono tautuhi anō ki ''{email}'',engari rā kei te tīariari he pūkete mō taua wāhitau īmēra.' diff --git a/lang/nb.yml b/lang/nb.yml index 91dd9f758..f1dc4c74d 100644 --- a/lang/nb.yml +++ b/lang/nb.yml @@ -152,7 +152,4 @@ nb: LOGIN: 'Logg inn' LOSTPASSWORDHEADER: 'Mistet passord' NOTEPAGESECURED: 'Den siden er sikret. Skriv inn gyldig innloggingsinfo så kommer du inn.' - NOTERESETLINKINVALID: '

Lenken for å nullstille passordet er ugyldig eller utgått.

Du kan kreve en ny her eller endre passordet etter at du har logget inn.

' NOTERESETPASSWORD: 'Skriv inn epostadressen din og vi vil sende deg en lenke som nullstiller passordet.' - PASSWORDSENTHEADER: 'Lenke for nullstilling av passord ble sendt til ''{email}''' - PASSWORDSENTTEXT: 'Takk! En lenke for å lage nytt passord er sendt til ''{email}'', forutsatt at det eksisterer en konto for denne epostadressen.' diff --git a/lang/nl.yml b/lang/nl.yml index 64f191213..242eccc45 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -1,4 +1,26 @@ nl: + SilverStripe\Admin\LeftAndMain: + VersionUnknown: onbekend + SilverStripe\AssetAdmin\Forms\UploadField: + Dimensions: Afmetingen + EDIT: Bewerken + EDITINFO: 'Bewerk dit bestand' + REMOVE: Verwijder + SilverStripe\Control\ChangePasswordEmail_ss: + CHANGEPASSWORDFOREMAIL: 'Het wachtwoord voor het account met e-mailadres {email} is aangepast. Indien u uw wachtwoord niet heeft aangepast kunt u dat doen met onderstaande link.' + CHANGEPASSWORDTEXT1: 'U heeft het wachtwoord veranderd voor' + CHANGEPASSWORDTEXT3: 'Wachtwoord veranderen' + HELLO: Hallo + SilverStripe\Control\Email\ForgotPasswordEmail_ss: + HELLO: Hallo + TEXT1: 'Hier is uw' + TEXT2: 'link om uw wachtwoord opnieuw aan te maken' + TEXT3: voor + SilverStripe\Control\RequestProcessor: + INVALID_REQUEST: 'Fout bij verwerken' + REQUEST_ABORTED: 'Fout bij verwerken (geannuleerd)' + SilverStripe\Core\Manifest\VersionProvider: + VERSIONUNKNOWN: Onbekend SilverStripe\Forms\CheckboxField: NOANSWER: Nee YESANSWER: Ja @@ -8,6 +30,8 @@ nl: ATLEAST: 'Een wachtwoord moet tenminste {min} karakters hebben.' BETWEEN: 'Een wachtwoord moet tussen de {min} en {max} karakters hebben' CURRENT_PASSWORD_ERROR: 'Het wachtwoord dat u heeft ingevoerd is niet juist.' + CURRENT_PASSWORD_MISSING: 'Voer uw huidige wachtwoord in.' + LOGGED_IN_ERROR: 'U moet ingelogd zijn om uw wachtwoord te kunnen veranderen!' MAXIMUM: 'Een wachtwoord mag maximaal {max} karakters hebben.' SHOWONCLICKTITLE: 'Verander wachtwoord' SilverStripe\Forms\CurrencyField: @@ -16,12 +40,20 @@ nl: 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})' + SilverStripe\Forms\DatetimeField: + VALIDDATEMAXDATETIME: 'De datum moet ouder of gelijk zijn aan de maximale datum ({datetime})' + VALIDDATETIMEFORMAT: 'Vul een geldige datum in ({format})' + VALIDDATETIMEMINDATE: 'De datum moet nieuwer of gelijk zijn aan de minimale datum ({datetime})' SilverStripe\Forms\DropdownField: CHOOSE: (Kies) + CHOOSE_MODEL: '(Selecteer {name})' SOURCE_VALIDATION: 'Selecteer een optie uit de lijst. {value} is geen geldige keuze.' SilverStripe\Forms\EmailField: VALIDATION: 'Gelieve een e-mailadres in te voeren.' + SilverStripe\Forms\FileUploadReceiver: + FIELDNOTSET: 'Bestandsinformatie niet gevonden' SilverStripe\Forms\Form: + BAD_METHOD: 'Dit formulier moet middels {method} verzonden worden' CSRF_EXPIRED_MESSAGE: 'Uw sessie is verlopen. Verzend het formulier opnieuw.' CSRF_FAILED_MESSAGE: 'Er lijkt een technisch probleem te zijn. Klik op de knop terug, vernieuw uw browser, en probeer het opnieuw.' VALIDATIONPASSWORDSDONTMATCH: 'Wachtwoorden komen niet overeen' @@ -30,7 +62,10 @@ nl: VALIDATOR: Validator VALIDCURRENCY: 'Vul een geldige munteenheid in' SilverStripe\Forms\FormField: + EXAMPLE: 'bijv. {format}' NONE: geen + SilverStripe\Forms\FormScaffolder: + TABMAIN: Hoofdgedeelte SilverStripe\Forms\GridField\GridField: Add: '{name} toevoegen' CSVEXPORT: 'Exporteer naar CSV' @@ -60,27 +95,67 @@ nl: DeletePermissionsFailure: 'Onvoldoende rechten om te verwijderen' Deleted: '{type} {name} verwijderd' Save: Opslaan + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Bewerken + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'U kunt uzelf niet verwijderen van deze groep, omdat u dan geen admin-rechten meer heeft.' + SilverStripe\Forms\GridField\GridFieldPaginator: + OF: van + Page: Pagina + View: Bekijk SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Aantal FIELDLABELCURRENCY: Munteenheid + INVALID_CURRENCY: 'Valuta {currency} is niet toegestaan' + SilverStripe\Forms\MultiSelectField: + SOURCE_VALIDATION: 'Selecteer een optie uit de lijst. {value} is geen geldige keuze.' SilverStripe\Forms\NullableField: IsNullLabel: 'Is null' SilverStripe\Forms\NumericField: VALIDATION: '''{value}'' is geen getal, enkel getallen worden door dit veld geaccepteerd' SilverStripe\Forms\TimeField: VALIDATEFORMAT: 'Vul een geldig datumformaat in ({format})' + SilverStripe\ORM\DataObject: + PLURALNAME: 'Data objecten' + PLURALS: + one: 'Data object' + other: '{count} Data objecten' + SINGULARNAME: 'Data object' SilverStripe\ORM\FieldType\DBBoolean: ANY: Elke NOANSWER: Nee YESANSWER: Ja SilverStripe\ORM\FieldType\DBDate: + DAYS_SHORT_PLURALS: + one: '{count} dag' + other: '{count} dagen' + HOURS_SHORT_PLURALS: + one: '{count} uur' + other: '{count} uren' LessThanMinuteAgo: 'minder dan één minuut' + MINUTES_SHORT_PLURALS: + one: '{count} minuut' + other: '{count} minuten' + MONTHS_SHORT_PLURALS: + one: '{count} maand' + other: '{count} maanden' + SECONDS_SHORT_PLURALS: + one: '{count} seconde' + other: '{count} seconden' TIMEDIFFAGO: '{difference} geleden' TIMEDIFFIN: 'in {difference}' + YEARS_SHORT_PLURALS: + one: '{count} jaar' + other: '{count} jaren' SilverStripe\ORM\FieldType\DBEnum: ANY: Elke + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'Teveel keuzes in de lijst; een alternatief veld wordt getoond.' + SilverStripe\ORM\Hierarchy: + LIMITED_TITLE: 'Teveel onderliggende items ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: InfiniteLoopNotAllowed: 'Oneindige lus gevonden in "{type}" hiërarchie. Wijzig het hogere niveau om dit op te lossen' + LIMITED_TITLE: 'Teveel onderliggende items ({count})' SilverStripe\ORM\ValidationException: DEFAULT_ERROR: Validatiefout SilverStripe\Security\BasicAuth: @@ -91,34 +166,60 @@ nl: PASSWORDEXPIRED: '

Uw wachtwoord is verlopen. Kies een nieuw wachtwoord.

' SilverStripe\Security\CMSSecurity: INVALIDUSER: '

Ongeldige gebruiker Log hier opnieuw in om verder te gaan.

' + LOGIN_MESSAGE: 'Sessie is verlopen' + LOGIN_TITLE: '

U kunt verder met wat u aan het doen was, door opnieuw in te loggen.

' SUCCESS: Succes SUCCESSCONTENT: '

U bent ingelogd. Klik hier als u niet automatisch wordt doorgestuurd.

' + SUCCESS_TITLE: 'Inloggen is gelukt' + SilverStripe\Security\DefaultAdminService: + DefaultAdminFirstname: 'Standaard Beheerder' SilverStripe\Security\Group: AddRole: 'Voeg een rol toe aan deze groep' Code: 'Groep code' DefaultGroupTitleAdministrators: Beheerders DefaultGroupTitleContentAuthors: 'Inhoud Auteurs' Description: 'Omschrijving ' + GROUPNAME: 'Groep naam' GroupReminder: 'Als u de bovenliggende groep selecteert, neemt deze groep alle rollen over' HierarchyPermsError: 'U moet (ADMIN) rechten hebben om de bovenliggende groep "{group}" toe te kennen' Locked: 'Gesloten?' + MEMBERS: Leden + NEWGROUP: 'Nieuwe groep' NoRoles: 'Geen rollen gevonden' + PERMISSIONS: Rechten + PLURALNAME: Groepen + PLURALS: + one: 'Een groep' + other: '{count} groepen' Parent: 'Bovenliggende groep' + ROLES: Rollen + ROLESDESCRIPTION: 'Rollen zijn logische groeperingen van rechten die in het Rollen tabblad gewijzigd kunnen worden.
Rollen worden automatisch overgenomen van bovenliggende groepen.' RolesAddEditLink: 'Rollen beheren' + SINGULARNAME: Groep Sort: Sorteer-richting has_many_Permissions: Rechten many_many_Members: Leden SilverStripe\Security\LoginAttempt: + Email: 'E-mailadres ' + EmailHashed: 'E-mailadres (versleuteld)' IP: 'IP adres' + PLURALNAME: Inlogpogingen + PLURALS: + one: 'Een inlogpoging' + other: '{count} inlogpogingen' + SINGULARNAME: Inlogpogingen Status: Status SilverStripe\Security\Member: ADDGROUP: 'Groep toevoegen' BUTTONCHANGEPASSWORD: 'Wachtwoord veranderen' BUTTONLOGIN: Inloggen BUTTONLOGINOTHER: 'Als iemand anders inloggen' + BUTTONLOGOUT: Uitloggen BUTTONLOSTPASSWORD: 'Ik ben mijn wachtwoord vergeten' CONFIRMNEWPASSWORD: 'Bevestig het nieuwe wachtwoord' CONFIRMPASSWORD: 'Bevestig wachtwoord' + CURRENT_PASSWORD: 'Huidige wachtwoord' + EDIT_PASSWORD: 'Nieuw wachtwoord' EMAIL: E-mail EMPTYNEWPASSWORD: 'Het nieuwe wachtwoord mag niet leeg zijn, probeer opnieuw' ENTEREMAIL: 'Typ uw e-mailadres om een link te ontvangen waarmee u uw wachtwoord kunt resetten.' @@ -128,13 +229,21 @@ nl: ERRORWRONGCRED: 'De ingevulde gegevens lijken niet correct. Probeer het nog een keer.' FIRSTNAME: Voornaam INTERFACELANG: 'Interface taal' + KEEPMESIGNEDIN: 'Houd mij ingelogd' LOGGEDINAS: 'U bent ingelogd als {name}.' NEWPASSWORD: 'Nieuw wachtwoord' PASSWORD: Wachtwoord PASSWORDEXPIRED: 'Uw wachtwoord is verlopen. Kies een nieuw wachtwoord.' + PLURALNAME: Leden + PLURALS: + one: 'Een lid' + other: '{count} leden' + REMEMBERME: 'Onthoud mij voor volgende keer? (voor {count} dagen op dit apparaat)' + SINGULARNAME: Lid SUBJECTPASSWORDCHANGED: 'Uw wachtwoord is veranderd' SUBJECTPASSWORDRESET: 'Link om uw wachtwoord opnieuw aan te maken' SURNAME: Achternaam + VALIDATIONADMINLOSTACCESS: 'Niet mogelijk om alle admin-groepen te verwijderen van uw profiel' ValidationIdentifierFailed: 'Een bestaande gebruiker #{id} kan niet dezelfde unieke velden hebben ({name} = {value}))' WELCOMEBACK: 'Welkom terug, {firstname}' YOUROLDPASSWORD: 'Uw oude wachtwoord' @@ -143,15 +252,38 @@ nl: db_LockedOutUntil: 'Gesloten tot' db_Password: Wachtwoord db_PasswordExpiry: 'Wachtwoord vervaldatum' + SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm: + AUTHENTICATORNAME: Inlogformulier + BUTTONFORGOTPASSWORD: 'Wachtwoord vergeten' + BUTTONLOGIN: 'Opnieuw inloggen' + BUTTONLOGOUT: Uitloggen + SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: + ERRORWRONGCRED: 'De ingevulde gegevens lijken niet correct. Probeer het nog een keer.' + NoPassword: 'Er is geen wachtwoord voor deze gebruiker.' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'E-mail & wachtwoord' + SilverStripe\Security\MemberPassword: + PLURALNAME: Gebruikerswachtwoorden + PLURALS: + one: 'Een gebruikerswachtwoord' + other: '{count} Gebruikerswachtwoorden' + SINGULARNAME: Gebruikerswachtwoord SilverStripe\Security\PasswordValidator: LOWCHARSTRENGTH: 'Maak a.u.b. uw wachtwoord sterker door enkele van de volgende karakters te gebruiken: {chars}' PREVPASSWORD: 'U heeft dit wachtwoord in het verleden al gebruikt, kies a.u.b. een nieuw wachtwoord.' TOOSHORT: 'Het wachtwoord is te kort, het moet minimaal {minimum} karakters hebben' SilverStripe\Security\Permission: AdminGroup: Beheerder + CMS_ACCESS_CATEGORY: 'CMS toegang' CONTENT_CATEGORY: Inhoudsrechten FULLADMINRIGHTS: 'Volledige admin rechten' FULLADMINRIGHTS_HELP: 'Impliceert en overstemt alle andere toegewezen rechten.' + PERMISSIONS_CATEGORY: 'Rollen en toegangsrechten' + PLURALNAME: Rechten + PLURALS: + one: Machtiging + other: '{count} rechten' + SINGULARNAME: Machtiging UserPermissionsIntro: 'Groepen aan deze gebruiker toewijzen zullen diens permissies aanpassen. Zie de sectie Groepen voor meer informatie over machtigingen voor afzonderlijke groepen.' SilverStripe\Security\PermissionCheckboxSetField: AssignedTo: 'toegewezen aan "{title}"' @@ -161,21 +293,37 @@ nl: SilverStripe\Security\PermissionRole: OnlyAdminCanApply: 'Alleen admin kan doorvoeren' PLURALNAME: Rollen + PLURALS: + one: 'Een rol' + other: '{count} rollen' SINGULARNAME: Rol Title: Titel SilverStripe\Security\PermissionRoleCode: + PLURALNAME: 'Permissie codes' + PLURALS: + one: 'Een permissiecode' + other: '{count} permissiecodes' PermsError: 'U moet (ADMIN) rechten hebben om de code "{code}" toe te kennen' + SINGULARNAME: Permissiecode + SilverStripe\Security\RememberLoginHash: + PLURALNAME: 'Versleutelde logins' + PLURALS: + one: 'Een versleutelde login' + other: '{count} versleutelde logins' + SINGULARNAME: 'Versleutelde login' SilverStripe\Security\Security: ALREADYLOGGEDIN: 'U hebt geen toegang tot deze pagina. Als u een andere account met de nodige rechten hebt, kan u hieronder opnieuw inloggen.' BUTTONSEND: 'Nieuw wachtwoord aanmaken' CHANGEPASSWORDBELOW: 'U kunt uw wachtwoord hieronder veranderen.' CHANGEPASSWORDHEADER: 'Verander uw wachtwoord' + CONFIRMLOGOUT: 'Klik op onderstaande knop om uit te loggen.' ENTERNEWPASSWORD: 'Voer een nieuw wachtwoord in.' ERRORPASSWORDPERMISSION: 'U moet ingelogd zijn om uw wachtwoord te kunnen veranderen!' LOGIN: 'Meld aan' + LOGOUT: Uitloggen LOSTPASSWORDHEADER: 'Wachtwoord vergeten' NOTEPAGESECURED: 'Deze pagina is beveiligd. Voer uw gegevens in en u wordt automatisch doorgestuurd.' - NOTERESETLINKINVALID: '

De link om uw wachtwoord te kunnen wijzigen is niet meer geldig.

U kunt een nieuwe link aanvragen of uw wachtwoord aanpassen door in te loggen.

' + NOTERESETLINKINVALID: '

De reset link is ongeldig of komen te vervallen.

Je kan hier een nieuwe link aanvragen of het wachtwoord veranderen nadat je bent ingelogd.

' NOTERESETPASSWORD: 'Voer uw e-mailadres in en we sturen een link waarmee u een nieuw wachtwoord kunt instellen.' - PASSWORDSENTHEADER: 'Wachtwoord herstel link verzonden naar {email}' - PASSWORDSENTTEXT: 'Bedankt! Er is een link verstuurd naar {email} om uw wachtwoord opnieuw in te stellen, in de veronderstelling dat er een account bestaat voor dit e-mailadres.' + PASSWORDRESETSENTHEADER: 'link om uw wachtwoord opnieuw aan te maken' + PASSWORDRESETSENTTEXT: 'Bedankt! Er is een link verstuurd om uw wachtwoord opnieuw in te stellen (mits het mailadres reeds bekend is bij ons).' diff --git a/lang/pl.yml b/lang/pl.yml index c0359c922..1905acb38 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -1,7 +1,37 @@ 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: @@ -10,23 +40,38 @@ pl: 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}' @@ -50,55 +95,145 @@ pl: DeletePermissionsFailure: 'Brak uprawnień do usuwania' Deleted: 'Usunięto {type} {name}' Save: Zapisz + 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.' @@ -108,12 +243,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' @@ -122,15 +268,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}"' @@ -139,20 +312,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ę.

' NOTERESETPASSWORD: 'Wpisz adres e-mail, na który mamy wysłać link gdzie możesz zresetować swoje hasło' - PASSWORDSENTHEADER: 'Link resetujący hasła został wysłany do ''{email}''' - PASSWORDSENTTEXT: 'Dziękujemy! Link resetujący hasło został wysłany do ''{email}'', o ile konto użytkownika dla takiego e-maila istnieje.' diff --git a/lang/ru.yml b/lang/ru.yml index 73602e013..add72be7f 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -339,7 +339,4 @@ ru: LOGOUT: Выйти LOSTPASSWORDHEADER: 'Восстановление пароля' NOTEPAGESECURED: 'Эта страница защищена. Пожалуйста, введите свои учетные данные для входа.' - NOTERESETLINKINVALID: '

Неверная ссылка переустановки пароля или время действия ссылки истекло.

Вы можете повторно запросить ссылку, щелкнув здесь, или поменять пароль, войдя в систему.

' NOTERESETPASSWORD: 'Введите Ваш адрес email, и Вам будет отправлена ссылка, по которой Вы сможете переустановить свой пароль' - PASSWORDSENTHEADER: 'Ссылка для переустановки пароля выслана на ''{email}''' - PASSWORDSENTTEXT: 'Ссылка переустановки пароля была выслана на адрес ''{email}'' (письмо дойдет до получателя только в том случае, если аккаунт с таким электронным адресом действительно зарегистрирован).' diff --git a/lang/sk.yml b/lang/sk.yml index 861045fc6..88f4bf260 100644 --- a/lang/sk.yml +++ b/lang/sk.yml @@ -228,7 +228,4 @@ sk: LOGIN: Prihlásiť LOSTPASSWORDHEADER: 'Zabudnuté heslo' NOTEPAGESECURED: 'Táto stránka je zabezpečená. Zadajte svoje prihlasovacie údaje a my Vám zároveň pošleme práva.' - NOTERESETLINKINVALID: '

Odkaz na resetovanie hesla nie je platný alebo je vypršala jeho platnosť.

Môžete požiadať o nový tu alebo zmeňte svoje heslo po prihlásení.

' NOTERESETPASSWORD: 'Zadajte svoju e-mailovú adresu a my Vám pošleme odkaz na resetovanie hesla' - PASSWORDSENTHEADER: 'Odkaz na resetovanie hesla bol odoslaný na ''{email}''' - PASSWORDSENTTEXT: 'Ďakujeme! Resetovací odkaz bol odoslaný na ''''{email}'''', pokiaľ účet existuje pre túto emailovú adresu.' diff --git a/lang/sl.yml b/lang/sl.yml index bccd51826..5496aff5f 100644 --- a/lang/sl.yml +++ b/lang/sl.yml @@ -135,7 +135,4 @@ sl: LOGIN: Prijava LOSTPASSWORDHEADER: 'Izgubljeno geslo' NOTEPAGESECURED: 'Stran je zaščitena. Da bi lahko nadaljevali, vpišite svoje podatke.' - NOTERESETLINKINVALID: '

Povezava za ponastavitev gesla je napačna ali pa je njena veljavnost potekla.

Tukaj lahko zaprosite za novo povezavo or pa zamenjate geslo, ko se prijavite v sistem.

' NOTERESETPASSWORD: 'Vpišite e-naslov, na katerega vam bomo poslali povezavo za ponastavitev gesla' - PASSWORDSENTHEADER: 'Povezava za ponastavitev gesla je bila poslana na e-naslov ''{email}''.' - PASSWORDSENTTEXT: 'Hvala! Povezava za ponastavitev gesla je bila poslana na e-naslov ''{email}'', ki je naveden kot e-naslov vašega računa. ' diff --git a/lang/sr.yml b/lang/sr.yml index 07f642278..622b657b3 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -151,7 +151,4 @@ sr: ERRORPASSWORDPERMISSION: 'Морате да будете пријављени да бисте променили своју лозинку!' LOGIN: Пријављивање NOTEPAGESECURED: 'Ова страна је обезбеђена. Унесите своје податке и ми ћемо вам послати садржај.' - NOTERESETLINKINVALID: '

Линк за ресетовање лозинке је погрешан или је истекло време за његово коришћење.

Можете да захтевате нови овде или да промените Вашу лозинку након што се пријавите.

' NOTERESETPASSWORD: 'Унесите своју адресу е-поште и ми ћемо вам послати линк помоћу којег можете да промените своју лозинку' - PASSWORDSENTHEADER: 'Линк за ресетовање лозинке послат је на адресу е-поште: ''{email}''' - PASSWORDSENTTEXT: 'Хвала Вам! Линк за ресетовање лозинке је послат не адресу е-поште ''{email}''. Порука ће стићи примаоцу само ако постоји регистрован налог са том адресом е-поште.' diff --git a/lang/sr@latin.yml b/lang/sr@latin.yml index 1b210ae51..f91aa9337 100644 --- a/lang/sr@latin.yml +++ b/lang/sr@latin.yml @@ -150,7 +150,4 @@ sr@latin: ERRORPASSWORDPERMISSION: 'Morate da budete prijavljeni da biste promenili svoju lozinku!' LOGIN: Prijavljivanje NOTEPAGESECURED: 'Ova strana je obezbeđena. Unesite svoje podatke i mi ćemo vam poslati sadržaj.' - NOTERESETLINKINVALID: '

Link za resetovanje lozinke je pogrešan ili je isteklo vreme za njegovo korišćenje.

Možete da zahtevate novi ovde ili da promenite Vašu lozinku nakon što se prijavite.

' NOTERESETPASSWORD: 'Unesite svoju adresu e-pošte i mi ćemo vam poslati link pomoću kojeg možete da promenite svoju lozinku' - PASSWORDSENTHEADER: 'Link za resetovanje lozinke poslat je na adresu e-pošte: ''{email}''' - PASSWORDSENTTEXT: 'Hvala Vam! Link za resetovanje lozinke je poslat ne adresu e-pošte ''{email}''. Poruka će stići primaocu samo ako postoji registrovan nalog sa tom adresom e-pošte.' diff --git a/lang/sr_RS.yml b/lang/sr_RS.yml index fc79812da..298049430 100644 --- a/lang/sr_RS.yml +++ b/lang/sr_RS.yml @@ -150,7 +150,4 @@ sr_RS: ERRORPASSWORDPERMISSION: 'Морате да будете пријављени да бисте променили своју лозинку!' LOGIN: Пријављивање NOTEPAGESECURED: 'Ова страна је обезбеђена. Унесите своје податке и ми ћемо вам послати садржај.' - NOTERESETLINKINVALID: '

Линк за ресетовање лозинке је погрешан или је истекло време за његово коришћење.

Можете да захтевате нови овде или да промените Вашу лозинку након што се пријавите.

' NOTERESETPASSWORD: 'Унесите своју адресу е-поште и ми ћемо вам послати линк помоћу којег можете да промените своју лозинку' - PASSWORDSENTHEADER: 'Линк за ресетовање лозинке послат је на адресу е-поште: ''{email}''' - PASSWORDSENTTEXT: 'Хвала Вам! Линк за ресетовање лозинке је послат не адресу е-поште ''{email}''. Порука ће стићи примаоцу само ако постоји регистрован налог са том адресом е-поште.' diff --git a/lang/sr_RS@latin.yml b/lang/sr_RS@latin.yml index f39b36058..453ffa17d 100644 --- a/lang/sr_RS@latin.yml +++ b/lang/sr_RS@latin.yml @@ -151,7 +151,4 @@ sr_RS@latin: ERRORPASSWORDPERMISSION: 'Morate da budete prijavljeni da biste promenili svoju lozinku!' LOGIN: Prijavljivanje NOTEPAGESECURED: 'Ova strana je obezbeđena. Unesite svoje podatke i mi ćemo vam poslati sadržaj.' - NOTERESETLINKINVALID: '

Link za resetovanje lozinke je pogrešan ili je isteklo vreme za njegovo korišćenje.

Možete da zahtevate novi ovde ili da promenite Vašu lozinku nakon što se prijavite.

' NOTERESETPASSWORD: 'Unesite svoju adresu e-pošte i mi ćemo vam poslati link pomoću kojeg možete da promenite svoju lozinku' - PASSWORDSENTHEADER: 'Link za resetovanje lozinke poslat je na adresu e-pošte: ''{email}''' - PASSWORDSENTTEXT: 'Hvala Vam! Link za resetovanje lozinke je poslat ne adresu e-pošte ''{email}''. Poruka će stići primaocu samo ako postoji registrovan nalog sa tom adresom e-pošte.' diff --git a/lang/sv.yml b/lang/sv.yml index f35c2ee99..f2a18d659 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -93,7 +93,12 @@ sv: DeletePermissionsFailure: 'Rättighet för att radera saknas' Deleted: 'Raderade {type} {name}' Save: Spara + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Ändra + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'Du kan inte radera dig själv från den här gruppen, då du då kommer att förlora dina admin-rättigheter' SilverStripe\Forms\GridField\GridFieldPaginator: + OF: av Page: Sida View: Visa SilverStripe\Forms\MoneyField: @@ -108,6 +113,12 @@ sv: VALIDATION: '''{value}'' är inget nummer, bara siffror (utan mellanslag) kan accepteras för det här fältet' SilverStripe\Forms\TimeField: VALIDATEFORMAT: 'Var god att ange tid i ett giltigt format ({format})' + SilverStripe\ORM\DataObject: + PLURALNAME: Dataobjekt + PLURALS: + one: 'Ett dataobjekt' + other: '{count} Dataobjekt' + SINGULARNAME: Dataobjekt SilverStripe\ORM\FieldType\DBBoolean: ANY: 'Vilken som helst' NOANSWER: Nej @@ -136,6 +147,8 @@ sv: other: '{count} år' SilverStripe\ORM\FieldType\DBEnum: ANY: 'Vilken som helst' + SilverStripe\ORM\FieldType\DBForeignKey: + DROPDOWN_THRESHOLD_FALLBACK_MESSAGE: 'För många relaterade objekt; använder fallback-fält' SilverStripe\ORM\Hierarchy: LIMITED_TITLE: 'För många barn ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: @@ -221,6 +234,7 @@ sv: PLURALS: one: 'En medlem' other: '{count} medlemmar' + REMEMBERME: 'Kom ihåg mig nästa gång? (i {count} dagar på denna enhet)' SINGULARNAME: Medlem SUBJECTPASSWORDCHANGED: 'Ditt lösenord har ändrats' SUBJECTPASSWORDRESET: 'Din återställningslänk' @@ -288,7 +302,6 @@ sv: LOGOUT: 'Logga ut' LOSTPASSWORDHEADER: 'Bortglömt lösenord' NOTEPAGESECURED: 'Den här sidan är låst. Fyll i dina uppgifter nedan så skickar vi dig vidare.' - NOTERESETLINKINVALID: '

Återställningslänk för lösenord är felaktig eller för gammal.

Du kan begära en ny här eller ändra ditt lösenord när du loggat in.

' NOTERESETPASSWORD: 'Ange din e-postadress så skickar vi en länk med vilken du kan återställa ditt lösenord' - PASSWORDSENTHEADER: 'Återställningslänk för lösenord har skickats till ''{email}''' - PASSWORDSENTTEXT: 'Tack en återställningslänk har skickats till ''{email}'', förutsatt att ett konto med den addressen finns.' + PASSWORDRESETSENTHEADER: 'Återställningslänk för lösenord skickad' + PASSWORDRESETSENTTEXT: 'Tack. En återställningslänk har skickats, förutsatt att ett konto med denna adress existerar.' diff --git a/lang/zh.yml b/lang/zh.yml index 26c8e2893..748ee671c 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -166,7 +166,4 @@ zh: LOGIN: 登录 LOSTPASSWORDHEADER: 忘记密码 NOTEPAGESECURED: 该页面受安全保护。请在下面输入您的证书,然后我们会立即将您引导至该页面。 - NOTERESETLINKINVALID: '

密码重设链接无效或已过期。

您可以在这里 要求一个新的或在登录后更改您的密码。

' NOTERESETPASSWORD: 请输入您的电子邮件地址,然后我们会将一个链接发送给您,您可以用它来重设您的密码 - PASSWORDSENTHEADER: '密码重设链接已发送至''{email}''' - PASSWORDSENTTEXT: '谢谢!复位链接已发送到 ''{email}'',假定此电子邮件地址存在一个帐户。' From 55674959f8a48237bf1fdee9ccac149301bd9619 Mon Sep 17 00:00:00 2001 From: Vagrant Default User Date: Wed, 7 Nov 2018 22:38:52 +1300 Subject: [PATCH 059/175] Added 4.1.3 changelog --- docs/en/04_Changelogs/4.1.3.md | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/en/04_Changelogs/4.1.3.md b/docs/en/04_Changelogs/4.1.3.md index 26ac3041c..c7b1ba1b4 100644 --- a/docs/en/04_Changelogs/4.1.3.md +++ b/docs/en/04_Changelogs/4.1.3.md @@ -10,3 +10,39 @@ behaviour to that of SilverStripe 3 where `Extension` instances are of lowest im default value. If you rely on your `Extension` or module providing an overriding config value, please move this to yaml. + +## Change Log + +### Security + + * 2018-11-06 [6cb1bf5](https://github.com/silverstripe/silverstripe-admin/commit/6cb1bf53a6fd5b54b6f7bbe7a1d7b939e176cf53) Add CSRF protection (Aaron Carlino) - See [ss-2018-007](https://www.silverstripe.org/download/security-releases/ss-2018-007) + * 2018-08-21 [af000be](https://github.com/silverstripe/silverstripe-framework/commit/af000bea9b16ea553cae7f7f662f74ab8dc343df) Add confirmation token to dev/build (Loz Calver) - See [ss-2018-019](https://www.silverstripe.org/download/security-releases/ss-2018-019) + * 2018-07-29 [5425195](https://github.com/silverstripe/silverstripe-framework/commit/54251952387394d72b221e797a80edfbf9a973ee) Ignore arguments in mysqli::real_connect backtrace calls (Robbie Averill) - See [ss-2018-018](https://www.silverstripe.org/download/security-releases/ss-2018-018) + +### Features and Enhancements + + * 2018-04-18 [fef734b](https://github.com/silverstripe/recipe-core/commit/fef734b5484d86f5afd4e857c556b8c1d8d66c16) Provide default IIS rewriting rules with recipe (Damian Mooyman) + +### Bugfixes + + * 2018-10-24 [e72fc9e](https://github.com/silverstripe/silverstripe-framework/commit/e72fc9e3d0f35a1d43f55f83f9919f67d72fb7cb) DataObject singleton creation (#8516) (Sam Minnée) + * 2018-09-18 [bbe7c66](https://github.com/silverstripe/silverstripe-asset-admin/commit/bbe7c660cf40d4c942eaf6e76755eeaf46c63471) Add `AssetAdmin::getMinimalistObjectFromData()` to build file metadata for UploadField (#829) (Maxime Rainville) + * 2018-09-13 [5c102de](https://github.com/silverstripe/silverstripe-cms/commit/5c102decbde43395e14aeff83a20c4c6f1d048ae) Improve performance of CMSMain::getArchiveWarningMessage (#2231) (Maxime Rainville) + * 2018-09-03 [1c4311d](https://github.com/silverstripe/silverstripe-asset-admin/commit/1c4311d4e6548600272daa0ce83afa12cf7e99c3) description for docs.silverstripe.org (wernerkrauss) + * 2018-09-03 [b922c0d](https://github.com/silverstripe/silverstripe-framework/commit/b922c0d7327b5d0222dd280afcb64f83a09ea859) Check scheme is truthy before setting it to the request (Robbie Averill) + * 2018-08-28 [d651d0f](https://github.com/silverstripe/silverstripe-framework/commit/d651d0fbfcababeaf317b27cb00b4f33b9d99eab) Use base class (not remapping target class) when looking up whether object is versioned (Robbie Averill) + * 2018-08-27 [4da5569](https://github.com/silverstripe/silverstripe-framework/commit/4da5569232505ee574e0b5106ff2116611393aa4) ensure createFromVariables takes correct params on CLIRequestBuilder (Scott Hutchinson) + * 2018-08-15 [0c713b5](https://github.com/silverstripe/silverstripe-assets/commit/0c713b5b1eb6a08ac00dcadb187b8b3ef7115fc4) Fix routing for files with dots in filename (Damian Mooyman) + * 2018-08-14 [27ac001](https://github.com/silverstripe/silverstripe-framework/commit/27ac001d5b27cce4f80ce4b3335c14708b116830) email rendering should not include requirements (Thomas Portelange) + * 2018-08-14 [8ec551e](https://github.com/silverstripe/silverstripe-cms/commit/8ec551e57b04d00d6897d06c2779557f0ec8109d) Broken "show as list" (#2232) (Maxime Rainville) + * 2018-07-26 [fea9ef7](https://github.com/silverstripe/silverstripe-admin/commit/fea9ef7d2a53904086f9fad6eedba7bb307c8578) #579 BUG Ambiguous column RecordID when doing batch actions (Ed Linklater) + * 2018-07-14 [a0e0bed](https://github.com/silverstripe/recipe-core/commit/a0e0bed7e7fe83b98264563efdeffa82d0d01d04) Use Injector to create PasswordValidators (Daniel Hensby) + * 2018-07-14 [8703839](https://github.com/silverstripe/silverstripe-framework/commit/8703839eb142ba0414f4d84f885ff898c39d6786) updateValidatePassword calls need to be masked from backtraces (Daniel Hensby) + * 2018-07-12 [e80c7e7](https://github.com/silverstripe/silverstripe-cms/commit/e80c7e712b916712d4ec7b6b8359ccf71dc9da04) Restore button now has warning colour and correct icon (Robbie Averill) + * 2018-07-12 [d122995](https://github.com/silverstripe/silverstripe-framework/commit/d1229956523d69f63c9e725b261c0142d5ee1de3) Duplicate config values for cascade_duplicates no longer duplicate their duplicates (Robbie Averill) + * 2018-06-19 [725212a](https://github.com/silverstripe/silverstripe-framework/commit/725212a707f6b724aff6548c3680b2cd66e9a6bb) Allow dispatcher in Embed to be configured with injector (#8192) (Robbie Averill) + * 2018-06-13 [932eb2b](https://github.com/silverstripe/silverstripe-cms/commit/932eb2b22dfe6c30473b1cf973661c28c5b9c635) Fix CMS components failing to register on other CMS sections (#2182) (Damian Mooyman) + * 2018-05-21 [bf5b578](https://github.com/silverstripe/silverstripe-admin/commit/bf5b5787685765c35c175c303f3f7ee719ac9453) Adding a min-width to flexbox-area-grow that allows flex blocks to shrink below their content width (Guy) + * 2018-05-18 [9531535](https://github.com/silverstripe/silverstripe-framework/commit/953153500d490f5b5abf7283c34242c3b22a855a) Polymorphic relationship class columns have obsolete class names remapped (Robbie Averill) + * 2018-05-08 [97a8f56](https://github.com/silverstripe/silverstripe-admin/commit/97a8f56c43ddb3c77a5bbc452755d44afb9a9472) Add missing focus styles for preview options (fixes silverstripe/silverstripe-framework #2101) (Loz Calver) + * 2018-03-29 [4acec33](https://github.com/silverstripe/silverstripe-framework/commit/4acec33562e4e1230092eee7d76c2b8061ffc914) Fixed bug in config merging priorities so that config values set by extensions are now least important instead of most important (Daniel Hensby) From 687e8c84b13f661bf9e644ae60d96fa6f3cfcceb Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Wed, 7 Nov 2018 23:20:11 +1300 Subject: [PATCH 060/175] Add changelog --- docs/en/04_Changelogs/rc/4.3.0-rc1.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/en/04_Changelogs/rc/4.3.0-rc1.md diff --git a/docs/en/04_Changelogs/rc/4.3.0-rc1.md b/docs/en/04_Changelogs/rc/4.3.0-rc1.md new file mode 100644 index 000000000..7584cc59b --- /dev/null +++ b/docs/en/04_Changelogs/rc/4.3.0-rc1.md @@ -0,0 +1,13 @@ +# 4.3.0-rc1 + + + +## Change Log + +### Features and Enhancements + + * 2018-07-25 [79a5ea3](https://github.com/silverstripe/recipe-cms/commit/79a5ea3dace336558ef4d5be4a86cc1a0e84badc) Add versioned-admin (Luke Edwards) + +### Bugfixes + + * 2018-06-15 [5e4ad34](https://github.com/silverstripe/silverstripe-installer/commit/5e4ad341622565cc998bd8537ad3ec7a6a6a7913) Fix incorrect base recipe dependency (Damian Mooyman) From 9c474a71b9898d9cb07ca58c07a066edc9797024 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Wed, 7 Nov 2018 23:41:26 +1300 Subject: [PATCH 061/175] MINOR: Use all-inclusive 4.3.0-rc1 backlog --- docs/en/04_Changelogs/rc/4.3.0-rc1.md | 284 +++++++++++++++++++++++++- 1 file changed, 283 insertions(+), 1 deletion(-) diff --git a/docs/en/04_Changelogs/rc/4.3.0-rc1.md b/docs/en/04_Changelogs/rc/4.3.0-rc1.md index 7584cc59b..fe242d44a 100644 --- a/docs/en/04_Changelogs/rc/4.3.0-rc1.md +++ b/docs/en/04_Changelogs/rc/4.3.0-rc1.md @@ -4,10 +4,292 @@ ## Change Log +### Security + + * 2018-10-24 [88391f2](https://github.com/silverstripe/silverstripe-graphql/commit/88391f27463cac553360d7d94b0760e797247855) CSRF protection (Aaron Carlino) - See [ss-2018-007](https://www.silverstripe.org/download/security-releases/ss-2018-007) + * 2018-08-21 [3dbb10625](https://github.com/silverstripe/silverstripe-framework/commit/3dbb10625c6918094a18cd9a29f1f9daca8129c5) Add confirmation token to dev/build (Loz Calver) - See [ss-2018-019](https://www.silverstripe.org/download/security-releases/ss-2018-019) + * 2018-08-12 [3424431](https://github.com/silverstripe/silverstripe-admin/commit/3424431a5ed52fc9aae97423268aea5eba7334a2) Add CSRF to Apollo (Aaron Carlino) - See [ss-2018-007](https://www.silverstripe.org/download/security-releases/ss-2018-007) + * 2018-07-29 [637b4225c](https://github.com/silverstripe/silverstripe-framework/commit/637b4225c6a85bfa0d59e516a8c602203cc980d9) Ignore arguments in mysqli::real_connect backtrace calls (Robbie Averill) - See [ss-2018-018](https://www.silverstripe.org/download/security-releases/ss-2018-018) + +### API Changes + + * 2018-10-23 [d9e8341](https://github.com/silverstripe/silverstripe-versioned-admin/commit/d9e83412a309cb0f03cdd50a218f4dc45b470821) Deprecate HistoryControllerFactory and default to CMSPageHistoryV… (#71) (Maxime Rainville) + * 2018-10-05 [5276b6cbb](https://github.com/silverstripe/silverstripe-framework/commit/5276b6cbb1ebd49d704734d940b0467c51b0d064) Add FieldList::getContainerField (Maxime Rainville) + * 2018-10-04 [dba237e](https://github.com/silverstripe/silverstripe-admin/commit/dba237e5d6ab13a7a5f8cbc9a9e594d98478c566) Allow Gridfield to be lazy loadable. (Maxime Rainville) + * 2018-10-04 [bdb53979a](https://github.com/silverstripe/silverstripe-framework/commit/bdb53979aaed743f278b299e3dab7b13e321fbe7) Create a new GridFieldLazyLoader GridField component (Maxime Rainville) + * 2018-09-28 [cb2b9498f](https://github.com/silverstripe/silverstripe-framework/commit/cb2b9498fb33bd57a19ba6a7ceed6d3fbc976d70) Deprecate updateSearchContextCallback and updateSearchFormCallback (Robbie Averill) + * 2018-09-03 [f48ab77](https://github.com/silverstripe/silverstripe-assets/commit/f48ab777c2845d7b414ebae0ff7903c6bb423340) Add `show_file_link_tracking` config to `FileLinkTracking` extension to control visibility of the File Tracking tab (bergice) + * 2018-08-31 [01db5c9e9](https://github.com/silverstripe/silverstripe-framework/commit/01db5c9e98f5406e8d6b0f1f4ea660edb24b199f) Add Link Tracking section to Relations developer guide and describe `show_sitetree_link_tracking`, `show_file_link_tracking`. (bergice) + * 2018-08-31 [115ed92e](https://github.com/silverstripe/silverstripe-cms/commit/115ed92e0aed63df1620700955afd6248fa97fae) Add `show_sitetree_link_tracking` config to `SiteTreeLinkTracking` extension to control visibility of the Link Tracking tab (bergice) + * 2018-08-05 [02be4cc](https://github.com/silverstripe/silverstripe-graphql/commit/02be4cc3e1cab7c1ed8e8f9d6dde6ee102364fa6) Multi-schema support (#169) (Aaron Carlino) + * 2018-07-31 [47e3ae2](https://github.com/silverstripe/silverstripe-versioned-admin/commit/47e3ae27ea451f769c5e90f4c31589a5265c337c) Versions from and to are now stored as objects in the store (#29) (Dylan Wagstaff) + * 2018-07-12 [0343203](https://github.com/silverstripe/silverstripe-versioned/commit/03432030e99a496c6cd81f4bec780164851aa955) Add new RestoreAction and canRestoreToDraft method (#168) (Luke Edwards) + ### Features and Enhancements + * 2018-11-01 [2f2fb2b](https://github.com/silverstripe/silverstripe-admin/commit/2f2fb2bfdcd88b54ac9426f172bec77a3c210172) Improve loading screen and indicator (#582) (Luke Edwards) + * 2018-10-29 [e51be58](https://github.com/silverstripe/silverstripe-asset-admin/commit/e51be58685527b512bf20e7542d506aef14f4ed8) Move Remove button into preview-image message (#859) (Sacha Judd) + * 2018-10-29 [2440432](https://github.com/silverstripe/silverstripe-asset-admin/commit/24404324790a26b0c19f68f862bb2679618199ee) Move the replace file into the more options action set (#848) (Sacha Judd) + * 2018-10-29 [952f37a](https://github.com/silverstripe/silverstripe-admin/commit/952f37abdc78d9f87da20461537c88cf570d7cba) Adding an HOC to provide DragDropContext for consumers of ReactDND (#711) (Guy Marriott) + * 2018-10-26 [2c3665d](https://github.com/silverstripe/silverstripe-admin/commit/2c3665d83375364249adf02c2eef6382d73e7c43) Adding a component for a generic popover filled with buttons (#684) (Guy Marriott) + * 2018-10-19 [e88b8e7](https://github.com/silverstripe/silverstripe-admin/commit/e88b8e7b2fa4c47e62d737ea3a3ccee8c7ff8794) Expose TabsActions (#703) (Raissa North) + * 2018-10-18 [761a6f7](https://github.com/silverstripe/silverstripe-admin/commit/761a6f7f6f809c36f21dd804ce1d6325909b5d1c) Reverse argument signature of methods using path (#698) (Aaron Carlino) + * 2018-10-17 [902fec0](https://github.com/silverstripe/silverstripe-asset-admin/commit/902fec0f89bb49783b2dc5f64b4d0c24c33531fb) Extensible readFiles query (#847) (Aaron Carlino) + * 2018-10-17 [437e53f2f](https://github.com/silverstripe/silverstripe-framework/commit/437e53f2fec17bc778cc7bba39de322d43214441) Some minor refactoring of the PDO and MySQLi connectors (Robbie Averill) + * 2018-10-15 [3e2ce31](https://github.com/silverstripe/silverstripe-admin/commit/3e2ce3138d88674fc27aa465dc9be9ea4533c830) Add nested fields, args, distribute args to fields (#683) (Aaron Carlino) + * 2018-10-08 [2775895d0](https://github.com/silverstripe/silverstripe-framework/commit/2775895d03af93d207079637a8eb6afdfad5ab01) Adding a helper to find a form field by label content (Guy Marriott) + * 2018-10-05 [6e66c48](https://github.com/silverstripe/silverstripe-admin/commit/6e66c48d65b52d568fbd34f8a7c630b4a829c2c9) Add CMS community help menu to cms-menu (#615) (Sacha Judd) + * 2018-10-04 [9ea7b58a8](https://github.com/silverstripe/silverstripe-framework/commit/9ea7b58a8f26ca8856211da30eed5751706d0c4b) Add memory cache to ThemeResourceLoader::findTemplate() (Robbie Averill) + * 2018-10-03 [90afb2037](https://github.com/silverstripe/silverstripe-framework/commit/90afb2037a6d2d9be3f605aad9ddfbda0bbab2e7) TabSet react component is no longer structural (Raissa North) + * 2018-10-03 [0be4a9a](https://github.com/silverstripe/silverstripe-admin/commit/0be4a9abfa4482deb2753c50c47dbf0192581a12) Adding an extension point to FormBuilderLoader after redux form is initialised (Raissa North) + * 2018-10-02 [cba5d30](https://github.com/silverstripe/silverstripe-admin/commit/cba5d307f499471b88f77fd201df2a4baacf0038) Connect Tabs component to redux-form to handle activeTab state (Raissa North) + * 2018-10-02 [e1f2f89d3](https://github.com/silverstripe/silverstripe-framework/commit/e1f2f89d37e44c706d350d02774733dd867ccdc7) Add test for PHP 7.3 support (SS4 version) (Sam Minnee) + * 2018-09-28 [bd37b90a](https://github.com/silverstripe/silverstripe-cms/commit/bd37b90a3a5811555248ef616698efbe24466b11) Add CMSMain.enable_archive_warning_message config (Sam Minnee) + * 2018-09-27 [4b155b9](https://github.com/silverstripe/silverstripe-graphql/commit/4b155b982ca6fd737aa70e281ce679bb43404060) Add getSortableFields to return sortable fields for query (#185) (Robbie Averill) + * 2018-09-27 [25759ffc5](https://github.com/silverstripe/silverstripe-framework/commit/25759ffc5fe532a239b1487ca6b025140d2e144f) Show file path on PHP parser exceptions (Ingo Schommer) + * 2018-09-25 [5b7a84141](https://github.com/silverstripe/silverstripe-framework/commit/5b7a84141b0fbef66a9f3f52a9ccee12e02ef1e0) Add Hierarchy::prepopulate_numchildren_cache() (#8380) (Sam Minnée) + * 2018-09-21 [e2701a4](https://github.com/silverstripe/silverstripe-admin/commit/e2701a4cd00ffc2136b935038d8422a25acfa2dd) TinyMCE inline toolbar for images and embeds (Luke Edwards) + * 2018-09-20 [c928e83](https://github.com/silverstripe/silverstripe-versioned/commit/c928e830825ec8ae5336d254835f754a11bac350) Backport of DataList for list of versions, for SS 4.3.x (Robbie Averill) + * 2018-09-19 [d9a6c10](https://github.com/silverstripe/silverstripe-admin/commit/d9a6c10a157d6c1fd17351b0097ce9d8c5336ecf) Form state & schema persists across form remounting (Guy Marriott) + * 2018-09-19 [40dde226f](https://github.com/silverstripe/silverstripe-framework/commit/40dde226fd1b8997308ef0f5718763a298295cdf) Add ?showqueries=backtrace (Sam Minnee) + * 2018-09-19 [6de12e1](https://github.com/silverstripe/silverstripe-asset-admin/commit/6de12e1375d146d61052f1a6dd001aa3a6c437bc) TinyMCE inline toolbar for images and embeds (Luke Edwards) + * 2018-09-17 [588bf83e1](https://github.com/silverstripe/silverstripe-framework/commit/588bf83e1238a79793da4bc4145b7597ae2626be) Add hideNav flag to schema defaults (Raissa North) + * 2018-09-17 [a800ce7](https://github.com/silverstripe/silverstripe-admin/commit/a800ce714a99a631b8a7401322df0653e2ee476c) Add hideNav flag to allow hiding of navigation in Tabs (Raissa North) + * 2018-09-12 [d8bf873](https://github.com/silverstripe/silverstripe-admin/commit/d8bf873e1236ff5cb634cce0a47c992366ae6139) Use bootstrap modal footer, use our icon font for close icon (Luke Edwards) + * 2018-09-12 [c03c685](https://github.com/silverstripe/silverstripe-asset-admin/commit/c03c685da247b8baa6d4180816bda35bc29fcf2d) Use bootstrap modal footer, use our icon font for close icon (Luke Edwards) + * 2018-09-03 [2394194](https://github.com/silverstripe/silverstripe-admin/commit/239419424ee001200830bc8b77dfef3ce7a05d7e) HtmlEditorField component for react rich text (Dylan Wagstaff) + * 2018-08-28 [b5d322d](https://github.com/silverstripe/silverstripe-versioned-admin/commit/b5d322d277efd1fec846f6fca3a92d317542a7ae) HistoryViewerField now uses schemaData to inject necessary input props (Robbie Averill) + * 2018-08-24 [2b335b4](https://github.com/silverstripe/silverstripe-graphql/commit/2b335b4239946f9a6fb1d525452cf1fe6d22a9ce) Proof of concept of cached graphql queries (#166) (Damian Mooyman) + * 2018-08-22 [a257c1c](https://github.com/silverstripe/silverstripe-admin/commit/a257c1cea84ebcdd27df5043c3690bdcb09f43da) Add toggleCallback function (Raissa North) + * 2018-08-22 [8153d12](https://github.com/silverstripe/silverstripe-admin/commit/8153d12537b53bd208ddb28675aa82bd84da2a16) Add dropdownToggleClassName prop to ActionMenu (Raissa North) + * 2018-08-20 [26262ea](https://github.com/silverstripe/silverstripe-admin/commit/26262ea3a9d64e4172ed277845a53e58c48772ef) Insert link shortcut for HTMLEditorField (#599) (Luke Edwards) + * 2018-08-12 [8d3b022](https://github.com/silverstripe/silverstripe-assets/commit/8d3b02259167ce08fd30742475a29219d64df0c8) Support setting quality on a per-image basis (#153) (Loz Calver) + * 2018-08-05 [9d0ae97](https://github.com/silverstripe/silverstripe-admin/commit/9d0ae970e5cdf2ec8710b471884d75b5485b9309) ViewModeToggle states are now stored in constants (Robbie Averill) + * 2018-08-01 [4213eeb](https://github.com/silverstripe/silverstripe-asset-admin/commit/4213eeb1513662ef2dd177065310c2062532a424) Use the new general purpose search component. (#812) (Maxime Rainville) + * 2018-08-01 [5a00d84](https://github.com/silverstripe/silverstripe-admin/commit/5a00d8462ec2806c640f1214971f4747f276d398) General purpose search form component (#572) (Maxime Rainville) + * 2018-07-30 [163ca65](https://github.com/silverstripe/silverstripe-graphql/commit/163ca65a1f2e858c7d47d4b15775153ea0451d5b) DataObjectScaffolder instantiation is now handled through Injector (Robbie Averill) + * 2018-07-30 [24d3023](https://github.com/silverstripe/silverstripe-graphql/commit/24d3023b0d361fee66b4e68aca1814b7dfdbcac2) Allow non-internal input types passed as args (#168) (Aaron Carlino) * 2018-07-25 [79a5ea3](https://github.com/silverstripe/recipe-cms/commit/79a5ea3dace336558ef4d5be4a86cc1a0e84badc) Add versioned-admin (Luke Edwards) + * 2018-07-24 [dee3fc2](https://github.com/silverstripe/silverstripe-versioned-admin/commit/dee3fc286fc7448e24222640167e37ae95ec705b) History Viewer now uses ViewModeToggle to control the preview panel (Robbie Averill) + * 2018-07-24 [064bb9a](https://github.com/silverstripe/silverstripe-versioned-admin/commit/064bb9a6544a78212ba23b946b57454ebed4dbdb) Add behat tests for using compare mode (Robbie Averill) + * 2018-07-23 [a8e0d63](https://github.com/silverstripe/silverstripe-versioned-admin/commit/a8e0d6394456b00a41bf815b1a6c50a6a09e49a2) styles and fixes for styling and code cleanliness (Robbie Averill) + * 2018-07-19 [e029b73](https://github.com/silverstripe/silverstripe-versioned-admin/commit/e029b73106ef9bffa53ba38822e3b5a1ba975cdf) Alter components to allow for compare mode (Dylan Wagstaff) + * 2018-07-19 [75c2c85](https://github.com/silverstripe/silverstripe-versioned-admin/commit/75c2c8572a32139dce267976aa58fb0d765d1a83) Add schema endpoint and shell method for returning compareForm (Robbie Averill) + * 2018-07-18 [db72f6d](https://github.com/silverstripe/silverstripe-versioned-admin/commit/db72f6d1149eabbc902f0725a75c38858e464210) Adjust history viewer to allow for compare mode (Dylan Wagstaff) + * 2018-07-18 [359a260](https://github.com/silverstripe/silverstripe-versioned-admin/commit/359a260ddba5d29fdbc22b154897dd45c0797c3f) Dropdown atop history viewer for holding actions (Raissa North) + * 2018-07-18 [11ea083](https://github.com/silverstripe/silverstripe-versioned-admin/commit/11ea083ba1749722e3fb76685e602e23f7f301ad) Adding a compare mode active notice to the history viewer (Guy Marriott) + * 2018-07-17 [337da78](https://github.com/silverstripe/silverstripe-campaign-admin/commit/337da782bd520beebdb734542b5403b69257d058) Update webpack-config constraint (Raissa North) + * 2018-07-16 [786446fb](https://github.com/silverstripe/silverstripe-reports/commit/786446fb670905832b6bfe49775b9c2eaff262cc) Use Injector to create new class instances and pass $params (Robbie Averill) + * 2018-07-12 [f2ebdb7f](https://github.com/silverstripe/silverstripe-cms/commit/f2ebdb7f5ec894e8c0c8f29b9aac984aef4ca8ed) add SiteTree::updateAnchorsOnPage() for user defining additional page anchors (Will Rossiter) + * 2018-07-12 [114b0a5ea](https://github.com/silverstripe/silverstripe-framework/commit/114b0a5ea7ea6b8f33b8c9b8d1611e5ee6619a1c) Option for secure "remember me" cookie (Ingo Schommer) + * 2018-07-12 [05a6c17](https://github.com/silverstripe/silverstripe-versioned-admin/commit/05a6c17bd3330fe85a74a59c3dcc4a75fc88b75c) Archive admin for managing archived records (#29) (Luke Edwards) + * 2018-07-12 [3292a8b77](https://github.com/silverstripe/silverstripe-framework/commit/3292a8b773c5b29a69b72718f996a36f3daead1d) Add `columnUnique` API SS_List classes. (Al Twohill) + * 2018-07-09 [0fc7660](https://github.com/silverstripe/silverstripe-versioned-admin/commit/0fc766020c1f970c42a173016b98e7dc865516aa) Add diff view form transform for comparisons (Dylan Wagstaff) + * 2018-07-06 [6c1a34c](https://github.com/silverstripe/silverstripe-campaign-admin/commit/6c1a34cc2c9bfbc3bca471c7ca77976bac194918) Make use of ViewModeToggle component. (Raissa North) + * 2018-07-01 [73d3da2](https://github.com/silverstripe/silverstripe-admin/commit/73d3da2bc8566cb1cb5da0124b7deb513728b5ab) Pattern library now has FormAction examples (Robbie Averill) + * 2018-04-15 [5108734](https://github.com/silverstripe/silverstripe-admin/commit/5108734b19edbe9d6404b747e25059a586b009fe) Add ViewModeToggle component (Raissa North) + * 2018-01-30 [1857f00](https://github.com/silverstripe/silverstripe-admin/commit/1857f00bd2b172acb933064d3d454e91bc855200) Add tests for Form component (Robbie Averill) + * 2018-01-30 [cc945f0](https://github.com/silverstripe/silverstripe-admin/commit/cc945f09aac38dea25ec57538c51e6089ddac124) Add tests for CompositeField (Robbie Averill) ### Bugfixes - * 2018-06-15 [5e4ad34](https://github.com/silverstripe/silverstripe-installer/commit/5e4ad341622565cc998bd8537ad3ec7a6a6a7913) Fix incorrect base recipe dependency (Damian Mooyman) + * 2018-11-05 [7fd4a4e](https://github.com/silverstripe/silverstripe-admin/commit/7fd4a4ef76733968ed3babbc14564b2d0f7417b3) Fix duplicate plugins on HTML editor fields (#721) (Luke Edwards) + * 2018-11-05 [4a65d59](https://github.com/silverstripe/silverstripe-admin/commit/4a65d59dc340719f67df1d2c1998ea2645d58473) Fix form changes triggered, GridField add existing (#743) (Luke Edwards) + * 2018-11-02 [97180c261](https://github.com/silverstripe/silverstripe-framework/commit/97180c261258861b5b2b91609a71d044456625d7) Fix readonly grid state always being truthy (#8562) (Luke Edwards) + * 2018-11-02 [12e2cc3](https://github.com/silverstripe/silverstripe-asset-admin/commit/12e2cc37a7a2806310ac7ffd2ad704fb7ad37fe0) Fix duplicate plugins on HTML editor fields (#861) (Luke Edwards) + * 2018-11-02 [d9b1721a](https://github.com/silverstripe/silverstripe-cms/commit/d9b1721ac32829a33f54a9e628680cb3894191b3) Fix duplicate plugins on HTML editor fields (#2307) (Luke Edwards) + * 2018-11-01 [8866e7674](https://github.com/silverstripe/silverstripe-framework/commit/8866e7674a1a9c2be48c8e9532cfcaa667cdf7b5) Fix duplicate plugins on HTML editor fields (#8559) (Luke Edwards) + * 2018-11-01 [55f95b7bc](https://github.com/silverstripe/silverstripe-framework/commit/55f95b7bc8f91384df459bd70c87cacf92225f68) many many through not sorting by join table (#8534) (Michael Strong) + * 2018-10-31 [af7086a](https://github.com/silverstripe/silverstripe-asset-admin/commit/af7086a3adf86b06b6c6b6f938acabf2cfa3352b) Remove outdated CSS Safari hack interfering with the search panel and submit button (Serge Latyntcev) + * 2018-10-31 [2ef7bd29](https://github.com/silverstripe/silverstripe-cms/commit/2ef7bd29754f84fa6eafce08c00d6e1e794713af) IE11+Edge17 Pages tree List View button (Serge Latyntcev) + * 2018-10-30 [3f4d5ae0](https://github.com/silverstripe/silverstripe-cms/commit/3f4d5ae03e9fee10c54f3e628a194d03c07b5c3a) Bypass cached versions to prevent stale state (Aaron Carlino) + * 2018-10-30 [4b0e69a](https://github.com/silverstripe/silverstripe-admin/commit/4b0e69a084028eb60c9d3da90e648acd87946d73) Add aria-expanded to help menu toggle for screenreader accessibility (Sacha Judd) + * 2018-10-30 [2900ac6](https://github.com/silverstripe/silverstripe-admin/commit/2900ac6481b5cca1df1b6708522a60d8a946b790) Remove text-align start with IE supported left (Raissa North) + * 2018-10-29 [f2467d3](https://github.com/silverstripe/silverstripe-admin/commit/f2467d37241ca08b0cc4a112d1dc9054adf891a8) Fix search filtering and clearing (#687) (Luke Edwards) + * 2018-10-26 [3284bf48d](https://github.com/silverstripe/silverstripe-framework/commit/3284bf48d6e3da8b2b1a7831e2d7fe4b401e2fd6) Fix search filtering relations and clear filters (#8477) (Luke Edwards) + * 2018-10-24 [e72fc9e3d](https://github.com/silverstripe/silverstripe-framework/commit/e72fc9e3d0f35a1d43f55f83f9919f67d72fb7cb) DataObject singleton creation (#8516) (Sam Minnée) + * 2018-10-22 [df86335](https://github.com/silverstripe/silverstripe-admin/commit/df863357b1413508f985f6f12a48f5d414a6d75f) Fix decimal search filter not showing up (bergice) + * 2018-10-20 [7f6f5c9ec](https://github.com/silverstripe/silverstripe-framework/commit/7f6f5c9ec9352172f37f8980d823e85c1c39062a) Flush extra methods cache on DataObjects after each unit test class has finished (Robbie Averill) + * 2018-10-19 [311fd62d9](https://github.com/silverstripe/silverstripe-framework/commit/311fd62d9527a47586d90a6f4e2c80922d15d44f) getExtensionInstance can return null, add a case to handle that (Robbie Averill) + * 2018-10-19 [a6855ec](https://github.com/silverstripe/silverstripe-admin/commit/a6855ecf02d41378b1ad03729a103d193aecd853) Remove deprecated help_link definition in testGetHelpLinks (Robbie Averill) + * 2018-10-19 [7c65916](https://github.com/silverstripe/silverstripe-asset-admin/commit/7c659167f2eda63d882a097f2f413b9f3cb79e31) Use fixtured file title in test assertion (Robbie Averill) + * 2018-10-19 [a28e2e183](https://github.com/silverstripe/silverstripe-framework/commit/a28e2e183e1d0684dd32bc7bcf72d4a9c573a8f4) Fix enum filter in Search component from adding `Any` as a filter (bergice) + * 2018-10-18 [e3d0bcb](https://github.com/silverstripe/silverstripe-admin/commit/e3d0bcb051e45d3e7b90dcdf6554e97d173ef6ce) Change one tab not all tabs (Raissa North) + * 2018-10-17 [87a5d07](https://github.com/silverstripe/silverstripe-admin/commit/87a5d07b9f22b894dd9f4397ff50868e662b79b2) Fix body overflow causing scroll bars (Loz Calver) + * 2018-10-17 [d71ee0c](https://github.com/silverstripe/silverstripe-admin/commit/d71ee0ce9898e73c9a7d913356fc6bfe6c2b42fc) Fixes #674 TinyMCE width - this should match form field widths at lower width resolutions but expand up to the max width on wider resolutions (bergice) + * 2018-10-16 [a3d611f](https://github.com/silverstripe/silverstripe-admin/commit/a3d611f0b7f6cd024783a7037245364237329375) Fix `ENTER` not triggering form save button (bergice) + * 2018-10-16 [c35e18110](https://github.com/silverstripe/silverstripe-framework/commit/c35e18110baa72756f2a5378b7e7d4d7803c7c33) Gridfield pagination detected as form change (Luke Edwards) + * 2018-10-16 [5d626fa](https://github.com/silverstripe/silverstripe-admin/commit/5d626fa53b6c67e586d6c6d4d19471709175e8f4) Don’t track gridstate changes as form edits (Luke Edwards) + * 2018-10-16 [a6a174399](https://github.com/silverstripe/silverstripe-framework/commit/a6a17439976710b2311558d363b5467fa429dcca) Fix `ENTER` not triggering form save button as `GridField`s used `submit` type buttons (bergice) + * 2018-10-16 [3d3c407](https://github.com/silverstripe/silverstripe-admin/commit/3d3c407caf099831af0e7b3a6320cad61e5801b0) Fix long gridfield actions overflowing (Luke Edwards) + * 2018-10-15 [ab259af](https://github.com/silverstripe/silverstripe-errorpage/commit/ab259af0707518f94561909c989617e155fd3b1b) Move phpcs to composer dependency, update Travis for it, add 7.2 to Travis (Robbie Averill) + * 2018-10-15 [ab0d7d9](https://github.com/silverstripe/silverstripe-versioned/commit/ab0d7d9e8c18b0e5ae6ee3e352317bbe0c70de53) Fix codesniffer runs in Travis (Robbie Averill) + * 2018-10-14 [c0c446a](https://github.com/silverstripe/silverstripe-versioned/commit/c0c446ad8f29dd66398feb38f5d92fa4f60a4a8b) Fix relations between staged/unstaged objects (Harsh Chokshi) + * 2018-10-11 [0aa2d66](https://github.com/silverstripe/silverstripe-admin/commit/0aa2d6615b7f207791716b6e8654d16940597be4) Use correct lazy loadable class names for GridFieldLazyLoader (Robbie Averill) + * 2018-10-11 [ee21c4201](https://github.com/silverstripe/silverstripe-framework/commit/ee21c42011fd40b2065bb2acb868a427e2232d0a) Re-instate missing SS_DATABASE_SUFFIX functionality (fixes #7966) (Loz Calver) + * 2018-10-11 [4702a22](https://github.com/silverstripe/silverstripe-admin/commit/4702a223ed2c85cd8a55501351526648a70c41b7) Defensively programming some possible failure points (Guy Marriott) + * 2018-10-11 [0db2f84ad](https://github.com/silverstripe/silverstripe-framework/commit/0db2f84ade9b1e8e2811cd7c32bf5f3510544c74) Persist TinyMCE updates when writing with Behat (Guy Marriott) + * 2018-10-10 [e941a56](https://github.com/silverstripe/silverstripe-admin/commit/e941a56b9e918908f16712f2e7be1b43d5810062) Changing the value of a TinyMCE field will correctly trigger a change in the React component (Guy Marriott) + * 2018-10-09 [f710c5c](https://github.com/silverstripe/silverstripe-admin/commit/f710c5cdcd2cf95fdaa738f55c0f2529fcbe826d) Only hide overflow from inactive chosen fields (Robbie Averill) + * 2018-10-09 [56d562193](https://github.com/silverstripe/silverstripe-framework/commit/56d56219345b4a8ba318261af98bcd62f3ce060d) Flush extra_methods statics between test runs (Robbie Averill) + * 2018-10-09 [d1281a571](https://github.com/silverstripe/silverstripe-framework/commit/d1281a571a56dca9d40b59a7baf31d32b09a37f5) Escape HTML in PHPDoc to fix API docs from rendering incorrectly (Robbie Averill) + * 2018-10-09 [522b288](https://github.com/silverstripe/silverstripe-admin/commit/522b28890e6b11b3a324a38c65199f96f86c4b2f) ModelAdmin pagination with a filter (Luke Edwards) + * 2018-10-08 [4766cae](https://github.com/silverstripe/silverstripe-admin/commit/4766cae7918e75c3b47d69487fecdb69b2993077) Retain polyfill for display block style in .collapse.show with Bootstrap 4.1.x (Robbie Averill) + * 2018-10-08 [6e649b57](https://github.com/silverstripe/silverstripe-cms/commit/6e649b570d70d83729527ea8fbbc069426f11338) CMSMain::duplicate() now checks canCreate() but not canEdit() (Robbie Averill) + * 2018-10-08 [c4788803e](https://github.com/silverstripe/silverstripe-framework/commit/c4788803ee7b903bc45541ccc0ef8446cf99922f) Remove unused cacheData prop from #8451 (Robbie Averill) + * 2018-10-08 [884a12c](https://github.com/silverstripe/silverstripe-admin/commit/884a12c864dd856a4beb8e636eb56c46be2dfa2e) Add fix for potential tabnabbing on community help links (Sacha Judd) + * 2018-10-08 [979dd38](https://github.com/silverstripe/silverstripe-assets/commit/979dd385947900b9df48928ad7ba4c2eb7a1361f) Fix migrating files with an incorrect class (Luke Edwards) + * 2018-10-08 [fdb53311b](https://github.com/silverstripe/silverstripe-framework/commit/fdb53311bac68bcee5f6b026c0f526c98ea1da65) Fix linting issue. (Maxime Rainville) + * 2018-10-08 [e06bb05](https://github.com/silverstripe/silverstripe-admin/commit/e06bb051d29c9aceb0d6863637bf038ae0715777) Ensure TinyMCE field changes are persisted before updating redux state (Guy Marriott) + * 2018-10-06 [8c7459a70](https://github.com/silverstripe/silverstripe-framework/commit/8c7459a7082ab3880202a3541bd11ed183465ef1) Fix CompositeField test that relied on a DropdownField bug (Sam Minnee) + * 2018-10-05 [e5d3b28a4](https://github.com/silverstripe/silverstripe-framework/commit/e5d3b28a4d10cb4d960897d37071246532ab8ebc) Don’t break validation on selects without a source. (Sam Minnee) + * 2018-10-05 [98568262f](https://github.com/silverstripe/silverstripe-framework/commit/98568262f2c5d7cc9a9cd39af158d5df7dce12a7) Fixed phpcs violations (Robbie Averill) + * 2018-10-04 [fafd9dad6](https://github.com/silverstripe/silverstripe-framework/commit/fafd9dad6d60731c0ed6695a2df5535ea433632e) fixing name of constant ASSETS_PATH (Philipp Staender) + * 2018-10-04 [0fc06e51e](https://github.com/silverstripe/silverstripe-framework/commit/0fc06e51e5020b8959310682bade02f97653dc73) Drop seconds from DBDatetime::Nice() to restore SS3 behaviour. (Sam Minnee) + * 2018-10-03 [19af1ac](https://github.com/silverstripe/silverstripe-graphql/commit/19af1ac6d77089a5365ed9f1892306fdd943a2ca) Add codesniffer as a dev dependency and use it in Travis (Robbie Averill) + * 2018-10-03 [4668fab](https://github.com/silverstripe/silverstripe-assets/commit/4668fabc3a02d996a0a9be13245cf3ab3fba1079) Shortcode provider does not always request a protected asset grant, add tests for FlysystemAssetStore (Robbie Averill) + * 2018-10-03 [ce9496d](https://github.com/silverstripe/silverstripe-admin/commit/ce9496d2b9bcda50a4e74c386d31bac7c4dc0939) Quote injector alias references, deprecated and removed support for in Symfony 4 (Robbie Averill) + * 2018-10-03 [d535e71](https://github.com/silverstripe/silverstripe-graphql/commit/d535e71ef81165ac6e02c8b27b1e577b3a291b65) Quote injector alias references, deprecated and removed support for in Symfony 4 (Robbie Averill) + * 2018-10-03 [4740346ed](https://github.com/silverstripe/silverstripe-framework/commit/4740346ed8766549f0f948a4396954227f2494bb) Make ArrayList::limit() consistent with DataList::limit() (Sam Minnee) + * 2018-10-03 [0cc72c91a](https://github.com/silverstripe/silverstripe-framework/commit/0cc72c91ada58d6927dab6e93bfe785b623f3e7a) Use DELETE FROM instead of TRUNCATE for clearTable (Sam Minnee) + * 2018-10-02 [5970fc241](https://github.com/silverstripe/silverstripe-framework/commit/5970fc2417bcd29cfc85c209117b7ed6625141ad) Moving test to correct director (Guy Marriott) + * 2018-10-02 [79c2b5ad4](https://github.com/silverstripe/silverstripe-framework/commit/79c2b5ad427f4e95c8fb51b46c4ba31cdf2997c1) Use DELETE FROM instead of TRUNCATE for clearTable (Sam Minnee) + * 2018-10-01 [f2cbc1dfb](https://github.com/silverstripe/silverstripe-framework/commit/f2cbc1dfbb8b1972eeb72d230f7b5cc2ebad26ee) Don’t use USE_FRM in MySQL repair. Fixes #6300. (Sam Minnee) + * 2018-10-01 [638e6ec28](https://github.com/silverstripe/silverstripe-framework/commit/638e6ec2814b4b4cbabd0adc0e166c4812b94740) Throw deprecation notice on limit=0 (Sam Minnee) + * 2018-10-01 [ad87890b2](https://github.com/silverstripe/silverstripe-framework/commit/ad87890b2e92f3f4092bbf9a70ab0d439d40ce31) Don’t change state in ArrayList::getIterator() (Sam Minnee) + * 2018-10-01 [63cabc7](https://github.com/silverstripe/silverstripe-assets/commit/63cabc7fc84f295a95e88f2ce37f940b61b97223) Keep folder Name and Title in sync on update (Luke Edwards) + * 2018-10-01 [5c7b0da](https://github.com/silverstripe/silverstripe-admin/commit/5c7b0da18d894d32f3884fcd2f4e18e8ccd7b629) Searching now allows + symbols, use own method over jQuery serialisation (Robbie Averill) + * 2018-10-01 [71dad5f68](https://github.com/silverstripe/silverstripe-framework/commit/71dad5f68518b9052b657c8dc70d4581fb771e98) Append any fields that don’t match name in insertBefore/insertAfter (Sam Minnee) + * 2018-10-01 [b0c4c5a1](https://github.com/silverstripe/silverstripe-cms/commit/b0c4c5a1775c95e1abd878f233e78b009f5d01ec) Updating SiteTree search fields to work with new search namespacing (Guy Marriott) + * 2018-10-01 [81292c5](https://github.com/silverstripe/silverstripe-asset-admin/commit/81292c52f04690349ea8d3634398faeda2190f8d) Fix outdated data in Apollo GraphQL cache when deleting/moving files (bergice) + * 2018-10-01 [5422e28](https://github.com/silverstripe/silverstripe-asset-admin/commit/5422e28635cec8f285eb422fa85f57f4418c09b8) Folder sort incorrect (Luke Edwards) + * 2018-09-28 [231d6d9a9](https://github.com/silverstripe/silverstripe-framework/commit/231d6d9a9f388e10cf77149aec22e947db648644) New members now receive the configured default locale, not the current locale (Robbie Averill) + * 2018-09-28 [ac1fe5e9d](https://github.com/silverstripe/silverstripe-framework/commit/ac1fe5e9d5de92dbdd7c03c187471fe6b5d8d7c0) joinClass's default_sort is used when nothing else has been set already (Robbie Averill) + * 2018-09-27 [fa4e031](https://github.com/silverstripe/silverstripe-admin/commit/fa4e031ef961215653e315ea441059c6945e5e3b) Update field names in Behat tests for new namespaces (Robbie Averill) + * 2018-09-27 [44b92c90](https://github.com/silverstripe/silverstripe-cms/commit/44b92c90bc18e629a73584a2a2eb0db8a02d740a) Update field names in Behat tests for new search form namespacing (Robbie Averill) + * 2018-09-27 [c54e7317d](https://github.com/silverstripe/silverstripe-framework/commit/c54e7317d2016727b1e2083996fc925fe862e9ab) Avoid having search fields with the same names as form elements (Guy Marriott) + * 2018-09-27 [2e41ea8](https://github.com/silverstripe/silverstripe-admin/commit/2e41ea83b95509ba1f68cf895c49fd846ec15841) Avoid having search fields with the same name as form elements (Guy Marriott) + * 2018-09-25 [dc59bd8](https://github.com/silverstripe/silverstripe-versioned/commit/dc59bd8e5613442b214f951ca16ec376e1ee1cda) Published GraphQL field now correctly indicates whether the record's version is published (Robbie Averill) + * 2018-09-25 [05b372c](https://github.com/silverstripe/silverstripe-versioned/commit/05b372c85f6720c931ac5dceba5fd05a84c74482) Use Hierarchy::prepopulateTreeDataCache() in CMS. (#183) (Sam Minnée) + * 2018-09-25 [5bfc37ff](https://github.com/silverstripe/silverstripe-cms/commit/5bfc37ff4bd22e8bbc02dc4f6dae59d25a4d5e67) Use Hierarchy::prepopulateTreeDataCache() in CMS (#2266) (Sam Minnée) + * 2018-09-24 [0276f6c08](https://github.com/silverstripe/silverstripe-framework/commit/0276f6c089ff5557a36eaf7367c8fc75fc6af20c) Revert semver break in adding GridField type hint to method signature (Robbie Averill) + * 2018-09-24 [f76fb26](https://github.com/silverstripe/silverstripe-asset-admin/commit/f76fb269b3450077623045ce84726dfd60f92894) fix psr (Thomas Portelange) + * 2018-09-24 [5e069ec](https://github.com/silverstripe/silverstripe-versioned/commit/5e069ec85c3b3cb74054b5cc18012531cfe22ce6) fix inferReciprocalComponent called on unsaved (Thomas Portelange) + * 2018-09-24 [9b5425d](https://github.com/silverstripe/silverstripe-graphql/commit/9b5425d5ba8a25eb799743e62733c57eb2837175) Incorrect parameter order of (Guy Marriott) + * 2018-09-24 [a2bb70c46](https://github.com/silverstripe/silverstripe-framework/commit/a2bb70c46dec00a6c9164bcc134e3fdc64a452e9) Don't flush manifests in tests by default (Ingo Schommer) + * 2018-09-21 [1d5ecd342](https://github.com/silverstripe/silverstripe-framework/commit/1d5ecd342e417b4707a3bbc34e97949bffd14afb) Prevent error on valid response status codes (Damian Mooyman) + * 2018-09-20 [9a89aad](https://github.com/silverstripe/silverstripe-admin/commit/9a89aad5df0d5c67a5575a7e20d723cf9d6c4d95) Whitelist nonce parameters from JS resources to be loaded. (Luke Edwards) + * 2018-09-20 [16b3d18](https://github.com/silverstripe/silverstripe-assets/commit/16b3d18ebdf8896ed216faf2a33daa729c6b2c09) FlysystemAssetStore::getAsURL() only grant for protected filesystems (Christopher Darling) + * 2018-09-20 [a9b2443](https://github.com/silverstripe/silverstripe-admin/commit/a9b244349435028c7b55b30475a9fe4d50207fc1) Revert changes to default dropdownToggleClassNames on ActionMenu (Sacha Judd) + * 2018-09-19 [b98c87a6c](https://github.com/silverstripe/silverstripe-framework/commit/b98c87a6c51baa6696ef9f077775f633c4c5ecd4) Ensure existing session can be accessed if headers_sent() (Sam Minnee) + * 2018-09-18 [bbe7c66](https://github.com/silverstripe/silverstripe-asset-admin/commit/bbe7c660cf40d4c942eaf6e76755eeaf46c63471) Add `AssetAdmin::getMinimalistObjectFromData()` to build file metadata for UploadField (#829) (Maxime Rainville) + * 2018-09-18 [db63f55fb](https://github.com/silverstripe/silverstripe-framework/commit/db63f55fbb8e635e4e7215b7b7eff4e1f1cb7b22) Changes being detected on TreeMulti as values not sorted (Luke Edwards) + * 2018-09-17 [d597166](https://github.com/silverstripe/silverstripe-versioned/commit/d5971661cec5fdf12dbfe895fad08ce6bbb05e25) Performance optimisation for draft pages in treeview (Sam Minnee) + * 2018-09-14 [0bab772](https://github.com/silverstripe/silverstripe-versioned-admin/commit/0bab77230efff516d109177ec05b22aec45c8c35) Remove apollo and graphql requirements from module, not implemented (Robbie Averill) + * 2018-09-14 [ac1de94](https://github.com/silverstripe/silverstripe-versioned-admin/commit/ac1de94890e3226ced8019609a48dc22d785b0ff) Versioned-admin now declares its dependency on silverstripe-graphql (Robbie Averill) + * 2018-09-13 [5c102dec](https://github.com/silverstripe/silverstripe-cms/commit/5c102decbde43395e14aeff83a20c4c6f1d048ae) Improve performance of CMSMain::getArchiveWarningMessage (#2231) (Maxime Rainville) + * 2018-09-12 [41c0b8fb](https://github.com/silverstripe/silverstripe-cms/commit/41c0b8fb85b7ac11f18eb1813f9e063e13cbafa2) Fix 'Insert links into a page' test (Luke Edwards) + * 2018-09-10 [fb0d81d](https://github.com/silverstripe/silverstripe-admin/commit/fb0d81d6c02bff292e69a0dcd42e3e01be728c01) Remove action menu toggle styles (Sacha Judd) + * 2018-09-10 [8ae0ef0](https://github.com/silverstripe/silverstripe-versioned/commit/8ae0ef0002a229d233f7395cfed15c979c3f1698) Do not update LeftAndMain link with Stage param (#173) (Maxime Rainville) + * 2018-09-04 [fbd8843](https://github.com/silverstripe/silverstripe-assets/commit/fbd88434cfc89eac7d75e34cdcc48f97821198ff) Remove unnecessary UploadTest\Validator (Sam Minnee) + * 2018-09-04 [40c7a0a](https://github.com/silverstripe/silverstripe-assets/commit/40c7a0aac6390237515cd30d9b23de8e7ad0f5ba) Better error message for invalid upload (Sam Minnee) + * 2018-09-03 [1c4311d](https://github.com/silverstripe/silverstripe-asset-admin/commit/1c4311d4e6548600272daa0ce83afa12cf7e99c3) fix description for docs.silverstripe.org (wernerkrauss) + * 2018-09-03 [b922c0d73](https://github.com/silverstripe/silverstripe-framework/commit/b922c0d7327b5d0222dd280afcb64f83a09ea859) Check scheme is truthy before setting it to the request (Robbie Averill) + * 2018-09-03 [1dd4c7b](https://github.com/silverstripe/silverstripe-versioned-admin/commit/1dd4c7b052d20170c6b5624083ca61f070ee2c49) Text collector translations now compile without errors (Robbie Averill) + * 2018-09-03 [641208dc](https://github.com/silverstripe/silverstripe-siteconfig/commit/641208dcd29a7afaf70c3100a1b02a0bd149b667) Text collector translations now compile without errors (Robbie Averill) + * 2018-09-03 [225445931](https://github.com/silverstripe/silverstripe-framework/commit/22544593101ce670a809f3b354f5ff850840006b) Text collector translations now compile without errors (Robbie Averill) + * 2018-08-31 [f5869a5](https://github.com/silverstripe/silverstripe-campaign-admin/commit/f5869a56ba1c34568d424a2cf71000abfa0ef206) Do not render view mode toggle on campaign toolbar if the campaign is empty (bergice) + * 2018-08-31 [68c2c976d](https://github.com/silverstripe/silverstripe-framework/commit/68c2c976d4813607a420ac4cda7b01f0a7aee8c7) Fix alignment test step definition (#8354) (Luke Edwards) + * 2018-08-30 [234b795f8](https://github.com/silverstripe/silverstripe-framework/commit/234b795f89657c6b25da6101a9fc878e3297c301) Use classes for TinyMCE alignment buttons (Luke Edwards) + * 2018-08-30 [5488b31](https://github.com/silverstripe/silverstripe-admin/commit/5488b31f84f47a18d40cf34d7da53d498b169496) Add explicit `0` z-index to `cms-content` so the menu toggle can render above it (#620) (Andre Kiste) + * 2018-08-30 [463fdef](https://github.com/silverstripe/silverstripe-admin/commit/463fdefde0f827d783f9a519f261d1e4d35c01ab) Remove "more" action icon size, add btn-sm and fix icon alignment in gridfield (Sacha Judd) + * 2018-08-28 [dbfc25302](https://github.com/silverstripe/silverstripe-framework/commit/dbfc253021bce3997af0934b9015215047bbac7b) Fix incorrect version number in 4.3.0 changelog (Loz Calver) + * 2018-08-28 [d1951c94](https://github.com/silverstripe/silverstripe-cms/commit/d1951c946fe143e79ea6a7e1ee55ae90586c8a33) Sort history viewer versions in descending order (Robbie Averill) + * 2018-08-28 [10ef38f](https://github.com/silverstripe/silverstripe-campaign-admin/commit/10ef38f039fa4bf29be764ffd99196b0f9b62554) Hide 1px left border in preview component if we are in 'Preview Only' mode (bergice) + * 2018-08-28 [d651d0fbf](https://github.com/silverstripe/silverstripe-framework/commit/d651d0fbfcababeaf317b27cb00b4f33b9d99eab) Use base class (not remapping target class) when looking up whether object is versioned (Robbie Averill) + * 2018-08-27 [2ab622f](https://github.com/silverstripe/silverstripe-admin/commit/2ab622f88a5a179a7186b46699055cf761d3b749) Fix Add mock store to the loadComponent AppolloProvider (Maxime Rainville) + * 2018-08-27 [4da556923](https://github.com/silverstripe/silverstripe-framework/commit/4da5569232505ee574e0b5106ff2116611393aa4) ensure createFromVariables takes correct params on CLIRequestBuilder (Scott Hutchinson) + * 2018-08-27 [f3230c78](https://github.com/silverstripe/silverstripe-reports/commit/f3230c78d4e3731a10a5f4c508bc68c6a8534866) Use requestVar() to include post vars as well as get vars (Robbie Averill) + * 2018-08-24 [e196475](https://github.com/silverstripe/silverstripe-assets/commit/e196475220e1b97cc61f8e026b55984d2e240e0d) Graceful validation of image shortcode (Aaron Carlino) + * 2018-08-24 [9b7b476](https://github.com/silverstripe/silverstripe-versioned-admin/commit/9b7b47689558f184ed7b448fd81ef80ed0c47926) Label RestoreAction clearer as 'Restore *to* draft' (#42) (Luke Edwards) + * 2018-08-24 [2b16e2a](https://github.com/silverstripe/silverstripe-admin/commit/2b16e2afbaa322ef4ab2fce6c2add9b8f5596ba4) GridField delete button to offer archive action if possible (#602) (Luke Edwards) + * 2018-08-24 [6164d01d6](https://github.com/silverstripe/silverstripe-framework/commit/6164d01d65648ce6b25a7ef82fabaa10b81565d0) GridField delete button to offer archive action if possible (#8325) (Luke Edwards) + * 2018-08-23 [f37dd74](https://github.com/silverstripe/silverstripe-admin/commit/f37dd74be7afae5e40e85ce2a90a4d92bf7e80bb) Site tree items do not disappear on save with source file comments enabled (Robbie Averill) + * 2018-08-23 [d6b1d31](https://github.com/silverstripe/silverstripe-versioned-admin/commit/d6b1d31f7aee8a57d97d387870b7e7c02ba98f58) Check ID is numeric before using it (Robbie Averill) + * 2018-08-22 [1b67bb08c](https://github.com/silverstripe/silverstripe-framework/commit/1b67bb08c8b61ad7e5324ef07eaea2834772b818) Fix failing HTML button test step (Luke Edwards) + * 2018-08-21 [6be253c](https://github.com/silverstripe/silverstripe-versioned-admin/commit/6be253c1b59fd04443591912abeb2dfe8d3dbc2a) Use get_one_by_stage when fetching SiteTree for a specific stage (Guy Marriott) + * 2018-08-21 [7d18f3a](https://github.com/silverstripe/silverstripe-versioned-admin/commit/7d18f3a5ec17a8893d346bc0743071c38d15146c) Disable compare mode when there is only one version available (Robbie Averill) + * 2018-08-20 [6684fc2](https://github.com/silverstripe/silverstripe-versioned-admin/commit/6684fc2d829ea966b36ada7bc5f2cace7a59e7e8) Center alert message and conditionally show close button/selected message for IE (Raissa North) + * 2018-08-20 [dbab69669](https://github.com/silverstripe/silverstripe-framework/commit/dbab6966908f0a293ee6d469cec6b4650dc5a0f1) Message when changing password with invalid token now contains correct links to login (Robbie Averill) + * 2018-08-20 [9da7f99](https://github.com/silverstripe/silverstripe-versioned/commit/9da7f991f33ac16070b2e47b764b216a87f96622) Draft content requiring login message now correctly renders HTML link (Robbie Averill) + * 2018-08-17 [160d595e2](https://github.com/silverstripe/silverstripe-framework/commit/160d595e226edcbaa64a47a0be74193a8b8058cc) fix trailing whitespace (maks) + * 2018-08-17 [16217f365](https://github.com/silverstripe/silverstripe-framework/commit/16217f3655c28ddcf6a721bca82d45d65b91e3ed) fix accidentaly deleted comma (maks) + * 2018-08-17 [c361b09](https://github.com/silverstripe/silverstripe-admin/commit/c361b091b1640c25f1d23914489212fce1e29377) overflow of chosen dropdowns when inactive (Scott Hutchinson) + * 2018-08-16 [81e9c0c](https://github.com/silverstripe/silverstripe-versioned-admin/commit/81e9c0ce3bfad85a9e78d18b4c0d3fc4a245eab2) When exiting compare mode, the version shown is the last selected version to compare from (Robbie Averill) + * 2018-08-16 [61c046c](https://github.com/silverstripe/silverstripe-versioned/commit/61c046c9cd6ce97456b6123a05438c7cd05d07cc) If archive's possible switch GridField delete button with archive (Luke Edwards) + * 2018-08-16 [66cd3af](https://github.com/silverstripe/silverstripe-admin/commit/66cd3af09fcf68bf177a46ac57434442642d1b7c) Filtering or paginating a gridfield causing a change event (Luke Edwards) + * 2018-08-15 [726c464](https://github.com/silverstripe/silverstripe-versioned-admin/commit/726c464461fc3291ef004408d39bc77e8c8f3367) Don't indicate pointer cursor when hovering a selected version in the list (Robbie Averill) + * 2018-08-15 [7d94029](https://github.com/silverstripe/silverstripe-versioned-admin/commit/7d94029547ce8857729967153a3e6c85391e7c54) Inconsistent background and border colours in compare mode notice (Robbie Averill) + * 2018-08-15 [c189f14](https://github.com/silverstripe/silverstripe-versioned-admin/commit/c189f14322f8980f4d3218a64439db916ae32dfe) Reduce size of border radius on compare mode notice close button (Robbie Averill) + * 2018-08-15 [d9154bffb](https://github.com/silverstripe/silverstripe-framework/commit/d9154bffbf7b0031e5bd3ed1f68db3fae6ab5959) text/json is not a valid mimetype (Daniel Hensby) + * 2018-08-15 [d18b5ee](https://github.com/silverstripe/silverstripe-campaign-admin/commit/d18b5eed63e081a4cbcbb30edcf51839a2ae3461) text/json is not a valid mimetype (Daniel Hensby) + * 2018-08-15 [41a2a0c](https://github.com/silverstripe/silverstripe-admin/commit/41a2a0c38c073d82b96fd4fa2fa09bea3b556aa5) text/json is not a mimetype (Daniel Hensby) + * 2018-08-15 [0db594b2d](https://github.com/silverstripe/silverstripe-framework/commit/0db594b2d39c93dd2e911414bee5520c84048906) Remove double escaping of HTML values in print views (Robbie Averill) + * 2018-08-15 [0c713b5](https://github.com/silverstripe/silverstripe-assets/commit/0c713b5b1eb6a08ac00dcadb187b8b3ef7115fc4) Fix routing for files with dots in filename (Damian Mooyman) + * 2018-08-14 [fcaa9ba](https://github.com/silverstripe/silverstripe-versioned/commit/fcaa9ba7a68a839d84ff23d32275c510d0f9890e) Restore and archive action improvements (Luke Edwards) + * 2018-08-14 [fc7f712](https://github.com/silverstripe/silverstripe-admin/commit/fc7f7120a67ce95b03f13b4c8fb90b36f810f7b1) Modal response animation appearing outside the modal (#601) (Luke Edwards) + * 2018-08-14 [873873dc3](https://github.com/silverstripe/silverstripe-framework/commit/873873dc303ce2041aa23e365464133a359e1561) Pass request to dummy controller before calling init (Robbie Averill) + * 2018-08-14 [27ac001d5](https://github.com/silverstripe/silverstripe-framework/commit/27ac001d5b27cce4f80ce4b3335c14708b116830) email rendering should not include requirements (Thomas Portelange) + * 2018-08-14 [8ec551e5](https://github.com/silverstripe/silverstripe-cms/commit/8ec551e57b04d00d6897d06c2779557f0ec8109d) Broken "show as list" (#2232) (Maxime Rainville) + * 2018-08-12 [9f5b0086c](https://github.com/silverstripe/silverstripe-framework/commit/9f5b0086cb1a0259c5c87ea205390c5e69dcae90) Paginating a gridfield causing a change event (Luke Edwards) + * 2018-08-10 [8611e47](https://github.com/silverstripe/silverstripe-versioned-admin/commit/8611e472f62cc7f94fed1e6a9b1239a76cdf008a) Allow more width for version numbers in list, so alignment of state column is unaffected (Raissa North) + * 2018-08-10 [d4995f52](https://github.com/silverstripe/silverstripe-cms/commit/d4995f5204f020f75fbddb3e49b944a54be5c6c2) Separating ModelAsController catch-all route to apply after all other configuration (Guy Marriott) + * 2018-08-08 [5596ae2](https://github.com/silverstripe/silverstripe-versioned-admin/commit/5596ae2ff1b8ee99fa0667b8f0caa7007debb629) Remove new gridfield search, data query needs to be altered to work (Luke Edwards) + * 2018-08-08 [eed1ca9](https://github.com/silverstripe/silverstripe-versioned-admin/commit/eed1ca93d3da1f74c06995bb603e062c4042aaab) Change list item anchor for span with role="button" to justify using tabIndex="0" (Robbie Averill) + * 2018-08-08 [e14ab99](https://github.com/silverstripe/silverstripe-graphql/commit/e14ab991f5c99cee6b1bdfa18ab07a1e4b40961e) Don't rely on return value of GraphQL scaffolding providers (#171) (Guy Marriott) + * 2018-08-08 [aed0726](https://github.com/silverstripe/silverstripe-versioned-admin/commit/aed072680836a30ea482faf1aa8c72b93a7a5eda) Add roles to list and list items, make list items keyboard accessible (Robbie Averill) + * 2018-08-07 [c2b54c7](https://github.com/silverstripe/silverstripe-admin/commit/c2b54c72a990df9d453bedc307f613f23107bfad) graphql route getting overwritten (Aaron Carlino) + * 2018-08-06 [df7396e8](https://github.com/silverstripe/silverstripe-cms/commit/df7396e8845eea7a75e73237de9ee7e4cb6568f6) CMS routes are now run after #coreroutes without re-including itself (Robbie Averill) + * 2018-08-06 [2614804](https://github.com/silverstripe/silverstripe-versioned-admin/commit/26148046d967ddc80df76b4e280731ed338bb8df) getLatestVersion now looks at the LatestDraftVersion version property (Robbie Averill) + * 2018-08-06 [e7cb0156](https://github.com/silverstripe/silverstripe-cms/commit/e7cb0156c69a3701b248dbbae4e72f8c0b372efd) Use LatestDraftVersion in GraphQL query to determine latest draft version (Robbie Averill) + * 2018-08-06 [13372f9a3](https://github.com/silverstripe/silverstripe-framework/commit/13372f9a37d1cb19f658404c79c2be6fbfa557b1) Installer redirect to home/ (without domain) (Michael Strong) + * 2018-08-06 [855e1f2](https://github.com/silverstripe/silverstripe-versioned-admin/commit/855e1f2ec7eb03674d3a3d3f4b051ae86924d949) Margins are correct when both in and out of a GridField, and colours used in compare warning (Robbie Averill) + * 2018-08-02 [24927c5](https://github.com/silverstripe/silverstripe-campaign-admin/commit/24927c5aa18f9adc8ef79f0adf879f6bcd5c130c) Ensure only toolbar buttons that are immediate descendants of toolbars are given margins (Robbie Averill) + * 2018-08-01 [a981584](https://github.com/silverstripe/silverstripe-admin/commit/a9815845c0e923587fa81bdbac77be43f6d4dd1a) Remove rogue CSS margin on toolbar buttons. Implemented in campaign-admin preview instead. (Robbie Averill) + * 2018-08-01 [405d8a3](https://github.com/silverstripe/silverstripe-campaign-admin/commit/405d8a3213852f4a35e7cf8101df72c206d1b2f9) Toolbar button margins are constrained to campaign previews, and update ViewModeActions name (Robbie Averill) + * 2018-08-01 [6889a1a](https://github.com/silverstripe/silverstripe-admin/commit/6889a1adf0005e91288c4e2ddc6a2f3ea1b6b593) ViewModeToggle now uses BEM class naming convention (Robbie Averill) + * 2018-08-01 [58d128e](https://github.com/silverstripe/silverstripe-versioned-admin/commit/58d128ebe02223a4b9412673140441a5e3ec7a5f) Update case on viewModeActions import and recompile (Robbie Averill) + * 2018-08-01 [8e611ac](https://github.com/silverstripe/silverstripe-versioned-admin/commit/8e611acd818aea731548edb9dcaee19ab2920e9b) Convert compareType to use version objects, restructure tests and use variables for colours (Robbie Averill) + * 2018-08-01 [763138f](https://github.com/silverstripe/silverstripe-versioned-admin/commit/763138fdd4eb2bb84019b437cb7f373c2fe540ca) remove double-margins from version comparison (Dylan Wagstaff) + * 2018-07-30 [2758348](https://github.com/silverstripe/silverstripe-versioned-admin/commit/2758348be0b84fdda27d0dbf5ee4078eece6929e) fix rebase (Dylan Wagstaff) + * 2018-07-30 [420c3f8](https://github.com/silverstripe/silverstripe-asset-admin/commit/420c3f807d7426af2c76d778faa4ef26ab5dda11) Editor should ignore drag-and-drop files (#814) (Luke Edwards) + * 2018-07-30 [fde7b9ddc](https://github.com/silverstripe/silverstripe-framework/commit/fde7b9ddc5da697395897249819b3c52530692b6) Specify minimum composer version (Maxime Rainville) + * 2018-07-27 [85b4b48fb](https://github.com/silverstripe/silverstripe-framework/commit/85b4b48fb5489cdba4b18cbf510d883986dd61c1) Restore default delete action on GridFieldConfig_RecordEditor (Maxime Rainville) + * 2018-07-27 [67254da1](https://github.com/silverstripe/silverstripe-reports/commit/67254da18599f0fe86921098524ec3303d9de41e) Apply missing class to report header. (Maxime Rainville) + * 2018-07-27 [0d90cdb05](https://github.com/silverstripe/silverstripe-framework/commit/0d90cdb05d058763e5e52720ab653c5cc391dc3b) Altering ID of authenticator tabs to resolve ID conflict (Guy Marriott) + * 2018-07-26 [900ca9c8d](https://github.com/silverstripe/silverstripe-framework/commit/900ca9c8d75b70b13b425365022ec3f1f0ebe461) Recommend install of upgrader with PHAR exec. (Maxime Rainville) + * 2018-07-26 [fea9ef7](https://github.com/silverstripe/silverstripe-admin/commit/fea9ef7d2a53904086f9fad6eedba7bb307c8578) #579 BUG Ambiguous column RecordID when doing batch actions (Ed Linklater) + * 2018-07-25 [0035f4a90](https://github.com/silverstripe/silverstripe-framework/commit/0035f4a90728d9e109b12585b32491a2afeaa916) Fix backtick in changelog breaking sentence formatting (Michal Kleiner) + * 2018-07-24 [3e044dc](https://github.com/silverstripe/silverstripe-versioned-admin/commit/3e044dcba6c558a42b2afa4e52c484a0fbb7e2a2) History viewer list is now rendered as an unordered list instead of a table (Robbie Averill) + * 2018-07-24 [6c421b5](https://github.com/silverstripe/silverstripe-versioned-admin/commit/6c421b5ef15f2ffd21ec8d36ebbc7de2cb034ea0) Badge margin is moved to the text to allow it to break lines nicely on mobile (Robbie Averill) + * 2018-07-23 [a0487e5](https://github.com/silverstripe/silverstripe-admin/commit/a0487e59fc04af0d15e66d4c2874051288b4e63e) Treat readonly as disabled and fix handling for ui-constructive class (Robbie Averill) + * 2018-07-23 [55dc009](https://github.com/silverstripe/silverstripe-versioned-admin/commit/55dc0095b992cae9500109a94f0050bf797b7099) SET_COMPARE_MODE reducer no longer clears previously set compareFrom value (Robbie Averill) + * 2018-07-23 [35afd7f](https://github.com/silverstripe/silverstripe-versioned-admin/commit/35afd7fbb2f0bd330fc68fc9b52197ccd024be63) Padding and scrolling around detail views is now consistent (Robbie Averill) + * 2018-07-23 [9ac05c7](https://github.com/silverstripe/silverstripe-versioned-admin/commit/9ac05c7c8fd2438ebb9f8a3941197c46cd46ed65) Set min-width for compare enabled table to 100% to prevent margin overflow (Robbie Averill) + * 2018-07-23 [9d26ba0](https://github.com/silverstripe/silverstripe-versioned-admin/commit/9d26ba0a71633b0afc455563eac19137579186b9) Fix margins when choosing versions to compare (#11) (Raissa North) + * 2018-07-23 [92eab59](https://github.com/silverstripe/silverstripe-versioned-admin/commit/92eab5969d664531efb909ffee54476fb052c4f7) selecting two rows in list view UI issues (Guy Marriott) + * 2018-07-16 [e1296d48](https://github.com/silverstripe/silverstripe-reports/commit/e1296d4813ac1b677aa7a612ba0ad3b2ba62ccae) Filter var can be returned correctly from get variables as a fallback (Robbie Averill) + * 2018-07-13 [d1024ee](https://github.com/silverstripe/silverstripe-assets/commit/d1024ee00b12c3a212fe12d168f4521e2188274b) Add HTMLFragment casting to $Tag (#148) (Jake Bentvelzen) + * 2018-07-12 [a8e5616](https://github.com/silverstripe/silverstripe-admin/commit/a8e56166f01ef0fb8ecd46edb4eddde30447cdb0) Update GridField.js so it works with new Archive View (#559) (Luke Edwards) + * 2018-07-12 [599a4420b](https://github.com/silverstripe/silverstripe-framework/commit/599a4420bf0d982343faa6145afaf6592566bb40) Improve GridFieldViewButton to work with new Archive Admin (#8240) (Luke Edwards) + * 2018-07-11 [c2347310](https://github.com/silverstripe/silverstripe-cms/commit/c23473103e4f93b82d60f1260923c9e413d02c41) URLSegment field styling fixes #2193 (Maxime Rainville) + * 2018-07-05 [91068c23b](https://github.com/silverstripe/silverstripe-framework/commit/91068c23b5cb448fe63ae9f40875a8f0818dbe1f) Make column query not distinct (Al Twohill) + * 2018-07-05 [730fc42](https://github.com/silverstripe/silverstripe-admin/commit/730fc42ef3e6d25345516f1583d05bf968bf762c) Fix test for password recovery (Ingo Schommer) + * 2018-07-01 [3262665b2](https://github.com/silverstripe/silverstripe-framework/commit/3262665b2d6bb6d56186be2e2e370b853f13c6b5) Fix link and turn of phrase. (Maxime Rainville) + * 2018-06-29 [cc9b36e01](https://github.com/silverstripe/silverstripe-framework/commit/cc9b36e01124349ea7ccbd0902d1b01c764f82f7) fix link (Lukas) + * 2018-06-27 [8ccebf8](https://github.com/silverstripe/silverstripe-admin/commit/8ccebf813e95980363a92ec37332d2241327441f) Stop sslink from hijacking anchor plugin (Will Rossiter) + * 2018-06-22 [f9de357](https://github.com/silverstripe/silverstripe-admin/commit/f9de35724c4fb75c8a6d38e4f5b9185531fc961c) - Grid field headers misaligned (Petar Simic) + * 2018-06-18 [2f1c2992f](https://github.com/silverstripe/silverstripe-framework/commit/2f1c2992f8f61b4a87a0a363db289a19ac5a821b) Default cache state should be `no-cache` (Daniel Hensby) + * 2018-06-17 [25b0a18](https://github.com/silverstripe/silverstripe-admin/commit/25b0a18a743e81951a2d2df387b6e5442d0253c3) Fix display of GridField link existing button (Luke Edwards) + * 2018-05-18 [953153500](https://github.com/silverstripe/silverstripe-framework/commit/953153500d490f5b5abf7283c34242c3b22a855a) Polymorphic relationship class columns have obsolete class names remapped (Robbie Averill) + * 2018-05-08 [97a8f56](https://github.com/silverstripe/silverstripe-admin/commit/97a8f56c43ddb3c77a5bbc452755d44afb9a9472) Add missing focus styles for preview options (fixes silverstripe/silverstripe-framework #2101) (Loz Calver) \ No newline at end of file From 3f321f935a0b7faa3d4ef788b4f69425db979095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Werner=20M=2E=20Krau=C3=9F?= Date: Wed, 7 Nov 2018 17:01:36 +0100 Subject: [PATCH 062/175] Convert::memstring2bytes should return integer value bytes are by nature an integer fixes #8572 --- src/Core/Convert.php | 6 +++--- tests/php/Core/ConvertTest.php | 32 ++++++++++++++++---------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Core/Convert.php b/src/Core/Convert.php index 7c8717724..3e4158111 100644 --- a/src/Core/Convert.php +++ b/src/Core/Convert.php @@ -556,7 +556,7 @@ class Convert * Preserves integer values like "1024" or "-1" * * @param string $memString A memory limit string, such as "64M" - * @return float + * @return int */ public static function memstring2bytes($memString) { @@ -568,10 +568,10 @@ class Convert if ($unit) { // Find the position of the unit in the ordered string which is the power // of magnitude to multiply a kilobyte by - return round($size * pow(1024, stripos('bkmgtpezy', $unit[0]))); + return (int)round($size * pow(1024, stripos('bkmgtpezy', $unit[0]))); } - return round($size); + return (int)round($size); } /** diff --git a/tests/php/Core/ConvertTest.php b/tests/php/Core/ConvertTest.php index 679c611e7..d1c1139d3 100644 --- a/tests/php/Core/ConvertTest.php +++ b/tests/php/Core/ConvertTest.php @@ -2,12 +2,12 @@ namespace SilverStripe\Core\Tests; +use Exception; +use InvalidArgumentException; use SilverStripe\Core\Convert; use SilverStripe\Dev\SapphireTest; use SilverStripe\View\Parsers\URLSegmentFilter; use stdClass; -use Exception; -use InvalidArgumentException; /** * Test various functions on the {@link Convert} class. @@ -576,15 +576,15 @@ XML public function memString2BytesProvider() { return [ - ['-1', (float)-1], - ['2048', (float)(2 * 1024)], - ['2k', (float)(2 * 1024)], - ['512M', (float)(512 * 1024 * 1024)], - ['512MiB', (float)(512 * 1024 * 1024)], - ['512 mbytes', (float)(512 * 1024 * 1024)], - ['512 megabytes', (float)(512 * 1024 * 1024)], - ['1024g', (float)(1024 * 1024 * 1024 * 1024)], - ['1024G', (float)(1024 * 1024 * 1024 * 1024)] + 'infinite' => ['-1', -1], + 'integer' => ['2048', 2 * 1024], + 'kilo' => ['2k', 2 * 1024], + 'mega' => ['512M', 512 * 1024 * 1024], + 'MiB' => ['512MiB', 512 * 1024 * 1024], + 'mbytes' => ['512 mbytes', 512 * 1024 * 1024], + 'megabytes' => ['512 megabytes', 512 * 1024 * 1024], + 'giga' => ['1024g', 1024 * 1024 * 1024 * 1024], + 'G' => ['1024G', 1024 * 1024 * 1024 * 1024] ]; } @@ -607,11 +607,11 @@ XML { return [ [200, '200B'], - [(2 * 1024), '2K'], - [(512 * 1024 * 1024), '512M'], - [(512 * 1024 * 1024 * 1024), '512G'], - [(512 * 1024 * 1024 * 1024 * 1024), '512T'], - [(512 * 1024 * 1024 * 1024 * 1024 * 1024), '512P'] + [2 * 1024, '2K'], + [512 * 1024 * 1024, '512M'], + [512 * 1024 * 1024 * 1024, '512G'], + [512 * 1024 * 1024 * 1024 * 1024, '512T'], + [512 * 1024 * 1024 * 1024 * 1024 * 1024, '512P'] ]; } } From bc0f17fb0972749cf5ca7b0b1815a7f330727e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Werner=20M=2E=20Krau=C3=9F?= Date: Wed, 7 Nov 2018 17:45:01 +0100 Subject: [PATCH 063/175] Cleanup Convert * remove unneeded parenthesis and double quotes * simplify flow / remove unneeded else --- src/Core/Convert.php | 84 +++++++++++++++++----------------- tests/php/Core/ConvertTest.php | 20 ++++---- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/Core/Convert.php b/src/Core/Convert.php index 313d64aae..3ff3336ce 100644 --- a/src/Core/Convert.php +++ b/src/Core/Convert.php @@ -70,9 +70,9 @@ class Convert } return $val; - } else { - return self::raw2att($val); } + + return self::raw2att($val); } /** @@ -93,16 +93,16 @@ class Convert } return $val; - } else { - return trim( - preg_replace( - '/_+/', - '_', - preg_replace('/[^a-zA-Z0-9\-_:.]+/', '_', $val) - ), - '_' - ); } + + return trim( + preg_replace( + '/_+/', + '_', + preg_replace('/[^a-zA-Z0-9\-_:.]+/', '_', $val) + ), + '_' + ); } /** @@ -119,9 +119,9 @@ class Convert $val[$k] = self::raw2xml($v); } return $val; - } else { - return htmlspecialchars($val, ENT_QUOTES, 'UTF-8'); } + + return htmlspecialchars($val, ENT_QUOTES, 'UTF-8'); } /** @@ -137,14 +137,14 @@ class Convert $val[$k] = self::raw2js($v); } return $val; - } else { - return str_replace( - // Intercepts some characters such as <, >, and & which can interfere - array("\\", '"', "\n", "\r", "'", "<", ">", "&"), - array("\\\\", '\"', '\n', '\r', "\\'", "\\x3c", "\\x3e", "\\x26"), - $val - ); } + + return str_replace( + // Intercepts some characters such as <, >, and & which can interfere + array("\\", '"', "\n", "\r", "'", '<', '>', '&'), + array("\\\\", '\"', '\n', '\r', "\\'", "\\x3c", "\\x3e", "\\x26"), + $val + ); } /** @@ -195,13 +195,13 @@ class Convert $val[$k] = self::raw2sql($v, $quoted); } return $val; - } else { - if ($quoted) { - return DB::get_conn()->quoteString($val); - } else { - return DB::get_conn()->escapeString($val); - } } + + if ($quoted) { + return DB::get_conn()->quoteString($val); + } + + return DB::get_conn()->escapeString($val); } /** @@ -233,14 +233,14 @@ class Convert $val[$k] = self::xml2raw($v); } return $val; - } else { - // More complex text needs to use html2raw instead - if (strpos($val, '<') !== false) { - return self::html2raw($val); - } else { - return html_entity_decode($val, ENT_QUOTES, 'UTF-8'); - } } + + // More complex text needs to use html2raw instead + if (strpos($val, '<') !== false) { + return self::html2raw($val); + } + + return html_entity_decode($val, ENT_QUOTES, 'UTF-8'); } /** @@ -332,7 +332,7 @@ class Convert $xml = get_object_vars($xml); } if (is_array($xml)) { - if (count($xml) == 0) { + if (count($xml) === 0) { return (string)$x; } // for CDATA $r = []; @@ -359,9 +359,9 @@ class Convert { if (preg_match('/^[a-z+]+\:\/\/[a-zA-Z0-9$-_.+?&=!*\'()%]+$/', $string)) { return "$string"; - } else { - return $string; } + + return $string; } /** @@ -387,8 +387,8 @@ class Convert $config = $defaultConfig; } - $data = preg_replace("/][^>]*)?>.*?<\/style[^>]*>/is", "", $data); - $data = preg_replace("/][^>]*)?>.*?<\/script[^>]*>/is", "", $data); + $data = preg_replace("/][^>]*)?>.*?<\/style[^>]*>/is", '', $data); + $data = preg_replace("/][^>]*)?>.*?<\/script[^>]*>/is", '', $data); if ($config['ReplaceBoldAsterisk']) { $data = preg_replace('%<(strong|b)( [^>]*)?>|%i', '*', $data); @@ -412,7 +412,7 @@ class Convert // Compress whitespace if ($config['CompressWhitespace']) { - $data = preg_replace("/\s+/u", " ", $data); + $data = preg_replace("/\s+/u", ' ', $data); } // Parse newline tags @@ -421,9 +421,9 @@ class Convert $data = preg_replace("/\s*<[Dd][Ii][Vv]([^A-Za-z0-9>][^>]*)?> */u", "\n\n", $data); $data = preg_replace("/\n\n\n+/", "\n\n", $data); - $data = preg_replace("/<[Bb][Rr]([^A-Za-z0-9>][^>]*)?> */", "\n", $data); - $data = preg_replace("/<[Tt][Rr]([^A-Za-z0-9>][^>]*)?> */", "\n", $data); - $data = preg_replace("/<\/[Tt][Dd]([^A-Za-z0-9>][^>]*)?> */", " ", $data); + $data = preg_replace('/<[Bb][Rr]([^A-Za-z0-9>][^>]*)?> */', "\n", $data); + $data = preg_replace('/<[Tt][Rr]([^A-Za-z0-9>][^>]*)?> */', "\n", $data); + $data = preg_replace("/<\/[Tt][Dd]([^A-Za-z0-9>][^>]*)?> */", ' ', $data); $data = preg_replace('/<\/p>/i', "\n\n", $data); // Replace HTML entities diff --git a/tests/php/Core/ConvertTest.php b/tests/php/Core/ConvertTest.php index af8d454c1..a37fbc81c 100644 --- a/tests/php/Core/ConvertTest.php +++ b/tests/php/Core/ConvertTest.php @@ -133,7 +133,7 @@ class ConvertTest extends SapphireTest $this->assertEquals( "That's absolutely correct", Convert::html2raw($val7), - "Single quotes are decoded correctly" + 'Single quotes are decoded correctly' ); $val8 = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor ' . 'incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud ' . 'exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute ' . 'irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla ' . 'pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia ' . 'deserunt mollit anim id est laborum.'; @@ -280,7 +280,7 @@ PHP protected function assertEqualsQuoted($expected, $actual) { $message = sprintf( - "Expected \"%s\" but given \"%s\"", + 'Expected "%s" but given "%s"', addcslashes($expected, "\r\n"), addcslashes($actual, "\r\n") ); @@ -296,8 +296,8 @@ PHP foreach (array("\r\n", "\r", "\n") as $nl) { // Base case: no action $this->assertEqualsQuoted( - "Base case", - Convert::nl2os("Base case", $nl) + 'Base case', + Convert::nl2os('Base case', $nl) ); // Mixed formats @@ -426,7 +426,7 @@ XML // Test without doctype validation $expected = array( 'result' => array( - "Now include SOME_SUPER_LONG_STRING lots of times to expand the in-memory size of this XML structure", + 'Now include SOME_SUPER_LONG_STRING lots of times to expand the in-memory size of this XML structure', array( 'long' => array( array( @@ -607,11 +607,11 @@ XML { return [ [200, '200B'], - [(2 * 1024), '2K'], - [(512 * 1024 * 1024), '512M'], - [(512 * 1024 * 1024 * 1024), '512G'], - [(512 * 1024 * 1024 * 1024 * 1024), '512T'], - [(512 * 1024 * 1024 * 1024 * 1024 * 1024), '512P'] + [2 * 1024, '2K'], + [512 * 1024 * 1024, '512M'], + [512 * 1024 * 1024 * 1024, '512G'], + [512 * 1024 * 1024 * 1024 * 1024, '512T'], + [512 * 1024 * 1024 * 1024 * 1024 * 1024, '512P'] ]; } From 4b4fbabed5d70bf577e4b0d6fdbc9dab9da80451 Mon Sep 17 00:00:00 2001 From: Serge Latyntcev Date: Thu, 8 Nov 2018 14:36:16 +1300 Subject: [PATCH 064/175] FIX TreeMultiselectField passes value 'unchanged' as null to ORM for 'ID' column key --- src/Forms/TreeMultiselectField.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Forms/TreeMultiselectField.php b/src/Forms/TreeMultiselectField.php index 72de4dc11..1c0166f64 100644 --- a/src/Forms/TreeMultiselectField.php +++ b/src/Forms/TreeMultiselectField.php @@ -261,4 +261,30 @@ class TreeMultiselectField extends TreeDropdownField $copy->setTitleField($this->getTitleField()); return $copy; } + + /** + * {@inheritdoc} + * + * @deprecated 4.0..5.0 + */ + protected function objectForKey($key) + { + /** + * Fixes https://github.com/silverstripe/silverstripe-framework/issues/8332 + * + * Due to historic reasons, the default (empty) value for this field is 'unchanged', even though + * the field is usually integer on the database side. + * MySQL handles that gracefully and returns an empty result in that case, + * whereas some other databases (e.g. PostgreSQL) do not support comparison + * of numeric types with string values, issuing a database error. + * + * This fix is not ideal, but supposed to keep backward compatibility for SS4. + * Since SS5 this method should be removed and NULL should be used instead of 'unchanged'. + */ + if ($this->getKeyField() ==='ID' && $key === 'unchanged') { + $key = null; + } + + return parent::objectForKey($key); + } } From 1db568cdb6bf7f6d0438b83b95d76f77999fc0f2 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Thu, 8 Nov 2018 16:25:41 +1300 Subject: [PATCH 065/175] Update translations --- lang/af.yml | 3 - lang/ar.yml | 8 -- lang/az.yml | 3 - lang/bg.yml | 8 -- lang/bs.yml | 3 - lang/ca.yml | 3 - lang/cs.yml | 8 -- lang/da.yml | 319 ++++++++++++++++++++++++++++++++++++++++++- lang/de.yml | 8 -- lang/de_DE.yml | 159 +++++++++++++++++++++ lang/en.yml | 2 - lang/eo.yml | 19 +-- lang/es.yml | 8 -- lang/es_AR.yml | 3 - lang/es_MX.yml | 2 - lang/et_EE.yml | 8 -- lang/fa_IR.yml | 6 - lang/fi.yml | 20 +-- lang/fo.yml | 3 - lang/fr.yml | 20 +-- lang/gl_ES.yml | 2 - lang/hr.yml | 5 - lang/id.yml | 8 -- lang/id_ID.yml | 8 -- lang/is.yml | 3 - lang/it.yml | 23 +--- lang/ja.yml | 8 -- lang/lt.yml | 8 -- lang/lv.yml | 3 - lang/mi.yml | 8 -- lang/nb.yml | 8 -- lang/nl.yml | 152 +++++++++++++++++++-- lang/pl.yml | 207 ++++++++++++++++++++++++++-- lang/pt.yml | 3 - lang/pt_BR.yml | 3 - lang/ro.yml | 2 - lang/ru.yml | 8 -- lang/sk.yml | 10 -- lang/sl.yml | 8 -- lang/sl_SI.yml | 5 - lang/sr.yml | 8 -- lang/sr@latin.yml | 8 -- lang/sr_RS.yml | 8 -- lang/sr_RS@latin.yml | 8 -- lang/sv.yml | 137 +++++++++++++++++-- lang/th.yml | 4 - lang/tr.yml | 3 - lang/uk.yml | 3 - lang/zh.yml | 8 -- 49 files changed, 962 insertions(+), 320 deletions(-) create mode 100644 lang/de_DE.yml 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..518bf1344 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: العملة @@ -155,7 +150,4 @@ ar: LOGIN: دخول LOSTPASSWORDHEADER: 'كلمة مرور مفقودة' NOTEPAGESECURED: 'هذه الصفحة محمية بكلمة مرور ، أدخل بيانات دخولك بالأسفل ليتم السماح لك بالوصول للصفحة' - NOTERESETLINKINVALID: "

رابط إعادة تعيين كلمة المرور غير صحيح أو نفذت صلاحيته.

\n

\nيمكنك طلب رابط جديد <\"{a href=\"{link1\"> هنا \n أو تغيير كلمة المرور الخاصة بك بعد <\"{a href=\"{link2\"> تسجيل دخولك.\n

" NOTERESETPASSWORD: 'أدخل بريدك الإلكتروني و سيتم إرسال رابط إعادة تهيئة كلمة المرور ' - PASSWORDSENTHEADER: 'رابط استعادة كلمة المرور تم إرساله إلى ''{بريدك}''' - PASSWORDSENTTEXT: 'شكرا لك! تم إرسال رابط إعادة تعيين إلى ''{بريدك}''، بشرط وجود حساب قائم بالنسبة لعنوان هذا البريد الإلكتروني .' 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..241841f1e 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: Страница @@ -318,7 +313,4 @@ bg: LOGOUT: Изход LOSTPASSWORDHEADER: 'Забравена парола' NOTEPAGESECURED: 'Тази страница е защитена. Въведете вашите данни по-долу, за да продължите.' - NOTERESETLINKINVALID: '

Връзката за нулиране на парола не е вярна или е просрочена.

Можете да заявите нова тук или да промените паролата си след като влезете.

' NOTERESETPASSWORD: 'Въведете вашият email адрес и ще ви изпратим линк, с който ще можете да смените паролата си' - PASSWORDSENTHEADER: 'Връзка за нулиране на парола беше изпратена на ''{email}''' - PASSWORDSENTTEXT: 'Благодарим ви! Връзка за нулиране на паролата беше изпратен на ''{email}'', ако съществува акаунт с този имейл адрес.' 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..759f0268e 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 @@ -199,7 +194,4 @@ cs: LOGIN: Přihlásit LOSTPASSWORDHEADER: 'Zapomenuté heslo' NOTEPAGESECURED: 'Tato stránka je zabezpečená. Vložte své přihlašovací údaje a my Vám zároveň pošleme práva.' - NOTERESETLINKINVALID: '

Odkaz na resetování hesla není platný nebo je prošlý.

Můžete požádat o nový zde nebo změňte své heslo až se přihlásíte.

' NOTERESETPASSWORD: 'Zadejte svou e-mailovou adresu a bude vám zaslán nulovací odkaz pro Vaše heslo' - PASSWORDSENTHEADER: 'Odkaz na resetování hesla byl odeslán na ''{email}''' - PASSWORDSENTTEXT: 'Děkujeme! Resetovací odkaz byl odeslán na ''{email}'', pokud účet existuje pro tuto emailovou adresu.' diff --git a/lang/da.yml b/lang/da.yml index 8dc512b23..e203c1876 100644 --- a/lang/da.yml +++ b/lang/da.yml @@ -1,5 +1,322 @@ da: + SilverStripe\Admin\LeftAndMain: + VersionUnknown: ukendt + SilverStripe\AssetAdmin\Forms\UploadField: + Dimensions: Dimensioner + EDIT: Rediger + EDITINFO: 'Rediger denne fil' + REMOVE: Fjern + SilverStripe\Control\ChangePasswordEmail_ss: + CHANGEPASSWORDFOREMAIL: 'Koden for kontoen med email addressen {email} er ændret. Hvis du ikke har skiftet din kode, så skift venligst din kode ved at klikke på linket herunder' + CHANGEPASSWORDTEXT1: 'Du skiftede dit kodeord for' + CHANGEPASSWORDTEXT3: 'Skift kodeord' + HELLO: Hej + SilverStripe\Control\Email\ForgotPasswordEmail_ss: + HELLO: Hej + TEXT1: 'Her er din' + TEXT2: 'link til at nulstille dit kodeord' + TEXT3: for + SilverStripe\Control\RequestProcessor: + INVALID_REQUEST: 'Ugyldig forespørgsel' + REQUEST_ABORTED: 'Forespørgsel annulleret' + SilverStripe\Core\Manifest\VersionProvider: + VERSIONUNKNOWN: Ukendt + SilverStripe\Forms\CheckboxField: + NOANSWER: Nej + YESANSWER: Ja + SilverStripe\Forms\CheckboxSetField_ss: + NOOPTIONSAVAILABLE: 'Ingen tilgængelige muligheder' + SilverStripe\Forms\ConfirmedPasswordField: + ATLEAST: 'Kodeord skal være mindst {min} tegn lang.' + BETWEEN: 'Kodeord skal være {min} til {max} karakterer lang.' + CURRENT_PASSWORD_ERROR: 'Det nuværende kodeord du har indtastet er ikke korrekt.' + CURRENT_PASSWORD_MISSING: 'Du skal indtaste dit nuværende kodeord.' + LOGGED_IN_ERROR: 'Du skal være logget ind for at skifte dit kodeord.' + MAXIMUM: 'Kodeord må maks være {max} tegn lang' + SHOWONCLICKTITLE: 'Skift kodeord' + SilverStripe\Forms\CurrencyField: + CURRENCYSYMBOL: DKK + SilverStripe\Forms\DateField: + VALIDDATEFORMAT2: 'Indtats venligst et gyldigt datoformat ({format})' + VALIDDATEMAXDATE: 'Din dato skal være ældre end eller matche den maksimalt tilladte dato ({date})' + VALIDDATEMINDATE: 'Din dato skal være yngre end eller matche den minimum tilladte dato ({date})' + SilverStripe\Forms\DatetimeField: + VALIDDATEMAXDATETIME: 'Din dato skal være ældre end eller matche den maksimalt tilladte dato ({datetime})' + VALIDDATETIMEFORMAT: 'Indtats venligst et gyldigt dato- og tidsformat ({format})' + VALIDDATETIMEMINDATE: 'Din dato skal være yngre end eller matche den minimum tilladte dato og tid ({datetime})' + SilverStripe\Forms\DropdownField: + CHOOSE: (Vælg) + CHOOSE_MODEL: '(Vælg {name})' + SOURCE_VALIDATION: 'Venligst vælg en eksisterende værdi fra listen. {value} er ikke en tilladt mulighed' + SilverStripe\Forms\EmailField: + VALIDATION: 'Indtast venligst en emailadresse' + SilverStripe\Forms\FileUploadReceiver: + FIELDNOTSET: 'Fil information ikke fundet' + SilverStripe\Forms\Form: + BAD_METHOD: 'Denne form kræver en {method} indsendelse' + CSRF_EXPIRED_MESSAGE: 'Din session er udløbet. Venligst gensend formularen.' + CSRF_FAILED_MESSAGE: 'Det ser ud til der har været et teknisk problem. Klik venligst på tilbageknappen, tryk opdater i din browser og prøv igen.' + VALIDATIONPASSWORDSDONTMATCH: 'Kodeordene er ikke identiske' + VALIDATIONPASSWORDSNOTEMPTY: 'Kodeord kan ikke være tomme' + VALIDATIONSTRONGPASSWORD: 'Kodeord skal mindst have et tal og et alfanumerisk tegn' + VALIDATOR: Validering + VALIDCURRENCY: 'Indtast venligst en gyldig valuta' + SilverStripe\Forms\FormField: + EXAMPLE: 'f.eks. {format}' + NONE: ingen + SilverStripe\Forms\FormScaffolder: + TABMAIN: Primær SilverStripe\Forms\GridField\GridField: - Filter: Filter + Add: 'Tilføj {name}' + CSVEXPORT: 'Eksporter til CSV' + CSVIMPORT: 'Importer CSV' + Filter: Filtrer + FilterBy: 'Filtrer på' + Find: Find + LinkExisting: 'Link eksisterende' + NewRecord: 'Ny {type}' + NoItemsFound: 'Ingen elementer fundet' + PRINTEDAT: 'Printet d.' + PRINTEDBY: 'Printet af' + PlaceHolder: 'Find {type}' + PlaceHolderWithLabels: 'Find {type} på {name}' + Print: Print + RelationSearch: Relationssøgning + ResetFilter: Nulstil + SilverStripe\Forms\GridField\GridFieldDeleteAction: + Delete: Slet + DeletePermissionsFailure: 'Ingen slette rettigheder' + EditPermissionsFailure: 'Ingen rettighed til at fjerne emnet' + UnlinkRelation: Fjern + SilverStripe\Forms\GridField\GridFieldDetailForm: + CancelBtn: Annuller + Create: Opret + Delete: Slet + DeletePermissionsFailure: 'Ingen slette rettigheder' + Deleted: 'Slet {type} {name}' + Save: Gem + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'Kan ikke fjerne dig selv fra denne gruppe, du vil miste administrator rettigheder' + SilverStripe\Forms\GridField\GridFieldPaginator: + OF: af + Page: Side + View: Vis + SilverStripe\Forms\MoneyField: + FIELDLABELAMOUNT: Beløb + FIELDLABELCURRENCY: Valuta + INVALID_CURRENCY: 'Valuta {currency} er ikke i listen over tilladte valutaer' + SilverStripe\Forms\MultiSelectField: + SOURCE_VALIDATION: 'Vælg venligst eksisterende værdier fra listen. Ugyldig mulighed(er) {value} valgt' + SilverStripe\Forms\NullableField: + IsNullLabel: 'Er Null' + SilverStripe\Forms\NumericField: + VALIDATION: '''{value}'' er ikke et tal, kun tal accepteres i dette felt' + SilverStripe\Forms\TimeField: + VALIDATEFORMAT: 'Indtats venligst et gyldigt tidsformat ({format})' + SilverStripe\ORM\DataObject: + PLURALNAME: Dataobjekter + PLURALS: + one: 'Et dataobjekt' + other: '{count} dataobjekter' + SINGULARNAME: Dataobjekt + SilverStripe\ORM\FieldType\DBBoolean: + ANY: Enhver + NOANSWER: Nej + YESANSWER: Ja + SilverStripe\ORM\FieldType\DBDate: + DAYS_SHORT_PLURALS: + one: '{count} dag' + other: '{count} dage' + HOURS_SHORT_PLURALS: + one: '{count} time' + other: '{count} timer' + LessThanMinuteAgo: 'mindre end et minut' + MINUTES_SHORT_PLURALS: + one: '{count} minut' + other: '{count} minutter' + MONTHS_SHORT_PLURALS: + one: '{count} måned' + other: '{count} måneder' + SECONDS_SHORT_PLURALS: + one: '{count} sekund' + other: '{count} sekunder' + TIMEDIFFAGO: '{difference} siden' + TIMEDIFFIN: 'i {difference}' + YEARS_SHORT_PLURALS: + one: '{count} år' + other: '{count} år' + SilverStripe\ORM\FieldType\DBEnum: + ANY: Enhver + SilverStripe\ORM\Hierarchy: + LIMITED_TITLE: 'For mange underelementer ({count})' + SilverStripe\ORM\Hierarchy\Hierarchy: + InfiniteLoopNotAllowed: 'Uendeligt løkke fundet i "{type}" hierarkiet. Ændre venligst det overliggende element for at løse dette' + LIMITED_TITLE: 'For mange underelementer ({count})' + SilverStripe\ORM\ValidationException: + DEFAULT_ERROR: Valideringsfejl + SilverStripe\Security\BasicAuth: + ENTERINFO: 'Indtast venligst et brugernavn og kodeord.' + ERRORNOTADMIN: 'Den bruger er ikke en administrator.' + ERRORNOTREC: 'Brugernavn / kodeord kunne ikke genkendes' + SilverStripe\Security\CMSMemberLoginForm: + PASSWORDEXPIRED: '

Dit kodeord er udløbet. Vælg venligst et nyt.

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

Ugyldig bruger. Log venligst ind igen her for at fortsætte.

' + LOGIN_MESSAGE: '

Din session er løbet ud pga. inaktivitet

' + LOGIN_TITLE: 'Log ind igen, for at fortsætte hvor du slap.' + SUCCESS: Succes + SUCCESSCONTENT: '

Logget ind. Hvis du ikke automatisk viderestilles så klik her

' + SUCCESS_TITLE: 'Logget ind med sucess' + SilverStripe\Security\DefaultAdminService: + DefaultAdminFirstname: 'Standard admin' + SilverStripe\Security\Group: + AddRole: 'Tilføj en rolle for denne gruppe' + Code: 'Gruppe kode' + DefaultGroupTitleAdministrators: Administratorer + DefaultGroupTitleContentAuthors: Indholdsforfattere + Description: Beskrivelse + GROUPNAME: Gruppenavn + GroupReminder: 'Hvis du vælger en overliggende gruppe, får denne gruppe alle dens roller' + HierarchyPermsError: 'Kan ikke tildele overliggende gruppe "{group}" med fortrinsrettigheder (kræver ADMIN adgang)' + Locked: 'Låst?' + MEMBERS: Brugere + NEWGROUP: 'Ny gruppe' + NoRoles: 'Ingen roller fundet' + PERMISSIONS: Rettigheder + PLURALNAME: Grupper + PLURALS: + one: 'En gruppe' + other: '{count} grupper' + Parent: 'Overliggende gruppe' + ROLES: Roller + ROLESDESCRIPTION: 'Roller er et prædefineret sæt af rettigheder, som kan tildeles grupper.
De bliver nedarvet fra en overliggende grupper hvis krævet.' + RolesAddEditLink: 'Administrer roller' + SINGULARNAME: Gruppe + Sort: Sortering + has_many_Permissions: Rettigheder + many_many_Members: Brugere + SilverStripe\Security\LoginAttempt: + Email: 'Email adresse' + EmailHashed: 'Email adresse (hashed)' + IP: 'IP addresse' + PLURALNAME: Loginforsøg + PLURALS: + one: 'Et loginforsøg' + other: '{count} loginforsøg' + SINGULARNAME: 'Login forsøg' + Status: Status + SilverStripe\Security\Member: + ADDGROUP: 'Tilføj gruppe' + BUTTONCHANGEPASSWORD: 'Skift kodeord' + BUTTONLOGIN: 'Log ind' + BUTTONLOGINOTHER: 'Log ind med en anden bruger' + BUTTONLOGOUT: 'Log ud' + BUTTONLOSTPASSWORD: 'Jeg har glemt mit kodeord' + CONFIRMNEWPASSWORD: 'Bekræft nyt kodeord' + CONFIRMPASSWORD: 'Bekræft kodeord' + CURRENT_PASSWORD: 'Nuværende kodeord' + EDIT_PASSWORD: 'Nyt kodeord' + EMAIL: Email + EMPTYNEWPASSWORD: 'Det nye kodeord kan ikke være tom, prøv venligst igen' + ENTEREMAIL: 'Indtast venligst en email adresse for at få et nulstillingslink.' + ERRORLOCKEDOUT2: 'Din konto er blevet midlertidigt deaktiveret pga. for mange fejlslagne loginforsøg. Forsøg venligst igen om {count} minutter.' + ERRORNEWPASSWORD: 'Du har indtastet dit nye kodeord forskelligt, forsøg igen' + ERRORPASSWORDNOTMATCH: 'Dit nuværende kodeord matcher ikke, forsøg venligst igen' + ERRORWRONGCRED: 'De indtastede værdier ser ikke ud til at være korrekte. Forsøg venligst igen.' + FIRSTNAME: Fornavn + INTERFACELANG: 'Sprog i brugerfladen' + KEEPMESIGNEDIN: 'Hold mig logget ind' + LOGGEDINAS: 'Du er logget ind som {name}.' + NEWPASSWORD: 'Nyt kodeord' + PASSWORD: Kodeord + PASSWORDEXPIRED: 'Dit kodeord er udløbet. Vælg venligst et nyt.' + PLURALNAME: Brugere + PLURALS: + one: 'En bruger' + other: '{count} brugere' + REMEMBERME: 'Husk mig til næste gang? (i {count} dage på denne enhed)' + SINGULARNAME: Bruger + SUBJECTPASSWORDCHANGED: 'Dit kodeord er blevet ændret' + SUBJECTPASSWORDRESET: 'Link til at nulstille dit kodeord' + SURNAME: Efternavn + VALIDATIONADMINLOSTACCESS: 'Kan ikke fjerne alle admin grupper fra din profil' + ValidationIdentifierFailed: 'Kan ikke overskrive eksisterende bruger #{id} med identisk identifikator ({name} = {value}))' + WELCOMEBACK: 'Velkommen tilbage, {firstname}' + YOUROLDPASSWORD: 'Dit gamle kodeord' + belongs_many_many_Groups: Grupper + db_Locale: 'Sprog i brugerfladen' + db_LockedOutUntil: 'Låst ude indtil' + db_Password: Kodeord + db_PasswordExpiry: Kodeordsudløbsdato + SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm: + AUTHENTICATORNAME: 'CMS bruger loginform' + BUTTONFORGOTPASSWORD: 'Glemt kodeord' + BUTTONLOGIN: 'Log mig ind igen' + BUTTONLOGOUT: 'Log ud' + SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: + ERRORWRONGCRED: 'De indtastede værdier ser ikke ud til at være korrekte. Forsøg venligst igen.' + NoPassword: 'Der er ikke en kode på denne bruger.' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'Email og kodeord' + SilverStripe\Security\MemberPassword: + PLURALNAME: 'Bruger kodeord' + PLURALS: + one: 'Et bruger kodeord' + other: '{count} bruger kodeord' + SINGULARNAME: 'Bruger kodeord' + SilverStripe\Security\PasswordValidator: + LOWCHARSTRENGTH: 'Forøg venligst kodeordets styrke, ved at tilføje nogle af følgende tegn: {chars}' + PREVPASSWORD: 'Du har tidligere brugt dette kodeord, vælg venligst et nyt kodeord' + TOOSHORT: 'Kodeordet er for kort, det skal mindst være {minimum} eller flere tegn langt' SilverStripe\Security\Permission: + AdminGroup: Administrator + CMS_ACCESS_CATEGORY: 'CMS Adgang' CONTENT_CATEGORY: Indholdsrettigheder + FULLADMINRIGHTS: 'Fuld administrator rettighed' + FULLADMINRIGHTS_HELP: 'Indebærer og overskriver alle andre tildelte rettigheder.' + PERMISSIONS_CATEGORY: 'Roller og adgangsrettigheder' + PLURALNAME: Rettigheder + PLURALS: + one: 'En rettighed' + other: '{count} rettigheder' + SINGULARNAME: Rettighed + UserPermissionsIntro: 'Tildeling af grupper til denne bruger, ændrer de rettigheder brugeren har. Se gruppe området for rettigheds detaljer på de individuelle grupper.' + SilverStripe\Security\PermissionCheckboxSetField: + AssignedTo: 'tildelt til "{title}"' + FromGroup: 'nedarvet fra gruppen "{title}"' + FromRole: 'nedarvet fra rollen "{title}"' + FromRoleOnGroup: 'nedarvet fra rollen "{roletitle}" på gruppen "{grouptitle}"' + SilverStripe\Security\PermissionRole: + OnlyAdminCanApply: 'Kun administratorer kan tilføje' + PLURALNAME: Roller + PLURALS: + one: 'En rolle' + other: '{count} roller' + SINGULARNAME: Rolle + Title: Titel + SilverStripe\Security\PermissionRoleCode: + PLURALNAME: 'Rettigheds rolle koder' + PLURALS: + one: 'En rettigheds rolle kode' + other: '{count} rettigheds rolle koder' + PermsError: 'Kan ikke tildele koden "{code}" med fortrinsrettigheder (kræver ADMIN adgang)' + SINGULARNAME: 'Rettighed rolle kode' + SilverStripe\Security\RememberLoginHash: + PLURALNAME: 'Login hashes' + PLURALS: + one: 'Et login hash' + other: '{count} Login Hashes' + SINGULARNAME: 'Login hash' + SilverStripe\Security\Security: + ALREADYLOGGEDIN: 'Du har ikke adgang til denne side. Hvis du har en anden bruger der har adgang til denne side, kan du logge ind med denne herunder.' + BUTTONSEND: 'Send mig linket til at nulstille kodeordet' + CHANGEPASSWORDBELOW: 'Du kan ændre dit kodeord herunder.' + CHANGEPASSWORDHEADER: 'Skift dit kodeord' + CONFIRMLOGOUT: 'Klik venligst på knappen herunder, for at bekræfte at du vil logge ud.' + ENTERNEWPASSWORD: 'Indtast venligst et nyt kodeord.' + ERRORPASSWORDPERMISSION: 'Du skal være logget ind, for at kunne ændre dit kodeord!' + LOGIN: 'Log ind' + LOGOUT: 'Log ud' + LOSTPASSWORDHEADER: 'Glemt kodeord' + NOTEPAGESECURED: 'Denne side er beskyttet. Indtast dine loginoplysninger herunder for at få adgang.' + NOTERESETPASSWORD: 'Indtast din email adresse, så sender vi dig et link som du kan nulstille dit kodeord med' diff --git a/lang/de.yml b/lang/de.yml index 3bbf8cdb5..77e2f1584 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 @@ -195,7 +190,4 @@ de: LOGIN: Anmelden LOSTPASSWORDHEADER: 'Passwort vergessen' NOTEPAGESECURED: 'Diese Seite ist geschützt. Bitte melden Sie sich an und Sie werden sofort weitergeleitet.' - NOTERESETLINKINVALID: '

Der Link zum Zurücksetzen des Passworts ist entweder nicht korrekt oder abgelaufen

Sie können einen neuen Link anfordern oder Ihr Passwort nach dem einloggen ändern.

' NOTERESETPASSWORD: 'Geben Sie Ihre E-Mail-Adresse ein und wir werden Ihnen einen Link zuschicken, mit dem Sie Ihr Passwort zurücksetzen können.' - PASSWORDSENTHEADER: 'Der Link zum Zurücksetzen des Passworts wurde an ''{email}'' gesendet' - PASSWORDSENTTEXT: 'Vielen Dank! Wenn ein Account zu der E-Mail Adresse ''{email}'' existiert, wurde eine E-Mail mit dem Link zum Zurücksetzen des Passworts verschickt.' diff --git a/lang/de_DE.yml b/lang/de_DE.yml new file mode 100644 index 000000000..e1bea0c6e --- /dev/null +++ b/lang/de_DE.yml @@ -0,0 +1,159 @@ +de_DE: + SilverStripe\Admin\LeftAndMain: + VersionUnknown: Unbekannt + SilverStripe\AssetAdmin\Forms\UploadField: + Dimensions: Maße + EDIT: Bearbeiten + EDITINFO: 'Datei bearbeiten' + REMOVE: Entfernen + SilverStripe\Control\ChangePasswordEmail_ss: + CHANGEPASSWORDFOREMAIL: 'Das Passwort für das Konto mit der E-Mail-Adresse {email} wurde geändert. Wenn Sie Ihr Passwort nicht geändert haben, ändern Sie bitte Ihr Passwort mit dem folgenden Link' + CHANGEPASSWORDTEXT1: 'Sie haben Ihr Passwort für geändert' + CHANGEPASSWORDTEXT3: 'Passwort ändern' + HELLO: Hallo + SilverStripe\Control\Email\ForgotPasswordEmail_ss: + HELLO: Hallo + TEXT1: 'Hier ist dein' + TEXT2: 'Link zum Zurücksetzen des Passworts' + TEXT3: für + SilverStripe\Core\Manifest\VersionProvider: + VERSIONUNKNOWN: Unbekannt + SilverStripe\Forms\CheckboxField: + NOANSWER: Nein + YESANSWER: Ja + SilverStripe\Forms\CheckboxSetField_ss: + NOOPTIONSAVAILABLE: 'Keine Optionen vorhanden' + SilverStripe\Forms\ConfirmedPasswordField: + ATLEAST: 'Passwörter müssen mindestens {min} Zeichen lang sein.' + BETWEEN: 'Passwörter müssen {min} bis {max} Zeichen lang sein.' + SHOWONCLICKTITLE: 'Passwort ändern' + SilverStripe\Forms\DateField: + TODAY: heute + SilverStripe\Forms\DropdownField: + CHOOSE: (Auswählen) + CHOOSE_MODEL: '({name} auswählen)' + SilverStripe\Forms\GridField\GridField: + Add: '{name} hinzufügen' + CSVEXPORT: 'Als CSV exportieren' + CSVIMPORT: 'CSV importieren' + Filter: Filtern + FilterBy: 'Filtern nach' + Print: Drucken + ResetFilter: Zurücksetzen + SilverStripe\Forms\GridField\GridFieldDeleteAction: + Delete: Löschen + SilverStripe\Forms\GridField\GridFieldDetailForm: + CancelBtn: Abbrechen + Save: Speichern + SilverStripe\Forms\GridField\GridFieldEditButton: + EDIT: Bearbeiten + SilverStripe\Forms\GridField\GridFieldFilterHeader: + Search: 'Suche "{name}"' + SilverStripe\Forms\GridField\GridFieldPaginator: + Page: Seite + SilverStripe\Forms\MoneyField: + FIELDLABELAMOUNT: Betrag + FIELDLABELCURRENCY: Währung + INVALID_CURRENCY: 'Die Währung {currency} ist nicht in der Liste der erlauben Währungen' + SilverStripe\ORM\DataObject: + PLURALNAME: DatenObjekte + PLURALS: + one: 'Ein DatenObjekt' + other: '{count} DatenObjekte' + SINGULARNAME: DatenObjekt + SilverStripe\ORM\FieldType\DBBoolean: + NOANSWER: Nein + YESANSWER: Ja + SilverStripe\ORM\FieldType\DBDate: + DAYS_SHORT_PLURALS: + one: '{count} Tag' + other: '{count} Tage' + HOURS_SHORT_PLURALS: + one: '{count} Stunde' + other: '{count} Stunden' + LessThanMinuteAgo: 'weniger als 1 Minute' + MINUTES_SHORT_PLURALS: + one: '{count} Minute' + other: '{count} Minuten' + MONTHS_SHORT_PLURALS: + one: '{count} Monat' + other: '{count} Monate' + SECONDS_SHORT_PLURALS: + one: '{count} Sekunde' + other: '{count} Sekunden' + TIMEDIFFAGO: 'vor {difference}' + TIMEDIFFIN: 'in {difference}' + YEARS_SHORT_PLURALS: + one: '{count} Jahr' + other: '{count} Jahre' + SilverStripe\Security\BasicAuth: + ENTERINFO: 'Bitte geben Sie einen Benutzernamen und ein Passwort ein' + ERRORNOTADMIN: 'Dieser Benutzer ist kein Administrator' + ERRORNOTREC: 'Dieser Benutzer bzw. dieses Passwort ist unbekannt' + SilverStripe\Security\CMSMemberLoginForm: + PASSWORDEXPIRED: '

Ihr Passwort ist abgelaufen. Bitte wählen Sie ein neues Passwort.

' + SilverStripe\Security\CMSSecurity: + SUCCESS_TITLE: 'Login erfolgreich' + SilverStripe\Security\DefaultAdminService: + DefaultAdminFirstname: 'Standard Administrator' + SilverStripe\Security\Group: + DefaultGroupTitleAdministrators: Administratoren + DefaultGroupTitleContentAuthors: Inhaltsautoren + Description: Beschreibung + GROUPNAME: Gruppenname + MEMBERS: Mitglieder + NEWGROUP: 'Neue Gruppe' + NoRoles: 'Keine Rollen gefunden' + PLURALNAME: Gruppen + PLURALS: + one: 'Eine Gruppe' + other: '{count} Gruppen' + SINGULARNAME: Gruppe + Sort: Sortierreihenfolge + has_many_Permissions: Rechte + many_many_Members: Mitglieder + SilverStripe\Security\LoginAttempt: + Email: 'E-Mail Adresse' + EmailHashed: 'E-Mail Adresse (gehasht)' + IP: 'IP Adresse' + PLURALNAME: Loginversuche + PLURALS: + one: 'Ein Loginversuch' + other: '{count} Loginversuche' + SINGULARNAME: Loginversuch + Status: Status + SilverStripe\Security\Member: + ADDGROUP: 'Gruppe hinzufügen' + BUTTONCHANGEPASSWORD: 'Passwort ändern' + BUTTONLOGIN: Einloggen + BUTTONLOGINOTHER: 'Als jemand anderes einloggen' + BUTTONLOGOUT: Ausloggen + BUTTONLOSTPASSWORD: 'Ich habe mein Passwort vergessen' + CONFIRMNEWPASSWORD: 'Neues Passwort bestätigen' + CONFIRMPASSWORD: 'Passwort bestätigen' + CURRENT_PASSWORD: 'Derzeitiges Passwort' + EDIT_PASSWORD: 'Neues Passwort' + EMAIL: E-Mail + EMPTYNEWPASSWORD: 'Das neue Passwort kann nicht leer sein, bitte versuchen Sie es erneut' + ENTEREMAIL: 'Bitte geben Sie Ihre E-Mail Adresse an. Sie erhalten eine E-Mail mit einem Link zum Zurücksetzen des Passworts.' + FIRSTNAME: Vorname + NEWPASSWORD: 'Neues Passwort' + PASSWORD: Passwort + PASSWORDEXPIRED: 'Ihr Passwort ist abgelaufen. Bitte wählen Sie ein neues Passwort.' + PLURALNAME: Mitglieder + PLURALS: + one: 'Ein Mitglied' + other: '{count} Mitglieder' + SINGULARNAME: Mitglied + SURNAME: Nachname + WELCOMEBACK: 'Willkommen zurück, {firstname}' + YOUROLDPASSWORD: 'Ihr altes Passwort' + belongs_many_many_Groups: Grouppen + db_Password: Passwort + SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm: + BUTTONFORGOTPASSWORD: 'Passwort vergessen' + SilverStripe\Security\Permission: + AdminGroup: Administrator + SilverStripe\Security\Security: + LOGIN: Einloggen + LOGOUT: Ausloggen diff --git a/lang/en.yml b/lang/en.yml index ef6596a01..2a6fa6c18 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -37,8 +37,6 @@ en: SilverStripe\Forms\CurrencyField: CURRENCYSYMBOL: $ SilverStripe\Forms\DateField: - NOTSET: 'not set' - TODAY: today VALIDDATEFORMAT2: 'Please enter a valid date format ({format})' VALIDDATEMAXDATE: 'Your date has to be older or matching the maximum allowed date ({date})' VALIDDATEMINDATE: 'Your date has to be newer or matching the minimum allowed date ({date})' diff --git a/lang/eo.yml b/lang/eo.yml index 6660d9b1b..cc792bab4 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,23 +95,12 @@ 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: OF: de Page: Paĝo View: Vido - SilverStripe\Forms\GridField\GridFieldVersionedState: - ADDEDTODRAFTHELP: 'Ero ankoraŭ estas ne publikigita' - ADDEDTODRAFTSHORT: Malneto - ARCHIVEDPAGEHELP: 'Ero estas forigita el malneta kaj publika' - ARCHIVEDPAGESHORT: Enarkivigita - MODIFIEDONDRAFTHELP: 'Ero enhavas nepublikigitajn ŝanĝojn' - MODIFIEDONDRAFTSHORT: Ŝanĝita - ONLIVEONLYSHORT: 'Nur ĉe publika' - ONLIVEONLYSHORTHELP: 'Ero estas publikigita, sed ĝi estas forigita el malneto' SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Kvanto FIELDLABELCURRENCY: Kurzo @@ -270,6 +256,8 @@ eo: SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: ERRORWRONGCRED: 'La donitaj detaloj ŝajnas malĝustaj. Bonvole reprovu.' NoPassword: 'Mankas pasvorto por ĉi tiu membro.' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'Retpoŝtadreso kaj pasvorto' SilverStripe\Security\MemberPassword: PLURALNAME: 'Membraj pasvortoj' PLURALS: @@ -331,7 +319,4 @@ eo: LOGOUT: Elsaluti LOSTPASSWORDHEADER: 'Perdis pasvorton' NOTEPAGESECURED: 'Tiu paĝo estas sekurigita. Enigu viajn akreditaĵojn sube kaj vi aliros pluen.' - NOTERESETLINKINVALID: '

La pasvorta reagorda ligilo estas malvalida aŭ finiĝis.

Vi povas peti novan ĉi tie aŭ ŝanĝi vian pasvorton post vi ensalutis.

' NOTERESETPASSWORD: 'Enigu vian retpoŝtan adreson kaj ni sendos al vi ligilon per kiu vi povas reagordi vian pasvorton' - PASSWORDSENTHEADER: 'Pasvorta reagorda ligilo sendiĝis al ''{email}''' - PASSWORDSENTTEXT: 'Dankon! Reagordita ligilo sendiĝis al ''{email}'', kondiĉe ke konto ekzistas por tiu retadreso.' diff --git a/lang/es.yml b/lang/es.yml index 5251bdbf6..debd4b4a7 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 @@ -254,7 +249,4 @@ es: LOGIN: Entrar LOSTPASSWORDHEADER: '¿Contraseña Perdida?' NOTEPAGESECURED: 'Esa página está protegida. Introduzca sus datos de acreditación a continuación y lo enviaremos a ella en un momento.' - NOTERESETLINKINVALID: '

El enlace para restablecer la contraseña es inválido o ha expirado.

Usted puede solicitar uno nuevo aqui o cambiar su contraseña después de que se haya conectado.

' NOTERESETPASSWORD: 'Introduzca su dirección de e-mail, y le enviaremos un enlace, con el cual podrá restaurar su contraseña' - PASSWORDSENTHEADER: 'Un enlace para restablecer la contraseña ha sido enviado a ''{email}''' - PASSWORDSENTTEXT: 'Gracias! Un enlace para restablecer la contraseña ha sido enviado a ''{email}'', siempre que una cuenta exista para la dirección de email indicada.' 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..a8fb3b686 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 @@ -144,7 +139,4 @@ et_EE: ERRORPASSWORDPERMISSION: 'Pead olema sisseloginud, et parooli muuta!' LOGIN: 'Logi sisse' NOTEPAGESECURED: 'See leht on turvatud. Sisesta enda andmed allpool ja me saadame sind otse edasi' - NOTERESETLINKINVALID: '

Parooli lähtestamise link on kehtetu või aegunud.

Saate taotleda uut linki siin või muuta parooli pärast sisselogimist.

' NOTERESETPASSWORD: 'Sisesta oma email ja me saadame sulle lingi kus saad oma parooli tühistada.' - PASSWORDSENTHEADER: 'Parooli lähtestamise link saadeti aadressile ''{email}''' - PASSWORDSENTTEXT: 'Aitäh! Lähtestamislink saadeti aadressile ''{email}'' eeldusel, et selle e-posti aadressiga seotud konto on olemas.' diff --git a/lang/fa_IR.yml b/lang/fa_IR.yml index 14c967ba9..b9aa0ebea 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: 'واحد پول' @@ -173,4 +168,3 @@ fa_IR: ERRORPASSWORDPERMISSION: 'جهت تغییر گذرواژه خود باید وارد شده باشید!' LOGIN: ورود LOSTPASSWORDHEADER: 'فراموشی گذرواژه' - PASSWORDSENTHEADER: 'لینک ازنوسازی گذرواژه به ''{email}'' ارسال شد' diff --git a/lang/fi.yml b/lang/fi.yml index 53eae99ed..01899f2af 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,23 +95,12 @@ 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: OF: / Page: Sivu View: Näytä - SilverStripe\Forms\GridField\GridFieldVersionedState: - ADDEDTODRAFTHELP: 'Kohdetta ei ole julkaistu vielä' - ADDEDTODRAFTSHORT: Luonnos - ARCHIVEDPAGEHELP: 'Kohde on poistettu luonnoksista ja julkaisusta' - ARCHIVEDPAGESHORT: Arkistoitu - MODIFIEDONDRAFTHELP: 'Kohteella on julkaisemattomia muutoksia' - MODIFIEDONDRAFTSHORT: Muokattu - ONLIVEONLYSHORT: 'Vain julkaistuna' - ONLIVEONLYSHORTHELP: 'Kohde on julkaistu, mutta poistettu luonnoksista' SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Määrä FIELDLABELCURRENCY: Valuutta @@ -211,6 +197,7 @@ fi: many_many_Members: Jäsenet SilverStripe\Security\LoginAttempt: Email: Sähköpostiosoite + EmailHashed: 'Sähköpostiosoite (tiivistetty)' IP: IP-osoite PLURALNAME: Kirjautumisyritykset PLURALS: @@ -269,6 +256,8 @@ fi: SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: ERRORWRONGCRED: 'Antamasi tiedot eivät näytä oikeilta. Yritä uudelleen.' NoPassword: 'Tällä käyttäjällä ei ole salasanaa' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'Sähköpostiosoite & salasana' SilverStripe\Security\MemberPassword: PLURALNAME: 'Käyttäjän salasanat' PLURALS: @@ -330,7 +319,4 @@ fi: LOGOUT: 'Kirjaudu ulos' LOSTPASSWORDHEADER: 'Unohtunut salasana' NOTEPAGESECURED: 'Tämä sivu on suojattu. Syötä tunnistetietosi alle niin pääset eteenpäin.' - NOTERESETLINKINVALID: '

Salasanan palautuslinkki on virheellinen tai vanhentunut.

Voit pyytää uuden napsauttamalla tästä tai vaihtaa salasanasi kirjautumisen jälkeen.

' NOTERESETPASSWORD: 'Syötä sähköpostiosoitteesi ja lähetämme sinulle linkin, jonka avulla saat palautettua salasanasi' - PASSWORDSENTHEADER: 'Salasanan palautuslinkki lähetettiin osoitteeseen ''{email}''' - PASSWORDSENTTEXT: 'Kiitos! Salasanan palautuslinkki lähetettiin osoitteeseen ''{email}'', joka on liitettynä tähän käyttäjätiliin.' 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 c6f59e67f..450cfdb87 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})' @@ -86,7 +84,6 @@ fr: RelationSearch: 'Rechercher relations' ResetFilter: Réinitialiser SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Supprimer Delete: Supprimer DeletePermissionsFailure: 'Vous n’avez pas les autorisations pour supprimer' EditPermissionsFailure: 'Pas de permissions pour délier l''enregistrement' @@ -98,23 +95,12 @@ fr: DeletePermissionsFailure: 'Vous n’avez pas les autorisations pour supprimer' Deleted: '{type} {name} supprimés' Save: Enregistrer - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Editer SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: UnlinkSelfFailure: 'Impossible de retirer votre propre profil de ce groupe, vous perdriez vos droits d''administration' SilverStripe\Forms\GridField\GridFieldPaginator: OF: de Page: Page View: Vue - SilverStripe\Forms\GridField\GridFieldVersionedState: - ADDEDTODRAFTHELP: 'L''élément n''a pas encore été publié' - ADDEDTODRAFTSHORT: Brouillon - ARCHIVEDPAGEHELP: 'Elément retiré du brouillon et du site public' - ARCHIVEDPAGESHORT: Archivé - MODIFIEDONDRAFTHELP: 'L''élément comporte des modifications non sauvegardées' - MODIFIEDONDRAFTSHORT: Modifié - ONLIVEONLYSHORT: 'Site public uniquement' - ONLIVEONLYSHORTHELP: 'L''élément a été publié, mais sa version brouillon a été supprimée' SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Quantité FIELDLABELCURRENCY: Devise @@ -211,6 +197,7 @@ fr: many_many_Members: Membres SilverStripe\Security\LoginAttempt: Email: 'Adresse email' + EmailHashed: 'Adresse Email (hashed)' IP: 'Adresse IP' PLURALNAME: 'Tentatives de connexion' PLURALS: @@ -269,6 +256,8 @@ fr: SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: ERRORWRONGCRED: 'Les renseignements fournis semblent incorrects. Merci de réessayer.' NoPassword: 'Ce membre n''a pas de mot de passe' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'Email & Mot de passe' SilverStripe\Security\MemberPassword: PLURALNAME: 'Mots de passe utilisateur' PLURALS: @@ -330,7 +319,4 @@ fr: LOGOUT: 'Se déconnecter' LOSTPASSWORDHEADER: 'Mot de passe oublié' NOTEPAGESECURED: 'Cette page est sécurisée. Entrez vos identifiants ci-dessous et vous pourrez y avoir accès.' - NOTERESETLINKINVALID: '

Le lien de réinitialisation du mot de passe n’est pas valide ou a expiré.

Vous pouvez en demander un nouveau en suivant ce lien ou changer de mot de passe après connexion.

' NOTERESETPASSWORD: 'Entrez votre adresse email et nous vous enverrons un lien pour modifier votre mot de passe' - PASSWORDSENTHEADER: "Lien de réinitialisation de mot de passe envoyé à «\_{email}\_»" - PASSWORDSENTTEXT: "Merci\_! Un lien de réinitialisation vient d’être envoyé à «\_{email}\_», à condition que cette adresse existe." 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..c9c06bbff 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' @@ -172,7 +167,4 @@ id: LOGIN: Masuk LOSTPASSWORDHEADER: 'Kata Kunci yang Terlupa' NOTEPAGESECURED: 'Laman ini diamankan. Isikan data berikut untuk dikirimkan hak akses Anda.' - NOTERESETLINKINVALID: '

Tautan penggantian kata kunci tidak valid atau sudah kadaluarsa.

Anda dapat meminta yang baru di sini atau mengganti kata kunci setelah Anda masuk.

' NOTERESETPASSWORD: 'Isikan alamat email Anda untuk mendapatkan tautan penggantian kata kunci' - PASSWORDSENTHEADER: 'Tautan penggantian kata kunci dikirimkan ke ''{email}''' - PASSWORDSENTTEXT: 'Terimakasih! Tautan reset telah dikirim ke ''{email}'', berisi informasi akun untuk alamat email ini.' diff --git a/lang/id_ID.yml b/lang/id_ID.yml index 9d4b6a922..11d1f2bb3 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' @@ -171,7 +166,4 @@ id_ID: LOGIN: Masuk LOSTPASSWORDHEADER: 'Kata Kunci yang Terlupa' NOTEPAGESECURED: 'Laman ini diamankan. Isikan data berikut untuk dikirimkan hak akses Anda.' - NOTERESETLINKINVALID: '

Tautan penggantian kata kunci tidak valid atau sudah kadaluarsa.

Anda dapat meminta yang baru di sini atau mengganti kata kunci setelah Anda masuk.

' NOTERESETPASSWORD: 'Isikan alamat email Anda untuk mendapatkan tautan penggantian kata kunci' - PASSWORDSENTHEADER: 'Tautan penggantian kata kunci dikirimkan ke ''{email}''' - PASSWORDSENTTEXT: 'Terimakasih! Tautan reset telah dikirim ke ''{email}'', berisi informasi akun untuk alamat email ini.' 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 dddfb094a..adbdeb0fd 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,21 +95,12 @@ it: DeletePermissionsFailure: 'Non hai i permessi per eliminare' Deleted: 'Eliminato {type} {name}' Save: Salva - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Modifica + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'Non è possibile rimuovere te stesso da questo gruppo, perderesti i diritti di admin' SilverStripe\Forms\GridField\GridFieldPaginator: OF: di Page: Pagina View: Visualizza - SilverStripe\Forms\GridField\GridFieldVersionedState: - ADDEDTODRAFTHELP: 'L''elemento non è stato ancora pubblicato' - ADDEDTODRAFTSHORT: Bozza - ARCHIVEDPAGEHELP: 'Elemento rimosso da bozza e live' - ARCHIVEDPAGESHORT: Archiviato - MODIFIEDONDRAFTHELP: 'L''elemento ha modifiche nascoste' - MODIFIEDONDRAFTSHORT: Modificato - ONLIVEONLYSHORT: 'Solo su live' - ONLIVEONLYSHORTHELP: 'L''elemento è pubblicato, ma è stato eliminato dal sito bozza' SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Importo FIELDLABELCURRENCY: Valuta @@ -209,6 +197,7 @@ it: many_many_Members: Membri SilverStripe\Security\LoginAttempt: Email: 'Indirizzo e-mail' + EmailHashed: 'Indirizzo email (hash)' IP: 'Indirizzo IP' PLURALNAME: 'Tentativi d''accesso' PLURALS: @@ -250,6 +239,7 @@ it: SUBJECTPASSWORDCHANGED: 'La tua password è stata cambiata' SUBJECTPASSWORDRESET: 'Link per azzerare la tua password' SURNAME: Cognome + VALIDATIONADMINLOSTACCESS: 'Non è possibile rimuovere tutti i gruppi admin dal tuo profilo' ValidationIdentifierFailed: 'Non posso sovrascrivere l''utente esistente #{id} con identificatore identico ({name} = {value}))' WELCOMEBACK: 'Bentornato, {firstname}' YOUROLDPASSWORD: 'La tua vecchia password' @@ -266,6 +256,8 @@ it: SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: ERRORWRONGCRED: 'I dettagli forniti non sembrano corretti. Per favore riprovare.' NoPassword: 'Manca la password per questo utente.' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'E-mail & Password' SilverStripe\Security\MemberPassword: PLURALNAME: 'Password utenti' PLURALS: @@ -327,7 +319,4 @@ it: LOGOUT: Scollegati LOSTPASSWORDHEADER: 'Password smarrita' NOTEPAGESECURED: 'La pagina è protetta. Inserisci le credenziali qui sotto per poter andare avanti.' - NOTERESETLINKINVALID: '

Il link per azzerare la password non è valido o è scaduto.

Puoi richiederne uno nuovo qui o cambiare la tua password dopo che ti sei connesso.

' NOTERESETPASSWORD: 'Inserisci il tuo indirizzo e-mail e ti verrà inviato un link per poter azzerare la tua password.' - PASSWORDSENTHEADER: 'Link per azzeramento della password inviato a ''{email}''' - PASSWORDSENTTEXT: 'Grazie! Un link di azzeramento è stato inviato a ''{email}'', fornito un account esistente per questo indirizzo e-mail.' diff --git a/lang/ja.yml b/lang/ja.yml index 7f76ad481..433a832e2 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: 通貨 @@ -151,7 +146,4 @@ ja: ERRORPASSWORDPERMISSION: パスワードを変更する為に、ログインしなければなりません! LOGIN: ログイン NOTEPAGESECURED: このページはセキュリティで保護されております証明書キーを下記に入力してください。こちらからすぐに送信します - NOTERESETLINKINVALID: '

パスワードのリセットリンクは有効でないか期限切れです。

新しいパスワードを要求することができます ここ もしくはパスワードを変更することができます ログインした後 .

' NOTERESETPASSWORD: メールアドレスを入力してください、パスワードをリセットするURLを送信致します - PASSWORDSENTHEADER: 'パスワードリセットリンクは ''{email}'' に送信されました' - PASSWORDSENTTEXT: 'ありがとうございました! リセットリンクは、''{email}'' に、このアカウントが存在することを前提として送信されました。' diff --git a/lang/lt.yml b/lang/lt.yml index 5f70dd239..3ca7adbff 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 @@ -172,7 +167,4 @@ lt: LOGIN: Prisijungti LOSTPASSWORDHEADER: 'Slaptažodžio atstatymas' NOTEPAGESECURED: 'Šis puslapis yra apsaugotas. Įveskite savo duomenis į žemiau esančius laukelius.' - NOTERESETLINKINVALID: '

Neteisinga arba negaliojanti slaptažodžio atstatymo nuoroda.

Galite atsisiųsti naują čia arba pasikeisti slaptažodį po to, kai prisijungsite.

' NOTERESETPASSWORD: 'Įveskite savo e. pašto adresą ir atsiųsime slaptažodžio atstatymui skirtą nuorodą' - PASSWORDSENTHEADER: 'Slaptažodžio atstatymo nuoroda nusiųsta į ''{email}''' - PASSWORDSENTTEXT: 'Atstatymo nuoroda nusiųsta į ''{email}''' 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..e9fe2308e 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 @@ -154,7 +149,4 @@ mi: LOGIN: Takiuru LOSTPASSWORDHEADER: 'Kupuhipa Ngaro' NOTEPAGESECURED: 'Kua ngita tēnā whārangi. Tāurua ō taipitoptio tuakiri ki raro, ā, mā mātou koe e tuku kia haere tonu.' - NOTERESETLINKINVALID: '

He muhu, kua mōnehu rānei te hono tautuhi kupuhipa anō.

Ka taea te tono i te mea hōui konei ka huri rānei i tō kupuhipa ā muri i tōtakiuru.

' NOTERESETPASSWORD: 'Tāurua tō wāhitau īmēra, mā mātou e tuku tētahi hono ki a koe e taea ai te tautuhi anō i tō kupuhipa' - PASSWORDSENTHEADER: 'I tukuna he hono tautuhi kupuhipa anō ki ''{email}''' - PASSWORDSENTTEXT: 'Kia ora! Kua tukuna he hono tautuhi anō ki ''{email}'',engari rā kei te tīariari he pūkete mō taua wāhitau īmēra.' diff --git a/lang/nb.yml b/lang/nb.yml index 2825f7543..f1dc4c74d 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 @@ -157,7 +152,4 @@ nb: LOGIN: 'Logg inn' LOSTPASSWORDHEADER: 'Mistet passord' NOTEPAGESECURED: 'Den siden er sikret. Skriv inn gyldig innloggingsinfo så kommer du inn.' - NOTERESETLINKINVALID: '

Lenken for å nullstille passordet er ugyldig eller utgått.

Du kan kreve en ny her eller endre passordet etter at du har logget inn.

' NOTERESETPASSWORD: 'Skriv inn epostadressen din og vi vil sende deg en lenke som nullstiller passordet.' - PASSWORDSENTHEADER: 'Lenke for nullstilling av passord ble sendt til ''{email}''' - PASSWORDSENTTEXT: 'Takk! En lenke for å lage nytt passord er sendt til ''{email}'', forutsatt at det eksisterer en konto for denne epostadressen.' diff --git a/lang/nl.yml b/lang/nl.yml index 2903dff05..f4e35af66 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -1,4 +1,26 @@ nl: + SilverStripe\Admin\LeftAndMain: + VersionUnknown: onbekend + SilverStripe\AssetAdmin\Forms\UploadField: + Dimensions: Afmetingen + EDIT: Bewerken + EDITINFO: 'Bewerk dit bestand' + REMOVE: Verwijder + SilverStripe\Control\ChangePasswordEmail_ss: + CHANGEPASSWORDFOREMAIL: 'Het wachtwoord voor het account met e-mailadres {email} is aangepast. Indien u uw wachtwoord niet heeft aangepast kunt u dat doen met onderstaande link.' + CHANGEPASSWORDTEXT1: 'U heeft het wachtwoord veranderd voor' + CHANGEPASSWORDTEXT3: 'Wachtwoord veranderen' + HELLO: Hallo + SilverStripe\Control\Email\ForgotPasswordEmail_ss: + HELLO: Hallo + TEXT1: 'Hier is uw' + TEXT2: 'link om uw wachtwoord opnieuw aan te maken' + TEXT3: voor + SilverStripe\Control\RequestProcessor: + INVALID_REQUEST: 'Fout bij verwerken' + REQUEST_ABORTED: 'Fout bij verwerken (geannuleerd)' + SilverStripe\Core\Manifest\VersionProvider: + VERSIONUNKNOWN: Onbekend SilverStripe\Forms\CheckboxField: NOANSWER: Nee YESANSWER: Ja @@ -8,22 +30,30 @@ nl: ATLEAST: 'Een wachtwoord moet tenminste {min} karakters hebben.' BETWEEN: 'Een wachtwoord moet tussen de {min} en {max} karakters hebben' CURRENT_PASSWORD_ERROR: 'Het wachtwoord dat u heeft ingevoerd is niet juist.' + CURRENT_PASSWORD_MISSING: 'Voer uw huidige wachtwoord in.' + LOGGED_IN_ERROR: 'U moet ingelogd zijn om uw wachtwoord te kunnen veranderen!' MAXIMUM: 'Een wachtwoord mag maximaal {max} karakters hebben.' SHOWONCLICKTITLE: 'Verander wachtwoord' 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})' + SilverStripe\Forms\DatetimeField: + VALIDDATEMAXDATETIME: 'De datum moet ouder of gelijk zijn aan de maximale datum ({datetime})' + VALIDDATETIMEFORMAT: 'Vul een geldige datum in ({format})' + VALIDDATETIMEMINDATE: 'De datum moet nieuwer of gelijk zijn aan de minimale datum ({datetime})' SilverStripe\Forms\DropdownField: CHOOSE: (Kies) + CHOOSE_MODEL: '(Selecteer {name})' SOURCE_VALIDATION: 'Selecteer een optie uit de lijst. {value} is geen geldige keuze.' SilverStripe\Forms\EmailField: VALIDATION: 'Gelieve een e-mailadres in te voeren.' + SilverStripe\Forms\FileUploadReceiver: + FIELDNOTSET: 'Bestandsinformatie niet gevonden' SilverStripe\Forms\Form: + BAD_METHOD: 'Dit formulier moet middels {method} verzonden worden' CSRF_EXPIRED_MESSAGE: 'Uw sessie is verlopen. Verzend het formulier opnieuw.' CSRF_FAILED_MESSAGE: 'Er lijkt een technisch probleem te zijn. Klik op de knop terug, vernieuw uw browser, en probeer het opnieuw.' VALIDATIONPASSWORDSDONTMATCH: 'Wachtwoorden komen niet overeen' @@ -32,7 +62,10 @@ nl: VALIDATOR: Validator VALIDCURRENCY: 'Vul een geldige munteenheid in' SilverStripe\Forms\FormField: + EXAMPLE: 'bijv. {format}' NONE: geen + SilverStripe\Forms\FormScaffolder: + TABMAIN: Hoofdgedeelte SilverStripe\Forms\GridField\GridField: Add: '{name} toevoegen' CSVEXPORT: 'Exporteer naar CSV' @@ -51,7 +84,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,29 +95,63 @@ nl: DeletePermissionsFailure: 'Onvoldoende rechten om te verwijderen' Deleted: '{type} {name} verwijderd' Save: Opslaan - SilverStripe\Forms\GridField\GridFieldEditButton_ss: - EDIT: Bewerken + SilverStripe\Forms\GridField\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'U kunt uzelf niet verwijderen van deze groep, omdat u dan geen admin-rechten meer heeft.' + SilverStripe\Forms\GridField\GridFieldPaginator: + OF: van + Page: Pagina + View: Bekijk SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Aantal FIELDLABELCURRENCY: Munteenheid + INVALID_CURRENCY: 'Valuta {currency} is niet toegestaan' + SilverStripe\Forms\MultiSelectField: + SOURCE_VALIDATION: 'Selecteer een optie uit de lijst. {value} is geen geldige keuze.' SilverStripe\Forms\NullableField: IsNullLabel: 'Is null' SilverStripe\Forms\NumericField: VALIDATION: '''{value}'' is geen getal, enkel getallen worden door dit veld geaccepteerd' SilverStripe\Forms\TimeField: VALIDATEFORMAT: 'Vul een geldig datumformaat in ({format})' + SilverStripe\ORM\DataObject: + PLURALNAME: 'Data objecten' + PLURALS: + one: 'Data object' + other: '{count} Data objecten' + SINGULARNAME: 'Data object' SilverStripe\ORM\FieldType\DBBoolean: ANY: Elke NOANSWER: Nee YESANSWER: Ja SilverStripe\ORM\FieldType\DBDate: + DAYS_SHORT_PLURALS: + one: '{count} dag' + other: '{count} dagen' + HOURS_SHORT_PLURALS: + one: '{count} uur' + other: '{count} uren' LessThanMinuteAgo: 'minder dan één minuut' + MINUTES_SHORT_PLURALS: + one: '{count} minuut' + other: '{count} minuten' + MONTHS_SHORT_PLURALS: + one: '{count} maand' + other: '{count} maanden' + SECONDS_SHORT_PLURALS: + one: '{count} seconde' + other: '{count} seconden' TIMEDIFFAGO: '{difference} geleden' TIMEDIFFIN: 'in {difference}' + YEARS_SHORT_PLURALS: + one: '{count} jaar' + other: '{count} jaren' SilverStripe\ORM\FieldType\DBEnum: ANY: Elke + SilverStripe\ORM\Hierarchy: + LIMITED_TITLE: 'Teveel onderliggende items ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: InfiniteLoopNotAllowed: 'Oneindige lus gevonden in "{type}" hiërarchie. Wijzig het hogere niveau om dit op te lossen' + LIMITED_TITLE: 'Teveel onderliggende items ({count})' SilverStripe\ORM\ValidationException: DEFAULT_ERROR: Validatiefout SilverStripe\Security\BasicAuth: @@ -96,34 +162,60 @@ nl: PASSWORDEXPIRED: '

Uw wachtwoord is verlopen. Kies een nieuw wachtwoord.

' SilverStripe\Security\CMSSecurity: INVALIDUSER: '

Ongeldige gebruiker Log hier opnieuw in om verder te gaan.

' + LOGIN_MESSAGE: 'Sessie is verlopen' + LOGIN_TITLE: '

U kunt verder met wat u aan het doen was, door opnieuw in te loggen.

' SUCCESS: Succes SUCCESSCONTENT: '

U bent ingelogd. Klik hier als u niet automatisch wordt doorgestuurd.

' + SUCCESS_TITLE: 'Inloggen is gelukt' + SilverStripe\Security\DefaultAdminService: + DefaultAdminFirstname: 'Standaard Beheerder' SilverStripe\Security\Group: AddRole: 'Voeg een rol toe aan deze groep' Code: 'Groep code' DefaultGroupTitleAdministrators: Beheerders DefaultGroupTitleContentAuthors: 'Inhoud Auteurs' Description: 'Omschrijving ' + GROUPNAME: 'Groep naam' GroupReminder: 'Als u de bovenliggende groep selecteert, neemt deze groep alle rollen over' HierarchyPermsError: 'U moet (ADMIN) rechten hebben om de bovenliggende groep "{group}" toe te kennen' Locked: 'Gesloten?' + MEMBERS: Leden + NEWGROUP: 'Nieuwe groep' NoRoles: 'Geen rollen gevonden' + PERMISSIONS: Rechten + PLURALNAME: Groepen + PLURALS: + one: 'Een groep' + other: '{count} groepen' Parent: 'Bovenliggende groep' + ROLES: Rollen + ROLESDESCRIPTION: 'Rollen zijn logische groeperingen van rechten die in het Rollen tabblad gewijzigd kunnen worden.
Rollen worden automatisch overgenomen van bovenliggende groepen.' RolesAddEditLink: 'Rollen beheren' + SINGULARNAME: Groep Sort: Sorteer-richting has_many_Permissions: Rechten many_many_Members: Leden SilverStripe\Security\LoginAttempt: + Email: 'E-mailadres ' + EmailHashed: 'E-mailadres (versleuteld)' IP: 'IP adres' + PLURALNAME: Inlogpogingen + PLURALS: + one: 'Een inlogpoging' + other: '{count} inlogpogingen' + SINGULARNAME: Inlogpogingen Status: Status SilverStripe\Security\Member: ADDGROUP: 'Groep toevoegen' BUTTONCHANGEPASSWORD: 'Wachtwoord veranderen' BUTTONLOGIN: Inloggen BUTTONLOGINOTHER: 'Als iemand anders inloggen' + BUTTONLOGOUT: Uitloggen BUTTONLOSTPASSWORD: 'Ik ben mijn wachtwoord vergeten' CONFIRMNEWPASSWORD: 'Bevestig het nieuwe wachtwoord' CONFIRMPASSWORD: 'Bevestig wachtwoord' + CURRENT_PASSWORD: 'Huidige wachtwoord' + EDIT_PASSWORD: 'Nieuw wachtwoord' EMAIL: E-mail EMPTYNEWPASSWORD: 'Het nieuwe wachtwoord mag niet leeg zijn, probeer opnieuw' ENTEREMAIL: 'Typ uw e-mailadres om een link te ontvangen waarmee u uw wachtwoord kunt resetten.' @@ -133,13 +225,21 @@ nl: ERRORWRONGCRED: 'De ingevulde gegevens lijken niet correct. Probeer het nog een keer.' FIRSTNAME: Voornaam INTERFACELANG: 'Interface taal' + KEEPMESIGNEDIN: 'Houd mij ingelogd' LOGGEDINAS: 'U bent ingelogd als {name}.' NEWPASSWORD: 'Nieuw wachtwoord' PASSWORD: Wachtwoord PASSWORDEXPIRED: 'Uw wachtwoord is verlopen. Kies een nieuw wachtwoord.' + PLURALNAME: Leden + PLURALS: + one: 'Een lid' + other: '{count} leden' + REMEMBERME: 'Onthoud mij voor volgende keer? (voor {count} dagen op dit apparaat)' + SINGULARNAME: Lid SUBJECTPASSWORDCHANGED: 'Uw wachtwoord is veranderd' SUBJECTPASSWORDRESET: 'Link om uw wachtwoord opnieuw aan te maken' SURNAME: Achternaam + VALIDATIONADMINLOSTACCESS: 'Niet mogelijk om alle admin-groepen te verwijderen van uw profiel' ValidationIdentifierFailed: 'Een bestaande gebruiker #{id} kan niet dezelfde unieke velden hebben ({name} = {value}))' WELCOMEBACK: 'Welkom terug, {firstname}' YOUROLDPASSWORD: 'Uw oude wachtwoord' @@ -148,15 +248,38 @@ nl: db_LockedOutUntil: 'Gesloten tot' db_Password: Wachtwoord db_PasswordExpiry: 'Wachtwoord vervaldatum' + SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm: + AUTHENTICATORNAME: Inlogformulier + BUTTONFORGOTPASSWORD: 'Wachtwoord vergeten' + BUTTONLOGIN: 'Opnieuw inloggen' + BUTTONLOGOUT: Uitloggen + SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: + ERRORWRONGCRED: 'De ingevulde gegevens lijken niet correct. Probeer het nog een keer.' + NoPassword: 'Er is geen wachtwoord voor deze gebruiker.' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'E-mail & wachtwoord' + SilverStripe\Security\MemberPassword: + PLURALNAME: Gebruikerswachtwoorden + PLURALS: + one: 'Een gebruikerswachtwoord' + other: '{count} Gebruikerswachtwoorden' + SINGULARNAME: Gebruikerswachtwoord SilverStripe\Security\PasswordValidator: LOWCHARSTRENGTH: 'Maak a.u.b. uw wachtwoord sterker door enkele van de volgende karakters te gebruiken: {chars}' PREVPASSWORD: 'U heeft dit wachtwoord in het verleden al gebruikt, kies a.u.b. een nieuw wachtwoord.' TOOSHORT: 'Het wachtwoord is te kort, het moet minimaal {minimum} karakters hebben' SilverStripe\Security\Permission: AdminGroup: Beheerder + CMS_ACCESS_CATEGORY: 'CMS toegang' CONTENT_CATEGORY: Inhoudsrechten FULLADMINRIGHTS: 'Volledige admin rechten' FULLADMINRIGHTS_HELP: 'Impliceert en overstemt alle andere toegewezen rechten.' + PERMISSIONS_CATEGORY: 'Rollen en toegangsrechten' + PLURALNAME: Rechten + PLURALS: + one: Machtiging + other: '{count} rechten' + SINGULARNAME: Machtiging UserPermissionsIntro: 'Groepen aan deze gebruiker toewijzen zullen diens permissies aanpassen. Zie de sectie Groepen voor meer informatie over machtigingen voor afzonderlijke groepen.' SilverStripe\Security\PermissionCheckboxSetField: AssignedTo: 'toegewezen aan "{title}"' @@ -166,21 +289,34 @@ nl: SilverStripe\Security\PermissionRole: OnlyAdminCanApply: 'Alleen admin kan doorvoeren' PLURALNAME: Rollen + PLURALS: + one: 'Een rol' + other: '{count} rollen' SINGULARNAME: Rol Title: Titel SilverStripe\Security\PermissionRoleCode: + PLURALNAME: 'Permissie codes' + PLURALS: + one: 'Een permissiecode' + other: '{count} permissiecodes' PermsError: 'U moet (ADMIN) rechten hebben om de code "{code}" toe te kennen' + SINGULARNAME: Permissiecode + SilverStripe\Security\RememberLoginHash: + PLURALNAME: 'Versleutelde logins' + PLURALS: + one: 'Een versleutelde login' + other: '{count} versleutelde logins' + SINGULARNAME: 'Versleutelde login' SilverStripe\Security\Security: ALREADYLOGGEDIN: 'U hebt geen toegang tot deze pagina. Als u een andere account met de nodige rechten hebt, kan u hieronder opnieuw inloggen.' BUTTONSEND: 'Nieuw wachtwoord aanmaken' CHANGEPASSWORDBELOW: 'U kunt uw wachtwoord hieronder veranderen.' CHANGEPASSWORDHEADER: 'Verander uw wachtwoord' + CONFIRMLOGOUT: 'Klik op onderstaande knop om uit te loggen.' ENTERNEWPASSWORD: 'Voer een nieuw wachtwoord in.' ERRORPASSWORDPERMISSION: 'U moet ingelogd zijn om uw wachtwoord te kunnen veranderen!' LOGIN: 'Meld aan' + LOGOUT: Uitloggen LOSTPASSWORDHEADER: 'Wachtwoord vergeten' NOTEPAGESECURED: 'Deze pagina is beveiligd. Voer uw gegevens in en u wordt automatisch doorgestuurd.' - NOTERESETLINKINVALID: '

De link om uw wachtwoord te kunnen wijzigen is niet meer geldig.

U kunt een nieuwe link aanvragen of uw wachtwoord aanpassen door in te loggen.

' NOTERESETPASSWORD: 'Voer uw e-mailadres in en we sturen een link waarmee u een nieuw wachtwoord kunt instellen.' - PASSWORDSENTHEADER: 'Wachtwoord herstel link verzonden naar {email}' - PASSWORDSENTTEXT: 'Bedankt! Er is een link verstuurd naar {email} om uw wachtwoord opnieuw in te stellen, in de veronderstelling dat er een account bestaat voor dit e-mailadres.' diff --git a/lang/pl.yml b/lang/pl.yml index 03753f33b..1905acb38 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}' @@ -41,7 +84,6 @@ pl: RelationSearch: 'Wyszukiwanie powiązań' ResetFilter: Resetuj SilverStripe\Forms\GridField\GridFieldDeleteAction: - DELETE_DESCRIPTION: Usuń Delete: Usuń DeletePermissionsFailure: 'Brak uprawnień do usuwania' EditPermissionsFailure: 'Nie masz uprawnień, aby odłączyć rekord' @@ -53,57 +95,145 @@ pl: DeletePermissionsFailure: 'Brak uprawnień do usuwania' Deleted: 'Usunięto {type} {name}' 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 +243,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 +268,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,20 +312,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ę.

' NOTERESETPASSWORD: 'Wpisz adres e-mail, na który mamy wysłać link gdzie możesz zresetować swoje hasło' - PASSWORDSENTHEADER: 'Link resetujący hasła został wysłany do ''{email}''' - PASSWORDSENTTEXT: 'Dziękujemy! Link resetujący hasło został wysłany do ''{email}'', o ile konto użytkownika dla takiego e-maila istnieje.' 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..add72be7f 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: Страница @@ -344,7 +339,4 @@ ru: LOGOUT: Выйти LOSTPASSWORDHEADER: 'Восстановление пароля' NOTEPAGESECURED: 'Эта страница защищена. Пожалуйста, введите свои учетные данные для входа.' - NOTERESETLINKINVALID: '

Неверная ссылка переустановки пароля или время действия ссылки истекло.

Вы можете повторно запросить ссылку, щелкнув здесь, или поменять пароль, войдя в систему.

' NOTERESETPASSWORD: 'Введите Ваш адрес email, и Вам будет отправлена ссылка, по которой Вы сможете переустановить свой пароль' - PASSWORDSENTHEADER: 'Ссылка для переустановки пароля выслана на ''{email}''' - PASSWORDSENTTEXT: 'Ссылка переустановки пароля была выслана на адрес ''{email}'' (письмо дойдет до получателя только в том случае, если аккаунт с таким электронным адресом действительно зарегистрирован).' diff --git a/lang/sk.yml b/lang/sk.yml index dba7ecaaf..88f4bf260 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,12 +87,8 @@ 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\GridField\GridFieldVersionedState: - ONLIVEONLYSHORT: 'Len publikované' SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Množstvo FIELDLABELCURRENCY: Mena @@ -235,7 +228,4 @@ sk: LOGIN: Prihlásiť LOSTPASSWORDHEADER: 'Zabudnuté heslo' NOTEPAGESECURED: 'Táto stránka je zabezpečená. Zadajte svoje prihlasovacie údaje a my Vám zároveň pošleme práva.' - NOTERESETLINKINVALID: '

Odkaz na resetovanie hesla nie je platný alebo je vypršala jeho platnosť.

Môžete požiadať o nový tu alebo zmeňte svoje heslo po prihlásení.

' NOTERESETPASSWORD: 'Zadajte svoju e-mailovú adresu a my Vám pošleme odkaz na resetovanie hesla' - PASSWORDSENTHEADER: 'Odkaz na resetovanie hesla bol odoslaný na ''{email}''' - PASSWORDSENTTEXT: 'Ďakujeme! Resetovací odkaz bol odoslaný na ''''{email}'''', pokiaľ účet existuje pre túto emailovú adresu.' diff --git a/lang/sl.yml b/lang/sl.yml index 7e5138547..5496aff5f 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 @@ -140,7 +135,4 @@ sl: LOGIN: Prijava LOSTPASSWORDHEADER: 'Izgubljeno geslo' NOTEPAGESECURED: 'Stran je zaščitena. Da bi lahko nadaljevali, vpišite svoje podatke.' - NOTERESETLINKINVALID: '

Povezava za ponastavitev gesla je napačna ali pa je njena veljavnost potekla.

Tukaj lahko zaprosite za novo povezavo or pa zamenjate geslo, ko se prijavite v sistem.

' NOTERESETPASSWORD: 'Vpišite e-naslov, na katerega vam bomo poslali povezavo za ponastavitev gesla' - PASSWORDSENTHEADER: 'Povezava za ponastavitev gesla je bila poslana na e-naslov ''{email}''.' - PASSWORDSENTTEXT: 'Hvala! Povezava za ponastavitev gesla je bila poslana na e-naslov ''{email}'', ki je naveden kot e-naslov vašega računa. ' 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..622b657b3 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: Валута @@ -156,7 +151,4 @@ sr: ERRORPASSWORDPERMISSION: 'Морате да будете пријављени да бисте променили своју лозинку!' LOGIN: Пријављивање NOTEPAGESECURED: 'Ова страна је обезбеђена. Унесите своје податке и ми ћемо вам послати садржај.' - NOTERESETLINKINVALID: '

Линк за ресетовање лозинке је погрешан или је истекло време за његово коришћење.

Можете да захтевате нови овде или да промените Вашу лозинку након што се пријавите.

' NOTERESETPASSWORD: 'Унесите своју адресу е-поште и ми ћемо вам послати линк помоћу којег можете да промените своју лозинку' - PASSWORDSENTHEADER: 'Линк за ресетовање лозинке послат је на адресу е-поште: ''{email}''' - PASSWORDSENTTEXT: 'Хвала Вам! Линк за ресетовање лозинке је послат не адресу е-поште ''{email}''. Порука ће стићи примаоцу само ако постоји регистрован налог са том адресом е-поште.' diff --git a/lang/sr@latin.yml b/lang/sr@latin.yml index 957457b82..f91aa9337 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 @@ -155,7 +150,4 @@ sr@latin: ERRORPASSWORDPERMISSION: 'Morate da budete prijavljeni da biste promenili svoju lozinku!' LOGIN: Prijavljivanje NOTEPAGESECURED: 'Ova strana je obezbeđena. Unesite svoje podatke i mi ćemo vam poslati sadržaj.' - NOTERESETLINKINVALID: '

Link za resetovanje lozinke je pogrešan ili je isteklo vreme za njegovo korišćenje.

Možete da zahtevate novi ovde ili da promenite Vašu lozinku nakon što se prijavite.

' NOTERESETPASSWORD: 'Unesite svoju adresu e-pošte i mi ćemo vam poslati link pomoću kojeg možete da promenite svoju lozinku' - PASSWORDSENTHEADER: 'Link za resetovanje lozinke poslat je na adresu e-pošte: ''{email}''' - PASSWORDSENTTEXT: 'Hvala Vam! Link za resetovanje lozinke je poslat ne adresu e-pošte ''{email}''. Poruka će stići primaocu samo ako postoji registrovan nalog sa tom adresom e-pošte.' diff --git a/lang/sr_RS.yml b/lang/sr_RS.yml index 62d6aef15..298049430 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: Валута @@ -155,7 +150,4 @@ sr_RS: ERRORPASSWORDPERMISSION: 'Морате да будете пријављени да бисте променили своју лозинку!' LOGIN: Пријављивање NOTEPAGESECURED: 'Ова страна је обезбеђена. Унесите своје податке и ми ћемо вам послати садржај.' - NOTERESETLINKINVALID: '

Линк за ресетовање лозинке је погрешан или је истекло време за његово коришћење.

Можете да захтевате нови овде или да промените Вашу лозинку након што се пријавите.

' NOTERESETPASSWORD: 'Унесите своју адресу е-поште и ми ћемо вам послати линк помоћу којег можете да промените своју лозинку' - PASSWORDSENTHEADER: 'Линк за ресетовање лозинке послат је на адресу е-поште: ''{email}''' - PASSWORDSENTTEXT: 'Хвала Вам! Линк за ресетовање лозинке је послат не адресу е-поште ''{email}''. Порука ће стићи примаоцу само ако постоји регистрован налог са том адресом е-поште.' diff --git a/lang/sr_RS@latin.yml b/lang/sr_RS@latin.yml index c4c4b6900..453ffa17d 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 @@ -156,7 +151,4 @@ sr_RS@latin: ERRORPASSWORDPERMISSION: 'Morate da budete prijavljeni da biste promenili svoju lozinku!' LOGIN: Prijavljivanje NOTEPAGESECURED: 'Ova strana je obezbeđena. Unesite svoje podatke i mi ćemo vam poslati sadržaj.' - NOTERESETLINKINVALID: '

Link za resetovanje lozinke je pogrešan ili je isteklo vreme za njegovo korišćenje.

Možete da zahtevate novi ovde ili da promenite Vašu lozinku nakon što se prijavite.

' NOTERESETPASSWORD: 'Unesite svoju adresu e-pošte i mi ćemo vam poslati link pomoću kojeg možete da promenite svoju lozinku' - PASSWORDSENTHEADER: 'Link za resetovanje lozinke poslat je na adresu e-pošte: ''{email}''' - PASSWORDSENTTEXT: 'Hvala Vam! Link za resetovanje lozinke je poslat ne adresu e-pošte ''{email}''. Poruka će stići primaocu samo ako postoji registrovan nalog sa tom adresom e-pošte.' diff --git a/lang/sv.yml b/lang/sv.yml index 7a8b5b903..88a3b3959 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -1,10 +1,35 @@ sv: + SilverStripe\Admin\LeftAndMain: + VersionUnknown: okänd + SilverStripe\AssetAdmin\Forms\UploadField: + Dimensions: Dimensioner + EDIT: Redigera + EDITINFO: 'Redigera filen' + REMOVE: 'Ta bort' + SilverStripe\Control\ChangePasswordEmail_ss: + CHANGEPASSWORDFOREMAIL: 'Lösenordet för kontot med e-postadressen {email} har ändrats. Om du inte ändrade ditt lösenord, vänligen ändra lösenordet via länken nedan.' + CHANGEPASSWORDTEXT1: 'Du har ändrat ditt lösenord för' + CHANGEPASSWORDTEXT3: 'Ändra lösenord' + HELLO: Hej + SilverStripe\Control\Email\ForgotPasswordEmail_ss: + HELLO: Hej + TEXT1: 'Här är din' + TEXT2: 'återställningslänk för lösenord' + TEXT3: för + SilverStripe\Control\RequestProcessor: + INVALID_REQUEST: 'Ogiltig förfrågan' + REQUEST_ABORTED: 'Förfrågan avbruten' + SilverStripe\Core\Manifest\VersionProvider: + VERSIONUNKNOWN: Okänd SilverStripe\Forms\CheckboxField: NOANSWER: Nej YESANSWER: Ja + SilverStripe\Forms\CheckboxSetField_ss: + NOOPTIONSAVAILABLE: 'Inga tillgängliga val' SilverStripe\Forms\ConfirmedPasswordField: ATLEAST: 'Lösenord måste vara minst {min} tecken långa.' BETWEEN: 'Lösenord måste vara {min} till {max} tecken långa.' + CURRENT_PASSWORD_ERROR: 'Lösenordet du angav är inte korrekt' CURRENT_PASSWORD_MISSING: 'Du måste ange ditt nuvarande lösenord' LOGGED_IN_ERROR: 'Du måste vara inloggad för att ändra ditt lösenord' MAXIMUM: 'Lösenord får som längst vara {max} tecken långa.' @@ -12,17 +37,23 @@ 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})' + SilverStripe\Forms\DatetimeField: + VALIDDATEMAXDATETIME: 'Angivet datum måste vara tidigare eller samma som det senast godkända datumet ({datetime})' + VALIDDATETIMEFORMAT: 'Vänligen ange datum och tid i ett giltigt format ({format})' + VALIDDATETIMEMINDATE: 'Angivet datum måste vara senare eller samma som det tidigast godkända datumet ({datetime})' SilverStripe\Forms\DropdownField: CHOOSE: (Välj) + CHOOSE_MODEL: '(Välj {name})' SOURCE_VALIDATION: 'Vänligen välj att värde i listan. {value} är inget giltigt val' SilverStripe\Forms\EmailField: VALIDATION: 'Var snäll och ange en epostadress' + SilverStripe\Forms\FileUploadReceiver: + FIELDNOTSET: 'Hittade ingen filinformation' SilverStripe\Forms\Form: + BAD_METHOD: 'Detta formulär måste skickas med metoden {method}' CSRF_EXPIRED_MESSAGE: 'Din session har upphört. Var god och skicka in formuläret på nytt.' CSRF_FAILED_MESSAGE: 'Ett tekniskt fel uppstod. Var god klicka på bakåt-knappen, ladda om sidan i webbläsaren och försök igen' VALIDATIONPASSWORDSDONTMATCH: 'Lösenorden stämmer inte överrens ' @@ -31,10 +62,12 @@ sv: VALIDATOR: Validator VALIDCURRENCY: 'Var vänlig ange en korrekt valuta' SilverStripe\Forms\FormField: + EXAMPLE: 't.ex. {format}' NONE: ingen SilverStripe\Forms\GridField\GridField: Add: 'Lägg till {name}' CSVEXPORT: 'Exportera till CSV' + CSVIMPORT: 'Importera från CSV' Filter: Filtrera FilterBy: 'Filtrera på' Find: Hitta @@ -49,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' @@ -61,29 +93,65 @@ 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\GridFieldGroupDeleteAction: + UnlinkSelfFailure: 'Du kan inte radera dig själv från den här gruppen, då du då kommer att förlora dina admin-rättigheter' + SilverStripe\Forms\GridField\GridFieldPaginator: + OF: av + Page: Sida + View: Visa SilverStripe\Forms\MoneyField: FIELDLABELAMOUNT: Belopp FIELDLABELCURRENCY: Valuta + INVALID_CURRENCY: 'Valutan {currency} finns inte med i listan med tillåtna valutor' + SilverStripe\Forms\MultiSelectField: + SOURCE_VALIDATION: 'Vänligen välj ett värde i listan. {value} är inget giltigt val' SilverStripe\Forms\NullableField: IsNullLabel: 'Är NULL' SilverStripe\Forms\NumericField: VALIDATION: '''{value}'' är inget nummer, bara siffror (utan mellanslag) kan accepteras för det här fältet' SilverStripe\Forms\TimeField: VALIDATEFORMAT: 'Var god att ange tid i ett giltigt format ({format})' + SilverStripe\ORM\DataObject: + PLURALNAME: Dataobjekt + PLURALS: + one: 'Ett dataobjekt' + other: '{count} Dataobjekt' + SINGULARNAME: Dataobjekt SilverStripe\ORM\FieldType\DBBoolean: ANY: 'Vilken som helst' NOANSWER: Nej YESANSWER: Ja SilverStripe\ORM\FieldType\DBDate: + DAYS_SHORT_PLURALS: + one: '{count} dag' + other: '{count} dagar' + HOURS_SHORT_PLURALS: + one: '{count} timme' + other: '{count} timmar' LessThanMinuteAgo: 'mindre än en minut' + MINUTES_SHORT_PLURALS: + one: '{count} min' + other: '{count} min' + MONTHS_SHORT_PLURALS: + one: '{count} månad' + other: '{count} månader' + SECONDS_SHORT_PLURALS: + one: '{count} sek' + other: '{count} sek' TIMEDIFFAGO: '{difference} sen' TIMEDIFFIN: 'om {difference}' + YEARS_SHORT_PLURALS: + one: '{count} år' + other: '{count} år' SilverStripe\ORM\FieldType\DBEnum: ANY: 'Vilken som helst' + SilverStripe\ORM\Hierarchy: + LIMITED_TITLE: 'För många barn ({count})' SilverStripe\ORM\Hierarchy\Hierarchy: InfiniteLoopNotAllowed: 'Oändlig loop hittades i hierarkin "{type}". Var vänlig ändra föräldern för att lösa detta' + LIMITED_TITLE: 'För många barn ({count})' + SilverStripe\ORM\ValidationException: + DEFAULT_ERROR: Valideringsfel SilverStripe\Security\BasicAuth: ENTERINFO: 'Var god ange användarnamn och lösenord' ERRORNOTADMIN: 'Den användaren är inte administratör' @@ -92,34 +160,58 @@ sv: PASSWORDEXPIRED: '

Ditt lösenard har gått ut. Vänligen ange ett nytt.

' SilverStripe\Security\CMSSecurity: INVALIDUSER: '

Ogiltig användare. Vänligen ange dina inloggnings-uppgifter igen för att fortsätta.

' + LOGIN_MESSAGE: '

Din session har upphört på grund av inaktivitet

' + LOGIN_TITLE: 'Fortsätt där du slutade genom att logga in igen' SUCCESS: Framgång SUCCESSCONTENT: '

Inloggningen lyckades. Klicka här om du inte skickas vidare automatiskt.

' + SUCCESS_TITLE: 'Inloggning lyckades' SilverStripe\Security\Group: AddRole: 'Lägg till roll för den här gruppen' Code: Gruppkod DefaultGroupTitleAdministrators: Administratörer DefaultGroupTitleContentAuthors: Författare Description: Beskrivning + GROUPNAME: Gruppnamn GroupReminder: 'Om du väljer en förälder till gruppen så kommer gruppen ärva alla förälderns roller' HierarchyPermsError: 'Den överordnade gruppen "{group}" kan inte ges priviligerad tillgång (adminrättigheter krävs)' Locked: 'Låst?' + MEMBERS: Medlemmar + NEWGROUP: 'Ny grupp' NoRoles: 'Inga roller fun' + PERMISSIONS: Behörigheter + PLURALNAME: Grupper + PLURALS: + one: 'En grupp' + other: '{count} grupper' Parent: 'Överordnad grupp' + ROLES: Roller + ROLESDESCRIPTION: 'Roller är fördefinierade samlingar med behörigheter och kan tilldelas grupper.
De ärvs från föräldragrupper om det krävs.' RolesAddEditLink: 'Hantera roller' + SINGULARNAME: Grupp Sort: Sorteringsordning has_many_Permissions: Behörigheter many_many_Members: Medlemmar SilverStripe\Security\LoginAttempt: + Email: E-postadress + EmailHashed: 'E-postadress (hash)' IP: IP-adress + PLURALNAME: Inloggningsförsök + PLURALS: + one: 'Ett inloggningsförsök' + other: '{count} inloggningsförsök' + SINGULARNAME: Inloggningsförsök Status: Status SilverStripe\Security\Member: ADDGROUP: 'Lägg till grupp' BUTTONCHANGEPASSWORD: 'Ändra lösenord' BUTTONLOGIN: 'Logga in' BUTTONLOGINOTHER: 'Logga in som annan användare' + BUTTONLOGOUT: 'Logga ut' BUTTONLOSTPASSWORD: 'Jag har glömt mitt lösenord' CONFIRMNEWPASSWORD: 'Bekräfta nytt lösenord' CONFIRMPASSWORD: 'Bekräfta lösenord' + CURRENT_PASSWORD: 'Nuvarande lösenord' + EDIT_PASSWORD: 'Nytt lösenord' EMAIL: E-post EMPTYNEWPASSWORD: 'Det nya lösenordet kan inte vara tomt, vänligen försök igen' ENTEREMAIL: 'Ange en e-postadress för att få en återställningslänk för lösenordet.' @@ -129,13 +221,21 @@ sv: ERRORWRONGCRED: 'Antingen e-postadressen eller lösenordet är fel. Försök igen.' FIRSTNAME: Förnamn INTERFACELANG: Gränssnittsspråk + KEEPMESIGNEDIN: 'Håll mig inloggad' LOGGEDINAS: 'Du är inloggad som {name}.' NEWPASSWORD: 'Nytt lösenord' PASSWORD: Lösenord PASSWORDEXPIRED: 'Ditt lösenord har gått ut. Vänligen ange ett nytt.' + PLURALNAME: Medlemmar + PLURALS: + one: 'En medlem' + other: '{count} medlemmar' + REMEMBERME: 'Kom ihåg mig nästa gång? (i {count} dagar på denna enhet)' + SINGULARNAME: Medlem SUBJECTPASSWORDCHANGED: 'Ditt lösenord har ändrats' SUBJECTPASSWORDRESET: 'Din återställningslänk' SURNAME: Efternamn + VALIDATIONADMINLOSTACCESS: 'Kan inte radera alla admin-grupper från din profil' ValidationIdentifierFailed: 'Kan inte skriva över medlem #{id} med samma identifierare ({name} = {value}))' WELCOMEBACK: 'Välkommen tillbaka, {firstname}' YOUROLDPASSWORD: 'Ditt gamla lösenord' @@ -144,15 +244,32 @@ sv: db_LockedOutUntil: 'Utelåst till' db_Password: Lösenord db_PasswordExpiry: 'Lösenordet upphör att gälla' + SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm: + AUTHENTICATORNAME: 'CMS inloggnings-formulär' + BUTTONFORGOTPASSWORD: 'Glömt lösenord' + BUTTONLOGIN: 'Logga in igen' + BUTTONLOGOUT: 'Logga ut' + SilverStripe\Security\MemberAuthenticator\MemberAuthenticator: + ERRORWRONGCRED: 'De angivna uppgifterna verkar inte vara korrekta. Var god försök igen.' + NoPassword: 'Det finns inget lösenord för den här medlemmen' + SilverStripe\Security\MemberAuthenticator\MemberLoginForm: + AUTHENTICATORNAME: 'E-post & lösenord' SilverStripe\Security\PasswordValidator: LOWCHARSTRENGTH: 'Var god och stärk ditt lösenord genom att lägga till något av följande tecken: {chars}' PREVPASSWORD: 'Du har redan använt samma lösenord tidigare, var god och välj ett nytt lösenord' TOOSHORT: 'Lösenordet är för kort, det måste innehålla {minimum} eller fler tecken.' SilverStripe\Security\Permission: AdminGroup: Administratör + CMS_ACCESS_CATEGORY: CMS-åtkomst CONTENT_CATEGORY: Innehållsåtkomst FULLADMINRIGHTS: 'Fulla administrativa rättigheter' FULLADMINRIGHTS_HELP: 'Implicerar och gäller över alla andra tillskrivna rättigheter.' + PERMISSIONS_CATEGORY: 'Roller och åtkomstbehörigheter' + PLURALNAME: Behörigheter + PLURALS: + one: 'En behörighet' + other: '{count} behörigheter' + SINGULARNAME: Behörighet UserPermissionsIntro: 'Genom att välja grupper för denna användare så ändras användarens rättigheter. Gå till grupper för att se vilka rättigheter de olika grupperna har.' SilverStripe\Security\PermissionCheckboxSetField: AssignedTo: 'tilldelad till "{title}"' @@ -161,6 +278,11 @@ sv: FromRoleOnGroup: 'ärvt från roll "{roletitle}" i grupp "{grouptitle}"' SilverStripe\Security\PermissionRole: OnlyAdminCanApply: 'Endast administratörer kan tillämpa denna roll' + PLURALNAME: Roller + PLURALS: + one: 'En roll' + other: '{count} roller' + SINGULARNAME: Roll Title: Rollnamn SilverStripe\Security\PermissionRoleCode: PermsError: 'Koden "{code}" kan inte ges privilegierad tillgång (adminrättigheter krävs)' @@ -169,12 +291,11 @@ sv: BUTTONSEND: 'Skicka återställningslänk' CHANGEPASSWORDBELOW: 'Du kan ändra ditt lösenord nedan' CHANGEPASSWORDHEADER: 'Ändra ditt lösenord' + CONFIRMLOGOUT: 'Vänligen klicka på knappen nedan för att bekräfta att du vill logga ut.' ENTERNEWPASSWORD: 'Var god ange ett nytt lösenord.' ERRORPASSWORDPERMISSION: 'Du måste vara inloggad för att kunna ändra ditt lösenord!' LOGIN: 'Logga in' + LOGOUT: 'Logga ut' LOSTPASSWORDHEADER: 'Bortglömt lösenord' NOTEPAGESECURED: 'Den här sidan är låst. Fyll i dina uppgifter nedan så skickar vi dig vidare.' - NOTERESETLINKINVALID: '

Återställningslänk för lösenord är felaktig eller för gammal.

Du kan begära en ny här eller ändra ditt lösenord när du loggat in.

' NOTERESETPASSWORD: 'Ange din e-postadress så skickar vi en länk med vilken du kan återställa ditt lösenord' - PASSWORDSENTHEADER: 'Återställningslänk för lösenord har skickats till ''{email}''' - PASSWORDSENTTEXT: 'Tack en återställningslänk har skickats till ''{email}'', förutsatt att ett konto med den addressen finns.' 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..748ee671c 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: 货币 @@ -171,7 +166,4 @@ zh: LOGIN: 登录 LOSTPASSWORDHEADER: 忘记密码 NOTEPAGESECURED: 该页面受安全保护。请在下面输入您的证书,然后我们会立即将您引导至该页面。 - NOTERESETLINKINVALID: '

密码重设链接无效或已过期。

您可以在这里 要求一个新的或在登录后更改您的密码。

' NOTERESETPASSWORD: 请输入您的电子邮件地址,然后我们会将一个链接发送给您,您可以用它来重设您的密码 - PASSWORDSENTHEADER: '密码重设链接已发送至''{email}''' - PASSWORDSENTTEXT: '谢谢!复位链接已发送到 ''{email}'',假定此电子邮件地址存在一个帐户。' From cf4b16ed380c336a7eaec7e46bec03f7767e10ba Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Thu, 8 Nov 2018 13:23:53 +0200 Subject: [PATCH 066/175] FIX Move password complexity requirements into framework --- _config/passwords.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 _config/passwords.yml diff --git a/_config/passwords.yml b/_config/passwords.yml new file mode 100644 index 000000000..fc865200a --- /dev/null +++ b/_config/passwords.yml @@ -0,0 +1,13 @@ +--- +Name: corepasswords +--- +SilverStripe\Core\Injector\Injector: + SilverStripe\Security\PasswordValidator: + properties: + MinLength: 8 + HistoricCount: 6 + +# In the case someone uses `new PasswordValidator` instead of Injector, provide some safe defaults through config. +SilverStripe\Security\PasswordValidator: + min_length: 8 + historic_count: 6 From 0111b98b18a273ca5c37013d880c5d21f94396af Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Thu, 4 Oct 2018 16:13:32 +1300 Subject: [PATCH 067/175] FIX: Ensure that types are preserved fetching from database MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This ensures that numeric fields appear in PHP as int/float values rather than strings, which allows the development of more type-safe PHP code. This doesn’t work on the legacy mysql driver and this will now throw a notice-level error. It requires mysqlnd. --- src/ORM/Connect/MySQLiConnector.php | 12 +++++++ src/ORM/Connect/PDOConnector.php | 34 ++++++++++++++++---- src/ORM/Connect/PDOQuery.php | 42 +++++++++++++++++++++++-- tests/php/ORM/DatabaseTest.php | 34 ++++++++++++++++++++ tests/php/ORM/DatabaseTest/MyObject.php | 5 ++- 5 files changed, 118 insertions(+), 9 deletions(-) diff --git a/src/ORM/Connect/MySQLiConnector.php b/src/ORM/Connect/MySQLiConnector.php index 43805c705..ba118db81 100644 --- a/src/ORM/Connect/MySQLiConnector.php +++ b/src/ORM/Connect/MySQLiConnector.php @@ -78,6 +78,18 @@ class MySQLiConnector extends DBConnector $this->dbConn = mysqli_init(); + // Use native types (MysqlND only) + if (defined('MYSQLI_OPT_INT_AND_FLOAT_NATIVE')) { + $this->dbConn->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true); + + // The alternative is not ideal, throw a notice-level error + } else { + user_error( + 'mysqlnd PHP library is not available, numeric values will be fetched from the DB as strings', + E_USER_NOTICE + ); + } + // Set SSL parameters if they exist. All parameters are required. if (array_key_exists('ssl_key', $parameters) && array_key_exists('ssl_cert', $parameters) && diff --git a/src/ORM/Connect/PDOConnector.php b/src/ORM/Connect/PDOConnector.php index 7d9f1af4a..6cea5590e 100644 --- a/src/ORM/Connect/PDOConnector.php +++ b/src/ORM/Connect/PDOConnector.php @@ -64,6 +64,12 @@ class PDOConnector extends DBConnector */ protected $cachedStatements = array(); + /** + * Driver + * @var string + */ + private $driver = null; + /** * Flush all prepared statements */ @@ -113,10 +119,11 @@ class PDOConnector extends DBConnector { $this->flushStatements(); - // Build DSN string // Note that we don't select the database here until explicitly // requested via selectDatabase - $driver = $parameters['driver'] . ":"; + $this->driver = $parameters['driver']; + + // Build DSN string $dsn = array(); // Typically this is false, but some drivers will request this @@ -195,13 +202,18 @@ class PDOConnector extends DBConnector $options[PDO::MYSQL_ATTR_SSL_CIPHER] = array_key_exists('ssl_cipher', $parameters) ? $parameters['ssl_cipher'] : self::config()->get('ssl_cipher_default'); } - if (self::is_emulate_prepare()) { - $options[PDO::ATTR_EMULATE_PREPARES] = true; + // Set emulate prepares (unless null / default) + $isEmulatePrepares = self::is_emulate_prepare(); + if (isset($isEmulatePrepares)) { + $options[PDO::ATTR_EMULATE_PREPARES] = (bool)$isEmulatePrepares; } + // Disable stringified fetches + $options[PDO::ATTR_STRINGIFY_FETCHES] = false; + // May throw a PDOException if fails $this->pdoConnection = new PDO( - $driver . implode(';', $dsn), + $this->driver . ':' . implode(';', $dsn), empty($parameters['username']) ? '' : $parameters['username'], empty($parameters['password']) ? '' : $parameters['password'], $options @@ -213,6 +225,16 @@ class PDOConnector extends DBConnector } } + + /** + * Return the driver for this connector + * E.g. 'mysql', 'sqlsrv', 'pgsql' + */ + public function getDriver() + { + return $this->driver; + } + public function getVersion() { return $this->pdoConnection->getAttribute(PDO::ATTR_SERVER_VERSION); @@ -383,7 +405,7 @@ class PDOConnector extends DBConnector } elseif ($statement) { // Count and return results $this->rowCount = $statement->rowCount(); - return new PDOQuery($statement); + return new PDOQuery($statement, $this); } // Ensure statement is closed diff --git a/src/ORM/Connect/PDOQuery.php b/src/ORM/Connect/PDOQuery.php index f5a9739b3..826432219 100644 --- a/src/ORM/Connect/PDOQuery.php +++ b/src/ORM/Connect/PDOQuery.php @@ -22,16 +22,54 @@ class PDOQuery extends Query * Hook the result-set given into a Query class, suitable for use by SilverStripe. * @param PDOStatement $statement The internal PDOStatement containing the results */ - public function __construct(PDOStatement $statement) + public function __construct(PDOStatement $statement, PDOConnector $conn) { $this->statement = $statement; // Since no more than one PDOStatement for any one connection can be safely // traversed, each statement simply requests all rows at once for safety. // This could be re-engineered to call fetchAll on an as-needed basis - $this->results = $statement->fetchAll(PDO::FETCH_ASSOC); + + // Special case for Postgres + if ($conn->getDriver() == 'pgsql') { + $this->results = $this->fetchAllPgsql($statement); + } else { + $this->results = $statement->fetchAll(PDO::FETCH_ASSOC); + } $statement->closeCursor(); } + /** + * Fetch a record form the statement with its type data corrected + * Necessary to fix float data retrieved from PGSQL + * Returns data as an array of maps + * @return array + */ + protected function fetchAllPgsql($statement) + { + $columnCount = $statement->columnCount(); + $columnMeta = []; + for ($i = 0; $i<$columnCount; $i++) { + $columnMeta[$i] = $statement->getColumnMeta($i); + } + + // Re-map fetched data using columnMeta + return array_map( + function ($rowArray) use ($columnMeta) { + $row = []; + foreach ($columnMeta as $i => $meta) { + // Coerce floats from string to float + // PDO PostgreSQL fails to do this + if (isset($meta['native_type']) && strpos($meta['native_type'], 'float') === 0) { + $rowArray[$i] = (float)$rowArray[$i]; + } + $row[$meta['name']] = $rowArray[$i]; + } + return $row; + }, + $statement->fetchAll(PDO::FETCH_NUM) + ); + } + public function seek($row) { $this->rowNum = $row - 1; diff --git a/tests/php/ORM/DatabaseTest.php b/tests/php/ORM/DatabaseTest.php index b9a6fca31..d05a44fc8 100644 --- a/tests/php/ORM/DatabaseTest.php +++ b/tests/php/ORM/DatabaseTest.php @@ -231,4 +231,38 @@ class DatabaseTest extends SapphireTest $this->assertInstanceOf('Exception', $ex); $this->assertEquals('error', $ex->getMessage()); } + + + public function testFieldTypes() + { + // Scaffold some data + $obj = new MyObject(); + $obj->MyField = "value"; + $obj->MyInt = 5; + $obj->MyFloat = 6.0; + $obj->MyBoolean = true; + $obj->write(); + + $record = DB::prepared_query( + 'SELECT * FROM "DatabaseTest_MyObject" WHERE "ID" = ?', + [ $obj->ID ] + )->record(); + + // IDs and ints are returned as ints + $this->assertInternalType('int', $record['ID']); + $this->assertInternalType('int', $record['MyInt']); + + $this->assertInternalType('float', $record['MyFloat']); + + // Booleans are returned as ints – we follow MySQL's lead + $this->assertInternalType('int', $record['MyBoolean']); + + // Strings and enums are returned as strings + $this->assertInternalType('string', $record['MyField']); + $this->assertInternalType('string', $record['ClassName']); + + // Dates are returned as strings + $this->assertInternalType('string', $record['Created']); + $this->assertInternalType('string', $record['LastEdited']); + } } diff --git a/tests/php/ORM/DatabaseTest/MyObject.php b/tests/php/ORM/DatabaseTest/MyObject.php index 83d7fced1..3d09cb1cf 100644 --- a/tests/php/ORM/DatabaseTest/MyObject.php +++ b/tests/php/ORM/DatabaseTest/MyObject.php @@ -13,6 +13,9 @@ class MyObject extends DataObject implements TestOnly private static $create_table_options = array(MySQLSchemaManager::ID => 'ENGINE=InnoDB'); private static $db = array( - 'MyField' => 'Varchar' + 'MyField' => 'Varchar', + 'MyInt' => 'Int', + 'MyFloat' => 'Float', + 'MyBoolean' => 'Boolean', ); } From 261539953568e18361d30d7603c22ad3cb7c8cef Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Thu, 4 Oct 2018 20:25:53 +1300 Subject: [PATCH 068/175] =?UTF-8?q?FIX:=20Use=20PDO=E2=80=99s=20built-in?= =?UTF-8?q?=20transaction=20support=20in=20MySQLDatabase.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ORM/Connect/Database.php | 11 ++ src/ORM/Connect/MySQLDatabase.php | 97 +++++++------- src/ORM/Connect/MySQLTransactionManager.php | 100 +++++++++++++++ src/ORM/Connect/NestedTransactionManager.php | 127 +++++++++++++++++++ src/ORM/Connect/PDOConnector.php | 96 ++++++++++++-- src/ORM/Connect/TransactionManager.php | 59 +++++++++ tests/php/ORM/DatabaseTest.php | 5 + tests/php/ORM/TransactionTest.php | 16 +-- 8 files changed, 445 insertions(+), 66 deletions(-) create mode 100644 src/ORM/Connect/MySQLTransactionManager.php create mode 100644 src/ORM/Connect/NestedTransactionManager.php create mode 100644 src/ORM/Connect/TransactionManager.php diff --git a/src/ORM/Connect/Database.php b/src/ORM/Connect/Database.php index 729770895..fa32db16d 100644 --- a/src/ORM/Connect/Database.php +++ b/src/ORM/Connect/Database.php @@ -583,6 +583,17 @@ abstract class Database */ abstract public function supportsTransactions(); + /** + * Does this database support savepoints in transactions + * By default it is assumed that they don't unless they are explicitly enabled. + * + * @return boolean Flag indicating support for savepoints in transactions + */ + public function supportsSavepoints() + { + return false; + } + /** * Invoke $callback within a transaction * diff --git a/src/ORM/Connect/MySQLDatabase.php b/src/ORM/Connect/MySQLDatabase.php index 74f155e30..c19004885 100644 --- a/src/ORM/Connect/MySQLDatabase.php +++ b/src/ORM/Connect/MySQLDatabase.php @@ -21,7 +21,7 @@ use Exception; * You are advised to backup your tables if changing settings on an existing database * `connection_charset` and `charset` should be equal, similarly so should `connection_collation` and `collation` */ -class MySQLDatabase extends Database +class MySQLDatabase extends Database implements TransactionManager { use Configurable; @@ -49,6 +49,13 @@ class MySQLDatabase extends Database */ private static $charset = 'utf8'; + /** + * Cache for getTransactionManager() + * + * @var TransactionManager + */ + private $transactionManager = null; + /** * Default collation * @@ -57,11 +64,6 @@ class MySQLDatabase extends Database */ private static $collation = 'utf8_general_ci'; - /** - * @var bool - */ - protected $transactionNesting = 0; - public function connect($parameters) { // Ensure that driver is available (required by PDO) @@ -298,73 +300,64 @@ class MySQLDatabase extends Database return $list; } + + /** + * Returns the TransactionManager to handle transactions for this database. + * + * @return TransactionManager + */ + protected function getTransactionManager() + { + if (!$this->transactionManager) { + // PDOConnector providers this + if ($this->connector instanceof TransactionManager) { + $this->transactionManager = new NestedTransactionManager($this->connector); + // Direct database access does not + } else { + $this->transactionManager = new NestedTransactionManager(new MySQLTransactionManager($this)); + } + } + return $this->transactionManager; + } public function supportsTransactions() { return true; } + public function supportsSavepoints() + { + return $this->getTransactionManager()->supportsSavepoints(); + } public function transactionStart($transactionMode = false, $sessionCharacteristics = false) { - if ($this->transactionNesting > 0) { - $this->transactionSavepoint('NESTEDTRANSACTION' . $this->transactionNesting); - } else { - // This sets the isolation level for the NEXT transaction, not the current one. - if ($transactionMode) { - $this->query('SET TRANSACTION ' . $transactionMode); - } - - $this->query('START TRANSACTION'); - - if ($sessionCharacteristics) { - $this->query('SET SESSION TRANSACTION ' . $sessionCharacteristics); - } - } - ++$this->transactionNesting; + $this->getTransactionManager()->transactionStart($transactionMode, $sessionCharacteristics); } public function transactionSavepoint($savepoint) { - $this->query("SAVEPOINT $savepoint"); + $this->getTransactionManager()->transactionSavepoint($savepoint); } public function transactionRollback($savepoint = false) { - // Named transaction - if ($savepoint) { - $this->query('ROLLBACK TO ' . $savepoint); - 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; + return $this->getTransactionManager()->transactionRollback($savepoint); } public function transactionDepth() { - return $this->transactionNesting; + return $this->getTransactionManager()->transactionDepth(); } public function transactionEnd($chain = false) { - // Fail if transaction isn't available - if (!$this->transactionNesting) { - return false; + $result = $this->getTransactionManager()->transactionEnd(); + + if ($chain) { + Deprecation::notice('4.4', '$chain argument is deprecated'); + return $this->getTransactionManager()->transactionStart(); } - --$this->transactionNesting; - if ($this->transactionNesting <= 0) { - $this->transactionNesting = 0; - $this->query('COMMIT AND ' . ($chain ? '' : 'NO ') . 'CHAIN'); - } - return true; + + return $result; } /** @@ -372,6 +365,12 @@ class MySQLDatabase extends Database */ protected function resetTransactionNesting() { + // Check whether to use a connector's built-in transaction methods + if ($this->connector instanceof TransactionalDBConnector) { + if ($this->transactionNesting > 0) { + $this->connector->transactionRollback(); + } + } $this->transactionNesting = 0; } diff --git a/src/ORM/Connect/MySQLTransactionManager.php b/src/ORM/Connect/MySQLTransactionManager.php new file mode 100644 index 000000000..452ce334a --- /dev/null +++ b/src/ORM/Connect/MySQLTransactionManager.php @@ -0,0 +1,100 @@ +dbConn = $dbConn; + } + + public function transactionStart($transactionMode = false, $sessionCharacteristics = false) + { + if ($transactionMode || $sessionCharacteristics) { + Deprecation::notice( + '4.4', + '$transactionMode and $sessionCharacteristics are deprecated and will be removed in SS5' + ); + } + + if ($this->inTransaction) { + throw new DatabaseException( + "Already in transaction, can't start another. Consider decorating with NestedTransactionManager." + ); + } + + // This sets the isolation level for the NEXT transaction, not the current one. + if ($transactionMode) { + $this->dbConn->query('SET TRANSACTION ' . $transactionMode); + } + + $this->dbConn->query('START TRANSACTION'); + + if ($sessionCharacteristics) { + $this->dbConn->query('SET SESSION TRANSACTION ' . $sessionCharacteristics); + } + + $this->inTransaction = true; + return true; + } + + public function transactionEnd($chain = false) + { + if (!$this->inTransaction) { + throw new DatabaseException("Not in transaction, can't end."); + } + + if ($chain) { + user_error( + "transactionEnd() chain argument no longer implemented. Use NestedTransactionManager", + E_USER_WARNING + ); + } + + $this->dbConn->query('COMMIT'); + + $this->inTransaction = false; + return true; + } + + public function transactionRollback($savepoint = null) + { + if (!$this->inTransaction) { + throw new DatabaseException("Not in transaction, can't roll back."); + } + + if ($savepoint) { + $this->dbConn->query("ROLLBACK TO SAVEPOINT $savepoint"); + } else { + $this->dbConn->query('ROLLBACK'); + $this->inTransaction = false; + } + + return true; + } + + public function transactionSavepoint($savepoint) + { + $this->dbConn->query("SAVEPOINT $savepoint"); + } + + public function transactionDepth() + { + return (int)$this->inTransaction; + } + + public function supportsSavepoints() + { + return true; + } +} diff --git a/src/ORM/Connect/NestedTransactionManager.php b/src/ORM/Connect/NestedTransactionManager.php new file mode 100644 index 000000000..46e8090a3 --- /dev/null +++ b/src/ORM/Connect/NestedTransactionManager.php @@ -0,0 +1,127 @@ +child = $child; + } + + /** + * Start a transaction + * @throws DatabaseException on failure + * @return bool True on success + */ + public function transactionStart($transactionMode = false, $sessionCharacteristics = false) + { + if ($this->transactionNesting <= 0) { + $this->transactionNesting = 1; + $this->child->transactionStart($transactionMode, $sessionCharacteristics); + } else { + if ($this->child->supportsSavepoints()) { + $this->child->transactionSavepoint("nesting" . $this->transactionNesting); + } + $this->transactionNesting++; + } + } + + public function transactionEnd($chain = false) + { + if ($this->mustRollback) { + throw new DatabaseException("Child transaction was rolled back, so parent can't be committed"); + } + + if ($this->transactionNesting < 1) { + throw new DatabaseException("Not within a transaction, so can't commit"); + } + + $this->transactionNesting--; + + if ($this->transactionNesting === 0) { + $this->child->transactionEnd(); + } + + if ($chain) { + return $this->transactionStart(); + } + } + + public function transactionRollback($savepoint = null) + { + if ($this->transactionNesting < 1) { + throw new DatabaseException("Not within a transaction, so can't roll back"); + } + + if ($savepoint) { + return $this->child->transactionRollback($savepoint); + } + + $this->transactionNesting--; + + if ($this->transactionNesting === 0) { + $this->child->transactionRollback(); + $this->mustRollback = false; + } else { + if ($this->child->supportsSavepoints()) { + $this->child->transactionRollback("nesting" . $this->transactionNesting); + $this->mustRollback = false; + + // Without savepoints, parent transactions must roll back if a child one has + } else { + $this->mustRollback = true; + } + } + } + + /** + * Return the depth of the transaction. + * + * @return int + */ + public function transactionDepth() + { + return $this->transactionNesting; + } + + public function transactionSavepoint($savepoint) + { + return $this->child->transactionSavepoint($savepoint); + } + + public function supportsSavepoints() + { + return $this->child->supportsSavepoints(); + } +} diff --git a/src/ORM/Connect/PDOConnector.php b/src/ORM/Connect/PDOConnector.php index 6cea5590e..85aba8d84 100644 --- a/src/ORM/Connect/PDOConnector.php +++ b/src/ORM/Connect/PDOConnector.php @@ -10,7 +10,7 @@ use InvalidArgumentException; /** * PDO driver database connector */ -class PDOConnector extends DBConnector +class PDOConnector extends DBConnector implements TransactionManager { /** @@ -21,6 +21,15 @@ class PDOConnector extends DBConnector */ private static $emulate_prepare = false; + /** + * Should we return everything as a string in order to allow transaction savepoints? + * This preserves the behaviour of <= 4.3, including some bugs. + * + * @config + * @var boolean + */ + private static $legacy_types = false; + /** * Default strong SSL cipher to be used * @@ -68,7 +77,13 @@ class PDOConnector extends DBConnector * Driver * @var string */ - private $driver = null; + protected $driver = null; + + /* + * Is a transaction currently active? + * @var bool + */ + protected $inTransaction = false; /** * Flush all prepared statements @@ -202,14 +217,19 @@ class PDOConnector extends DBConnector $options[PDO::MYSQL_ATTR_SSL_CIPHER] = array_key_exists('ssl_cipher', $parameters) ? $parameters['ssl_cipher'] : self::config()->get('ssl_cipher_default'); } - // Set emulate prepares (unless null / default) - $isEmulatePrepares = self::is_emulate_prepare(); - if (isset($isEmulatePrepares)) { - $options[PDO::ATTR_EMULATE_PREPARES] = (bool)$isEmulatePrepares; - } + if (static::config()->get('legacy_types')) { + $options[PDO::ATTR_STRINGIFY_FETCHES] = true; + $options[PDO::ATTR_EMULATE_PREPARES] = true; + } else { + // Set emulate prepares (unless null / default) + $isEmulatePrepares = self::is_emulate_prepare(); + if (isset($isEmulatePrepares)) { + $options[PDO::ATTR_EMULATE_PREPARES] = (bool)$isEmulatePrepares; + } - // Disable stringified fetches - $options[PDO::ATTR_STRINGIFY_FETCHES] = false; + // Disable stringified fetches + $options[PDO::ATTR_STRINGIFY_FETCHES] = false; + } // May throw a PDOException if fails $this->pdoConnection = new PDO( @@ -229,6 +249,8 @@ class PDOConnector extends DBConnector /** * Return the driver for this connector * E.g. 'mysql', 'sqlsrv', 'pgsql' + * + * @return string */ public function getDriver() { @@ -490,4 +512,60 @@ class PDOConnector extends DBConnector { return $this->databaseName && $this->pdoConnection; } + + public function transactionStart($transactionMode = false, $sessionCharacteristics = false) + { + $this->inTransaction = true; + + if ($transactionMode) { + $this->query("SET TRANSACTION $transactionMode"); + } + + if ($this->pdoConnection->beginTransaction()) { + if ($sessionCharacteristics) { + $this->query("SET SESSION CHARACTERISTICS AS TRANSACTION $sessionCharacteristics"); + } + return true; + } + return false; + } + + public function transactionEnd() + { + $this->inTransaction = false; + return $this->pdoConnection->commit(); + } + + public function transactionRollback($savepoint = null) + { + if ($savepoint) { + if ($this->supportsSavepoints()) { + $this->exec("ROLLBACK TO SAVEPOINT $savepoint"); + } else { + throw new DatabaseException("Savepoints not supported on this PDO connection"); + } + } + + $this->inTransaction = false; + return $this->pdoConnection->rollBack(); + } + + public function transactionDepth() + { + return (int)$this->inTransaction; + } + + public function transactionSavepoint($savepoint = null) + { + if ($this->supportsSavepoints()) { + $this->exec("SAVEPOINT $savepoint"); + } else { + throw new DatabaseException("Savepoints not supported on this PDO connection"); + } + } + + public function supportsSavepoints() + { + return static::config()->get('legacy_types'); + } } diff --git a/src/ORM/Connect/TransactionManager.php b/src/ORM/Connect/TransactionManager.php new file mode 100644 index 000000000..bec59835f --- /dev/null +++ b/src/ORM/Connect/TransactionManager.php @@ -0,0 +1,59 @@ +transactionStart(); $obj = new TransactionTest\TestObject(); $obj->Title = 'First page'; @@ -59,10 +62,10 @@ class TransactionTest extends SapphireTest $obj = new TransactionTest\TestObject(); $obj->Title = 'Second page'; $obj->write(); + DB::get_conn()->transactionEnd(); - //Create a savepoint here: - DB::get_conn()->transactionSavepoint('rollback'); - + // Third/Fourth in a rolled back transaction + DB::get_conn()->transactionStart(); $obj = new TransactionTest\TestObject(); $obj->Title = 'Third page'; $obj->write(); @@ -70,11 +73,8 @@ class TransactionTest extends SapphireTest $obj = new TransactionTest\TestObject(); $obj->Title = 'Fourth page'; $obj->write(); + DB::get_conn()->transactionRollback(); - //Revert to a savepoint: - DB::get_conn()->transactionRollback('rollback'); - - DB::get_conn()->transactionEnd(); $first = DataObject::get(TransactionTest\TestObject::class, "\"Title\"='First page'"); $second = DataObject::get(TransactionTest\TestObject::class, "\"Title\"='Second page'"); @@ -85,7 +85,7 @@ class TransactionTest extends SapphireTest $this->assertTrue(is_object($first) && $first->exists()); $this->assertTrue(is_object($second) && $second->exists()); - //These pages should NOT exist, we reverted to a savepoint: + //These pages should NOT exist, we rolled back $this->assertFalse(is_object($third) && $third->exists()); $this->assertFalse(is_object($fourth) && $fourth->exists()); } From a7b5de5de47319b204324d23e5a33403f28df5f7 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Thu, 4 Oct 2018 20:38:55 +1300 Subject: [PATCH 069/175] FIX: ensure that there are PGSQL builds both with and without PDO --- .travis.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index ff7c3f758..1411144aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,20 +20,20 @@ matrix: include: - php: 5.6 env: - - DB=MYSQL + - DB=PGSQL - PHPCS_TEST=1 - PHPUNIT_TEST=framework - php: 7.0 env: - DB=PGSQL + - PDO=1 - PHPUNIT_TEST=framework - php: 7.1 if: type IN (cron) env: - DB=MYSQL - - PDO=1 - PHPUNIT_COVERAGE_TEST=framework - php: 7.2 @@ -50,7 +50,6 @@ matrix: - php: 7.3.0RC1 env: - DB=MYSQL - - PDO=1 - PHPUNIT_TEST=framework sudo: required dist: xenial @@ -74,7 +73,7 @@ before_script: # Install composer dependencies - composer validate - mkdir ./public - - if [[ $DB == PGSQL ]]; then composer require silverstripe/postgresql:2.1.x-dev --no-update; fi + - if [[ $DB == PGSQL ]]; then composer require silverstripe/postgresql:2.2.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.4.x-dev silverstripe/admin:1.4.x-dev silverstripe/versioned:1.4.x-dev --no-update - if [[ $PHPUNIT_TEST == cms ]]; then composer require silverstripe/recipe-cms:4.4.x-dev --no-update; fi From 5531baa87f2cbfae76b5733016f860917813eea5 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Thu, 18 Oct 2018 20:19:39 +1300 Subject: [PATCH 070/175] FIX: Introduce readonly transaction test to all database. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should work on MySQL and PDO; let’s test this. --- tests/php/ORM/DatabaseTest.php | 51 --------------- tests/php/ORM/TransactionTest.php | 104 ++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 51 deletions(-) diff --git a/tests/php/ORM/DatabaseTest.php b/tests/php/ORM/DatabaseTest.php index 247224ae5..0fd108120 100644 --- a/tests/php/ORM/DatabaseTest.php +++ b/tests/php/ORM/DatabaseTest.php @@ -21,11 +21,6 @@ class DatabaseTest extends SapphireTest protected $usesDatabase = true; - /** - * Disable transactions so that we can test them - */ - protected $usesTransactions = false; - public function testDontRequireField() { $schema = DB::get_schema(); @@ -192,52 +187,6 @@ class DatabaseTest extends SapphireTest $this->assertTrue($db->canLock('DatabaseTest'), 'Can lock again after releasing it'); } - public function testTransactions() - { - $conn = DB::get_conn(); - if (!$conn->supportsTransactions()) { - $this->markTestSkipped("DB Doesn't support transactions"); - return; - } - - // Test that successful transactions are comitted - $obj = new DatabaseTest\MyObject(); - $failed = false; - $conn->withTransaction( - function () use (&$obj) { - $obj->MyField = 'Save 1'; - $obj->write(); - }, - function () use (&$failed) { - $failed = true; - } - ); - $this->assertEquals('Save 1', DatabaseTest\MyObject::get()->first()->MyField); - $this->assertFalse($failed); - - // Test failed transactions are rolled back - $ex = null; - $failed = false; - try { - $conn->withTransaction( - function () use (&$obj) { - $obj->MyField = 'Save 2'; - $obj->write(); - throw new Exception("error"); - }, - function () use (&$failed) { - $failed = true; - } - ); - } catch (Exception $ex) { - } - $this->assertTrue($failed); - $this->assertEquals('Save 1', DatabaseTest\MyObject::get()->first()->MyField); - $this->assertInstanceOf('Exception', $ex); - $this->assertEquals('error', $ex->getMessage()); - } - - public function testFieldTypes() { // Scaffold some data diff --git a/tests/php/ORM/TransactionTest.php b/tests/php/ORM/TransactionTest.php index 88617f26c..c6e892f28 100644 --- a/tests/php/ORM/TransactionTest.php +++ b/tests/php/ORM/TransactionTest.php @@ -5,6 +5,7 @@ namespace SilverStripe\ORM\Tests; use SilverStripe\ORM\DB; use SilverStripe\ORM\DataObject; use SilverStripe\Dev\SapphireTest; +use SilverStripe\Dev\Deprecation; use SilverStripe\ORM\Tests\TransactionTest\TestObject; class TransactionTest extends SapphireTest @@ -17,6 +18,20 @@ class TransactionTest extends SapphireTest TransactionTest\TestObject::class, ]; + private static $originalVersionInfo; + + protected function setUp() + { + parent::setUp(); + self::$originalVersionInfo = Deprecation::dump_settings(); + } + + protected function tearDown() + { + Deprecation::restore_settings(self::$originalVersionInfo); + parent::tearDown(); + } + public static function setUpBeforeClass() { parent::setUpBeforeClass(); @@ -25,8 +40,57 @@ class TransactionTest extends SapphireTest } } + public function testTransactions() + { + $conn = DB::get_conn(); + if (!$conn->supportsTransactions()) { + $this->markTestSkipped("DB Doesn't support transactions"); + return; + } + + // Test that successful transactions are comitted + $obj = new TestObject(); + $failed = false; + $conn->withTransaction( + function () use (&$obj) { + $obj->Title = 'Save 1'; + $obj->write(); + }, + function () use (&$failed) { + $failed = true; + } + ); + $this->assertEquals('Save 1', TestObject::get()->first()->Title); + $this->assertFalse($failed); + + // Test failed transactions are rolled back + $ex = null; + $failed = false; + try { + $conn->withTransaction( + function () use (&$obj) { + $obj->Title = 'Save 2'; + $obj->write(); + throw new \Exception("error"); + }, + function () use (&$failed) { + $failed = true; + } + ); + } catch (\Exception $ex) { + } + $this->assertTrue($failed); + $this->assertEquals('Save 1', TestObject::get()->first()->Title); + $this->assertInstanceOf('Exception', $ex); + $this->assertEquals('error', $ex->getMessage()); + } + public function testNestedTransaction() { + if (!DB::get_conn()->supportsSavepoints()) { + static::markTestSkipped('Current database does not support savepoints'); + } + $this->assertCount(0, TestObject::get()); try { DB::get_conn()->withTransaction(function () { @@ -89,4 +153,44 @@ class TransactionTest extends SapphireTest $this->assertFalse(is_object($third) && $third->exists()); $this->assertFalse(is_object($fourth) && $fourth->exists()); } + + public function testReadOnlyTransaction() + { + if (!DB::get_conn()->supportsTransactions()) { + $this->markTestSkipped('Current database is doesn\'t support transactions'); + return; + } + + // This feature is deprecated in 4.4, but we're still testing it. + Deprecation::notification_version('4.3.0'); + + $page = new TestObject(); + $page->Title = 'Read only success'; + $page->write(); + + DB::get_conn()->transactionStart('READ ONLY'); + + try { + $page = new TestObject(); + $page->Title = 'Read only page failed'; + $page->write(); + DB::get_conn()->transactionEnd(); + } catch (\Exception $e) { + //could not write this record + //We need to do a rollback or a commit otherwise we'll get error messages + DB::get_conn()->transactionRollback(); + } + + DataObject::flush_and_destroy_cache(); + + $success = DataObject::get_one(TestObject::class, "\"Title\"='Read only success'"); + $fail = DataObject::get_one(TestObject::class, "\"Title\"='Read only page failed'"); + + //This page should be in the system + $this->assertInternalType('object', $success); + $this->assertTrue($success->exists()); + + //This page should NOT exist, we had 'read only' permissions + $this->assertNull($fail); + } } From 0cc39af382513b48a29634556cb25dda42b7e02e Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Fri, 9 Nov 2018 11:08:36 +1300 Subject: [PATCH 071/175] DOC: Added documentation for strict type changes --- .../00_Model/08_SQL_Select.md | 12 ++++++++++++ docs/en/04_Changelogs/4.4.0.md | 14 ++++++++++++++ src/ORM/Connect/Query.php | 16 ++++++++++++++-- src/ORM/Connect/TransactionManager.php | 12 ++++++++++-- 4 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 docs/en/04_Changelogs/4.4.0.md diff --git a/docs/en/02_Developer_Guides/00_Model/08_SQL_Select.md b/docs/en/02_Developer_Guides/00_Model/08_SQL_Select.md index dc85ec060..ee4c31c81 100644 --- a/docs/en/02_Developer_Guides/00_Model/08_SQL_Select.md +++ b/docs/en/02_Developer_Guides/00_Model/08_SQL_Select.md @@ -291,6 +291,18 @@ $players = Player::get(); $map = $players->map('Name', 'NameWithBirthyear'); ``` +### Data types + +As of SilverStripe 4.4, the following PHP types will be used to return datbase content: + + * booleans will be an integer 1 or 0, to ensure consistency with MySQL that doesn't have native booleans. + * integer types returned as integers + * floating point / decimal types returned as floats + * strings returned as strings + * dates / datetimes returned as strings + +Up until SilverStripe 4.3, bugs meant that strings were used for every column type. + ## Related Lessons * [Building custom SQL](https://www.silverstripe.org/learn/lessons/v4/beyond-the-orm-building-custom-sql-1) diff --git a/docs/en/04_Changelogs/4.4.0.md b/docs/en/04_Changelogs/4.4.0.md new file mode 100644 index 000000000..f37ba13a6 --- /dev/null +++ b/docs/en/04_Changelogs/4.4.0.md @@ -0,0 +1,14 @@ +# 4.4.0 + +## Overview {#overview} + + - [Correct PHP types are now returned from database queries](/developer_guides/model/sql_select#data-types) + +## Upgrading {#upgrading} + +tbc + +## Changes to internal APIs + + - `PDOQuery::__construct()` now has a 2nd argument. If you have subclassed PDOQuery and overridden __construct() + you may see an E_STRICT error diff --git a/src/ORM/Connect/Query.php b/src/ORM/Connect/Query.php index 4a1b28b88..1f994dd7f 100644 --- a/src/ORM/Connect/Query.php +++ b/src/ORM/Connect/Query.php @@ -6,12 +6,24 @@ use SilverStripe\Core\Convert; use Iterator; /** - * Abstract query-result class. + * Abstract query-result class. A query result provides an iterator that returns a map for each record of a query + * result. + * + * The map should be keyed by the column names, and the values should use the following types: + * + * - boolean returned as integer 1 or 0 (to ensure consistency with MySQL that doesn't have native booleans) + * - integer types returned as integers + * - floating point / decimal types returned as floats + * - strings returned as strings + * - dates / datetimes returned as strings + * + * Note that until SilverStripe 4.3, bugs meant that strings were used for every column type. + * * Once again, this should be subclassed by an actual database implementation. It will only * ever be constructed by a subclass of SS_Database. The result of a database query - an iteratable object * that's returned by DB::SS_Query * - * Primarily, the SS_Query class takes care of the iterator plumbing, letting the subclasses focusing + * Primarily, the Query class takes care of the iterator plumbing, letting the subclasses focusing * on providing the specific data-access methods that are required: {@link nextRecord()}, {@link numRecords()} * and {@link seek()} */ diff --git a/src/ORM/Connect/TransactionManager.php b/src/ORM/Connect/TransactionManager.php index bec59835f..7dbb6be1f 100644 --- a/src/ORM/Connect/TransactionManager.php +++ b/src/ORM/Connect/TransactionManager.php @@ -3,7 +3,13 @@ namespace SilverStripe\ORM\Connect; /** - * Represents an object that is capable of controlling transactions + * Represents an object that is capable of controlling transactions. + * + * The TransactionManager might be the database connection itself, calling queries to orchestrate + * transactions, or a connector such as the PDOConnector. + * + * Generally speaking you should rely on your Database object to manage the creation of a TansactionManager + * for you; unless you are building new database connectors this should be treated as an internal API. */ interface TransactionManager { @@ -51,7 +57,9 @@ interface TransactionManager public function transactionDepth(); /** - * Return true if savepoints are supported by this transaction manager + * Return true if savepoints are supported by this transaction manager. + * Savepoints aren't supported by all database connectors (notably PDO doesn't support them) + * and should be used with caution. * * @return boolean */ From a843e136e8f653d2a979cdcdf0ced38575ae0e49 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Fri, 9 Nov 2018 11:09:07 +1300 Subject: [PATCH 072/175] Added 4.0.5 changelog --- docs/en/04_Changelogs/4.0.5.md | 100 +++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/docs/en/04_Changelogs/4.0.5.md b/docs/en/04_Changelogs/4.0.5.md index 0fc65d7f7..52bb06871 100644 --- a/docs/en/04_Changelogs/4.0.5.md +++ b/docs/en/04_Changelogs/4.0.5.md @@ -10,3 +10,103 @@ behaviour to that of SilverStripe 3 where `Extension` instances are of lowest im default value. If you rely on your `Extension` or module providing an overriding config value, please move this to yaml. + +## Change Log + +### Security + + * 2018-11-02 [aebaa46](https://github.com/silverstripe/silverstripe-admin/commit/aebaa46f1f8834fefa09bdaf85bfdd51229f58b3) Add CSRF to Apollo (Aaron Carlino) - See [ss-2018-007](https://www.silverstripe.org/download/security-releases/ss-2018-007) + * 2018-08-21 [8d7c2dafa](https://github.com/silverstripe/silverstripe-framework/commit/8d7c2dafabad505d769f3774c44e0595fb1a4cd9) Add confirmation token to dev/build (Loz Calver) - See [ss-2018-019](https://www.silverstripe.org/download/security-releases/ss-2018-019) + * 2018-07-29 [9aabe0a0f](https://github.com/silverstripe/silverstripe-framework/commit/9aabe0a0f7a061d87cc92923f8811e14d7a032f5) Ignore arguments in mysqli::real_connect backtrace calls (Robbie Averill) - See [ss-2018-018](https://www.silverstripe.org/download/security-releases/ss-2018-018) + * 2018-05-08 [19fdebfa2](https://github.com/silverstripe/silverstripe-framework/commit/19fdebfa245506626561bc9626d9ac325acb14da) Remove dotm, potm, jar, css, js, xltm from default File.allowed_extensions (Robbie Averill) - See [ss-2018-014](https://www.silverstripe.org/download/security-releases/ss-2018-014) + * 2018-04-11 [577138882](https://github.com/silverstripe/silverstripe-framework/commit/577138882163e4b8782ea043487944d30d88e753) Restrict non-admins from being assigned to admin groups (Damian Mooyman) - See [ss-2018-001](https://www.silverstripe.org/download/security-releases/ss-2018-001) + +### API Changes + + * 2017-09-12 [c54b07a95](https://github.com/silverstripe/silverstripe-framework/commit/c54b07a9528aeef3907b4342a725af10d9797cd8) Update to use new chromedriver + behat-extension + facebook/webdriver (Damian Mooyman) + +### Features and Enhancements + + * 2018-04-19 [1509a12fd](https://github.com/silverstripe/silverstripe-framework/commit/1509a12fdf0fe8cbd300271fd5c60c3d76647d84) Only run coverage test as a cron (Damian Mooyman) + * 2018-04-09 [87d69ba7](https://github.com/silverstripe/silverstripe-cms/commit/87d69ba75366ff63563e5b9b159fb643daa4f1d7) Use i18n template for page tree title (Damian Mooyman) + * 2018-03-05 [32637413d](https://github.com/silverstripe/silverstripe-framework/commit/32637413deceb1a3c647fd51a78e1352e91ee15a) Improve upgrade rules to support advanced upgrader rewrites (#7903) (Damian Mooyman) + * 2018-03-05 [8c35e339](https://github.com/silverstripe/silverstripe-cms/commit/8c35e3391cf334917c5e314c6f1c459e9d06fbbc) Improve upgrade rules to support advanced upgrader rewrites (#2114) (Damian Mooyman) + * 2018-03-01 [61cfcc5](https://github.com/silverstripe/silverstripe-versioned/commit/61cfcc52a46895b6ff7fdb12956e01c48bfcf343) Improve upgrade rules to support advanced upgrader rewrites (Damian Mooyman) + * 2018-03-01 [e77e0f7](https://github.com/silverstripe/silverstripe-assets/commit/e77e0f758ed243a249c6056dbfc44f48b9fa535d) Improve upgrade rules to support advanced upgrader rewrites (Damian Mooyman) + * 2018-02-12 [9ce21338a](https://github.com/silverstripe/silverstripe-framework/commit/9ce21338a3083c80128c5923eab3d8b968f4dd83) composer.json missing notice (zanderwar) + * 2017-11-15 [c7ab5846d](https://github.com/silverstripe/silverstripe-framework/commit/c7ab5846df7e3f460b1c38e04a0946a914a35c19) Don't infer trace if explicitly provided (Damian Mooyman) + +### Bugfixes + + * 2018-09-18 [bbe7c66](https://github.com/silverstripe/silverstripe-asset-admin/commit/bbe7c660cf40d4c942eaf6e76755eeaf46c63471) Add `AssetAdmin::getMinimalistObjectFromData()` to build file metadata for UploadField (#829) (Maxime Rainville) + * 2018-09-03 [1c4311d](https://github.com/silverstripe/silverstripe-asset-admin/commit/1c4311d4e6548600272daa0ce83afa12cf7e99c3) fix description for docs.silverstripe.org (wernerkrauss) + * 2018-09-03 [b922c0d73](https://github.com/silverstripe/silverstripe-framework/commit/b922c0d7327b5d0222dd280afcb64f83a09ea859) Check scheme is truthy before setting it to the request (Robbie Averill) + * 2018-08-28 [d651d0fbf](https://github.com/silverstripe/silverstripe-framework/commit/d651d0fbfcababeaf317b27cb00b4f33b9d99eab) Use base class (not remapping target class) when looking up whether object is versioned (Robbie Averill) + * 2018-07-26 [fea9ef7](https://github.com/silverstripe/silverstripe-admin/commit/fea9ef7d2a53904086f9fad6eedba7bb307c8578) #579 BUG Ambiguous column RecordID when doing batch actions (Ed Linklater) + * 2018-06-13 [a2a8004](https://github.com/silverstripe/silverstripe-admin/commit/a2a800464b8f430529ee291a8b75e422ceca7914) Update user help link to 4 (Sacha Judd) + * 2018-06-13 [932eb2b2](https://github.com/silverstripe/silverstripe-cms/commit/932eb2b22dfe6c30473b1cf973661c28c5b9c635) Fix CMS components failing to register on other CMS sections (#2182) (Damian Mooyman) + * 2018-06-12 [7b04949ca](https://github.com/silverstripe/silverstripe-framework/commit/7b04949caa11d6e5c8cace3453cf2ed29996fb06) Remove duplicate key (Damian Mooyman) + * 2018-06-12 [c9bcc07](https://github.com/silverstripe/silverstripe-assets/commit/c9bcc070fdbb76fef49f7564eb98a4a81e2ed65f) Remove duplicate .upgrade.yml keys (Damian Mooyman) + * 2018-06-12 [674b92c](https://github.com/silverstripe/silverstripe-admin/commit/674b92c125488cb6bc43cade4c93e9adccb27e9b) Fix invalid .upgrade.yml (Damian Mooyman) + * 2018-06-11 [2a51f34c3](https://github.com/silverstripe/silverstripe-framework/commit/2a51f34c3e3c44acd603def241ac4447e715b165) Prevent canonical URL causing a redirect on CLI unless explicitly enabled (Damian Mooyman) + * 2018-06-07 [29f9b1c18](https://github.com/silverstripe/silverstripe-framework/commit/29f9b1c18fb38dab912a0b9dcae63eacae19335d) Fix linting issues (Damian Mooyman) + * 2018-06-07 [e37e3e174](https://github.com/silverstripe/silverstripe-framework/commit/e37e3e1746e56c866ee875f41a7fddf61c926d9f) Fix test that relies on implicit ID order breaking postgres (Damian Mooyman) + * 2018-06-07 [66f57bd4d](https://github.com/silverstripe/silverstripe-framework/commit/66f57bd4dac0bd4c8106f8071ddc45103c2643f2) Only set MYSQL_ATTR_INIT_COMMAND when using mysql driver (fixes #8103) (Loz Calver) + * 2018-06-06 [c070e989c](https://github.com/silverstripe/silverstripe-framework/commit/c070e989c4de41441d1061d2678b461f3f13d63b) Safely handle empty injector factory responses (Damian Mooyman) + * 2018-06-04 [41e601a03](https://github.com/silverstripe/silverstripe-framework/commit/41e601a036307065d9ea2ba8862f67be738d402f) Regression from #8009 (Daniel Hensby) + * 2018-06-01 [5a5ba1e5c](https://github.com/silverstripe/silverstripe-framework/commit/5a5ba1e5c001de161fbeb19d6d662391dccc4c1e) Fix: negative values in read only currency field (Jonathon Menz) + * 2018-06-01 [582c69d32](https://github.com/silverstripe/silverstripe-framework/commit/582c69d32fd8f18e6c06bc0b4c0a7e3e87e67966) Fix issue with Disabled DateField always display (not set). (Maxime Rainville) + * 2018-05-29 [1cbf27e0f](https://github.com/silverstripe/silverstripe-framework/commit/1cbf27e0f47c3547914b03193d0f5f77c87ff8d5) PHP 5.3 compat for referencing $this in closure, and make method public for same reason (Robbie Averill) + * 2018-05-21 [bf5b578](https://github.com/silverstripe/silverstripe-admin/commit/bf5b5787685765c35c175c303f3f7ee719ac9453) Adding a min-width to flexbox-area-grow that allows flex blocks to shrink below their content width (Guy) + * 2018-05-18 [953153500](https://github.com/silverstripe/silverstripe-framework/commit/953153500d490f5b5abf7283c34242c3b22a855a) Polymorphic relationship class columns have obsolete class names remapped (Robbie Averill) + * 2018-05-08 [97a8f56](https://github.com/silverstripe/silverstripe-admin/commit/97a8f56c43ddb3c77a5bbc452755d44afb9a9472) Add missing focus styles for preview options (fixes silverstripe/silverstripe-framework #2101) (Loz Calver) + * 2018-05-02 [80bf0fc48](https://github.com/silverstripe/silverstripe-framework/commit/80bf0fc48774b2a25f95feb24ffcc9df8e5ad77c) bad syntax (Daniel Hensby) + * 2018-04-18 [fe4b90edc](https://github.com/silverstripe/silverstripe-framework/commit/fe4b90edc0ead9c6c77d606101bfbf568a963fb4) Duplicating many_many relationships looses the extra fields in 4.0 (UndefinedOffset) + * 2018-04-17 [f83691e7f](https://github.com/silverstripe/silverstripe-framework/commit/f83691e7f7e7a75657df1211673b72d9cf4c4b4f) Make invalid dev actions 404 not 500 error (Damian Mooyman) + * 2018-04-17 [af3a9f3ec](https://github.com/silverstripe/silverstripe-framework/commit/af3a9f3ec8a5465f841c5aa8ee1faf40c1b76bf4) Duplicating many_many relationships looses the extra fields (fixes #7973) (UndefinedOffset) + * 2018-04-10 [e11ba9a2d](https://github.com/silverstripe/silverstripe-framework/commit/e11ba9a2d7c89a1ecea8613589f05399b45a33bf) Fix many_many through crashing ModelAdmin (Damian Mooyman) + * 2018-04-08 [eeac1d1](https://github.com/silverstripe/silverstripe-admin/commit/eeac1d11800e70f19055bfa2ba4aec8b6a9b2ccb) Fix issue with selected values in large trees breaking initialisation (#476) (Damian Mooyman) + * 2018-03-29 [4acec3356](https://github.com/silverstripe/silverstripe-framework/commit/4acec33562e4e1230092eee7d76c2b8061ffc914) 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 [dd44deacb](https://github.com/silverstripe/silverstripe-framework/commit/dd44deacb462d80dbbda507fdb4e9527f049d3bd) Fix for "too few parameters" error when using DBMultiEnum (Andreas Lindahl) + * 2018-03-22 [cf5a0984](https://github.com/silverstripe/silverstripe-cms/commit/cf5a0984addf308d2cb10df9b67386be2a080f18) Correct SilverStripeNavigator correctly in templates (Daniel Hensby) + * 2018-03-15 [d17d93f7](https://github.com/silverstripe/silverstripe-cms/commit/d17d93f784a6e01f3d396c55adc623d69a90261a) Remove SearchForm results() function from allowed_actions (Steve Dixon) + * 2018-03-11 [2b9faf46](https://github.com/silverstripe/silverstripe-cms/commit/2b9faf46fe6606a9236f9e1ec987f9a22689a2c7) Fix InSection failing on non-page controllers (Damian Mooyman) + * 2018-03-07 [bf2cee398](https://github.com/silverstripe/silverstripe-framework/commit/bf2cee3989028aaa461e9f0f929724b7738c1399) Bugfix - Correct duplicate nesting of 'Content' to be returned to template (Joe Harvey) + * 2018-03-06 [5fee4a81a](https://github.com/silverstripe/silverstripe-framework/commit/5fee4a81aa880338fba7bb72731fd2b7be4643de) Files dataobjects with missing asset shouldn't un-attach themselves from parent object on save (Damian Mooyman) + * 2018-03-05 [dde13493](https://github.com/silverstripe/silverstripe-cms/commit/dde134936825e196ca97cb86ac3f5bc24d52278e) Fix invalid css classname in virtualpage (Damian Mooyman) + * 2018-03-05 [985a0af](https://github.com/silverstripe/silverstripe-admin/commit/985a0af292bb833ba48fa907a1b75892182ec390) Fix page icons (Damian Mooyman) + * 2018-03-02 [3bd714d](https://github.com/silverstripe/silverstripe-assets/commit/3bd714d293c3f6c12e8f7a0b3c7e054d99b410bd) Typo in "audio file" translation (Robbie Averill) + * 2018-03-01 [40c2e299a](https://github.com/silverstripe/silverstripe-framework/commit/40c2e299a0a9a63b4e64e14dff95e9f7d480db6e) Fix "mb_stripos(): Empty delimiter" warning when no search-keywords are given for `DBText::ContextSummary`. (Roman Schmid) + * 2018-02-27 [d91c6ed](https://github.com/silverstripe/silverstripe-admin/commit/d91c6ed0dc699f769802e5d1310f3fe111dd8ecf) Fix $CMSVersion appearing visually (Damian Mooyman) + * 2018-02-26 [b27102f81](https://github.com/silverstripe/silverstripe-framework/commit/b27102f810e873d287fa04678a4ff242c40699f6) Fix incorrect assets created when ASSETS_PATH !== BASE_PATH . '/assets' (Damian Mooyman) + * 2018-02-22 [012bfec5](https://github.com/silverstripe/silverstripe-cms/commit/012bfec5bf8e0902f3325c8e7fb237d48bd189ad) Bug field help text translations no longer need to be HTML encoded (Rick Hambrook) + * 2018-02-20 [83c4ab8d](https://github.com/silverstripe/silverstripe-cms/commit/83c4ab8d180954b3d80d16ed5f5764e3c647ca6d) Fix test regressions in CMS page filters (Damian Mooyman) + * 2018-02-19 [cfe82e9](https://github.com/silverstripe/silverstripe-assets/commit/cfe82e912616ca230b8fd29fba3bd3270fac2502) Fix behaviour towards versioned but unstagable records (Damian Mooyman) + * 2018-02-19 [4fc8166](https://github.com/silverstripe/silverstripe-versioned/commit/4fc816653e84c0a883a01afe30e16e8bd4129f53) Fix behaviour towards versioned but unstagable records (Damian Mooyman) + * 2018-02-19 [0e26c0664](https://github.com/silverstripe/silverstripe-framework/commit/0e26c066440d2591401e84d9688cbeef0595afcc) Fix behaviour towards versioned but unstagable records (Damian Mooyman) + * 2018-02-19 [3be0478e](https://github.com/silverstripe/silverstripe-cms/commit/3be0478e1c40cd2b9f577818596c4222b365b6b6) Fix behaviour towards versioned but unstagable records (Damian Mooyman) + * 2018-02-19 [8be3930](https://github.com/silverstripe/silverstripe-versioned/commit/8be393061e8578c3bd9056c6540e5f0bbff43801) Fix doRollbackTo() writing old / unsaved version over restored version (Damian Mooyman) + * 2018-02-16 [86addea1d](https://github.com/silverstripe/silverstripe-framework/commit/86addea1d2a7b2e28ae8115279ae358bcb46648a) Split HTML manipulation to onadd, so elements are not accidentally duplicated (Christopher Joe) + * 2018-02-13 [c767e472d](https://github.com/silverstripe/silverstripe-framework/commit/c767e472dc494408460ef47c27b8d34475da4ac6) DataObject singleton creation (Jonathon Menz) + * 2018-02-13 [f2b82b1f7](https://github.com/silverstripe/silverstripe-framework/commit/f2b82b1f77a60de4bf1b5807e1b820aad263ae1b) Fix docs for configuring before/after a specific config file (Christopher Joe) + * 2018-02-13 [c6095cf](https://github.com/silverstripe/silverstripe-config/commit/c6095cfc0a07a74bb932e2191215d06f102e992a) Fix word boundary issue with pathname matching (Christopher Joe) + * 2018-02-13 [1d27a14](https://github.com/silverstripe/silverstripe-admin/commit/1d27a14be75efb33a503f7f1c15b093ab3b59c7f) Remove border-radius add hover states to non-active tabs (Sacha Judd) + * 2018-02-12 [ad52ced](https://github.com/silverstripe/silverstripe-versioned/commit/ad52ced4353b8abe312aeacfb2c95657169feedc) Prevent nested permissions from breaking recursive publishing (Damian Mooyman) + * 2018-02-12 [0f08f85](https://github.com/silverstripe/silverstripe-admin/commit/0f08f85508d01a578015848caff032ae0fd62e4c) improve the browser warning logic show (Christopher Joe) + * 2018-02-08 [d86e5dfc](https://github.com/silverstripe/silverstripe-cms/commit/d86e5dfc883267ffaa0c43e9ece7576c4f42ed61) remove now superfluous print action destroyer (Dylan Wagstaff) + * 2018-02-08 [d3278d547](https://github.com/silverstripe/silverstripe-framework/commit/d3278d5470165bba14ee5026453ec7d529901f42) Add Nested DB transaction support (#7848) (Daniel Hensby) + * 2018-02-08 [0a486b8f5](https://github.com/silverstripe/silverstripe-framework/commit/0a486b8f5705242de523489190f3975d55b3b3e6) Fix issue with CLIDebugView failing on class name of existing class (Damian Mooyman) + * 2018-02-06 [0094c19](https://github.com/silverstripe/silverstripe-admin/commit/0094c19304eea5ac02daf42095da341315dae84f) Add text-colour to status-archived, remove span.badge styles (Sacha Judd) + * 2018-02-06 [6b38031a1](https://github.com/silverstripe/silverstripe-framework/commit/6b38031a1e16e94d5bafcbcce4bdcb2d6b3680ed) Fix Director::test() not persisting removed session keys on teardown (Damian Mooyman) + * 2018-02-06 [660dfd34a](https://github.com/silverstripe/silverstripe-framework/commit/660dfd34a828e7eb7dc8ef9986b201a14620d17f) Issue where default admin has no password encryption (Daniel Hensby) + * 2018-02-05 [28ca11dd7](https://github.com/silverstripe/silverstripe-framework/commit/28ca11dd7e5e9a1c4fd1f5d4acbec856adfb7176) Regex range identifier correctly escaped (Daniel Hensby) + * 2018-02-04 [1ff32b3](https://github.com/silverstripe/silverstripe-admin/commit/1ff32b347911c1a6f8521f31e79131db68ed3084) Ensure lang is detected from html tag (Martin P) + * 2018-01-26 [416915b08](https://github.com/silverstripe/silverstripe-framework/commit/416915b08248285083518850ad8d015ca8ed25c2) tableName is blank in CompositeDBField->addToQuery (Dominik Beerbohm) + * 2018-01-25 [cf69d0486](https://github.com/silverstripe/silverstripe-framework/commit/cf69d048665befa90eb43146f86cde984b876b3a) Fix ping including requirements (Damian Mooyman) + * 2018-01-24 [c2cd6b383](https://github.com/silverstripe/silverstripe-framework/commit/c2cd6b3832c6bc4775b2742df593b445c2aca391) Fix Member_GroupSet::removeAll() (fixes #3948) (Loz Calver) + * 2018-01-24 [f2b4c192e](https://github.com/silverstripe/silverstripe-framework/commit/f2b4c192ec4d70779f7c667a976e741a7f3a26c5) Fix UploadField cuts off “Save” button (closes #2862) (Loz Calver) + * 2018-01-23 [7384e3fc2](https://github.com/silverstripe/silverstripe-framework/commit/7384e3fc25987742ea08af74b704857a936e8ec0) Gridfields with dropdowns having lots of overflow (Scott Hutchinson) + * 2018-01-19 [5849820](https://github.com/silverstripe/silverstripe-asset-admin/commit/58498200190cba086477c158d1fe6112cf3b0a1e) Fix compatibility issue with chromedriver (Damian Mooyman) + * 2016-10-21 [8e5bb6fbd](https://github.com/silverstripe/silverstripe-framework/commit/8e5bb6fbdce0b2ca2d08a45534df2264db5e6b12) Fix : relObject() should return null if one of the node is null (Jason) + * 2016-03-15 [22b3a71ec](https://github.com/silverstripe/silverstripe-framework/commit/22b3a71ec0c8cd8c38030fa0bf5449abefafe8a3) fixing val reference to url in https hotlink (Denise Rivera) + * 2015-04-22 [1f63637b9](https://github.com/silverstripe/silverstripe-framework/commit/1f63637b9369d4644a92523ada5d1a5dc0576c12) for #4095, TinyMCE not able to modify props of embed media (bug 1) and invalid HTML inserted (bug 2) (Patrick Nelson) From 0f2eebe5d41698e3d8de74e4b2cf38ea89bf7d1e Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Fri, 9 Nov 2018 13:59:14 +0200 Subject: [PATCH 073/175] NEW Change to variadic calls in ListDecorator and add unit tests --- src/ORM/ListDecorator.php | 13 +- tests/php/ORM/ListDecoratorTest.php | 278 ++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+), 8 deletions(-) create mode 100644 tests/php/ORM/ListDecoratorTest.php diff --git a/src/ORM/ListDecorator.php b/src/ORM/ListDecorator.php index b141d548e..8620168ff 100644 --- a/src/ORM/ListDecorator.php +++ b/src/ORM/ListDecorator.php @@ -173,8 +173,7 @@ abstract class ListDecorator extends ViewableData implements SS_List, Sortable, */ public function sort() { - $args = func_get_args(); - return call_user_func_array(array($this->list, 'sort'), $args); + return $this->list->sort(...func_get_args()); } public function canFilterBy($by) @@ -192,8 +191,7 @@ abstract class ListDecorator extends ViewableData implements SS_List, Sortable, */ public function filter() { - $args = func_get_args(); - return call_user_func_array(array($this->list, 'filter'), $args); + return $this->list->filter(...func_get_args()); } /** @@ -220,7 +218,7 @@ abstract class ListDecorator extends ViewableData implements SS_List, Sortable, */ public function filterAny() { - return call_user_func_array(array($this->list, __FUNCTION__), func_get_args()); + return $this->list->filterAny(...func_get_args()); } /** @@ -242,7 +240,7 @@ abstract class ListDecorator extends ViewableData implements SS_List, Sortable, } $output = ArrayList::create(); foreach ($this->list as $item) { - if (call_user_func($callback, $item, $this->list)) { + if ($callback($item, $this->list)) { $output->push($item); } } @@ -286,8 +284,7 @@ abstract class ListDecorator extends ViewableData implements SS_List, Sortable, */ public function exclude() { - $args = func_get_args(); - return call_user_func_array(array($this->list, 'exclude'), $args); + return $this->list->exclude(...func_get_args()); } public function debug() diff --git a/tests/php/ORM/ListDecoratorTest.php b/tests/php/ORM/ListDecoratorTest.php new file mode 100644 index 000000000..6fe45fdd2 --- /dev/null +++ b/tests/php/ORM/ListDecoratorTest.php @@ -0,0 +1,278 @@ +list = $this->createMock(ArrayList::class); + $this->decorator = $this->getMockForAbstractClass(ListDecorator::class, [$this->list]); + } + + public function testGetIterator() + { + $this->list->expects($this->once())->method('getIterator')->willReturn('mock'); + $this->assertSame('mock', $this->decorator->getIterator()); + } + + public function testCanSortBy() + { + $this->list->expects($this->once())->method('canSortBy')->with('foo')->willReturn(true); + $this->assertTrue($this->decorator->canSortBy('foo')); + } + + public function testRemove() + { + $this->list->expects($this->once())->method('remove')->with('foo'); + $this->decorator->remove('foo'); + } + + /** + * @param array $input + * @dataProvider filterProvider + */ + public function testExclude($input) + { + $this->list->expects($this->once())->method('exclude')->with($input)->willReturn('mock'); + $this->assertSame('mock', $this->decorator->exclude($input)); + } + + /** + * @param array $input + * @dataProvider filterProvider + */ + public function testFilter($input) + { + $this->list->expects($this->once())->method('filter')->with($input)->willReturn('mock'); + $this->assertSame('mock', $this->decorator->filter($input)); + } + + /** + * @param array $input + * @dataProvider filterProvider + */ + public function testFilterAny($input) + { + $this->list->expects($this->once())->method('filterAny')->with($input)->willReturn('mock'); + $this->assertSame('mock', $this->decorator->filterAny($input)); + } + + /** + * @param array $input + * @dataProvider filterProvider + */ + public function testSort($input) + { + $this->list->expects($this->once())->method('sort')->with($input)->willReturn('mock'); + $this->assertSame('mock', $this->decorator->sort($input)); + } + + /** + * @return array[] + */ + public function filterProvider() + { + return [ + ['Name', 'Bob'], + ['Name', ['aziz', 'Bob']], + [['Name' =>'bob', 'Age' => 21]], + [['Name' =>'bob', 'Age' => [21, 43]]], + ]; + } + + public function testCanFilterBy() + { + $this->list->expects($this->once())->method('canFilterBy')->with('Title')->willReturn(false); + $this->assertFalse($this->decorator->canFilterBy('Title')); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage SS_Filterable::filterByCallback() passed callback must be callable, 'boolean' given + */ + public function testFilterByCallbackThrowsExceptionWhenGivenNonCallable() + { + $this->decorator->filterByCallback(true); + } + + public function testFilterByCallback() + { + $input = new ArrayList([ + ['Name' => 'Leslie'], + ['Name' => 'Maxime'], + ['Name' => 'Sal'], + ]); + + $callback = function ($item, SS_List $list) { + return $item->Name === 'Maxime'; + }; + + $this->decorator->setList($input); + $result = $this->decorator->filterByCallback($callback); + + $this->assertCount(1, $result); + $this->assertSame('Maxime', $result->first()->Name); + } + + public function testFind() + { + $this->list->expects($this->once())->method('find')->with('foo', 'bar')->willReturn('mock'); + $this->assertSame('mock', $this->decorator->find('foo', 'bar')); + } + + public function testDebug() + { + $this->list->expects($this->once())->method('debug')->willReturn('mock'); + $this->assertSame('mock', $this->decorator->debug()); + } + + public function testCount() + { + $this->list->expects($this->once())->method('count')->willReturn(5); + $this->assertSame(5, $this->decorator->Count()); + } + + public function testEach() + { + $callable = function () { + // noop + }; + $this->list->expects($this->once())->method('each')->with($callable)->willReturn('mock'); + $this->assertSame('mock', $this->decorator->each($callable)); + } + + public function testOffsetExists() + { + $this->list->expects($this->once())->method('offsetExists')->with('foo')->willReturn('mock'); + $this->assertSame('mock', $this->decorator->offsetExists('foo')); + } + + public function testGetList() + { + $this->assertSame($this->list, $this->decorator->getList()); + } + + public function testColumnUnique() + { + $this->list->expects($this->once())->method('columnUnique')->with('ID')->willReturn('mock'); + $this->assertSame('mock', $this->decorator->columnUnique('ID')); + } + + public function testMap() + { + $this->list->expects($this->once())->method('map')->with('ID', 'Title')->willReturn('mock'); + $this->assertSame('mock', $this->decorator->map('ID', 'Title')); + } + + public function testReverse() + { + $this->list->expects($this->once())->method('reverse')->willReturn('mock'); + $this->assertSame('mock', $this->decorator->reverse()); + } + + public function testOffsetGet() + { + $this->list->expects($this->once())->method('offsetGet')->with(2)->willReturn('mock'); + $this->assertSame('mock', $this->decorator->offsetGet(2)); + } + + public function testExists() + { + $this->list->expects($this->once())->method('exists')->willReturn(false); + $this->assertFalse($this->decorator->exists()); + } + + public function testByID() + { + $this->list->expects($this->once())->method('byID')->with(123)->willReturn('mock'); + $this->assertSame('mock', $this->decorator->byID(123)); + } + + public function testByIDs() + { + $this->list->expects($this->once())->method('byIDs')->with([1, 2])->willReturn('mock'); + $this->assertSame('mock', $this->decorator->byIDs([1, 2])); + } + + public function testToArray() + { + $this->list->expects($this->once())->method('toArray')->willReturn(['foo', 'bar']); + $this->assertSame(['foo', 'bar'], $this->decorator->toArray()); + } + + public function testToNestedArray() + { + $this->list->expects($this->once())->method('toNestedArray')->willReturn(['foo', 'bar']); + $this->assertSame(['foo', 'bar'], $this->decorator->toNestedArray()); + } + + public function testOffsetSet() + { + $this->list->expects($this->once())->method('offsetSet')->with('foo', 'bar'); + $this->decorator->offsetSet('foo', 'bar'); + } + + public function testOffsetUnset() + { + $this->list->expects($this->once())->method('offsetUnset')->with('foo'); + $this->decorator->offsetUnset('foo'); + } + + public function testLimit() + { + $this->list->expects($this->once())->method('limit')->with(5, 3)->willReturn('mock'); + $this->assertSame('mock', $this->decorator->limit(5, 3)); + } + + public function testTotalItems() + { + $this->list->expects($this->once())->method('count')->willReturn(5); + $this->assertSame(5, $this->decorator->TotalItems()); + } + + public function testAdd() + { + $this->list->expects($this->once())->method('add')->with('foo')->willReturn('mock'); + $this->decorator->add('foo'); + } + + public function testFirst() + { + $this->list->expects($this->once())->method('first')->willReturn(1); + $this->assertSame(1, $this->decorator->first()); + } + + public function testLast() + { + $this->list->expects($this->once())->method('last')->willReturn(10); + $this->assertSame(10, $this->decorator->last()); + } + + public function testColumn() + { + $this->list->expects($this->once())->method('column')->with('DOB')->willReturn('mock'); + $this->assertSame('mock', $this->decorator->column('DOB')); + } +} From bd5a815909d735c8223a552d8e79ebc13481a3aa Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Mon, 8 Oct 2018 17:07:50 +1300 Subject: [PATCH 074/175] FIX: Make all enums non-destructive, not just ClassName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change also renders a portion of DBSchemaManager irrelevant, that destructively “fixes” old values. This is in keeping with the non-destructive principle of dev/build, and some suggestions to move away from enum fields altogether. Fixes https://github.com/silverstripe/silverstripe-framework/issues/1387 --- docs/en/04_Changelogs/4.4.0.md | 2 +- src/ORM/Connect/DBSchemaManager.php | 31 -------- src/ORM/DataObject.php | 3 +- src/ORM/FieldType/DBClassName.php | 53 +------------- src/ORM/FieldType/DBEnum.php | 62 +++++++++++++++- tests/php/ORM/DBEnumTest.php | 70 +++++++++++++++++++ .../ORM/DataObjectSchemaGenerationTest.php | 10 +-- tests/php/ORM/FieldType/DBEnumTestObject.php | 15 ++++ tests/php/Security/SecurityTest.php | 4 +- 9 files changed, 158 insertions(+), 92 deletions(-) create mode 100644 tests/php/ORM/FieldType/DBEnumTestObject.php diff --git a/docs/en/04_Changelogs/4.4.0.md b/docs/en/04_Changelogs/4.4.0.md index f37ba13a6..1b761129b 100644 --- a/docs/en/04_Changelogs/4.4.0.md +++ b/docs/en/04_Changelogs/4.4.0.md @@ -6,7 +6,7 @@ ## Upgrading {#upgrading} -tbc + - dev/build is now non-destructive for all Enums, not just ClassNames. This means your data won't be lost if you're switching between versions, but watch out for code that breaks when it sees an unrecognised value! ## Changes to internal APIs diff --git a/src/ORM/Connect/DBSchemaManager.php b/src/ORM/Connect/DBSchemaManager.php index 38ef02fd6..20ea84242 100644 --- a/src/ORM/Connect/DBSchemaManager.php +++ b/src/ORM/Connect/DBSchemaManager.php @@ -713,37 +713,6 @@ MESSAGE $this->transCreateField($table, $field, $spec_orig); $this->alterationMessage("Field $table.$field: created as $spec_orig", "created"); } elseif ($fieldValue != $specValue) { - // If enums/sets are being modified, then we need to fix existing data in the table. - // Update any records where the enum is set to a legacy value to be set to the default. - foreach (array('enum', 'set') as $enumtype) { - if (preg_match("/^$enumtype/i", $specValue)) { - $newStr = preg_replace("/(^$enumtype\\s*\\(')|('\\).*)/i", "", $spec_orig); - $new = preg_split("/'\\s*,\\s*'/", $newStr); - - $oldStr = preg_replace("/(^$enumtype\\s*\\(')|('\\).*)/i", "", $fieldValue); - $old = preg_split("/'\\s*,\\s*'/", $oldStr); - - $holder = array(); - foreach ($old as $check) { - if (!in_array($check, $new)) { - $holder[] = $check; - } - } - if (count($holder)) { - $default = explode('default ', $spec_orig); - $default = $default[1]; - $query = "UPDATE \"$table\" SET $field=$default WHERE $field IN ("; - for ($i = 0; $i + 1 < count($holder); $i++) { - $query .= "'{$holder[$i]}', "; - } - $query .= "'{$holder[$i]}')"; - $this->query($query); - $amount = $this->database->affectedRows(); - $this->alterationMessage("Changed $amount rows to default value of field $field" - . " (Value: $default)"); - } - } - } $this->transAlterField($table, $field, $spec_orig); $this->alterationMessage( "Field $table.$field: changed to $specValue (from {$fieldValue})", diff --git a/src/ORM/DataObject.php b/src/ORM/DataObject.php index 3fbf3afa4..6c3730703 100644 --- a/src/ORM/DataObject.php +++ b/src/ORM/DataObject.php @@ -19,6 +19,7 @@ use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18nEntityProvider; use SilverStripe\ORM\Connect\MySQLSchemaManager; use SilverStripe\ORM\FieldType\DBClassName; +use SilverStripe\ORM\FieldType\DBEnum; use SilverStripe\ORM\FieldType\DBComposite; use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBField; @@ -3197,7 +3198,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity public static function reset() { // @todo Decouple these - DBClassName::clear_classname_cache(); + DBEnum::flushCache(); ClassInfo::reset_db_cache(); static::getSchema()->reset(); self::$_cache_get_one = array(); diff --git a/src/ORM/FieldType/DBClassName.php b/src/ORM/FieldType/DBClassName.php index 7b7689f7c..3255aef15 100644 --- a/src/ORM/FieldType/DBClassName.php +++ b/src/ORM/FieldType/DBClassName.php @@ -6,6 +6,7 @@ use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; +use SilverStripe\Dev\Deprecation; /** * Represents a classname selector, which respects obsolete clasess. @@ -29,14 +30,6 @@ class DBClassName extends DBEnum */ protected $record = null; - /** - * Classname spec cache for obsolete classes. The top level keys are the table, each of which contains - * nested arrays with keys mapped to field names. The values of the lowest level array are the classnames - * - * @var array - */ - protected static $classname_cache = array(); - private static $index = true; /** @@ -45,7 +38,8 @@ class DBClassName extends DBEnum */ public static function clear_classname_cache() { - self::$classname_cache = array(); + Deprecation::notice('4.3', 'Call DBEnum::flushCache() instead'); + DBEnum::flushCache(); } /** @@ -149,47 +143,6 @@ class DBClassName extends DBEnum return array_values($classNames); } - /** - * Get the list of classnames, including obsolete classes. - * - * If table or name are not set, or if it is not a valid field on the given table, - * then only known classnames are returned. - * - * Values cached in this method can be cleared via `DBClassName::clear_classname_cache();` - * - * @return array - */ - public function getEnumObsolete() - { - // Without a table or field specified, we can only retrieve known classes - $table = $this->getTable(); - $name = $this->getName(); - if (empty($table) || empty($name)) { - return $this->getEnum(); - } - - // Ensure the table level cache exists - if (empty(self::$classname_cache[$table])) { - self::$classname_cache[$table] = array(); - } - - // Check existing cache - if (!empty(self::$classname_cache[$table][$name])) { - return self::$classname_cache[$table][$name]; - } - - // Get all class names - $classNames = $this->getEnum(); - if (DB::get_schema()->hasField($table, $name)) { - $existing = DB::query("SELECT DISTINCT \"{$name}\" FROM \"{$table}\"")->column(); - $classNames = array_unique(array_merge($classNames, $existing)); - } - - // Cache and return - self::$classname_cache[$table][$name] = $classNames; - return $classNames; - } - public function setValue($value, $record = null, $markChanged = true) { parent::setValue($value, $record, $markChanged); diff --git a/src/ORM/FieldType/DBEnum.php b/src/ORM/FieldType/DBEnum.php index cb3126cec..3715365a7 100644 --- a/src/ORM/FieldType/DBEnum.php +++ b/src/ORM/FieldType/DBEnum.php @@ -20,7 +20,7 @@ class DBEnum extends DBString * * @var array */ - protected $enum = array(); + protected $enum = []; /** * Default value @@ -31,6 +31,22 @@ class DBEnum extends DBString private static $default_search_filter_class = 'ExactMatchFilter'; + /** + * Internal cache for obsolete enum values. The top level keys are the table, each of which contains + * nested arrays with keys mapped to field names. The values of the lowest level array are the enum values + * + * @var array + */ + protected static $enum_cache = []; + + /** + * Clear all cached enum values. + */ + public static function flushCache() + { + self::$enum_cache = []; + } + /** * Create a new Enum field, which is a value within a defined set, with an optional default. * @@ -88,7 +104,7 @@ class DBEnum extends DBString $parts = array( 'datatype' => 'enum', - 'enums' => $this->getEnum(), + 'enums' => $this->getEnumObsolete(), 'character set' => $charset, 'collate' => $collation, 'default' => $this->getDefault(), @@ -173,6 +189,48 @@ class DBEnum extends DBString return $this->enum; } + + /** + * Get the list of enum values, including obsolete values still present in the database + * + * If table or name are not set, or if it is not a valid field on the given table, + * then only known enum values are returned. + * + * Values cached in this method can be cleared via `DBEnum::flushCache();` + * + * @return array + */ + public function getEnumObsolete() + { + // Without a table or field specified, we can only retrieve known enum values + $table = $this->getTable(); + $name = $this->getName(); + if (empty($table) || empty($name)) { + return $this->getEnum(); + } + + // Ensure the table level cache exists + if (empty(self::$enum_cache[$table])) { + self::$enum_cache[$table] = array(); + } + + // Check existing cache + if (!empty(self::$enum_cache[$table][$name])) { + return self::$enum_cache[$table][$name]; + } + + // Get all enum values + $enumValues = $this->getEnum(); + if (DB::get_schema()->hasField($table, $name)) { + $existing = DB::query("SELECT DISTINCT \"{$name}\" FROM \"{$table}\"")->column(); + $enumValues = array_unique(array_merge($enumValues, $existing)); + } + + // Cache and return + self::$enum_cache[$table][$name] = $enumValues; + return $enumValues; + } + /** * Set enum options * diff --git a/tests/php/ORM/DBEnumTest.php b/tests/php/ORM/DBEnumTest.php index 3ed633f15..7fe37403a 100644 --- a/tests/php/ORM/DBEnumTest.php +++ b/tests/php/ORM/DBEnumTest.php @@ -5,9 +5,17 @@ namespace SilverStripe\ORM\Tests; use SilverStripe\Dev\SapphireTest; use SilverStripe\ORM\FieldType\DBEnum; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\DB; class DBEnumTest extends SapphireTest { + + protected $extraDataObjects = [ + FieldType\DBEnumTestObject::class, + ]; + + protected $usesDatabase = true; + public function testDefault() { /** @var DBEnum $enum1 */ @@ -28,4 +36,66 @@ class DBEnumTest extends SapphireTest $this->assertEquals('B', $enum4->getDefaultValue()); $this->assertEquals('B', $enum4->getDefault()); } + + public function testObsoleteValues() + { + $obj = new FieldType\DBEnumTestObject(); + $colourField = $obj->obj('Colour'); + $colourField->setTable('FieldType_DBEnumTestObject'); + + // Test values prior to any database content + $this->assertEquals( + ['Red', 'Blue', 'Green'], + $colourField->getEnumObsolete() + ); + + // Test values with a record + $obj->Colour = 'Red'; + $obj->write(); + DBEnum::flushCache(); + + $this->assertEquals( + ['Red', 'Blue', 'Green'], + $colourField->getEnumObsolete() + ); + + // If the value is removed from the enum, obsolete content is still retained + $colourField->setEnum(['Blue', 'Green', 'Purple']); + DBEnum::flushCache(); + + $this->assertEquals( + ['Blue', 'Green', 'Purple', 'Red'], // Red on the end now, because it's obsolete + $colourField->getEnumObsolete() + ); + + // Check that old and new data is preserved after a schema update + DB::get_schema()->schemaUpdate(function () use ($colourField) { + $colourField->requireField(); + }); + + $obj2 = new FieldType\DBEnumTestObject(); + $obj2->Colour = 'Purple'; + $obj2->write(); + + $this->assertEquals( + ['Purple', 'Red'], + FieldType\DBEnumTestObject::get()->sort('Colour')->column('Colour') + ); + + // Ensure that enum columns are retained + $colourField->setEnum(['Blue', 'Green']); + $this->assertEquals( + ['Blue', 'Green', 'Purple', 'Red'], + $colourField->getEnumObsolete() + ); + + // If obsolete records are deleted, the extra values go away + $obj->delete(); + $obj2->delete(); + DBEnum::flushCache(); + $this->assertEquals( + ['Blue', 'Green'], + $colourField->getEnumObsolete() + ); + } } diff --git a/tests/php/ORM/DataObjectSchemaGenerationTest.php b/tests/php/ORM/DataObjectSchemaGenerationTest.php index b366d5d51..73313d63b 100644 --- a/tests/php/ORM/DataObjectSchemaGenerationTest.php +++ b/tests/php/ORM/DataObjectSchemaGenerationTest.php @@ -5,7 +5,7 @@ namespace SilverStripe\ORM\Tests; use SilverStripe\Core\Config\Config; use SilverStripe\ORM\Connect\MySQLSchemaManager; use SilverStripe\ORM\DB; -use SilverStripe\ORM\FieldType\DBClassName; +use SilverStripe\ORM\FieldType\DBEnum; use SilverStripe\ORM\DataObject; use SilverStripe\Dev\SapphireTest; use SilverStripe\ORM\Tests\DataObjectSchemaGenerationTest\SortedObject; @@ -208,7 +208,7 @@ class DataObjectSchemaGenerationTest extends SapphireTest $schema = DataObject::getSchema(); // Test with blank entries - DBClassName::clear_classname_cache(); + DBEnum::flushCache(); $do1 = new TestObject(); $fields = $schema->databaseFields(TestObject::class, false); $this->assertEquals("DBClassName", $fields['ClassName']); @@ -224,7 +224,7 @@ class DataObjectSchemaGenerationTest extends SapphireTest // Test with instance of subclass $item1 = new TestIndexObject(); $item1->write(); - DBClassName::clear_classname_cache(); + DBEnum::flushCache(); $this->assertEquals( [ TestObject::class, @@ -237,7 +237,7 @@ class DataObjectSchemaGenerationTest extends SapphireTest // Test with instance of main class $item2 = new TestObject(); $item2->write(); - DBClassName::clear_classname_cache(); + DBEnum::flushCache(); $this->assertEquals( [ TestObject::class, @@ -252,7 +252,7 @@ class DataObjectSchemaGenerationTest extends SapphireTest $item1->write(); $item2 = new TestObject(); $item2->write(); - DBClassName::clear_classname_cache(); + DBEnum::flushCache(); $this->assertEquals( [ TestObject::class, diff --git a/tests/php/ORM/FieldType/DBEnumTestObject.php b/tests/php/ORM/FieldType/DBEnumTestObject.php new file mode 100644 index 000000000..b9c70d71c --- /dev/null +++ b/tests/php/ORM/FieldType/DBEnumTestObject.php @@ -0,0 +1,15 @@ + 'Enum("Red,Blue,Green")', + ]; +} diff --git a/tests/php/Security/SecurityTest.php b/tests/php/Security/SecurityTest.php index 9ac158758..1c81911d4 100644 --- a/tests/php/Security/SecurityTest.php +++ b/tests/php/Security/SecurityTest.php @@ -14,7 +14,7 @@ use SilverStripe\Dev\FunctionalTest; use SilverStripe\i18n\i18n; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; -use SilverStripe\ORM\FieldType\DBClassName; +use SilverStripe\ORM\FieldType\DBEnum; use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\ValidationResult; @@ -671,7 +671,7 @@ class SecurityTest extends FunctionalTest public function testDatabaseIsReadyWithInsufficientMemberColumns() { Security::clear_database_is_ready(); - DBClassName::clear_classname_cache(); + DBEnum::flushCache(); // Assumption: The database has been built correctly by the test runner, // and has all columns present in the ORM From a8d3b95175c8336999e617e3334551ada5a49919 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Fri, 9 Nov 2018 16:23:50 +1300 Subject: [PATCH 075/175] FIX: Make test work with utf8mb4 --- .../php/ORM/DataObjectSchemaGenerationTest/TestIndexObject.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/php/ORM/DataObjectSchemaGenerationTest/TestIndexObject.php b/tests/php/ORM/DataObjectSchemaGenerationTest/TestIndexObject.php index 72032715e..d0d7134a2 100644 --- a/tests/php/ORM/DataObjectSchemaGenerationTest/TestIndexObject.php +++ b/tests/php/ORM/DataObjectSchemaGenerationTest/TestIndexObject.php @@ -8,7 +8,7 @@ class TestIndexObject extends TestObject implements TestOnly { private static $table_name = 'DataObjectSchemaGenerationTest_IndexDO'; private static $db = [ - 'Title' => 'Varchar(255)', + 'Title' => 'Varchar(192)', 'Content' => 'Text', ]; From eba92d77df1c9fa678dab97e548bb9ba65dcb696 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 10 Nov 2018 10:04:17 +0200 Subject: [PATCH 076/175] Rename CheckboxFieldReadonlyTest for future PSR-2 compatibility --- ...kboxField_ReadonlyTest.php => CheckboxFieldReadonlyTest.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/php/Forms/{CheckboxField_ReadonlyTest.php => CheckboxFieldReadonlyTest.php} (88%) diff --git a/tests/php/Forms/CheckboxField_ReadonlyTest.php b/tests/php/Forms/CheckboxFieldReadonlyTest.php similarity index 88% rename from tests/php/Forms/CheckboxField_ReadonlyTest.php rename to tests/php/Forms/CheckboxFieldReadonlyTest.php index 4996deda8..e82156325 100644 --- a/tests/php/Forms/CheckboxField_ReadonlyTest.php +++ b/tests/php/Forms/CheckboxFieldReadonlyTest.php @@ -5,7 +5,7 @@ namespace SilverStripe\Forms\Tests; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\CheckboxField_Readonly; -class CheckboxField_ReadonlyTest extends SapphireTest +class CheckboxFieldReadonlyTest extends SapphireTest { public function testPerformReadonlyTransformation() { From bab84f31dc50c3ec251fa108793edad4dd9b78b6 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Sat, 10 Nov 2018 14:55:11 +0200 Subject: [PATCH 077/175] Remove underscores from variable test class names --- ...encyField_DisabledTest.php => CurrencyFieldDisabledTest.php} | 2 +- ...encyField_ReadonlyTest.php => CurrencyFieldReadonlyTest.php} | 2 +- .../{DateField_DisabledTest.php => DateFieldDisabledTest.php} | 2 +- ...ion_TabSetTest.php => PrintableTransformationTabSetTest.php} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename tests/php/Forms/{CurrencyField_DisabledTest.php => CurrencyFieldDisabledTest.php} (95%) rename tests/php/Forms/{CurrencyField_ReadonlyTest.php => CurrencyFieldReadonlyTest.php} (97%) rename tests/php/Forms/{DateField_DisabledTest.php => DateFieldDisabledTest.php} (98%) rename tests/php/Forms/{PrintableTransformation_TabSetTest.php => PrintableTransformationTabSetTest.php} (93%) diff --git a/tests/php/Forms/CurrencyField_DisabledTest.php b/tests/php/Forms/CurrencyFieldDisabledTest.php similarity index 95% rename from tests/php/Forms/CurrencyField_DisabledTest.php rename to tests/php/Forms/CurrencyFieldDisabledTest.php index ed832521c..a36efec58 100644 --- a/tests/php/Forms/CurrencyField_DisabledTest.php +++ b/tests/php/Forms/CurrencyFieldDisabledTest.php @@ -6,7 +6,7 @@ use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\CurrencyField_Disabled; use SilverStripe\ORM\FieldType\DBCurrency; -class CurrencyField_DisabledTest extends SapphireTest +class CurrencyFieldDisabledTest extends SapphireTest { public function testFieldWithValue() { diff --git a/tests/php/Forms/CurrencyField_ReadonlyTest.php b/tests/php/Forms/CurrencyFieldReadonlyTest.php similarity index 97% rename from tests/php/Forms/CurrencyField_ReadonlyTest.php rename to tests/php/Forms/CurrencyFieldReadonlyTest.php index 289327cd1..d403126e7 100644 --- a/tests/php/Forms/CurrencyField_ReadonlyTest.php +++ b/tests/php/Forms/CurrencyFieldReadonlyTest.php @@ -6,7 +6,7 @@ use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\CurrencyField_Readonly; use SilverStripe\ORM\FieldType\DBCurrency; -class CurrencyField_ReadonlyTest extends SapphireTest +class CurrencyFieldReadonlyTest extends SapphireTest { public function testPerformReadonlyTransformation() { diff --git a/tests/php/Forms/DateField_DisabledTest.php b/tests/php/Forms/DateFieldDisabledTest.php similarity index 98% rename from tests/php/Forms/DateField_DisabledTest.php rename to tests/php/Forms/DateFieldDisabledTest.php index 0108b751d..6fffdcd13 100644 --- a/tests/php/Forms/DateField_DisabledTest.php +++ b/tests/php/Forms/DateFieldDisabledTest.php @@ -10,7 +10,7 @@ use SilverStripe\ORM\FieldType\DBDatetime; /** * @skipUpgrade */ -class DateField_DisabledTest extends SapphireTest +class DateFieldDisabledTest extends SapphireTest { protected function setUp() { diff --git a/tests/php/Forms/PrintableTransformation_TabSetTest.php b/tests/php/Forms/PrintableTransformationTabSetTest.php similarity index 93% rename from tests/php/Forms/PrintableTransformation_TabSetTest.php rename to tests/php/Forms/PrintableTransformationTabSetTest.php index 11ca4efd8..12691ddde 100644 --- a/tests/php/Forms/PrintableTransformation_TabSetTest.php +++ b/tests/php/Forms/PrintableTransformationTabSetTest.php @@ -7,7 +7,7 @@ use SilverStripe\Forms\PrintableTransformation_TabSet; use SilverStripe\Forms\Tab; use SilverStripe\Forms\TabSet; -class PrintableTransformation_TabSetTest extends SapphireTest +class PrintableTransformationTabSetTest extends SapphireTest { public function testFieldHolder() { From bda7653ae330dc70c943c4a55386d9d29794d3ec Mon Sep 17 00:00:00 2001 From: Thor Chen Date: Mon, 12 Nov 2018 18:24:37 +1300 Subject: [PATCH 078/175] DOC: Fix outdated information and make enhancements for --- .../How_Tos/Extend_CMS_Interface.md | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md b/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md index 5f1efab02..826b09991 100644 --- a/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md +++ b/docs/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md @@ -24,10 +24,10 @@ the common `Page` object (a new PHP class `MyPage` will look for a `MyPage.ss` t We can use this to create a different base template with `LeftAndMain.ss` (which corresponds to the `LeftAndMain` PHP controller class). -Copy the template markup of the base implementation at `templates/SilverStripe/Admin/Includes/LeftAndMain_Menu.ss` +Copy the template markup of the base implementation at `templates/SilverStripe/Admin/Includes/LeftAndMain_MenuList.ss` from the `silverstripe/admin` module -into `app/templates/Includes/LeftAndMain_Menu.ss`. It will automatically be picked up by -the CMS logic. Add a new section into the `
    ` +into `app/templates/SilverStripe/Admin/Includes/LeftAndMain_MenuList.ss`. It will automatically be picked up by +the CMS logic. Add a new section into the `
      ` ```ss @@ -69,26 +69,42 @@ SilverStripe\Admin\LeftAndMain: - app/css/BookmarkedPages.css ``` +In order to let the frontend have the access to our `css` files, we need to `expose` them in the `composer.json`: + +```javascript + "extra": { + ... + "expose": [ + "app/css" + ] + }, +``` + +Then run `composer vendor-expose`. This command will publish all the `css` files under the `app/css` folder to their public-facing paths. + +> Note: don't forget to `flush`. + ## Create a "bookmark" flag on pages Now we'll define which pages are actually bookmarked, a flag that is stored in the database. For this we need to decorate the page record with a -`DataExtension`. Create a new file called `app/code/BookmarkedPageExtension.php` +`DataExtension`. Create a new file called `app/src/BookmarkedPageExtension.php` and insert the following code. ```php use SilverStripe\Forms\CheckboxField; +use SilverStripe\Forms\FieldList; use SilverStripe\ORM\DataExtension; -class BookmarkedPageExtension extends DataExtension +class BookmarkedPageExtension extends DataExtension { private static $db = [ 'IsBookmarked' => 'Boolean' ]; - public function updateCMSFields(FieldList $fields) + public function updateCMSFields(FieldList $fields) { $fields->addFieldToTab('Root.Main', new CheckboxField('IsBookmarked', "Show in CMS bookmarks?") @@ -117,7 +133,7 @@ pages from the database into the template we've already created (with hardcoded links)? Again, we extend a core class: The main CMS controller called `LeftAndMain`. -Add the following code to a new file `app/code/BookmarkedLeftAndMainExtension.php`; +Add the following code to a new file `app/src/BookmarkedLeftAndMainExtension.php`; ```php @@ -143,12 +159,12 @@ SilverStripe\Admin\LeftAndMain: ``` As the last step, replace the hardcoded links with our list from the database. -Find the `
        ` you created earlier in `app/admin/templates/LeftAndMain.ss` +Find the `
          ` you created earlier in `app/templates/SilverStripe/Admin/Includes/LeftAndMain_MenuList.ss` and replace it with the following: ```ss -
            +
              <% loop $BookmarkedPages %>