Merge branch '5.0' into 5

This commit is contained in:
Steve Boyd 2023-03-30 13:20:24 +13:00
commit c1427ff9c4
19 changed files with 669 additions and 121 deletions

View File

@ -4,15 +4,10 @@ on:
push:
pull_request:
workflow_dispatch:
# Every Tuesday at 2:20pm UTC
schedule:
- cron: '20 14 * * 2'
jobs:
ci:
name: CI
# Only run cron on the silverstripe account
if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule')
uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1
with:
# Turn phpcoverage off because it causes a segfault

16
.github/workflows/dispatch-ci.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: Dispatch CI
on:
# At 2:20 PM UTC, only on Tuesday and Wednesday
schedule:
- cron: '20 14 * * 2,3'
jobs:
dispatch-ci:
name: Dispatch CI
# Only run cron on the silverstripe account
if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule')
runs-on: ubuntu-latest
steps:
- name: Dispatch CI
uses: silverstripe/gha-dispatch-ci@v1

View File

@ -177,23 +177,32 @@ class Environment
}
/**
* Get value of environment variable
* Get value of environment variable.
* If the value is false, you should check Environment::hasEnv() to see
* if the value is an actual environment variable value or if the variable
* simply hasn't been set.
*
* @param string $name
* @return mixed Value of the environment variable, or false if not set
*/
public static function getEnv($name)
{
switch (true) {
case is_array(static::$env) && array_key_exists($name, static::$env):
return static::$env[$name];
case is_array($_ENV) && array_key_exists($name, $_ENV):
return $_ENV[$name];
case is_array($_SERVER) && array_key_exists($name, $_SERVER):
return $_SERVER[$name];
default:
return getenv($name);
if (array_key_exists($name, static::$env)) {
return static::$env[$name];
}
// isset() is used for $_ENV and $_SERVER instead of array_key_exists() to fix a very strange issue that
// occured in CI running silverstripe/recipe-kitchen-sink where PHP would timeout due apparently due to an
// excessively high number of array method calls. isset() is not used for static::$env above because
// values there may be null, and isset() will return false for null values
// Symfony also uses isset() for reading $_ENV and $_SERVER values
// https://github.com/symfony/dependency-injection/blob/6.2/EnvVarProcessor.php#L148
if (isset($_ENV[$name])) {
return $_ENV[$name];
}
if (isset($_SERVER[$name])) {
return $_SERVER[$name];
}
return getenv($name);
}
/**
@ -223,6 +232,18 @@ class Environment
static::$env[$name] = $value;
}
/**
* Check if an environment variable is set
*/
public static function hasEnv(string $name): bool
{
// See getEnv() for an explanation of why isset() is used for $_ENV and $_SERVER
return array_key_exists($name, static::$env)
|| isset($_ENV[$name])
|| isset($_SERVER[$name])
|| getenv($name) !== false;
}
/**
* Returns true if this script is being run from the command line rather than the web server
*

View File

@ -29,12 +29,22 @@ class Deprecation
* Must be configured outside of the config API, as deprecation API
* must be available before this to avoid infinite loops.
*
* This will be overriden by the SS_DEPRECATION_ENABLED environment if present
* This will be overriden by the SS_DEPRECATION_ENABLED environment variable if present
*
* @internal - Marked as internal so this and other private static's are not treated as config
*/
private static bool $currentlyEnabled = false;
/**
* @internal
*/
private static bool $shouldShowForHttp = false;
/**
* @internal
*/
private static bool $shouldShowForCli = true;
/**
* @internal
*/
@ -45,6 +55,11 @@ class Deprecation
*/
private static bool $insideWithNoReplacement = false;
/**
* @internal
*/
private static bool $isTriggeringError = false;
/**
* Buffer of user_errors to be raised
*
@ -62,12 +77,26 @@ class Deprecation
*/
private static bool $showNoReplacementNotices = false;
/**
* Enable throwing deprecation warnings. By default, this excludes warnings for
* deprecated code which is called by core Silverstripe modules.
*
* This will be overriden by the SS_DEPRECATION_ENABLED environment variable if present.
*
* @param bool $showNoReplacementNotices If true, deprecation warnings will also be thrown
* for deprecated code which is called by core Silverstripe modules.
*/
public static function enable(bool $showNoReplacementNotices = false): void
{
static::$currentlyEnabled = true;
static::$showNoReplacementNotices = $showNoReplacementNotices;
}
/**
* Disable throwing deprecation warnings.
*
* This will be overriden by the SS_DEPRECATION_ENABLED environment variable if present.
*/
public static function disable(): void
{
static::$currentlyEnabled = false;
@ -133,7 +162,68 @@ class Deprecation
if (!Director::isDev()) {
return false;
}
return static::$currentlyEnabled || Environment::getEnv('SS_DEPRECATION_ENABLED');
if (Environment::hasEnv('SS_DEPRECATION_ENABLED')) {
$envVar = Environment::getEnv('SS_DEPRECATION_ENABLED');
return self::varAsBoolean($envVar);
}
return static::$currentlyEnabled;
}
/**
* If true, any E_USER_DEPRECATED errors should be treated as coming
* directly from this class.
*/
public static function isTriggeringError(): bool
{
return self::$isTriggeringError;
}
/**
* Determine whether deprecation warnings should be included in HTTP responses.
* Does not affect logging.
*
* This will be overriden by the SS_DEPRECATION_SHOW_HTTP environment variable if present.
*/
public static function setShouldShowForHttp(bool $value): void
{
self::$shouldShowForHttp = $value;
}
/**
* Determine whether deprecation warnings should be included in CLI responses.
* Does not affect logging.
*
* This will be overriden by the SS_DEPRECATION_SHOW_CLI environment variable if present.
*/
public static function setShouldShowForCli(bool $value): void
{
self::$shouldShowForCli = $value;
}
/**
* If true, deprecation warnings should be included in HTTP responses.
* Does not affect logging.
*/
public static function shouldShowForHttp(): bool
{
if (Environment::hasEnv('SS_DEPRECATION_SHOW_HTTP')) {
$envVar = Environment::getEnv('SS_DEPRECATION_SHOW_HTTP');
return self::varAsBoolean($envVar);
}
return self::$shouldShowForHttp;
}
/**
* If true, deprecation warnings should be included in CLI responses.
* Does not affect logging.
*/
public static function shouldShowForCli(): bool
{
if (Environment::hasEnv('SS_DEPRECATION_SHOW_CLI')) {
$envVar = Environment::getEnv('SS_DEPRECATION_SHOW_CLI');
return self::varAsBoolean($envVar);
}
return self::$shouldShowForCli;
}
public static function outputNotices(): void
@ -141,24 +231,24 @@ class Deprecation
if (!self::isEnabled()) {
return;
}
$outputMessages = [];
// using a while loop with array_shift() to ensure that self::$userErrorMessageBuffer will have
// have values removed from it before calling user_error()
while (count(self::$userErrorMessageBuffer)) {
$count = 0;
$origCount = count(self::$userErrorMessageBuffer);
while ($origCount > $count) {
$count++;
$arr = array_shift(self::$userErrorMessageBuffer);
$message = $arr['message'];
// often the same deprecation message appears dozens of times, which isn't helpful
// only need to show a single instance of each message
if (in_array($message, $outputMessages)) {
continue;
}
$calledInsideWithNoReplacement = $arr['calledInsideWithNoReplacement'];
if ($calledInsideWithNoReplacement && !self::$showNoReplacementNotices) {
continue;
}
self::$isTriggeringError = true;
user_error($message, E_USER_DEPRECATED);
$outputMessages[] = $message;
self::$isTriggeringError = false;
}
// Make absolutely sure the buffer is empty - array_shift seems to leave an item in the array
// if we're not using numeric keys.
self::$userErrorMessageBuffer = [];
}
/**
@ -178,10 +268,12 @@ class Deprecation
// try block needs to wrap all code in case anything inside the try block
// calls something else that calls Deprecation::notice()
try {
$data = null;
if ($scope === self::SCOPE_CONFIG) {
// Deprecated config set via yaml will only be shown in the browser when using ?flush=1
// It will not show in CLI when running dev/build flush=1
self::$userErrorMessageBuffer[] = [
$data = [
'key' => sha1($string),
'message' => $string,
'calledInsideWithNoReplacement' => self::$insideWithNoReplacement
];
@ -215,20 +307,26 @@ class Deprecation
if ($caller) {
$string = $caller . ' is deprecated.' . ($string ? ' ' . $string : '');
}
self::$userErrorMessageBuffer[] = [
$data = [
'key' => sha1($string),
'message' => $string,
'calledInsideWithNoReplacement' => self::$insideWithNoReplacement
];
}
if (!self::$haveSetShutdownFunction && self::isEnabled()) {
if ($data && !array_key_exists($data['key'], self::$userErrorMessageBuffer)) {
// Store de-duplicated data in a buffer to be outputted when outputNotices() is called
self::$userErrorMessageBuffer[$data['key']] = $data;
// Use a shutdown function rather than immediately calling user_error() so that notices
// do not interfere with setting session varibles i.e. headers already sent error
// it also means the deprecation notices appear below all phpunit output in CI
// which is far nicer than having it spliced between phpunit output
register_shutdown_function(function () {
self::outputNotices();
});
self::$haveSetShutdownFunction = true;
if (!self::$haveSetShutdownFunction && self::isEnabled()) {
register_shutdown_function(function () {
self::outputNotices();
});
self::$haveSetShutdownFunction = true;
}
}
} catch (BadMethodCallException $e) {
if ($e->getMessage() === InjectorLoader::NO_MANIFESTS_AVAILABLE) {
@ -242,4 +340,31 @@ class Deprecation
static::$insideNotice = false;
}
}
private static function varAsBoolean($val): bool
{
if (is_string($val)) {
$truthyStrings = [
'on',
'true',
'1',
];
if (in_array(strtolower($val), $truthyStrings, true)) {
return true;
}
$falsyStrings = [
'off',
'false',
'0',
];
if (in_array(strtolower($val), $falsyStrings, true)) {
return false;
}
}
return (bool) $val;
}
}

View File

@ -199,21 +199,25 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
public function canFilterAnyColumns($gridField)
{
$list = $gridField->getList();
if (!$this->checkDataType($list)) {
if (!($list instanceof Filterable) || !$this->checkDataType($list)) {
return false;
}
$columns = $gridField->getColumns();
foreach ($columns as $columnField) {
$metadata = $gridField->getColumnMetadata($columnField);
$title = $metadata['title'];
if ($title && $list->canFilterBy($columnField)) {
$modelClass = $gridField->getModelClass();
// note: searchableFields() will return summary_fields if there are no searchable_fields on the model
$searchableFields = array_keys($modelClass::singleton()->searchableFields());
$summaryFields = array_keys($modelClass::singleton()->summaryFields());
sort($searchableFields);
sort($summaryFields);
// searchable_fields has been explictily defined i.e. searchableFields() is not falling back to summary_fields
if ($searchableFields !== $summaryFields) {
return true;
}
// we have fallen back to summary_fields, check they are filterable
foreach ($searchableFields as $searchableField) {
if ($list->canFilterBy($searchableField)) {
return true;
}
}
return false;
}

View File

@ -188,25 +188,7 @@ class GridFieldSortableHeader extends AbstractGridFieldComponent implements Grid
}
}
} else {
if ($currentColumn == count($columns ?? [])) {
$filter = $gridField->getConfig()->getComponentByType(GridFieldFilterHeader::class);
if ($filter && $filter->canFilterAnyColumns($gridField)) {
$field = new LiteralField(
$fieldName,
sprintf(
'<button type="button" name="showFilter" aria-label="%s" title="%s"' .
' class="btn btn-secondary font-icon-search btn--no-text btn--icon-large grid-field__filter-open"></button>',
_t('SilverStripe\\Forms\\GridField\\GridField.OpenFilter', "Open search and filter"),
_t('SilverStripe\\Forms\\GridField\\GridField.OpenFilter', "Open search and filter")
)
);
} else {
$field = new LiteralField($fieldName, '<span class="non-sortable">' . $title . '</span>');
}
} else {
$field = new LiteralField($fieldName, '<span class="non-sortable">' . $title . '</span>');
}
$field = new LiteralField($fieldName, '<span class="non-sortable">' . $title . '</span>');
}
$forTemplate->Fields->push($field);
}

View File

@ -8,6 +8,7 @@ use Monolog\LogRecord;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Dev\Deprecation;
/**
* Output the error to the browser, with the given HTTP status code.
@ -138,6 +139,16 @@ class HTTPOutputHandler extends AbstractProcessingHandler
return $this;
}
protected function shouldShowError(int $errorCode): bool
{
// show all non-E_USER_DEPRECATED errors
// or E_USER_DEPRECATED errors when not triggering from the Deprecation class
// or our deprecations when the relevant shouldShow method returns true
return $errorCode !== E_USER_DEPRECATED
|| !Deprecation::isTriggeringError()
|| ($this->isCli() ? Deprecation::shouldShowForCli() : Deprecation::shouldShowForHttp());
}
/**
* @param array $record
* @return bool
@ -146,6 +157,14 @@ class HTTPOutputHandler extends AbstractProcessingHandler
{
ini_set('display_errors', 0);
// Suppress errors that should be suppressed
if (isset($record['context']['code'])) {
$errorCode = $record['context']['code'];
if (!$this->shouldShowError($errorCode)) {
return;
}
}
// TODO: This coupling isn't ideal
// See https://github.com/silverstripe/silverstripe-framework/issues/4484
if (Controller::has_curr()) {
@ -166,4 +185,12 @@ class HTTPOutputHandler extends AbstractProcessingHandler
$response->setBody($record['formatted']);
$response->output();
}
/**
* This method is required and must be protected for unit testing, since we can't mock static or private methods
*/
protected function isCli(): bool
{
return Director::is_cli();
}
}

