Skip to content

Commit

Permalink
Merge branch '6.0' into 6
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Feb 20, 2025
2 parents baba2d8 + 1a6cea4 commit 90d280e
Show file tree
Hide file tree
Showing 70 changed files with 1,564 additions and 185 deletions.
24 changes: 2 additions & 22 deletions src/Control/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -527,30 +527,10 @@ public function render($params = null): DBHTMLText

/**
* Returns the current controller.
*
* @return Controller
*/
public static function curr()
{
if (Controller::$controller_stack) {
return Controller::$controller_stack[0];
}
// This user_error() will be removed in the next major version of Silverstripe CMS
user_error("No current controller available", E_USER_WARNING);
return null;
}

/**
* Tests whether we have a currently active controller or not. True if there is at least 1
* controller in the stack.
*
* @return bool
* @deprecated 5.4.0 Will be removed without equivalent functionality to replace it
*/
public static function has_curr()
public static function curr(): ?Controller
{
Deprecation::noticeWithNoReplacment('5.4.0');
return Controller::$controller_stack ? true : false;
return Controller::$controller_stack[0] ?? null;
}

/**
Expand Down
5 changes: 1 addition & 4 deletions src/Control/Cookie.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,7 @@ public static function validateSameSite(string $sameSite): void
*/
private static function getRequest(): ?HTTPRequest
{
$request = null;
if (Controller::has_curr()) {
$request = Controller::curr()->getRequest();
}
$request = Controller::curr()?->getRequest();
// NullHTTPRequest always has a scheme of http - set to null so we can fallback on default_base_url
return ($request instanceof NullHTTPRequest) ? null : $request;
}
Expand Down
17 changes: 14 additions & 3 deletions src/Control/Middleware/AllowedHostsMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace SilverStripe\Control\Middleware;

