mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge branch '4.4' into 4
This commit is contained in:
commit
7873efde9c
@ -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;
|
||||||
|
|
||||||
|
@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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'))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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 : ''
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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')]);
|
||||||
|
@ -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&min: testy [at] mctest [dot] face
|
||||||
|
|
||||||
|
|
||||||
|
TEXT
|
||||||
|
;
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $formatter->output(404));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user