View File

@ -5,6 +5,7 @@ namespace SilverStripe\Logging;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Monolog\ErrorHandler as MonologHandler;
use Psr\Log\LogLevel;
class MonologErrorHandler implements ErrorHandler
{
@ -62,7 +63,13 @@ class MonologErrorHandler implements ErrorHandler
}
foreach ($loggers as $logger) {
MonologHandler::register($logger);
// Log deprecation warnings as WARNING, not NOTICE
// see https://github.com/Seldaek/monolog/blob/1.x/doc/01-usage.md#log-levels
$errorLevelMap = [
E_DEPRECATED => LogLevel::WARNING,
E_USER_DEPRECATED => LogLevel::WARNING,
];
MonologHandler::register($logger, $errorLevelMap);
}
}
}

View File

@ -583,7 +583,7 @@ class ArrayList extends ViewableData implements SS_List, Filterable, Sortable, L
$firstRecord = $this->first();
return is_array($firstRecord) ? array_key_exists($by, $firstRecord) : property_exists($by, $firstRecord ?? '');
return is_array($firstRecord) ? array_key_exists($by, $firstRecord) : property_exists($firstRecord, $by ?? '');
}
/**

View File

@ -28,22 +28,6 @@ class EmbedShortcodeProvider implements ShortcodeHandler
{
use Configurable;
/**
* A whitelist of shortcode attributes which are allowed in the resultant markup.
* Note that the tinymce plugin restricts attributes on the client-side separately.
*
* @config
* @deprecated 4.12.0 Removed without equivalent functionality to replace it
*/
private static array $attribute_whitelist = [
'url',
'thumbnail',
'class',
'width',
'height',
'caption',
];
/**
* Gets the list of shortcodes provided by this handler
*
@ -262,8 +246,17 @@ class EmbedShortcodeProvider implements ShortcodeHandler
*/
private static function buildAttributeListFromArguments(array $arguments, array $exclude = []): ArrayList
{
// A whitelist of shortcode attributes which are allowed in the resultant markup.
// Note that the tinymce plugin restricts attributes on the client-side separately.
$whitelist = [
'url',
'thumbnail',
'class',
'width',
'height',
'caption'
];
// Clean out any empty arguments and anything not whitelisted
$whitelist = static::config()->get('attribute_whitelist');
$arguments = array_filter($arguments, function ($value, $key) use ($whitelist) {
return in_array($key, $whitelist) && strlen(trim($value ?? ''));
}, ARRAY_FILTER_USE_BOTH);

View File

@ -6,7 +6,7 @@ use SilverStripe\Core\Environment;
if (!Environment::getEnv('SS_DATABASE_CLASS') && !Environment::getEnv('SS_DATABASE_USERNAME')) {
// The default settings let us define the database config via environment vars
// Database connection, including PDO and legacy ORM support
// Database connection, including legacy ORM support
switch (Environment::getEnv('DB')) {
case "PGSQL";
$pgDatabaseClass = 'PostgreSQLDatabase';

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Core\Tests;
use ReflectionProperty;
use SilverStripe\Core\Environment;
use SilverStripe\Dev\SapphireTest;
@ -66,4 +67,86 @@ class EnvironmentTest extends SapphireTest
$this->assertEquals('fail', $vars['test']);
$this->assertEquals('global', $GLOBALS['test']);
}
public function provideHasEnv()
{
$setAsOptions = [
'.env',
'_ENV',
'_SERVER',
'putenv',
];
$valueOptions = [
true,
false,
0,
1,
1.75,
'',
'0',
'some-value',
];
$scenarios = [];
foreach ($setAsOptions as $setAs) {
foreach ($valueOptions as $value) {
$scenarios[] = [
'setAs' => $setAs,
'value' => $value,
'expected' => true,
];
}
}
// `null` isn't a supported value outside of using the `.env` option.
$scenarios[] = [
'setAs' => '.env',
'value' => null,
'expected' => true,
];
$scenarios[] = [
'setAs' => null,
'value' => null,
'expected' => false,
];
return $scenarios;
}
/**
* @dataProvider provideHasEnv
*/
public function testHasEnv(?string $setAs, $value, bool $expected)
{
$name = '_ENVTEST_HAS_ENV';
// Set the value
switch ($setAs) {
case '.env':
Environment::setEnv($name, $value);
break;
case '_ENV':
$_ENV[$name] = $value;
break;
case '_SERVER':
$_SERVER[$name] = $value;
break;
case 'putenv':
$val = is_string($value) ? $value : json_encode($value);
putenv("$name=$val");
break;
default:
// null is no-op, to validate not setting it works as expected.
if ($setAs !== null) {
$this->fail("setAs value $setAs isn't taken into account correctly for this test.");
}
}
$this->assertSame($expected, Environment::hasEnv($name));
// unset the value
$reflectionEnv = new ReflectionProperty(Environment::class, 'env');
$reflectionEnv->setAccessible(true);
$reflectionEnv->setValue(array_diff($reflectionEnv->getValue(), [$name => $value]));
unset($_ENV[$name]);
unset($_SERVER[$name]);
putenv("$name");
}
}

View File

@ -3,7 +3,9 @@
namespace SilverStripe\Dev\Tests;
use PHPUnit\Framework\Error\Deprecated;
use ReflectionMethod;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Environment;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Dev\Tests\DeprecationTest\DeprecationTestObject;
@ -22,7 +24,7 @@ class DeprecationTest extends SapphireTest
protected function setup(): void
{
// Use custom error handler for two reasons:
// - Filter out errors for deprecated class properities unrelated to this unit test
// - Filter out errors for deprecated class properties unrelated to this unit test
// - Allow the use of expectDeprecation(), which doesn't work with E_USER_DEPRECATION by default
// https://github.com/laminas/laminas-di/pull/30#issuecomment-927585210
parent::setup();
@ -32,7 +34,7 @@ class DeprecationTest extends SapphireTest
if (str_contains($errstr, 'SilverStripe\\Dev\\Tests\\DeprecationTest')) {
throw new Deprecated($errstr, $errno, '', 1);
} else {
// Surpress any E_USER_DEPRECATED unrelated to this unit-test
// Suppress any E_USER_DEPRECATED unrelated to this unit-test
return true;
}
}
@ -46,7 +48,9 @@ class DeprecationTest extends SapphireTest
protected function tearDown(): void
{
if (!$this->noticesWereEnabled) {
if ($this->noticesWereEnabled) {
Deprecation::enable();
} else {
Deprecation::disable();
}
restore_error_handler();
@ -278,4 +282,167 @@ class DeprecationTest extends SapphireTest
Config::modify()->merge(DeprecationTestObject::class, 'array_config', ['abc']);
Deprecation::outputNotices();
}
public function provideConfigVsEnv()
{
return [
// env var not set - config setting is respected
[
// false is returned when the env isn't set, so this simulates simply not having
// set the variable in the first place
'envVal' => 'notset',
'configVal' => false,
'expected' => false,
],
[
'envVal' => 'notset',
'configVal' => true,
'expected' => true,
],
// env var is set and truthy, config setting is ignored
[
'envVal' => true,
'configVal' => false,
'expected' => true,
],
[
'envVal' => true,
'configVal' => true,
'expected' => true,
],
// env var is set and falsy, config setting is ignored
[
'envVal' => false,
'configVal' => false,
'expected' => false,
],
[
'envVal' => false,
'configVal' => true,
'expected' => false,
],
];
}
/**
* @dataProvider provideConfigVsEnv
*/
public function testEnabledConfigVsEnv($envVal, bool $configVal, bool $expected)
{
$this->runConfigVsEnvTest('SS_DEPRECATION_ENABLED', $envVal, $configVal, $expected);
}
/**
* @dataProvider provideConfigVsEnv
*/
public function testShowHttpConfigVsEnv($envVal, bool $configVal, bool $expected)
{
$this->runConfigVsEnvTest('SS_DEPRECATION_SHOW_HTTP', $envVal, $configVal, $expected);
}
/**
* @dataProvider provideConfigVsEnv
*/
public function testShowCliConfigVsEnv($envVal, bool $configVal, bool $expected)
{
$this->runConfigVsEnvTest('SS_DEPRECATION_SHOW_CLI', $envVal, $configVal, $expected);
}
private function runConfigVsEnvTest(string $varName, $envVal, bool $configVal, bool $expected)
{
$oldVars = Environment::getVariables();
if ($envVal === 'notset') {
if (Environment::hasEnv($varName)) {
$this->markTestSkipped("$varName is set, so we can't test behaviour when it's not");
return;
}
} else {
Environment::setEnv($varName, $envVal);
}
switch ($varName) {
case 'SS_DEPRECATION_ENABLED':
if ($configVal) {
Deprecation::enable();
} else {
Deprecation::disable();
}
$result = Deprecation::isEnabled();
break;
case 'SS_DEPRECATION_SHOW_HTTP':
$oldShouldShow = Deprecation::shouldShowForHttp();
Deprecation::setShouldShowForHttp($configVal);
$result = Deprecation::shouldShowForHttp();
Deprecation::setShouldShowForHttp($oldShouldShow);
break;
case 'SS_DEPRECATION_SHOW_CLI':
$oldShouldShow = Deprecation::shouldShowForCli();
Deprecation::setShouldShowForCli($configVal);
$result = Deprecation::shouldShowForCli();
Deprecation::setShouldShowForCli($oldShouldShow);
break;
}
Environment::setVariables($oldVars);
$this->assertSame($expected, $result);
}
public function provideVarAsBoolean()
{
return [
[
'rawValue' => true,
'expected' => true,
],
[
'rawValue' => 'true',
'expected' => true,
],
[
'rawValue' => 1,
'expected' => true,
],
[
'rawValue' => '1',
'expected' => true,
],
[
'rawValue' => 'on',
'expected' => true,
],
[
'rawValue' => false,
'expected' => false,
],
[
'rawValue' => 'false',
'expected' => false,
],
[
'rawValue' => 0,
'expected' => false,
],
[
'rawValue' => '0',
'expected' => false,
],
[
'rawValue' => 'off',
'expected' => false,
],
];
}
/**
* @dataProvider provideVarAsBoolean
*/
public function testVarAsBoolean($rawValue, bool $expected)
{
$reflectionVarAsBoolean = new ReflectionMethod(Deprecation::class, 'varAsBoolean');
$reflectionVarAsBoolean->setAccessible(true);
$this->assertSame($expected, $reflectionVarAsBoolean->invoke(null, $rawValue));
}
}

View File

@ -168,4 +168,30 @@ class GridFieldFilterHeaderTest extends SapphireTest
$this->assertEquals('ReallyCustomSearch', $this->component->getSearchField());
}
public function testCanFilterAnyColumns()
{
$gridField = $this->gridField;
$filterHeader = $gridField->getConfig()->getComponentByType(GridFieldFilterHeader::class);
// test that you can filter by something if searchable_fields is not defined
// silverstripe will scaffold db columns that are in the gridfield to be
// searchable by default
Config::modify()->remove(Team::class, 'searchable_fields');
$this->assertTrue($filterHeader->canFilterAnyColumns($gridField));
// test that you can filterBy if searchable_fields is defined
Config::modify()->set(Team::class, 'searchable_fields', ['Name']);
$this->assertTrue($filterHeader->canFilterAnyColumns($gridField));
// test that you can filterBy if searchable_fields even if it is not a legit field
// this is because we're making a blind assumption it will be filterable later in a SearchContext
Config::modify()->set(Team::class, 'searchable_fields', ['WhatIsThis']);
$this->assertTrue($filterHeader->canFilterAnyColumns($gridField));
// test that you cannot filter by non-db field when it falls back to summary_fields
Config::modify()->remove(Team::class, 'searchable_fields');
Config::modify()->set(Team::class, 'summary_fields', ['MySummaryField']);
$this->assertFalse($filterHeader->canFilterAnyColumns($gridField));
}
}

