Merge branch '4.4' into 4

This commit is contained in:
Serge Latyntcev 2019-10-18 10:58:19 +13:00
commit 7873efde9c
15 changed files with 110 additions and 43 deletions

View File

@ -49,9 +49,10 @@ class Email extends ViewableData
/** /**
* This will be set in the config on a site-by-site basis * This will be set in the config on a site-by-site basis
* @see https://docs.silverstripe.org/en/4/developer_guides/email/#administrator-emails
* *
* @config * @config
* @var string The default administrator email address. * @var string|array The default administrator email address or array of [email => name]
*/ */
private static $admin_email = null; private static $admin_email = null;

View File

@ -61,7 +61,7 @@ class GetParameter implements Rule, Bypass
{ {
return new Confirmation\Item( return new Confirmation\Item(
$token, $token,
_t(__CLASS__.'.CONFIRMATION_NAME', '"{key}" GET parameter', ['key' => $this->name]), _t(__CLASS__ . '.CONFIRMATION_NAME', '"{key}" GET parameter', ['key' => $this->name]),
sprintf('%s = "%s"', $this->name, $value) sprintf('%s = "%s"', $this->name, $value)
); );
} }

View File

@ -178,8 +178,8 @@ class Url implements Rule, Bypass
{ {
return new Confirmation\Item( return new Confirmation\Item(
$token, $token,
_t(__CLASS__.'.CONFIRMATION_NAME', 'URL is protected'), _t(__CLASS__ . '.CONFIRMATION_NAME', 'URL is protected'),
_t(__CLASS__.'.CONFIRMATION_DESCRIPTION', 'The URL is: "{url}"', ['url' => $url]) _t(__CLASS__ . '.CONFIRMATION_DESCRIPTION', 'The URL is: "{url}"', ['url' => $url])
); );
} }

View File

@ -34,8 +34,8 @@ class UrlPathStartswith implements Rule, Bypass
{ {
return new Confirmation\Item( return new Confirmation\Item(
$token, $token,
_t(__CLASS__.'.CONFIRMATION_NAME', 'URL begins with "{path}"', ['path' => $this->getPath()]), _t(__CLASS__ . '.CONFIRMATION_NAME', 'URL begins with "{path}"', ['path' => $this->getPath()]),
_t(__CLASS__.'.CONFIRMATION_DESCRIPTION', 'The complete URL is: "{url}"', ['url' => $url]) _t(__CLASS__ . '.CONFIRMATION_DESCRIPTION', 'The complete URL is: "{url}"', ['url' => $url])
); );
} }

View File

@ -282,10 +282,15 @@ trait CustomMethods
if (!isset(self::$extra_methods[$class][$method])) { if (!isset(self::$extra_methods[$class][$method])) {
continue; continue;
} }
$methodInfo = self::$extra_methods[$class][$method]; $methodInfo = self::$extra_methods[$class][$method];
if ($methodInfo['property'] === $property && $methodInfo['index'] === $index) { if (
// always check for property
(isset($methodInfo['property']) && $methodInfo['property'] === $property) &&
// check for index only if provided
(!$index || ($index && isset($methodInfo['index']) && $methodInfo['index'] === $index))
) {
unset(self::$extra_methods[$class][$method]); unset(self::$extra_methods[$class][$method]);
} }
} }

View File

@ -23,9 +23,9 @@ class DevConfirmationController extends Confirmation\Handler
$renderer = DebugView::create(); $renderer = DebugView::create();
echo $renderer->renderHeader(); echo $renderer->renderHeader();
echo $renderer->renderInfo( echo $renderer->renderInfo(
_t(__CLASS__.".INFO_TITLE", "Security Confirmation"), _t(__CLASS__ . ".INFO_TITLE", "Security Confirmation"),
Director::absoluteBaseURL(), Director::absoluteBaseURL(),
_t(__CLASS__.".INFO_DESCRIPTION", "Confirm potentially dangerous operation") _t(__CLASS__ . ".INFO_DESCRIPTION", "Confirm potentially dangerous operation")
); );
return $response; return $response;

View File