use InvalidArgumentException;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
Expand All @@ -12,14 +13,16 @@
class AllowedHostsMiddleware implements HTTPMiddleware
{
/**
* List of allowed hosts
* List of allowed hosts.
* Can be ['*'] to allow all hosts and disable the logged warning.
*
* @var array
*/
private $allowedHosts = [];

/**
* @return array List of allowed Host header values
* @return array List of allowed Host header values.
* Note that both an empty array and ['*'] can be used to allow all hosts.
*/
public function getAllowedHosts()
{
Expand All @@ -30,14 +33,21 @@ public function getAllowedHosts()
* Sets the list of allowed Host header values
* Can also specify a comma separated list
*
* Note that both an empty array and ['*'] can be used to allow all hosts.
*
* @param array|string $allowedHosts
* @return $this
*/
public function setAllowedHosts($allowedHosts)
{
if (is_string($allowedHosts)) {
if ($allowedHosts === null) {
$allowedHosts = [];
} elseif (is_string($allowedHosts)) {
$allowedHosts = preg_split('/ *, */', $allowedHosts ?? '');
}
if (count($allowedHosts) > 1 && in_array('*', $allowedHosts)) {
throw new InvalidArgumentException('The wildcard "*" cannot be used in conjunction with actual hosts.');
}
$this->allowedHosts = $allowedHosts;
return $this;
}
Expand All @@ -51,6 +61,7 @@ public function process(HTTPRequest $request, callable $delegate)

// check allowed hosts
if ($allowedHosts
&& $allowedHosts !== ['*']
&& !Director::is_cli()
&& !in_array($request->getHeader('Host'), $allowedHosts ?? [])
) {
Expand Down
28 changes: 28 additions & 0 deletions src/Core/BaseKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\Middleware\AllowedHostsMiddleware;
use SilverStripe\Core\Cache\ManifestCacheFactory;
use SilverStripe\Core\Config\ConfigLoader;
use SilverStripe\Core\Config\CoreConfigFactory;
Expand Down Expand Up @@ -221,6 +222,9 @@ protected function bootConfigs()
{
// After loading all other app manifests, include _config.php files
$this->getModuleLoader()->getManifest()->activateConfig();

// Ensure everything is set up correctly
$this->validateConfiguration();
}

/**
Expand Down Expand Up @@ -362,6 +366,7 @@ public function activate()
$this->getInjectorLoader()
->getManifest()
->registerService($this, Kernel::class);

return $this;
}

Expand Down Expand Up @@ -443,4 +448,27 @@ public function setThemeResourceLoader($themeResourceLoader)
$this->themeResourceLoader = $themeResourceLoader;
return $this;
}

/**
* Validate configuration of the application is in a good state, ready for use.
*
* This method can be used to warn developers of any misconfiguration, or configuration
* which is missing but should be set according to best practice.
*
* In some cases, this could be used to halt execution if configuration critical to operation
* has not been set.
*/
protected function validateConfiguration(): void
{
// Log a warning if allowed hosts hasn't been configured.
// This can include wildcard, but it must be explicitly set to ensure the developer is aware
// of the level of protection their application has against host header injection attacks.
$allowedHostsMiddleware = Injector::inst()->get(AllowedHostsMiddleware::class, true);
if (empty($allowedHostsMiddleware->getAllowedHosts())) {
Injector::inst()->get(LoggerInterface::class)->warning(
'Allowed hosts has not been set. Your application could be vulnerable to host header injection attacks.'
. ' Either set the SS_ALLOWED_HOSTS environment variable or the AllowedHosts property on ' . AllowedHostsMiddleware::class
);
}
}
}
22 changes: 5 additions & 17 deletions src/Core/Validation/FieldValidation/BigIntFieldValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,14 @@

use RunTimeException;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\ORM\FieldType\DBBigInt;

/**
* A field validator for 64-bit integers
* Will throw a RunTimeException if used on a 32-bit system
*/
class BigIntFieldValidator extends IntFieldValidator
{
/**
* The minimum value for a signed 64-bit integer.
* Defined as string instead of int otherwise will end up as a float
* on 64-bit systems
*
* When this is cast to an int in IntFieldValidator::__construct()
* it will be properly cast to an int
*/
protected const MIN_INT = '-9223372036854775808';

/**
* The maximum value for a signed 64-bit integer.
*/
protected const MAX_INT = '9223372036854775807';

public function __construct(
string $name,
mixed $value,
Expand All @@ -37,6 +23,8 @@ public function __construct(
if ($bits === 32) {
throw new RunTimeException('Cannot use BigIntFieldValidator on a 32-bit system');
}
$minValue ??= (int) DBBigInt::getMinValue();
$maxValue ??= (int) DBBigInt::getMaxValue();
}
$this->minValue = $minValue;
$this->maxValue = $maxValue;
Expand All @@ -51,10 +39,10 @@ protected function validateValue(): ValidationResult
// int values that are too large or too small will be cast to float
// on 64-bit systems and will fail the validation in IntFieldValidator
if (is_string($this->value)) {
if (!is_null($this->minValue) && bccomp($this->value, static::MIN_INT) === -1) {
if (!is_null($this->minValue) && bccomp($this->value, DBBigInt::getMinValue()) === -1) {
$result->addFieldError($this->name, $this->getTooSmallMessage());
}
if (!is_null($this->maxValue) && bccomp($this->value, static::MAX_INT) === 1) {
if (!is_null($this->maxValue) && bccomp($this->value, DBBigInt::getMaxValue()) === 1) {
$result->addFieldError($this->name, $this->getTooLargeMessage());
}
}
Expand Down
17 changes: 3 additions & 14 deletions src/Core/Validation/FieldValidation/IntFieldValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,24 @@
namespace SilverStripe\Core\Validation\FieldValidation;

use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\ORM\FieldType\DBInt;

/**
* Validates that a value is a 32-bit signed integer
*/
class IntFieldValidator extends NumericNonStringFieldValidator
{
/**
* The minimum value for a signed 32-bit integer.
* Defined as string instead of int because be cast to a float
* on 32-bit systems if defined as an int
*/
protected const MIN_INT = '-2147483648';

/**
* The maximum value for a signed 32-bit integer.
*/
protected const MAX_INT = '2147483647';

public function __construct(
string $name,
mixed $value,
?int $minValue = null,
?int $maxValue = null
) {
if (is_null($minValue)) {
$minValue = (int) static::MIN_INT;
$minValue = (int) DBInt::getMinValue();
}
if (is_null($maxValue)) {
$maxValue = (int) static::MAX_INT;
$maxValue = (int) DBInt::getMaxValue();
}
parent::__construct($name, $value, $minValue, $maxValue);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Dev/SapphireTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ protected function tearDown(): void
// Note: Ideally a clean Controller should be created for each test.
// Now all tests executed in a batch share the same controller.
if (class_exists(Controller::class)) {
$controller = Controller::has_curr() ? Controller::curr() : null;
$controller = Controller::curr();
if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
$response->setStatusCode(200);
$response->removeHeader('Location');
Expand Down
4 changes: 2 additions & 2 deletions src/Dev/TestSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ public function __destruct()
{
// Shift off anything else that's on the stack. This can happen if something throws
// an exception that causes a premature TestSession::__destruct() call
while (Controller::has_curr() && Controller::curr() !== $this->controller) {
while (Controller::curr() && Controller::curr() !== $this->controller) {
Controller::curr()->popCurrent();
}

if (Controller::has_curr()) {
if (Controller::curr()) {
$this->controller->popCurrent();
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Forms/CurrencyField.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public function setValue($value, $data = null)
. number_format((double)preg_replace('/[^0-9.\-]/', '', $value ?? ''), 2);
return $this;
}

/**
* Overwrite the datavalue before saving to the db ;-)
* return 0.00 if no value, or value is non-numeric
Expand Down
45 changes: 44 additions & 1 deletion src/Forms/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace SilverStripe\Forms;

use BadMethodCallException;
use SilverStripe\Admin\LeftAndMain;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HasRequestHandler;
use SilverStripe\Control\HTTPRequest;
Expand All @@ -22,6 +23,8 @@
use SilverStripe\View\SSViewer;
use SilverStripe\Model\ModelData;
use SilverStripe\Forms\Validation\Validator;
use SilverStripe\Security\SudoMode\SudoModeServiceInterface;
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;

/**
* Base class for all forms.
Expand Down Expand Up @@ -320,6 +323,46 @@ public function __construct(
$this->setupDefaultClasses();
}

/**
* Make the form require sudo mode, which will make the form readonly and add a sudo mode password field
* unless the current user previously activated sudo mode.
*
* Note that if the parent request handler for this form isn't LeftAndMain or GridFieldDetailForm_ItemRequest,
* sudo mode will not be required by this form.
*/
public function requireSudoMode(): void
{
// Check that the current request handler for the form is one that's used
// in an admin context where sudo mode makes sense
$classes = [
LeftAndMain::class,
GridFieldDetailForm_ItemRequest::class,
];
$enableSudoMode = false;
foreach ($classes as $class) {
if (is_a($this->getController(), $class)) {
$enableSudoMode = true;
break;
}
}
if (!$enableSudoMode) {
return;
}
// Check if sudo mode is currently enabled
$service = Injector::inst()->get(SudoModeServiceInterface::class);
$session = $this->getRequest()->getSession();
if ($service->check($session)) {
return;
}
// If sudo mode is not active, make the form readonly and add a sudo mode password field
$this->makeReadonly();
$field = SudoModePasswordField::create(SudoModePasswordField::FIELD_NAME);
// Manually call setForm() to the field as the field list is being updated after the
// form is created, which is when setForm() is normally being created
$field->setForm($this);
$this->Fields()->unshift($field);
}

/**
* @return bool
*/
Expand Down Expand Up @@ -384,7 +427,7 @@ protected function getRequest()
return $controller->getRequest();
}
// Fall back to current controller
if (Controller::has_curr() && !(Controller::curr()->getRequest() instanceof NullHTTPRequest)) {
if (Controller::curr() && !(Controller::curr()->getRequest() instanceof NullHTTPRequest)) {
return Controller::curr()->getRequest();
}
return null;
Expand Down
Loading

0 comments on commit 90d280e

Please sign in to comment.