View File

@ -24,4 +24,9 @@ class Team extends DataObject implements TestOnly
'Cheerleader' => Cheerleader::class,
'CheerleadersMom' => Mom::class
];
public function getMySummaryField()
{
return 'MY SUMMARY FIELD';
}
}

View File

@ -3,8 +3,11 @@
namespace SilverStripe\Logging\Tests;
use Monolog\Handler\HandlerInterface;
use ReflectionMethod;
use ReflectionProperty;
use SilverStripe\Control\Director;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Logging\DebugViewFriendlyErrorFormatter;
use SilverStripe\Logging\DetailedErrorFormatter;
@ -53,4 +56,131 @@ class HTTPOutputHandlerTest extends SapphireTest
$this->assertInstanceOf(DetailedErrorFormatter::class, $handler->getDefaultFormatter());
$this->assertInstanceOf(DetailedErrorFormatter::class, $handler->getFormatter());
}
public function provideShouldShowError()
{
$provide = [];
// See https://www.php.net/manual/en/errorfunc.constants.php
$errors = [
E_ERROR,
E_WARNING,
E_PARSE,
E_NOTICE,
E_CORE_ERROR,
E_CORE_WARNING,
E_COMPILE_ERROR,
E_COMPILE_WARNING,
E_USER_ERROR,
E_USER_WARNING,
E_USER_NOTICE,
E_RECOVERABLE_ERROR,
E_DEPRECATED,
E_USER_DEPRECATED,
];
foreach ($errors as $errorCode) {
// Display all errors regardless of type in this scenario
$provide[] = [
'errorCode' => $errorCode,
'triggeringError' => true,
'isCli' => true,
'shouldShow' => true,
'expected' => true,
];
// Don't display E_USER_DEPRECATED that we're triggering if shouldShow is false
$provide[] = [
'errorCode' => $errorCode,
'triggeringError' => true,
'isCli' => true,
'shouldShow' => false,
'expected' => ($errorCode !== E_USER_DEPRECATED) || false
];
// Display all errors regardless of type in this scenario
$provide[] = [
'errorCode' => $errorCode,
'triggeringError' => true,
'isCli' => false,
'shouldShow' => true,
'expected' => true
];
// Don't display E_USER_DEPRECATED that we're triggering if shouldShow is false
$provide[] = [
'errorCode' => $errorCode,
'triggeringError' => true,
'isCli' => false,
'shouldShow' => false,
'expected' => ($errorCode !== E_USER_DEPRECATED) || false
];
// All of the below have triggeringError set to false, in which case
// all errors should be displayed.
$provide[] = [
'errorCode' => $errorCode,
'triggeringError' => false,
'isCli' => true,
'shouldShow' => true,
'expected' => true
];
$provide[] = [
'errorCode' => $errorCode,
'triggeringError' => false,
'isCli' => false,
'shouldShow' => true,
'expected' => true
];
$provide[] = [
'errorCode' => $errorCode,
'triggeringError' => false,
'isCli' => true,
'shouldShow' => false,
'expected' => true
];
$provide[] = [
'errorCode' => $errorCode,
'triggeringError' => false,
'isCli' => false,
'shouldShow' => false,
'expected' => true
];
}
return $provide;
}
/**
* @dataProvider provideShouldShowError
*/
public function testShouldShowError(
int $errorCode,
bool $triggeringError,
bool $isCli,
bool $shouldShow,
bool $expected
) {
$reflectionShouldShow = new ReflectionMethod(HTTPOutputHandler::class, 'shouldShowError');
$reflectionShouldShow->setAccessible(true);
$reflectionTriggeringError = new ReflectionProperty(Deprecation::class, 'isTriggeringError');
$reflectionTriggeringError->setAccessible(true);
$cliShouldShowOrig = Deprecation::shouldShowForCli();
$httpShouldShowOrig = Deprecation::shouldShowForHttp();
$triggeringErrorOrig = Deprecation::isTriggeringError();
// Set the relevant item using $shouldShow, and the other always as true
// to show that these don't interfere with each other
if ($isCli) {
Deprecation::setShouldShowForCli($shouldShow);
Deprecation::setShouldShowForHttp(true);
} else {
Deprecation::setShouldShowForCli(true);
Deprecation::setShouldShowForHttp($shouldShow);
}
$reflectionTriggeringError->setValue($triggeringError);
$mockHandler = $this->getMockBuilder(HTTPOutputHandler::class)->onlyMethods(['isCli'])->getMock();
$mockHandler->method('isCli')->willReturn($isCli);
$result = $reflectionShouldShow->invoke($mockHandler, $errorCode);
$this->assertSame($expected, $result);
Deprecation::setShouldShowForCli($cliShouldShowOrig);
Deprecation::setShouldShowForHttp($httpShouldShowOrig);
$reflectionTriggeringError->setValue($triggeringErrorOrig);
}
}

