diff --git a/lang/en.yml b/lang/en.yml
index 35c20794dbd..c4e48704120 100644
--- a/lang/en.yml
+++ b/lang/en.yml
@@ -25,6 +25,10 @@ en:
SilverStripe\Control\RequestProcessor:
INVALID_REQUEST: 'Invalid request'
REQUEST_ABORTED: 'Request aborted'
+ SilverStripe\Dev\BulkLoader:
+ CANNOT_CREATE: "Not allowed to create '{type}' records"
+ CANNOT_DELETE: "Not allowed to delete '{type}' records"
+ CANNOT_EDIT: "Not allowed to edit '{type}' records"
SilverStripe\Dev\DevBuildController:
CAN_DEV_BUILD_DESCRIPTION: 'Can execute /dev/build'
CAN_DEV_BUILD_HELP: 'Can execute the build command (/dev/build).'
@@ -167,6 +171,9 @@ en:
INVALID: 'Please enter a valid URL'
SilverStripe\ORM\DataObject:
GENERALSEARCH: 'General Search'
+ NO_DUPLICATE: 'Cannot create duplicate {type}'
+ NO_DUPLICATE_MULTI_FIELD: 'Cannot create duplicate {type} - at least one of the following fields need to be changed: {fields}'
+ NO_DUPLICATE_SINGLE_FIELD: 'Cannot create duplicate {type} with "{field}" set to "{value}"'
PLURALNAME: 'Data Objects'
PLURALS:
one: 'A Data Object'
diff --git a/lang/it.yml b/lang/it.yml
index a58d0441c46..c84f726f70d 100644
--- a/lang/it.yml
+++ b/lang/it.yml
@@ -27,9 +27,22 @@ it:
SilverStripe\Control\RequestProcessor:
INVALID_REQUEST: 'Richiesta non valida'
REQUEST_ABORTED: 'Richiesta annullata'
+ SilverStripe\Dev\DevBuildController:
+ CAN_DEV_BUILD_DESCRIPTION: 'Può eseguire /dev/build'
+ CAN_DEV_BUILD_HELP: 'Può eseguire il comando di build (/dev/build).'
+ SilverStripe\Dev\DevConfigController:
+ CAN_DEV_CONFIG_DESCRIPTION: 'Può vedere /dev/config'
+ CAN_DEV_CONFIG_HELP: "Può vedere l'intera configurazione dell'applicazione (/dev/config)."
SilverStripe\Dev\DevConfirmationController:
INFO_DESCRIPTION: "Confermare l'operazione potenzialmente pericolosa"
INFO_TITLE: 'Conferma di Sicurezza'
+ SilverStripe\Dev\DevelopmentAdmin:
+ ALL_DEV_ADMIN_DESCRIPTION: 'Può vedere ed eseguire tutte le azioni in /dev'
+ ALL_DEV_ADMIN_HELP: 'Può vedere ed eseguire tutte le azioni in /dev'
+ PERMISSIONS_CATEGORY: 'Permessi di sviluppo'
+ SilverStripe\Dev\TaskRunner:
+ BUILDTASK_CAN_RUN_DESCRIPTION: 'Può vedere ed eseguire ogni cosa in /dev/tasks'
+ BUILDTASK_CAN_RUN_HELP: 'Può vedere ed eseguire tutti i task di build (/dev/tasks). Può comunque essere scavalcato da specifici permessi del task'
SilverStripe\Forms\CheckboxField:
YESANSWER: Sì
SilverStripe\Forms\CheckboxSetField_ss:
@@ -41,6 +54,7 @@ it:
CURRENT_PASSWORD_MISSING: 'Devi inserire la tua password attuale.'
LOGGED_IN_ERROR: 'Devi essere autenticato per poter cambiare la tua password.'
MAXIMUM: 'La password deve essere lunga almeno {max} caratteri.'
+ RANDOM_IF_EMPTY: 'Se lasciato vuoto una password casuale verrà generata automaticamente.'
SHOWONCLICKTITLE: 'Cambia password'
SilverStripe\Forms\DateField:
NOTSET: 'non impostato'
@@ -103,7 +117,7 @@ it:
Create: Crea
Delete: Elimina
DeletePermissionsFailure: 'Non hai i permessi per eliminare'
- Deleted: 'Eliminato {type} {name}'
+ Deleted: 'Eliminato {type} "{name}"'
Save: Salva
Saved: 'Salvato {name} {link}'
SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest:
@@ -111,6 +125,8 @@ it:
NEW: 'Aggiungi un nuovo record'
NEXT: 'Vai al prossimo record'
PREVIOUS: 'Vai al record precedente'
+ SAVEDUP: 'Salvato correttamente.'
+ SAVETOASTMESSAGE: '{type} "{title}" salvato correttamente.'
ViewPermissionsFailure: 'Pare tu non abbia i privilegi necessari per visualizzare "{ObjectTitle}"'
SilverStripe\Forms\GridField\GridFieldEditButton:
EDIT: Modifica
@@ -140,16 +156,25 @@ it:
IsNullLabel: 'è nullo.'
SilverStripe\Forms\NumericField:
VALIDATION: "'{value}' non è un numero, solo numeri possono essere accettati per questo campo"
+ SilverStripe\Forms\SearchableDropdownTrait:
+ SELECT: Seleziona...
+ SELECT_OR_TYPE_TO_SEARCH: 'Selezionare o digitare per cercare...'
+ TYPE_TO_SEARCH: 'Tipo di ricerca...'
SilverStripe\Forms\TextField:
VALIDATEMAXLENGTH: 'Il valore di {name} non deve superare i {maxLength} caratteri di lunghezza'
SilverStripe\Forms\TimeField:
VALIDATEFORMAT: "Inserisci un formato d'ora valido ({format})"
+ SilverStripe\Forms\UrlField:
+ INVALID: 'Prego inserire un URL valido'
SilverStripe\ORM\DataObject:
+ GENERALSEARCH: 'Ricerca Generale'
PLURALNAME: 'Data Object'
PLURALS:
many: '{count} Data Object'
one: 'Un Data Object'
other: '{count} Data Object'
+ many_many_FileTracking: 'Monitoraggio file'
+ many_many_LinkTracking: 'Monitoraggio link'
SilverStripe\ORM\FieldType\DBBoolean:
ANY: Qualsiasi
YESANSWER: Sì
@@ -180,6 +205,31 @@ it:
many: '{count} anni'
one: '{count} anno'
other: '{count} anni'
+ SilverStripe\ORM\FieldType\DBDatetime:
+ nDays:
+ many: '{count} giorni'
+ one: 'un giorno'
+ other: '{count} giorni'
+ nHours:
+ many: '{count} ore'
+ one: "un'ora"
+ other: '{count} ore'
+ nMinutes:
+ many: '{count} minuti'
+ one: 'un minuto'
+ other: '{count} minuti'
+ nMonths:
+ many: '{count} mesi'
+ one: 'un mese'
+ other: '{count} mesi'
+ nSeconds:
+ many: '{count} secondi'
+ one: 'un secondo'
+ other: '{count} secondi'
+ nYears:
+ many: '{count} anni'
+ one: 'un anno'
+ other: '{count} anni'
SilverStripe\ORM\FieldType\DBEnum:
ANY: Qualsiasi
SilverStripe\ORM\FieldType\DBForeignKey:
@@ -238,15 +288,29 @@ it:
SINGULARNAME: Gruppo
Sort: 'Tipo ordinamento'
ValidationIdentifierAlreadyExists: 'Esiste già un gruppo ({group}) con lo stesso {identifier}'
+ db_AccessAllSubsites: 'Accesso a tutti i sottositi'
+ db_Code: Codice
db_Description: Descrizione
+ db_HtmlEditorConfig: 'Configurazione editor HTML'
+ db_LastSynced: 'Ultima sincronizzazione'
+ db_Locked: Bloccato
db_Sort: Ordine
db_Title: Titolo
has_many_Groups: Gruppi
+ has_many_LDAPGroupMappings: 'Mappatura dei gruppi LDAP'
has_many_Permissions: Permessi
has_one_Parent: Parente
many_many_Members: Membri
many_many_Roles: Ruoli
+ many_many_SiteTreeContentReview: "Revisione contenuto dell'albero delle pagine"
many_many_Subsites: Sottositi
+ SilverStripe\Security\InheritedPermissionsExtension:
+ db_CanEditType: 'Tipo può modificare'
+ db_CanViewType: 'Tipo può visualizzare'
+ many_many_EditorGroups: 'Gruppi di editori'
+ many_many_EditorMembers: 'Membri editori'
+ many_many_ViewerGroups: 'Gruppi di visualizzatori'
+ many_many_ViewerMembers: 'Membri visualizzatori'
SilverStripe\Security\LoginAttempt:
Email: 'Indirizzo e-mail'
EmailHashed: 'Indirizzo email (hash)'
@@ -258,6 +322,7 @@ it:
other: "{count} tentativi d'accesso"
SINGULARNAME: "Tentativo d'accesso"
Status: Stato
+ db_EmailHashed: 'Email codificata'
db_Status: Stato
has_one_Member: Utente
SilverStripe\Security\Member:
@@ -271,6 +336,7 @@ it:
CONFIRMPASSWORD: 'Conferma password'
CURRENT_PASSWORD: 'Password Attuale'
EDIT_PASSWORD: 'Nuova password'
+ EMAIL_FAILED: "Errore durante l'invio dell'email per il reset delle credenziali."
EMPTYNEWPASSWORD: 'La nuova password non può essere vuota, riprova'
ENTEREMAIL: 'Inserisci un indirizzo e-mail per ricevere il link di azzeramento della password'
ERRORLOCKEDOUT2: 'Il tuo account è stato temporaneamente disabilitato perchè ci sono stati troppi tentativi di accesso errati. Riprova tra {count} minuti.'
@@ -289,6 +355,7 @@ it:
many: '{count} Utenti'
one: 'Un Utente'
other: '{count} Utenti'
+ RequiresPasswordChangeOnNextLogin: 'Richiede il cambio password al prossimo login'
SINGULARNAME: Utente
SUBJECTPASSWORDCHANGED: 'La tua password è stata cambiata'
SUBJECTPASSWORDRESET: 'Link per azzerare la tua password'
@@ -298,14 +365,39 @@ it:
ValidationIdentifierFailed: "Non posso sovrascrivere l'utente esistente #{id} con identificatore identico ({name} = {value}))"
WELCOMEBACK: 'Bentornato, {firstname}'
YOUROLDPASSWORD: 'La tua vecchia password'
+ belongs_many_many_BlogPosts: 'Articoli del blog'
belongs_many_many_Groups: Gruppi
+ db_AccountResetExpired: 'Reset credenziali scaduto'
+ db_AccountResetHash: 'Hash per il reset delle credenziali'
+ db_AutoLoginExpired: 'Auto login scaduto'
+ db_AutoLoginHash: "Hash per l'auto login"
+ db_BlogProfileSummary: 'Riassunto profilo per il blog'
+ db_DefaultRegisteredMethodID: 'ID metodo di registrazione di default'
db_Email: E-mail
+ db_FailedLoginCount: 'Conteggio login falliti'
db_FirstName: Nome
+ db_HasSkippedMFARegistration: 'Ha registrazione con skippedMFA'
+ db_IsExpired: 'È scaduto'
+ db_LastSynced: 'Ultima sincronizzazione'
db_Locale: 'Localizzazione interfaccia'
db_LockedOutUntil: 'Bloccato fino al'
+ db_PasswordEncryption: 'Password criptata'
db_PasswordExpiry: 'Data di scadenza della password'
+ db_Salt: Seme
db_Surname: Cognome
+ db_TempIDExpired: 'TempID scaduto'
+ db_TempIDHash: 'Hash di TempID'
db_URLSegment: 'Segmento URL'
+ db_Username: 'Nome utente'
+ has_many_LoggedPasswords: 'Password registrate'
+ has_many_LoginSessions: 'Sessioni di login'
+ has_many_RegisteredMFAMethods: 'Metodi RegisteredMFA'
+ has_many_RememberLoginHashes: 'Ricordare gli hash di login'
+ has_one_AFile: 'Un file'
+ has_one_AImage: "Un'immagine"
+ has_one_BlogProfileImage: 'Immagine profilo per il blog'
+ has_one_FavouritePage: 'Pagina preferita'
+ many_many_SiteTreeContentReview: "Revisione contenuto dell'albero delle pagine"
SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm:
AUTHENTICATORNAME: 'Form di Login Utente CMS'
BUTTONFORGOTPASSWORD: 'Password dimenticata'
@@ -321,6 +413,8 @@ it:
one: 'Una password utente'
other: '{count} password utente'
SINGULARNAME: 'Password utente'
+ db_PasswordEncryption: 'Password criptata'
+ db_Salt: Seme
has_one_Member: Utente
SilverStripe\Security\PasswordValidator:
LOWCHARSTRENGTH: 'Perfavore aumenta la sicurezza della password aggiungendo alcuni dei seguenti caratteri: {chars}'
@@ -340,6 +434,8 @@ it:
other: '{count} Permessi'
SINGULARNAME: Permesso
UserPermissionsIntro: 'Assegnando gruppi a questo utente modificherà i suoi permessi. Vedi la sezione gruppi per dettagli sui permessi dei singoli gruppi.'
+ db_Arg: Argomento
+ db_Code: Codice
db_Type: Tipo
has_one_Group: Gruppo
SilverStripe\Security\PermissionCheckboxSetField:
@@ -359,6 +455,7 @@ it:
belongs_many_many_Groups: Gruppi
db_OnlyAdminCanApply: "Solo l'amministratore può applicare"
db_Title: Titolo
+ has_many_Codes: Codici
SilverStripe\Security\PermissionRoleCode:
PLURALNAME: 'Codici di ruolo'
PLURALS:
@@ -367,6 +464,7 @@ it:
other: '{count} codici di ruolo'
PermsError: 'Non posso assegnare permessi privilegiati al codice "{code}" (richiede accesso ADMIN)'
SINGULARNAME: 'Codice di ruolo'
+ db_Code: Codice
has_one_Role: Ruolo
SilverStripe\Security\RememberLoginHash:
PLURALNAME: 'Hash di Login'
@@ -375,6 +473,9 @@ it:
one: 'Un Hash di Login'
other: '{count} Hash di Login'
SINGULARNAME: 'Hash di Login'
+ db_DeviceID: 'ID dispositivo'
+ db_ExpiryDate: 'Data scadenza'
+ has_one_LoginSession: 'Sessione di login'
has_one_Member: Utente
SilverStripe\Security\Security:
ALREADYLOGGEDIN: 'Non hai accesso a questa pagina. Se hai un altro account che può accederci, puoi autenticarti qui sotto.'
diff --git a/lang/nl.yml b/lang/nl.yml
index 87608a01298..10550f0ecfb 100644
--- a/lang/nl.yml
+++ b/lang/nl.yml
@@ -248,6 +248,13 @@ nl:
has_one_Parent: Bovenliggende
many_many_Members: Leden
many_many_Roles: Rollen
+ SilverStripe\Security\InheritedPermissionsExtension:
+ db_CanEditType: 'Kan bewerken type'
+ db_CanViewType: 'Kan bekijken type'
+ many_many_EditorGroups: Redacteursgroepen
+ many_many_EditorMembers: Redacteurs
+ many_many_ViewerGroups: Bekijkersgroepen
+ many_many_ViewerMembers: Bekijk-gebruikers
SilverStripe\Security\LoginAttempt:
Email: 'E-mailadres '
EmailHashed: 'E-mailadres (versleuteld)'
@@ -298,8 +305,10 @@ nl:
ValidationIdentifierFailed: 'Een bestaande gebruiker #{id} kan niet dezelfde unieke velden hebben ({name} = {value}))'
WELCOMEBACK: 'Welkom terug, {firstname}'
YOUROLDPASSWORD: 'Je oude wachtwoord'
+ belongs_many_many_BlogPosts: 'Blog Artikelen'
belongs_many_many_Groups: Groepen
db_AccountResetExpired: 'Het opnieuw instellen van het account is verlopen'
+ db_BlogProfileSummary: 'Blogprofiel samenvatting'
db_Email: E-mail
db_FirstName: Voornaam
db_LastSynced: 'Laatst gesynchroniseerd'
@@ -308,6 +317,7 @@ nl:
db_Password: Wachtwoord
db_PasswordExpiry: 'Wachtwoord vervaldatum'
db_Surname: Achternaam
+ has_one_BlogProfileImage: 'Blogprofiel afbeelding'
SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm:
AUTHENTICATORNAME: Inlogformulier
BUTTONFORGOTPASSWORD: 'Wachtwoord vergeten'
diff --git a/src/Forms/GridField/GridFieldDataColumns.php b/src/Forms/GridField/GridFieldDataColumns.php
index b7f08a7d817..10408e42452 100644
--- a/src/Forms/GridField/GridFieldDataColumns.php
+++ b/src/Forms/GridField/GridFieldDataColumns.php
@@ -6,6 +6,9 @@
use InvalidArgumentException;
use LogicException;
use SilverStripe\Model\ModelData;
+use SilverStripe\ORM\FieldType\DBField;
+use SilverStripe\ORM\FieldType\DBHTMLText;
+use SilverStripe\ORM\FieldType\DBHTMLVarchar;
/**
* @see GridField
@@ -38,6 +41,8 @@ class GridFieldDataColumns extends AbstractGridFieldComponent implements GridFie
private ?string $statusFlagColumn = null;
+ private bool $doEscapeFields = true;
+
/**
* Modify the list of columns displayed in the table.
* See {@link GridFieldDataColumns->getDisplayFields()} and {@link GridFieldDataColumns}.
@@ -202,6 +207,28 @@ public function getFieldFormatting()
return $this->fieldFormatting;
}
+ /**
+ * Determines whether this component escapes strings returned from getColumnContent().
+ *
+ * This is useful because by default strings are escaped for use in HTML. This
+ * means there are some circumstances in which the escaping done here can result
+ * in double escaping those values further down the line, such as use with
+ * GridFieldPrintButton which temporarily sets this to false.
+ */
+ public function setDoEscapeFields(bool $doEscapeFields): static
+ {
+ $this->doEscapeFields = $doEscapeFields;
+ return $this;
+ }
+
+ /**
+ * Get whether this component escapes strings returned from getColumnContent().
+ */
+ public function getDoEscapeFields(): bool
+ {
+ return $this->doEscapeFields;
+ }
+
/**
* HTML for the column, content of the
element.
*
@@ -295,12 +322,24 @@ protected function castValue($gridField, $fieldName, $value)
// If the value is an object, we do one of two things
if (method_exists($value, 'Nice')) {
// If it has a "Nice" method, call that & make sure the result is safe
- $value = nl2br(Convert::raw2xml($value->Nice()) ?? '');
+ $value = $value->Nice();
+ if ($this->getDoEscapeFields()) {
+ $value = nl2br(Convert::raw2xml($value));
+ }
} else {
- // Otherwise call forTemplate - the result of this should already be safe
- $value = $value->forTemplate();
+ if (!$this->getDoEscapeFields()
+ && is_a($value, DBField::class, false)
+ && !is_a($value, DBHTMLText::class, false)
+ && !is_a($value, DBHTMLVarchar::class, false)
+ ) {
+ // For DBFields other than HTML variants, if we're not escaping values, get the raw value.
+ $value = $value->RAW();
+ } else {
+ // Otherwise, check forTemplate() which is assumed to be safe.
+ $value = $value->forTemplate();
+ }
}
- } else {
+ } elseif ($this->getDoEscapeFields()) {
// Otherwise, just treat as a text string & make sure the result is safe
$value = nl2br(Convert::raw2xml($value) ?? '');
}
diff --git a/src/Forms/GridField/GridFieldFilterHeader.php b/src/Forms/GridField/GridFieldFilterHeader.php
index d62b07c0dcc..0cd2908a180 100755
--- a/src/Forms/GridField/GridFieldFilterHeader.php
+++ b/src/Forms/GridField/GridFieldFilterHeader.php
@@ -7,6 +7,7 @@
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\ClassInfo;
+use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\Schema\FormSchema;
@@ -295,7 +296,7 @@ public function getSearchFieldSchema(GridField $gridField)
$searchField = $searchField && property_exists($searchField, 'name') ? $searchField->name : null;
}
- // Prefix "Search__" onto the filters for the React component
+ // Prefix "Search__" onto the filters to match the field names in the actual form
$filters = $context->getSearchParams();
if (!empty($filters)) {
$filters = array_combine(array_map(function ($key) {
@@ -339,14 +340,10 @@ public function getSearchForm(GridField $gridField)
return $this->searchForm;
}
- // Append a prefix to search field names to prevent conflicts with other fields in the search form
- foreach ($searchFields as $field) {
- $field->setName('Search__' . $field->getName());
- }
-
- $columns = $gridField->getColumns();
+ $this->addSearchPrefixToFields($searchFields);
// Update field titles to match column titles
+ $columns = $gridField->getColumns();
foreach ($columns as $columnField) {
$metadata = $gridField->getColumnMetadata($columnField);
// Get the field name, without any modifications
@@ -359,9 +356,7 @@ public function getSearchForm(GridField $gridField)
}
}
- foreach ($searchFields->getIterator() as $field) {
- $field->addExtraClass('stacked no-change-track');
- }
+ $this->updateFieldClasses($searchFields);
$name = $this->getTitle(singleton($gridField->getModelClass()));
@@ -489,4 +484,30 @@ private function getBasicSearchContext(GridField $gridField, SearchContext $sear
return $basicSearchContext;
}
+
+ /*
+ * Append a prefix to search field names to prevent conflicts with other fields in the search form
+ */
+ private function addSearchPrefixToFields(FieldList $fields): void
+ {
+ foreach ($fields as $field) {
+ $field->setName('Search__' . $field->getName());
+ if ($field instanceof CompositeField) {
+ $this->addSearchPrefixToFields($field->getChildren());
+ }
+ }
+ }
+
+ /**
+ * Update CSS classes for form fields, including nested inside composite fields
+ */
+ private function updateFieldClasses(FieldList $fields): void
+ {
+ foreach ($fields as $field) {
+ $field->addExtraClass('stacked no-change-track');
+ if ($field instanceof CompositeField) {
+ $this->updateFieldClasses($field->getChildren());
+ }
+ }
+ }
}
diff --git a/src/Forms/GridField/GridFieldPrintButton.php b/src/Forms/GridField/GridFieldPrintButton.php
index bebc63d65a6..4501a31c3cf 100644
--- a/src/Forms/GridField/GridFieldPrintButton.php
+++ b/src/Forms/GridField/GridFieldPrintButton.php
@@ -4,11 +4,11 @@
use LogicException;
use SilverStripe\Control\HTTPRequest;
-use SilverStripe\Core\Convert;
use SilverStripe\Core\Extensible;
use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBHTMLText;
+use SilverStripe\ORM\FieldType\DBHTMLVarchar;
use SilverStripe\Security\Security;
use SilverStripe\Model\ArrayData;
use SilverStripe\View\Requirements;
@@ -231,7 +231,11 @@ public function generatePrintData(GridField $gridField)
$items = $gridField->getManipulatedList();
$itemRows = new ArrayList();
+ // If there's a GridFieldDataColumns component, ensure it doesn't escape raw strings
+ // as that would result in double escaping when we render out the print template.
$gridFieldColumnsComponent = $gridField->getConfig()->getComponentByType(GridFieldDataColumns::class);
+ $origDoEscapeFields = $gridFieldColumnsComponent?->getDoEscapeFields();
+ $gridFieldColumnsComponent?->setDoEscapeFields(false);
/** @var ModelData $item */
foreach ($items->limit(null) as $item) {
@@ -244,8 +248,13 @@ public function generatePrintData(GridField $gridField)
? strip_tags($gridFieldColumnsComponent->getColumnContent($gridField, $item, $field))
: $gridField->getDataFieldValue($item, $field);
+ // The value is used in a template, so to prevent XSS attacks we can't allow an HTML field here.
+ // Getting the raw string here means it will end up being default-casted to DBText which is safe.
+ if (is_a($value, DBHTMLText::class, false) || is_a($value, DBHTMLVarchar::class, false)) {
+ $value = $value->__toString();
+ }
$itemRow->push(new ArrayData([
- "CellString" => $value,
+ 'CellString' => $value,
]));
}
@@ -258,6 +267,8 @@ public function generatePrintData(GridField $gridField)
}
}
+ $gridFieldColumnsComponent?->setDoEscapeFields($origDoEscapeFields);
+
$ret = new ArrayData([
"Title" => $this->getTitle($gridField),
"Header" => $header,
diff --git a/src/ORM/DataObject.php b/src/ORM/DataObject.php
index 2ed8bb7c78a..6fb7ad2d0e4 100644
--- a/src/ORM/DataObject.php
+++ b/src/ORM/DataObject.php
@@ -4377,8 +4377,7 @@ public function provideI18nEntities()
$pluralName = $this->plural_name();
$singularName = $this->singular_name();
$conjunction = preg_match('/^[aeiou]/i', $singularName ?? '') ? 'An ' : 'A ';
- return [
- static::class . '.CLASS_DESCRIPTION' => $this->classDescription(),
+ $entities = [
static::class . '.SINGULARNAME' => $singularName,
static::class . '.PLURALNAME' => $pluralName,
static::class . '.PLURALS' => [
@@ -4386,6 +4385,11 @@ public function provideI18nEntities()
'other' => '{count} ' . $pluralName
]
];
+ $classDescription = $this->classDescription();
+ if ($classDescription) {
+ $entities[static::class . '.CLASS_DESCRIPTION'] = $classDescription;
+ }
+ return $entities;
}
/**
diff --git a/src/i18n/TextCollection/i18nTextCollector.php b/src/i18n/TextCollection/i18nTextCollector.php
index 00ec32d3702..b5b3904ec6c 100644
--- a/src/i18n/TextCollection/i18nTextCollector.php
+++ b/src/i18n/TextCollection/i18nTextCollector.php
@@ -882,7 +882,7 @@ public function collectFromCode($content, $fileName, Module $module)
$inTransFn = false;
$inConcat = false;
// Ensure key is valid before saving
- if (!empty($currentEntity[0])) {
+ if (!empty($currentEntity[0]) && !str_ends_with($currentEntity[0], '.')) {
$key = $currentEntity[0];
$default = $currentEntity[1] ?? '';
$comment = $currentEntity[2] ?? '';
diff --git a/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php b/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php
index 46829944d5b..40f76006b92 100644
--- a/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php
+++ b/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php
@@ -170,11 +170,17 @@ public function testGetSearchForm()
{
$searchForm = $this->component->getSearchForm($this->gridField);
$this->assertTrue($searchForm instanceof Form);
- $fields = $searchForm->Fields()->toArray();
+ $fields = $searchForm->Fields()->flattenFields()->toArray();
$this->assertEquals('Search__q', $fields[0]->Name);
$this->assertEquals('Search__Name', $fields[1]->Name);
$this->assertEquals('Search__City', $fields[2]->Name);
$this->assertEquals('Search__Cheerleader__Hat__Colour', $fields[3]->Name);
+ $this->assertEquals('Search__TestCompositeSingleTestCompositeNestedGroup', $fields[4]->Name);
+ $this->assertEquals('Search__TestCompositeSingle', $fields[5]->Name);
+ $this->assertEquals('Search__TestCompositeNestedGroup', $fields[6]->Name);
+ $this->assertEquals('Search__TestCompositeNested', $fields[7]->Name);
+ // Make sure there aren't additional fields we're not testing for
+ $this->assertCount(8, $fields);
$this->assertEquals('TeamsSearchForm', $searchForm->Name);
$this->assertTrue($searchForm->hasExtraClass('cms-search-form'));
foreach ($fields as $field) {
diff --git a/tests/php/Forms/GridField/GridFieldFilterHeaderTest/Team.php b/tests/php/Forms/GridField/GridFieldFilterHeaderTest/Team.php
index b39eefe5677..b862fdcd579 100644
--- a/tests/php/Forms/GridField/GridFieldFilterHeaderTest/Team.php
+++ b/tests/php/Forms/GridField/GridFieldFilterHeaderTest/Team.php
@@ -3,6 +3,8 @@
namespace SilverStripe\Forms\Tests\GridField\GridFieldFilterHeaderTest;
use SilverStripe\Dev\TestOnly;
+use SilverStripe\Forms\CompositeField;
+use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataObject;
class Team extends DataObject implements TestOnly
@@ -29,4 +31,16 @@ public function getMySummaryField()
{
return 'MY SUMMARY FIELD';
}
+
+ public function scaffoldSearchFields($_params = null)
+ {
+ $fields = parent::scaffoldSearchFields($_params);
+ $fields->add(new CompositeField([
+ new TextField('TestCompositeSingle'),
+ new CompositeField([
+ new TextField('TestCompositeNested'),
+ ])
+ ]));
+ return $fields;
+ }
}
diff --git a/tests/php/Forms/GridField/GridFieldPrintButtonTest.php b/tests/php/Forms/GridField/GridFieldPrintButtonTest.php
index ed73b6fcbca..e981cb0d156 100644
--- a/tests/php/Forms/GridField/GridFieldPrintButtonTest.php
+++ b/tests/php/Forms/GridField/GridFieldPrintButtonTest.php
@@ -6,6 +6,7 @@
use ReflectionMethod;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Controller;
+use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridFieldPrintButton;
@@ -16,6 +17,10 @@
use SilverStripe\Forms\Tests\GridField\GridFieldPrintButtonTest\TestObject;
use SilverStripe\Model\List\ArrayList;
use SilverStripe\Model\ArrayData;
+use SilverStripe\ORM\FieldType\DBField;
+use SilverStripe\ORM\FieldType\DBHTMLText;
+use SilverStripe\ORM\FieldType\DBHTMLVarchar;
+use SilverStripe\ORM\FieldType\DBText;
class GridFieldPrintButtonTest extends SapphireTest
{
@@ -93,6 +98,140 @@ public function testGeneratePrintData()
$this->assertSame($names, $foundNames);
}
+ public function provideHandlePrintEscaping(): array
+ {
+ return [
+ // Without data columns component
+ 'raw string pre-escaped' => [
+ 'value' => 'before<script>alert("hehehe");</script>after&',
+ 'useGridFieldDataColumns' => false,
+ 'expected' => 'before<script>alert("hehehe");</script>after&',
+ ],
+ 'raw string as HTML' => [
+ 'value' => 'beforeafter&',
+ 'useGridFieldDataColumns' => false,
+ 'expected' => 'before<script>alert("hehehe");</script>after&',
+ ],
+ 'DBText pre-escaped' => [
+ 'value' => (new DBText('field'))->setValue('before<script>alert("hehehe");</script>after&'),
+ 'useGridFieldDataColumns' => false,
+ 'expected' => 'before<script>alert("hehehe");</script>after&',
+ ],
+ 'DBText as HTML' => [
+ 'value' => (new DBText('field'))->setValue('beforeafter&'),
+ 'useGridFieldDataColumns' => false,
+ 'expected' => 'before<script>alert("hehehe");</script>after&',
+ ],
+ 'DBHTMLText pre-escaped' => [
+ 'value' => (new DBHTMLText('field'))->setValue('before<script>alert("hehehe");</script>after&'),
+ 'useGridFieldDataColumns' => false,
+ 'expected' => 'before<script>alert("hehehe");</script>after&',
+ ],
+ 'DBHTMLText as HTML' => [
+ 'value' => (new DBHTMLText('field'))->setValue('beforeafter&'),
+ 'useGridFieldDataColumns' => false,
+ 'expected' => 'before<script>alert("hehehe");</script>after&',
+ ],
+ 'DBHTMLVarchar pre-escaped' => [
+ 'value' => (new DBHTMLVarchar('field'))->setValue('before<script>alert("hehehe");</script>after&'),
+ 'useGridFieldDataColumns' => false,
+ 'expected' => 'before<script>alert("hehehe");</script>after&',
+ ],
+ 'DBHTMLVarchar as HTML' => [
+ 'value' => (new DBHTMLVarchar('field'))->setValue('beforeafter&'),
+ 'useGridFieldDataColumns' => false,
+ 'expected' => 'before<script>alert("hehehe");</script>after&',
+ ],
+ // With data columns component
+ 'raw string pre-escaped with datacolumns' => [
+ 'value' => 'before<script>alert("hehehe");</script>after&',
+ 'useGridFieldDataColumns' => true,
+ 'expected' => 'before<script>alert("hehehe");</script>after&',
+ ],
+ 'raw string pre-escaped with datacolumns' => [
+ 'value' => 'beforeafter&',
+ 'useGridFieldDataColumns' => true,
+ 'expected' => 'beforealert("hehehe");after&',
+ ],
+ 'DBText pre-escaped with datacolumns' => [
+ 'value' => (new DBText('field'))->setValue('before<script>alert("hehehe");</script>after&'),
+ 'useGridFieldDataColumns' => true,
+ 'expected' => 'before<script>alert("hehehe");</script>after&',
+ ],
+ 'DBText as HTML with datacolumns' => [
+ 'value' => (new DBText('field'))->setValue('beforeafter&'),
+ 'useGridFieldDataColumns' => true,
+ // Note stripped tags here
+ 'expected' => 'beforealert("hehehe");after&',
+ ],
+ 'DBHTMLText pre-escaped with datacolumns' => [
+ 'value' => (new DBHTMLText('field'))->setValue('before<script>alert("hehehe");</script>after&'),
+ 'useGridFieldDataColumns' => true,
+ 'expected' => 'before<script>alert("hehehe");</script>after&',
+ ],
+ 'DBHTMLText as HTML with datacolumns' => [
+ 'value' => (new DBHTMLText('field'))->setValue('beforeafter&'),
+ 'useGridFieldDataColumns' => true,
+ // Note stripped tags here
+ 'expected' => 'beforealert("hehehe");after&',
+ ],
+ 'DBHTMLVarchar pre-escaped with datacolumns' => [
+ 'value' => (new DBHTMLVarchar('field'))->setValue('before<script>alert("hehehe");</script>after&'),
+ 'useGridFieldDataColumns' => true,
+ 'expected' => 'before<script>alert("hehehe");</script>after&',
+ ],
+ 'DBHTMLVarchar as HTML with datacolumns' => [
+ 'value' => (new DBHTMLVarchar('field'))->setValue('beforeafter&'),
+ 'useGridFieldDataColumns' => true,
+ // Note stripped tags here
+ 'expected' => 'beforealert("hehehe");after&',
+ ],
+ ];
+ }
+
+ /**
+ * Explicitly tests that the following are both true:
+ * - XML entities are not double-escaped
+ * - XSS attack vectors are not introduced
+ *
+ * @dataProvider provideHandlePrintEscaping
+ */
+ public function testHandlePrintEscaping(string|DBField $value, bool $useGridFieldDataColumns, string $expected): void
+ {
+ $component = new GridFieldPrintButton();
+ $component->getPrintColumns();
+
+ $list = new ArrayList([new ArrayData(['Name' => $value])]);
+
+ $button = new GridFieldPrintButton();
+ $button->setPrintColumns(['Name' => 'My Name']);
+
+ // Get paginated gridfield config
+ $config = GridFieldConfig::create()
+ ->addComponent(new GridFieldPaginator(10))
+ ->addComponent($button);
+ if ($useGridFieldDataColumns) {
+ // If this component is present, GridFieldPrintButton uses it to get the value,
+ // and that includes some transformation of the value including escaping.
+ // So we need to check both with and without the component to ensure both scenarios
+ // present sane results.
+ $columns = new GridFieldDataColumns();
+ $columns->setDisplayFields(['Name' => 'My Name']);
+ $config->addComponent($columns);
+ }
+ $gridField = new GridField('testfield', 'testfield', $list, $config);
+ new Form(Controller::curr(), 'Form', new FieldList($gridField), new FieldList());
+
+ // Printed data should ignore pagination limit
+ $result = $button->handlePrint($gridField);
+
+ $parser = new CSSContentParser($result->__toString());
+ $cellContent = $parser->getBySelector('td');
+
+ $this->assertCount(1, $cellContent);
+ $this->assertSame(" | {$expected} | ", $cellContent[0]->asXML());
+ }
+
public function testGetPrintColumnsForGridFieldThrowsException()
{
$component = new GridFieldPrintButton();
diff --git a/tests/php/ORM/DataObjectTest.php b/tests/php/ORM/DataObjectTest.php
index bca67ffc708..7ed3106a0f0 100644
--- a/tests/php/ORM/DataObjectTest.php
+++ b/tests/php/ORM/DataObjectTest.php
@@ -2840,4 +2840,56 @@ public function testExceptionForUniqueIndexViolation(array $fieldsRecordOne, arr
$this->expectExceptionMessage($expectedMessage);
$record2->write();
}
+
+ public static function provideProvideI18nEntities(): array
+ {
+ return [
+ 'has-class-description' => [
+ 'classDescription' => 'A fluffy cloud',
+ 'expected' => true,
+ ],
+ 'no-class-description' => [
+ 'classDescription' => null,
+ 'expected' => false,
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideProvideI18nEntities
+ */
+ public function testProvideI18nEntities(?string $classDescription, bool $expected): void
+ {
+ $obj = new class extends DataObject {
+ public $classDescription;
+ public function singular_name()
+ {
+ return 'Cloud';
+ }
+ public function plural_name()
+ {
+ return 'Clouds';
+ }
+ public function classDescription()
+ {
+ return $this->classDescription;
+ }
+ };
+ $obj->classDescription = $classDescription;
+ $entities = $obj->provideI18nEntities();
+ // Fix up anonymous class keys
+ foreach ($entities as $key => $entity) {
+ unset($entities[$key]);
+ $newKey = preg_replace('#^.+?\.([A-Z_]+)$#', '$1', $key);
+ $entities[$newKey] = $entity;
+ }
+ $this->assertSame('Cloud', $entities['SINGULARNAME']);
+ $this->assertSame('Clouds', $entities['PLURALNAME']);
+ $this->assertSame(['one' => 'A Cloud', 'other' => '{count} Clouds'], $entities['PLURALS']);
+ if ($expected) {
+ $this->assertSame('A fluffy cloud', $entities['CLASS_DESCRIPTION']);
+ } else {
+ $this->assertFalse(array_key_exists('CLASS_DESCRIPTION', $entities));
+ }
+ }
}
diff --git a/tests/php/i18n/i18nTextCollectorTest.php b/tests/php/i18n/i18nTextCollectorTest.php
index c2f4a2a0640..6112e968967 100644
--- a/tests/php/i18n/i18nTextCollectorTest.php
+++ b/tests/php/i18n/i18nTextCollectorTest.php
@@ -1034,4 +1034,26 @@ public function testItCanUseVariableAsContext()
'TestEntity.REGULARCONTEXT' => "test {type}",
], $collectedTranslatables);
}
+
+ public function testDoesNotCollectInvalidKeys()
+ {
+ // From the code below this will previously collect `'.' => 'generic'`
+ // Code was added to i18nTextCollector::collectFromCode() to ignore keys
+ // that end with "."
+ $c = i18nTextCollector::create();
+ $mymodule = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule');
+ $php = <<<'PHP'
+ $data = [
+ $foo,
+ static::get_something($foo),
+ 'generic'
+ ];
+ _t(
+ __CLASS__ . '.' . ucfirst($foo) . 'Type',
+ $hello[$foo]
+ );
+ PHP;
+ $collectedTranslatables = $c->collectFromCode($php, null, $mymodule);
+ $this->assertEmpty($collectedTranslatables);
+ }
}