@ -18,6 +18,7 @@ use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\ORM\HasManyList; use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\ManyManyList; use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\RelationList;
use SilverStripe\ORM\SS_List; use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
@ -178,20 +179,6 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
return $controller->redirect($noActionURL, 302); return $controller->redirect($noActionURL, 302);
} }
$canView = $this->record->canView();
$canEdit = $this->record->canEdit();
$canDelete = $this->record->canDelete();
$canCreate = $this->record->canCreate();
if (!$canView) {
$controller = $this->getToplevelController();
// TODO More friendly error
return $controller->httpError(403);
}
// Build actions
$actions = $this->getFormActions();
// If we are creating a new record in a has-many list, then // If we are creating a new record in a has-many list, then
// pre-populate the record's foreign key. // pre-populate the record's foreign key.
if ($list instanceof HasManyList && !$this->record->isInDB()) { if ($list instanceof HasManyList && !$this->record->isInDB()) {
@ -200,6 +187,12 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
$this->record->$key = $id; $this->record->$key = $id;
} }
if (!$this->record->canView()) {
$controller = $this->getToplevelController();
// TODO More friendly error
return $controller->httpError(403);
}
$fields = $this->component->getFields(); $fields = $this->component->getFields();
if (!$fields) { if (!$fields) {
$fields = $this->record->getCMSFields(); $fields = $this->record->getCMSFields();
@ -219,20 +212,22 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
$this, $this,
'ItemEditForm', 'ItemEditForm',
$fields, $fields,
$actions, $this->getFormActions(),
$this->component->getValidator() $this->component->getValidator()
); );
$form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT); $form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT);
if ($this->record->ID && !$canEdit) { if ($this->record->ID && !$this->record->canEdit()) {
// Restrict editing of existing records // Restrict editing of existing records
$form->makeReadonly(); $form->makeReadonly();
// Hack to re-enable delete button if user can delete // Hack to re-enable delete button if user can delete
if ($canDelete) { if ($this->record->canDelete()) {
$form->Actions()->fieldByName('action_doDelete')->setReadonly(false); $form->Actions()->fieldByName('action_doDelete')->setReadonly(false);
} }
} elseif (!$this->record->ID && !$canCreate) { } elseif (!$this->record->ID
&& !$this->record->canCreate(null, $this->getCreateContext())
) {
// Restrict creation of new records // Restrict creation of new records
$form->makeReadonly(); $form->makeReadonly();
} }
@ -272,6 +267,25 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
return $form; return $form;
} }
/**
* Build context for verifying canCreate
* @see GridFieldAddNewButton::getHTMLFragments()
*
* @return array
*/
protected function getCreateContext()
{
$gridField = $this->gridField;
$context = [];
if ($gridField->getList() instanceof RelationList) {
$record = $gridField->getForm()->getRecord();
if ($record && $record instanceof DataObject) {
$context['Parent'] = $record;
}
}
return $context;
}
/** /**
* @return CompositeField Returns the right aligned toolbar group field along with its FormAction's * @return CompositeField Returns the right aligned toolbar group field along with its FormAction's
*/ */

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Logging; namespace SilverStripe\Logging;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\Debug; use SilverStripe\Dev\Debug;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\Email\Email; use SilverStripe\Control\Email\Email;
@ -133,13 +134,36 @@ class DebugViewFriendlyErrorFormatter implements FormatterInterface
$output = $renderer->renderHeader(); $output = $renderer->renderHeader();
$output .= $renderer->renderInfo("Website Error", $this->getTitle(), $this->getBody()); $output .= $renderer->renderInfo("Website Error", $this->getTitle(), $this->getBody());
$adminEmail = Email::config()->get('admin_email'); if (!is_null($contactInfo = $this->addContactAdministratorInfo())) {
if ($adminEmail) { $output .= $renderer->renderParagraph($contactInfo);
$mailto = Email::obfuscate($adminEmail);
$output .= $renderer->renderParagraph('Contact an administrator: ' . $mailto . '');
} }
$output .= $renderer->renderFooter(); $output .= $renderer->renderFooter();
return $output; return $output;
} }
/**
* Generate the line with admin contact info
*
* @return string|null
*/
private function addContactAdministratorInfo()
{
if (!$adminEmail = Email::config()->admin_email) {
return null;
}
if (is_string($adminEmail)) {
return 'Contact an administrator: ' . Email::obfuscate($adminEmail);
}
if (!is_array($adminEmail) || !count($adminEmail)) {
return null;
}
$email = array_keys($adminEmail)[0];
$name = array_values($adminEmail)[0];
return sprintf('Contact %s: %s', Convert::raw2xml($name), Email::obfuscate($email));
}
} }

View File