View File

@ -193,7 +193,7 @@ class DatabaseTest extends SapphireTest
$obj->MyInt = 5;
$obj->MyFloat = 6.0;
// Note: in non-PDO SQLite, whole numbers of a decimal field will be returned as integers rather than floats
// Note: in SQLite, whole numbers of a decimal field will be returned as integers rather than floats
$obj->MyDecimal = 7.1;
$obj->MyBoolean = true;
$obj->write();

View File

@ -217,37 +217,4 @@ class EmbedShortcodeProviderTest extends EmbedUnitTest
$html
);
}
public function testWhitelistIsConfigurable()
{
// Allow new whitelisted attribute
Config::modify()->merge(EmbedShortcodeProvider::class, 'attribute_whitelist', ['data-some-value']);
$url = 'https://www.youtube.com/watch?v=dM15HfUYwF0';
$html = $this->getShortcodeHtml(
$url,
$url,
<<<EOT
<link rel="alternate" type="application/json+oembed" href="https://www.youtube.com/oembed?format=json&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Da2tDOYkFCYo" title="The flying car completes first ever inter-city flight (Official Video)">
EOT,
<<<EOT
{"title":"The flying car completes first ever inter-city flight (Official Video)","author_name":"KleinVision","author_url":"https://www.youtube.com/channel/UCCHAHvcO7KSNmgXVRIJLNkw","type":"video","height":113,"width":200,"version":"1.0","provider_name":"YouTube","provider_url":"https://www.youtube.com/","thumbnail_height":360,"thumbnail_width":480,"thumbnail_url":"https://i.ytimg.com/vi/a2tDOYkFCYo/hqdefault.jpg","html":"\u003ciframe width=\u0022200\u0022 height=\u0022113\u0022 src=\u0022https://www.youtube.com/embed/a2tDOYkFCYo?feature=oembed\u0022 frameborder=\u00220\u0022 allow=\u0022accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\u0022 allowfullscreen\u003e\u003c/iframe\u003e"}
EOT,
[
'url' => $url,
'caption' => 'A nice video',
'width' => 779,
'height' => 437,
'data-some-value' => 'my-data',
'onmouseover' => 'alert(2)',
'style' => 'background-color:red;',
],
);
$this->assertEqualIgnoringWhitespace(
<<<EOT
<div data-some-value="my-data" style="width:779px;"><iframe width="779" height="437" src="https://www.youtube.com/embed/a2tDOYkFCYo?feature=oembed" frameborder="0" allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture" allowfullscreen></iframe><p class="caption">A nice video</p></div>
EOT,
$html
);
}
}