@ -99,8 +99,8 @@ class Form extends BaseForm
protected function buildActionList(Storage $storage) protected function buildActionList(Storage $storage)
{ {
$cancel = FormAction::create('doRefuse', _t(__CLASS__.'.REFUSE', 'Cancel')); $cancel = FormAction::create('doRefuse', _t(__CLASS__ . '.REFUSE', 'Cancel'));
$confirm = FormAction::create('doConfirm', _t(__CLASS__.'.CONFIRM', 'Run the action'))->setAutofocus(true); $confirm = FormAction::create('doConfirm', _t(__CLASS__ . '.CONFIRM', 'Run the action'))->setAutofocus(true);
if ($storage->getHttpMethod() === 'POST') { if ($storage->getHttpMethod() === 'POST') {
$confirm->setAttribute('formaction', htmlspecialchars($storage->getSuccessUrl())); $confirm->setAttribute('formaction', htmlspecialchars($storage->getSuccessUrl()));
@ -165,7 +165,7 @@ class Form extends BaseForm
protected function buildEmptyFieldList() protected function buildEmptyFieldList()
{ {
return FieldList::create( return FieldList::create(
HeaderField::create(null, _t(__CLASS__.'.EMPTY_TITLE', 'Nothing to confirm')) HeaderField::create(null, _t(__CLASS__ . '.EMPTY_TITLE', 'Nothing to confirm'))
); );
} }
} }

View File

@ -47,7 +47,7 @@ class Handler extends RequestHandler
public function index() public function index()
{ {
return [ return [
'Title' => _t(__CLASS__.'.FORM_TITLE', 'Confirm potentially dangerous action'), 'Title' => _t(__CLASS__ . '.FORM_TITLE', 'Confirm potentially dangerous action'),
'Form' => $this->Form() 'Form' => $this->Form()
]; ];
return $this; return $this;

View File

@ -125,7 +125,7 @@ class Storage
$token = $item->getToken(); $token = $item->getToken();
$salt = $this->getSessionSalt(); $salt = $this->getSessionSalt();
$salted = $salt.$token; $salted = $salt . $token;
return hash(static::HASH_ALGO, $salted, true); return hash(static::HASH_ALGO, $salted, true);
} }
@ -139,7 +139,7 @@ class Storage
{ {
$salt = $this->getSessionSalt(); $salt = $this->getSessionSalt();
return bin2hex(hash(static::HASH_ALGO, $salt.'cookie key', true)); return bin2hex(hash(static::HASH_ALGO, $salt . 'cookie key', true));
} }
/** /**
@ -151,7 +151,7 @@ class Storage
{ {
$salt = $this->getSessionSalt(); $salt = $this->getSessionSalt();
return base64_encode(hash(static::HASH_ALGO, $salt.'csrf token', true)); return base64_encode(hash(static::HASH_ALGO, $salt . 'csrf token', true));
} }
/** /**
@ -441,7 +441,7 @@ class Storage
'%s.%s%s', '%s.%s%s',
str_replace('\\', '.', __CLASS__), str_replace('\\', '.', __CLASS__),
$this->id, $this->id,
$key ? '.'.$key : '' $key ? '.' . $key : ''
); );
} }
} }

View File

@ -29,7 +29,7 @@ trait HttpRequestMockBuilder
$request->method('getSession')->willReturn($session); $request->method('getSession')->willReturn($session);
$request->method('getURL')->will($this->returnCallback(static function ($addParams) use ($url, $getVars) { $request->method('getURL')->will($this->returnCallback(static function ($addParams) use ($url, $getVars) {
return $addParams && count($getVars) ? $url.'?'.http_build_query($getVars) : $url; return $addParams && count($getVars) ? $url . '?' . http_build_query($getVars) : $url;
})); }));
$request->method('getVars')->willReturn($getVars); $request->method('getVars')->willReturn($getVars);

View File

@ -68,7 +68,7 @@ class ConfirmationMiddlewareTest extends SapphireTest
$this->assertFalse($next); $this->assertFalse($next);
$this->assertInstanceOf(HTTPResponse::class, $response); $this->assertInstanceOf(HTTPResponse::class, $response);
$this->assertEquals(302, $response->getStatusCode()); $this->assertEquals(302, $response->getStatusCode());
$this->assertEquals(Director::baseURL().'dev/confirm/middleware', $response->getHeader('location')); $this->assertEquals(Director::baseURL() . 'dev/confirm/middleware', $response->getHeader('location'));
// Test bypasses have more priority than rules // Test bypasses have more priority than rules
$middleware->setBypasses([new Url('dev/build')]); $middleware->setBypasses([new Url('dev/build')]);

View File

@ -92,4 +92,27 @@ TEXT
$this->assertSame('The Diary of Anne Frank', $formatter->output(200)); $this->assertSame('The Diary of Anne Frank', $formatter->output(200));
} }
public function testAdminEmailWithName()
{
Email::config()->set('admin_email', ['testy@mctest.face' => 'The ad&min']);
$formatter = new DebugViewFriendlyErrorFormatter();
$formatter->setTitle("There has been an error");
$formatter->setBody("The website server has not been able to respond to your request");
$expected = <<<TEXT
WEBSITE ERROR
There has been an error
-----------------------
The website server has not been able to respond to your request
Contact The ad&amp;min: testy [at] mctest [dot] face
TEXT
;
$this->assertEquals($expected, $formatter->output(404));
}
} }

View File

@ -14,7 +14,7 @@ class StorageTest extends SapphireTest
private function getNamespace($id) private function getNamespace($id)
{ {
return str_replace('\\', '.', Storage::class).'.'.$id; return str_replace('\\', '.', Storage::class) . '.' . $id;
} }
public function testNewStorage() public function testNewStorage()