Merge remote-tracking branch 'origin/3'

Conflicts:
	core/Constants.php
	docs/en/05_Contributing/01_Code.md
	tests/model/SQLQueryTest.php
This commit is contained in:
Ingo Schommer 2015-04-09 16:26:16 +12:00
commit 72a284c9b8
168 changed files with 3438 additions and 1090 deletions

View File

@ -322,25 +322,28 @@ abstract class ModelAdmin extends LeftAndMain {
* @return Form
*/
public function ImportForm() {
$modelName = $this->modelClass;
$modelSNG = singleton($this->modelClass);
$modelName = $modelSNG->i18n_singular_name();
// check if a import form should be generated
if(!$this->showImportForm || (is_array($this->showImportForm) && !in_array($modelName,$this->showImportForm))) {
if(!$this->showImportForm ||
(is_array($this->showImportForm) && !in_array($this->modelClass, $this->showImportForm))
) {
return false;
}
$importers = $this->getModelImporters();
if(!$importers || !isset($importers[$modelName])) return false;
if(!$importers || !isset($importers[$this->modelClass])) return false;
if(!singleton($modelName)->canCreate(Member::currentUser())) return false;
if(!$modelSNG->canCreate(Member::currentUser())) return false;
$fields = new FieldList(
new HiddenField('ClassName', _t('ModelAdmin.CLASSTYPE'), $modelName),
new HiddenField('ClassName', _t('ModelAdmin.CLASSTYPE'), $this->modelClass),
new FileField('_CsvFile', false)
);
// get HTML specification for each import (column names etc.)
$importerClass = $importers[$modelName];
$importer = new $importerClass($modelName);
$importerClass = $importers[$this->modelClass];
$importer = new $importerClass($this->modelClass);
$spec = $importer->getImportSpec();
$specFields = new ArrayList();
foreach($spec['fields'] as $name => $desc) {

View File

@ -9,7 +9,7 @@
/** ----------------------------------------------- Application Logo (CMS Logo) Must be 24px x 24px ------------------------------------------------ */
.cms .ss-ui-button { background-color: #e6e6e6; }
.cms .ss-ui-button.ui-state-hover { background-color: #f3f3f3; }
.cms .ss-ui-button.ss-ui-action-constructive { background-color: #1F9433; }
.cms .ss-ui-button.ss-ui-action-constructive { background-color: #1f9433; }
.cms .ss-ui-button.ss-ui-action-constructive.ui-state-hover { background-color: #23a93a; }
.cms .ss-ui-button.ss-gridfield-button-filter { background: #55a4d2 url(../../images/icons/filter-icons.png) no-repeat -14px 4px; }
@ -20,7 +20,7 @@
.ss-gridfield-button-filter.ss-ui-button.hover-alike { background-color: #338DC1; background-position: -16px 6px; filter: none; }
.ss-gridfield-button-reset.ss-ui-button { background: #e6e6e6 url(../images/filter-icons.png) no-repeat 8px 5px; filter: none; }
.ss-gridfield-button-reset.ss-ui-button.filtered:hover { background: #f00 url(../images/filter-icons.png) no-repeat 8px -17px; filter: none; }
.ss-gridfield-button-reset.ss-ui-button.filtered:hover { background: red url(../images/filter-icons.png) no-repeat 8px -17px; filter: none; }
.ss-gridfield-button-reset.ss-ui-button.filtered:active { background: #e60000 url(../images/filter-icons.png) no-repeat 9px -16px; filter: none; }
.cms table.ss-gridfield-table tr td { border-right: 1px solid #9a9a9a; }
@ -54,77 +54,77 @@ fieldset.switch-states .switch input.state-name { margin-left: -20px; }
.cms-content-controls .preview-size-selector { display: none; }
/** Helper SCSS file for generating sprites for the interface. */
.btn-icon-sprite, .ui-state-default .btn-icon-accept, .ui-widget-content .btn-icon-accept, .ui-state-default .btn-icon-accept_disabled, .ui-widget-content .btn-icon-accept_disabled, .ui-state-default .btn-icon-add, .ui-widget-content .btn-icon-add, .ui-state-default .btn-icon-addMedia, .ui-widget-content .btn-icon-addMedia, .ui-state-default .btn-icon-add_disabled, .ui-widget-content .btn-icon-add_disabled, .ui-state-default .btn-icon-addpage, .ui-widget-content .btn-icon-addpage, .ui-state-default .btn-icon-addpage_disabled, .ui-widget-content .btn-icon-addpage_disabled, .ui-state-default .btn-icon-arrow-circle-135-left, .ui-widget-content .btn-icon-arrow-circle-135-left, .ui-state-default .btn-icon-arrow-circle-double, .ui-widget-content .btn-icon-arrow-circle-double, .ui-state-default .btn-icon-back, .ui-widget-content .btn-icon-back, .ui-state-default .btn-icon-back_disabled, .ui-widget-content .btn-icon-back_disabled, .ui-state-default .btn-icon-chain--arrow, .ui-widget-content .btn-icon-chain--arrow, .ui-state-default .btn-icon-chain--exclamation, .ui-widget-content .btn-icon-chain--exclamation, .ui-state-default .btn-icon-chain--minus, .ui-widget-content .btn-icon-chain--minus, .ui-state-default .btn-icon-chain--pencil, .ui-widget-content .btn-icon-chain--pencil, .ui-state-default .btn-icon-chain--plus, .ui-widget-content .btn-icon-chain--plus, .ui-state-default .btn-icon-chain-small, .ui-widget-content .btn-icon-chain-small, .ui-state-default .btn-icon-chain-unchain, .ui-widget-content .btn-icon-chain-unchain, .ui-state-default .btn-icon-chain, .ui-widget-content .btn-icon-chain, .ui-state-default .btn-icon-cross-circle, .ui-widget-content .btn-icon-cross-circle, .ui-state-default .btn-icon-cross-circle_disabled, .ui-widget-content .btn-icon-cross-circle_disabled, .ui-state-default .btn-icon-cross, .ui-widget-content .btn-icon-cross, .ui-state-default .btn-icon-decline, .ui-widget-content .btn-icon-decline, .ui-state-default .btn-icon-decline_disabled, .ui-widget-content .btn-icon-decline_disabled, .ui-state-default .btn-icon-delete, .ui-widget-content .btn-icon-delete, .ui-state-default .btn-icon-deleteLight, .ui-widget-content .btn-icon-deleteLight, .ui-state-default .btn-icon-disk, .ui-widget-content .btn-icon-disk, .ui-state-default .btn-icon-document--pencil, .ui-widget-content .btn-icon-document--pencil, .ui-state-default .btn-icon-download-csv, .ui-widget-content .btn-icon-download-csv, .ui-state-default .btn-icon-drive-upload, .ui-widget-content .btn-icon-drive-upload, .ui-state-default .btn-icon-drive-upload_disabled, .ui-widget-content .btn-icon-drive-upload_disabled, .ui-state-default .btn-icon-grid_print, .ui-widget-content .btn-icon-grid_print, .ui-state-default .btn-icon-magnifier, .ui-widget-content .btn-icon-magnifier, .ui-state-default .btn-icon-minus-circle, .ui-widget-content .btn-icon-minus-circle, .ui-state-default .btn-icon-minus-circle_disabled, .ui-widget-content .btn-icon-minus-circle_disabled, .ui-state-default .btn-icon-navigation, .ui-widget-content .btn-icon-navigation, .ui-state-default .btn-icon-navigation_disabled, .ui-widget-content .btn-icon-navigation_disabled, .ui-state-default .btn-icon-network-cloud, .ui-widget-content .btn-icon-network-cloud, .ui-state-default .btn-icon-network-cloud_disabled, .ui-widget-content .btn-icon-network-cloud_disabled, .ui-state-default .btn-icon-pencil, .ui-widget-content .btn-icon-pencil, .ui-state-default .btn-icon-pencil_disabled, .ui-widget-content .btn-icon-pencil_disabled, .ui-state-default .btn-icon-plug-disconnect-prohibition, .ui-widget-content .btn-icon-plug-disconnect-prohibition, .ui-state-default .btn-icon-plug-disconnect-prohibition_disabled, .ui-widget-content .btn-icon-plug-disconnect-prohibition_disabled, .ui-state-default .btn-icon-preview, .ui-widget-content .btn-icon-preview, .ui-state-default .btn-icon-preview_disabled, .ui-widget-content .btn-icon-preview_disabled, .ui-state-default .btn-icon-settings, .ui-widget-content .btn-icon-settings, .ui-state-default .btn-icon-settings_disabled, .ui-widget-content .btn-icon-settings_disabled, .ui-state-default .btn-icon-unpublish, .ui-widget-content .btn-icon-unpublish, .ui-state-default .btn-icon-unpublish_disabled, .ui-widget-content .btn-icon-unpublish_disabled { background-image: url('../images/btn-icon-sf963a8adf3.png'); background-repeat: no-repeat; }
.btn-icon-sprite, .ui-state-default .btn-icon-accept, .ui-widget-content .btn-icon-accept, .ui-state-default .btn-icon-accept_disabled, .ui-widget-content .btn-icon-accept_disabled, .ui-state-default .btn-icon-add, .ui-widget-content .btn-icon-add, .ui-state-default .btn-icon-addMedia, .ui-widget-content .btn-icon-addMedia, .ui-state-default .btn-icon-add_disabled, .ui-widget-content .btn-icon-add_disabled, .ui-state-default .btn-icon-addpage, .ui-widget-content .btn-icon-addpage, .ui-state-default .btn-icon-addpage_disabled, .ui-widget-content .btn-icon-addpage_disabled, .ui-state-default .btn-icon-arrow-circle-135-left, .ui-widget-content .btn-icon-arrow-circle-135-left, .ui-state-default .btn-icon-arrow-circle-double, .ui-widget-content .btn-icon-arrow-circle-double, .ui-state-default .btn-icon-back, .ui-widget-content .btn-icon-back, .ui-state-default .btn-icon-back_disabled, .ui-widget-content .btn-icon-back_disabled, .ui-state-default .btn-icon-chain--arrow, .ui-widget-content .btn-icon-chain--arrow, .ui-state-default .btn-icon-chain--exclamation, .ui-widget-content .btn-icon-chain--exclamation, .ui-state-default .btn-icon-chain--minus, .ui-widget-content .btn-icon-chain--minus, .ui-state-default .btn-icon-chain--pencil, .ui-widget-content .btn-icon-chain--pencil, .ui-state-default .btn-icon-chain--plus, .ui-widget-content .btn-icon-chain--plus, .ui-state-default .btn-icon-chain-small, .ui-widget-content .btn-icon-chain-small, .ui-state-default .btn-icon-chain-unchain, .ui-widget-content .btn-icon-chain-unchain, .ui-state-default .btn-icon-chain, .ui-widget-content .btn-icon-chain, .ui-state-default .btn-icon-cross-circle, .ui-widget-content .btn-icon-cross-circle, .ui-state-default .btn-icon-cross-circle_disabled, .ui-widget-content .btn-icon-cross-circle_disabled, .ui-state-default .btn-icon-cross, .ui-widget-content .btn-icon-cross, .ui-state-default .btn-icon-decline, .ui-widget-content .btn-icon-decline, .ui-state-default .btn-icon-decline_disabled, .ui-widget-content .btn-icon-decline_disabled, .ui-state-default .btn-icon-delete, .ui-widget-content .btn-icon-delete, .ui-state-default .btn-icon-deleteLight, .ui-widget-content .btn-icon-deleteLight, .ui-state-default .btn-icon-disk, .ui-widget-content .btn-icon-disk, .ui-state-default .btn-icon-document--pencil, .ui-widget-content .btn-icon-document--pencil, .ui-state-default .btn-icon-download-csv, .ui-widget-content .btn-icon-download-csv, .ui-state-default .btn-icon-drive-upload, .ui-widget-content .btn-icon-drive-upload, .ui-state-default .btn-icon-drive-upload_disabled, .ui-widget-content .btn-icon-drive-upload_disabled, .ui-state-default .btn-icon-grid_print, .ui-widget-content .btn-icon-grid_print, .ui-state-default .btn-icon-magnifier, .ui-widget-content .btn-icon-magnifier, .ui-state-default .btn-icon-minus-circle, .ui-widget-content .btn-icon-minus-circle, .ui-state-default .btn-icon-minus-circle_disabled, .ui-widget-content .btn-icon-minus-circle_disabled, .ui-state-default .btn-icon-navigation, .ui-widget-content .btn-icon-navigation, .ui-state-default .btn-icon-navigation_disabled, .ui-widget-content .btn-icon-navigation_disabled, .ui-state-default .btn-icon-network-cloud, .ui-widget-content .btn-icon-network-cloud, .ui-state-default .btn-icon-network-cloud_disabled, .ui-widget-content .btn-icon-network-cloud_disabled, .ui-state-default .btn-icon-pencil, .ui-widget-content .btn-icon-pencil, .ui-state-default .btn-icon-pencil_disabled, .ui-widget-content .btn-icon-pencil_disabled, .ui-state-default .btn-icon-plug-disconnect-prohibition, .ui-widget-content .btn-icon-plug-disconnect-prohibition, .ui-state-default .btn-icon-plug-disconnect-prohibition_disabled, .ui-widget-content .btn-icon-plug-disconnect-prohibition_disabled, .ui-state-default .btn-icon-preview, .ui-widget-content .btn-icon-preview, .ui-state-default .btn-icon-preview_disabled, .ui-widget-content .btn-icon-preview_disabled, .ui-state-default .btn-icon-settings, .ui-widget-content .btn-icon-settings, .ui-state-default .btn-icon-settings_disabled, .ui-widget-content .btn-icon-settings_disabled, .ui-state-default .btn-icon-unpublish, .ui-widget-content .btn-icon-unpublish, .ui-state-default .btn-icon-unpublish_disabled, .ui-widget-content .btn-icon-unpublish_disabled { background: url('../images/btn-icon-s5a3074ba2a.png') no-repeat; }
.ui-state-default .btn-icon-accept, .ui-widget-content .btn-icon-accept { background-position: 0 0; }
.ui-state-default .btn-icon-accept_disabled, .ui-widget-content .btn-icon-accept_disabled { background-position: 0 -16px; }
.ui-state-default .btn-icon-add, .ui-widget-content .btn-icon-add { background-position: 0 -32px; }
.ui-state-default .btn-icon-addMedia, .ui-widget-content .btn-icon-addMedia { background-position: 0 -48px; }
.ui-state-default .btn-icon-add_disabled, .ui-widget-content .btn-icon-add_disabled { background-position: 0 -68px; }
.ui-state-default .btn-icon-addpage, .ui-widget-content .btn-icon-addpage { background-position: 0 -84px; }
.ui-state-default .btn-icon-addpage_disabled, .ui-widget-content .btn-icon-addpage_disabled { background-position: 0 -100px; }
.ui-state-default .btn-icon-arrow-circle-135-left, .ui-widget-content .btn-icon-arrow-circle-135-left { background-position: 0 -116px; }
.ui-state-default .btn-icon-arrow-circle-double, .ui-widget-content .btn-icon-arrow-circle-double { background-position: 0 -132px; }
.ui-state-default .btn-icon-back, .ui-widget-content .btn-icon-back { background-position: 0 -148px; }
.ui-state-default .btn-icon-back_disabled, .ui-widget-content .btn-icon-back_disabled { background-position: 0 -164px; }
.ui-state-default .btn-icon-chain--arrow, .ui-widget-content .btn-icon-chain--arrow { background-position: 0 -180px; }
.ui-state-default .btn-icon-chain--exclamation, .ui-widget-content .btn-icon-chain--exclamation { background-position: 0 -196px; }
.ui-state-default .btn-icon-chain--minus, .ui-widget-content .btn-icon-chain--minus { background-position: 0 -212px; }
.ui-state-default .btn-icon-chain--pencil, .ui-widget-content .btn-icon-chain--pencil { background-position: 0 -228px; }
.ui-state-default .btn-icon-chain--plus, .ui-widget-content .btn-icon-chain--plus { background-position: 0 -244px; }
.ui-state-default .btn-icon-chain-small, .ui-widget-content .btn-icon-chain-small { background-position: 0 -260px; }
.ui-state-default .btn-icon-chain-unchain, .ui-widget-content .btn-icon-chain-unchain { background-position: 0 -276px; }
.ui-state-default .btn-icon-chain, .ui-widget-content .btn-icon-chain { background-position: 0 -292px; }
.ui-state-default .btn-icon-cross-circle, .ui-widget-content .btn-icon-cross-circle { background-position: 0 -308px; }
.ui-state-default .btn-icon-cross-circle_disabled, .ui-widget-content .btn-icon-cross-circle_disabled { background-position: 0 -324px; }
.ui-state-default .btn-icon-cross, .ui-widget-content .btn-icon-cross { background-position: 0 -340px; }
.ui-state-default .btn-icon-decline, .ui-widget-content .btn-icon-decline { background-position: 0 -355px; }
.ui-state-default .btn-icon-decline_disabled, .ui-widget-content .btn-icon-decline_disabled { background-position: 0 -371px; }
.ui-state-default .btn-icon-delete, .ui-widget-content .btn-icon-delete { background-position: 0 -387px; }
.ui-state-default .btn-icon-deleteLight, .ui-widget-content .btn-icon-deleteLight { background-position: 0 -403px; }
.ui-state-default .btn-icon-disk, .ui-widget-content .btn-icon-disk { background-position: 0 -420px; }
.ui-state-default .btn-icon-document--pencil, .ui-widget-content .btn-icon-document--pencil { background-position: 0 -436px; }
.ui-state-default .btn-icon-download-csv, .ui-widget-content .btn-icon-download-csv { background-position: 0 -452px; }
.ui-state-default .btn-icon-drive-upload, .ui-widget-content .btn-icon-drive-upload { background-position: 0 -468px; }
.ui-state-default .btn-icon-drive-upload_disabled, .ui-widget-content .btn-icon-drive-upload_disabled { background-position: 0 -484px; }
.ui-state-default .btn-icon-grid_print, .ui-widget-content .btn-icon-grid_print { background-position: 0 -500px; }
.ui-state-default .btn-icon-magnifier, .ui-widget-content .btn-icon-magnifier { background-position: 0 -516px; }
.ui-state-default .btn-icon-minus-circle, .ui-widget-content .btn-icon-minus-circle { background-position: 0 -532px; }
.ui-state-default .btn-icon-minus-circle_disabled, .ui-widget-content .btn-icon-minus-circle_disabled { background-position: 0 -548px; }
.ui-state-default .btn-icon-navigation, .ui-widget-content .btn-icon-navigation { background-position: 0 -564px; }
.ui-state-default .btn-icon-navigation_disabled, .ui-widget-content .btn-icon-navigation_disabled { background-position: 0 -580px; }
.ui-state-default .btn-icon-network-cloud, .ui-widget-content .btn-icon-network-cloud { background-position: 0 -596px; }
.ui-state-default .btn-icon-network-cloud_disabled, .ui-widget-content .btn-icon-network-cloud_disabled { background-position: 0 -612px; }
.ui-state-default .btn-icon-pencil, .ui-widget-content .btn-icon-pencil { background-position: 0 -628px; }
.ui-state-default .btn-icon-pencil_disabled, .ui-widget-content .btn-icon-pencil_disabled { background-position: 0 -644px; }
.ui-state-default .btn-icon-plug-disconnect-prohibition, .ui-widget-content .btn-icon-plug-disconnect-prohibition { background-position: 0 -660px; }
.ui-state-default .btn-icon-plug-disconnect-prohibition_disabled, .ui-widget-content .btn-icon-plug-disconnect-prohibition_disabled { background-position: 0 -676px; }
.ui-state-default .btn-icon-preview, .ui-widget-content .btn-icon-preview { background-position: 0 -692px; }
.ui-state-default .btn-icon-preview_disabled, .ui-widget-content .btn-icon-preview_disabled { background-position: 0 -708px; }
.ui-state-default .btn-icon-settings, .ui-widget-content .btn-icon-settings { background-position: 0 -724px; }
.ui-state-default .btn-icon-settings_disabled, .ui-widget-content .btn-icon-settings_disabled { background-position: 0 -740px; }
.ui-state-default .btn-icon-unpublish, .ui-widget-content .btn-icon-unpublish { background-position: 0 -756px; }
.ui-state-default .btn-icon-unpublish_disabled, .ui-widget-content .btn-icon-unpublish_disabled { background-position: 0 -772px; }
.ui-state-default .btn-icon-accept, .ui-widget-content .btn-icon-accept { background-position: 0 -96px; }
.ui-state-default .btn-icon-accept_disabled, .ui-widget-content .btn-icon-accept_disabled { background-position: 0 -80px; }
.ui-state-default .btn-icon-add, .ui-widget-content .btn-icon-add { background-position: 0 0; }
.ui-state-default .btn-icon-addMedia, .ui-widget-content .btn-icon-addMedia { background-position: 0 -208px; }
.ui-state-default .btn-icon-add_disabled, .ui-widget-content .btn-icon-add_disabled { background-position: 0 -32px; }
.ui-state-default .btn-icon-addpage, .ui-widget-content .btn-icon-addpage { background-position: 0 -144px; }
.ui-state-default .btn-icon-addpage_disabled, .ui-widget-content .btn-icon-addpage_disabled { background-position: 0 -500px; }
.ui-state-default .btn-icon-arrow-circle-135-left, .ui-widget-content .btn-icon-arrow-circle-135-left { background-position: 0 -356px; }
.ui-state-default .btn-icon-arrow-circle-double, .ui-widget-content .btn-icon-arrow-circle-double { background-position: 0 -340px; }
.ui-state-default .btn-icon-back, .ui-widget-content .btn-icon-back { background-position: 0 -372px; }
.ui-state-default .btn-icon-back_disabled, .ui-widget-content .btn-icon-back_disabled { background-position: 0 -16px; }
.ui-state-default .btn-icon-chain--arrow, .ui-widget-content .btn-icon-chain--arrow { background-position: 0 -724px; }
.ui-state-default .btn-icon-chain--exclamation, .ui-widget-content .btn-icon-chain--exclamation { background-position: 0 -516px; }
.ui-state-default .btn-icon-chain--minus, .ui-widget-content .btn-icon-chain--minus { background-position: 0 -740px; }
.ui-state-default .btn-icon-chain--pencil, .ui-widget-content .btn-icon-chain--pencil { background-position: 0 -676px; }
.ui-state-default .btn-icon-chain--plus, .ui-widget-content .btn-icon-chain--plus { background-position: 0 -708px; }
.ui-state-default .btn-icon-chain-small, .ui-widget-content .btn-icon-chain-small { background-position: 0 -772px; }
.ui-state-default .btn-icon-chain-unchain, .ui-widget-content .btn-icon-chain-unchain { background-position: 0 -484px; }
.ui-state-default .btn-icon-chain, .ui-widget-content .btn-icon-chain { background-position: 0 -756px; }
.ui-state-default .btn-icon-cross-circle, .ui-widget-content .btn-icon-cross-circle { background-position: 0 -452px; }
.ui-state-default .btn-icon-cross-circle_disabled, .ui-widget-content .btn-icon-cross-circle_disabled { background-position: 0 -564px; }
.ui-state-default .btn-icon-cross, .ui-widget-content .btn-icon-cross { background-position: 0 -276px; }
.ui-state-default .btn-icon-decline, .ui-widget-content .btn-icon-decline { background-position: 0 -128px; }
.ui-state-default .btn-icon-decline_disabled, .ui-widget-content .btn-icon-decline_disabled { background-position: 0 -192px; }
.ui-state-default .btn-icon-delete, .ui-widget-content .btn-icon-delete { background-position: 0 -468px; }
.ui-state-default .btn-icon-deleteLight, .ui-widget-content .btn-icon-deleteLight { background-position: 0 -307px; }
.ui-state-default .btn-icon-disk, .ui-widget-content .btn-icon-disk { background-position: 0 -291px; }
.ui-state-default .btn-icon-document--pencil, .ui-widget-content .btn-icon-document--pencil { background-position: 0 -548px; }
.ui-state-default .btn-icon-download-csv, .ui-widget-content .btn-icon-download-csv { background-position: 0 -48px; }
.ui-state-default .btn-icon-drive-upload, .ui-widget-content .btn-icon-drive-upload { background-position: 0 -420px; }
.ui-state-default .btn-icon-drive-upload_disabled, .ui-widget-content .btn-icon-drive-upload_disabled { background-position: 0 -580px; }
.ui-state-default .btn-icon-grid_print, .ui-widget-content .btn-icon-grid_print { background-position: 0 -260px; }
.ui-state-default .btn-icon-magnifier, .ui-widget-content .btn-icon-magnifier { background-position: 0 -532px; }
.ui-state-default .btn-icon-minus-circle, .ui-widget-content .btn-icon-minus-circle { background-position: 0 -628px; }
.ui-state-default .btn-icon-minus-circle_disabled, .ui-widget-content .btn-icon-minus-circle_disabled { background-position: 0 -644px; }
.ui-state-default .btn-icon-navigation, .ui-widget-content .btn-icon-navigation { background-position: 0 -388px; }
.ui-state-default .btn-icon-navigation_disabled, .ui-widget-content .btn-icon-navigation_disabled { background-position: 0 -436px; }
.ui-state-default .btn-icon-network-cloud, .ui-widget-content .btn-icon-network-cloud { background-position: 0 -612px; }
.ui-state-default .btn-icon-network-cloud_disabled, .ui-widget-content .btn-icon-network-cloud_disabled { background-position: 0 -692px; }
.ui-state-default .btn-icon-pencil, .ui-widget-content .btn-icon-pencil { background-position: 0 -228px; }
.ui-state-default .btn-icon-pencil_disabled, .ui-widget-content .btn-icon-pencil_disabled { background-position: 0 -596px; }
.ui-state-default .btn-icon-plug-disconnect-prohibition, .ui-widget-content .btn-icon-plug-disconnect-prohibition { background-position: 0 -244px; }
.ui-state-default .btn-icon-plug-disconnect-prohibition_disabled, .ui-widget-content .btn-icon-plug-disconnect-prohibition_disabled { background-position: 0 -660px; }
.ui-state-default .btn-icon-preview, .ui-widget-content .btn-icon-preview { background-position: 0 -64px; }
.ui-state-default .btn-icon-preview_disabled, .ui-widget-content .btn-icon-preview_disabled { background-position: 0 -160px; }
.ui-state-default .btn-icon-settings, .ui-widget-content .btn-icon-settings { background-position: 0 -324px; }
.ui-state-default .btn-icon-settings_disabled, .ui-widget-content .btn-icon-settings_disabled { background-position: 0 -404px; }
.ui-state-default .btn-icon-unpublish, .ui-widget-content .btn-icon-unpublish { background-position: 0 -112px; }
.ui-state-default .btn-icon-unpublish_disabled, .ui-widget-content .btn-icon-unpublish_disabled { background-position: 0 -176px; }
.icon { text-indent: -9999px; border: none; outline: none; }
.icon.icon-24 { width: 24px; height: 24px; background: url('../images/menu-icons/24x24-s0dc15c36f9.png'); }
.icon.icon-24.icon-assetadmin { background-position: 0 -216px; }
.icon.icon-24.icon-cmsmain { background-position: 0 -192px; }
.icon.icon-24.icon-cmspagescontroller { background-position: 0 -168px; }
.icon.icon-24.icon-cmssettingscontroller { background-position: 0 -96px; }
.icon.icon-24 { width: 24px; height: 24px; background: url('../images/menu-icons/24x24-s391afdd013.png'); }
.icon.icon-24.icon-assetadmin { background-position: 0 -120px; }
.icon.icon-24.icon-cmsmain { background-position: 0 -48px; }
.icon.icon-24.icon-cmspagescontroller { background-position: 0 -216px; }
.icon.icon-24.icon-cmssettingscontroller { background-position: 0 0; }
.icon.icon-24.icon-securityadmin { background-position: 0 -24px; }
.icon.icon-24.icon-reportadmin { background-position: 0 -240px; }
.icon.icon-24.icon-commentadmin { background-position: 0 0; }
.icon.icon-24.icon-help { background-position: 0 -144px; }
.icon.icon-16 { width: 16px; height: 16px; background: url('../images/menu-icons/16x16-s3f4c846209.png'); }
.icon.icon-16.icon-assetadmin { background-position: 0 -144px; }
.icon.icon-16.icon-cmsmain { background-position: 0 -128px; }
.icon.icon-24.icon-reportadmin { background-position: 0 -72px; }
.icon.icon-24.icon-commentadmin { background-position: 0 -192px; }
.icon.icon-24.icon-help { background-position: 0 -96px; }
.icon.icon-16 { width: 16px; height: 16px; background: url('../images/menu-icons/16x16-sf5b94bb49b.png'); }
.icon.icon-16.icon-assetadmin { background-position: 0 -80px; }
.icon.icon-16.icon-cmsmain { background-position: 0 -16px; }
.icon.icon-16.icon-cmspagescontroller { background-position: 0 -112px; }
.icon.icon-16.icon-cmssettingscontroller { background-position: 0 -64px; }
.icon.icon-16.icon-securityadmin { background-position: 0 -16px; }
.icon.icon-16.icon-reportadmin { background-position: 0 -160px; }
.icon.icon-16.icon-commentadmin { background-position: 0 0; }
.icon.icon-16.icon-help { background-position: 0 -96px; }
.icon.icon-16.icon-cmssettingscontroller { background-position: 0 0; }
.icon.icon-16.icon-securityadmin { background-position: 0 -48px; }
.icon.icon-16.icon-reportadmin { background-position: 0 -32px; }
.icon.icon-16.icon-commentadmin { background-position: 0 -144px; }
.icon.icon-16.icon-help { background-position: 0 -64px; }
html { overflow: hidden; }

View File

@ -9,7 +9,7 @@
/** ----------------------------------------------- Application Logo (CMS Logo) Must be 24px x 24px ------------------------------------------------ */
.cms .ss-ui-button { background-color: #e6e6e6; }
.cms .ss-ui-button.ui-state-hover { background-color: #f3f3f3; }
.cms .ss-ui-button.ss-ui-action-constructive { background-color: #1F9433; }
.cms .ss-ui-button.ss-ui-action-constructive { background-color: #1f9433; }
.cms .ss-ui-button.ss-ui-action-constructive.ui-state-hover { background-color: #23a93a; }
.cms .ss-ui-button.ss-gridfield-button-filter { background: #55a4d2 url(../../images/icons/filter-icons.png) no-repeat -14px 4px; }
@ -20,7 +20,7 @@
.ss-gridfield-button-filter.ss-ui-button.hover-alike { background-color: #338DC1; background-position: -16px 6px; filter: none; }
.ss-gridfield-button-reset.ss-ui-button { background: #e6e6e6 url(../images/filter-icons.png) no-repeat 8px 5px; filter: none; }
.ss-gridfield-button-reset.ss-ui-button.filtered:hover { background: #f00 url(../images/filter-icons.png) no-repeat 8px -17px; filter: none; }
.ss-gridfield-button-reset.ss-ui-button.filtered:hover { background: red url(../images/filter-icons.png) no-repeat 8px -17px; filter: none; }
.ss-gridfield-button-reset.ss-ui-button.filtered:active { background: #e60000 url(../images/filter-icons.png) no-repeat 9px -16px; filter: none; }
.cms table.ss-gridfield-table tr td { border-right: 1px solid #9a9a9a; }

View File

@ -127,10 +127,10 @@ body, html { font-size: 12px; line-height: 16px; font-family: Arial, sans-serif;
.ui-widget-header { background-color: #b0bec7; padding: 8px 8px 6px 8px; border-bottom: 2px solid #8399a7; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2RkZTNlNyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzkyYTViMiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #dde3e7), color-stop(100%, #92a5b2)); background-image: -moz-linear-gradient(#dde3e7, #92a5b2); background-image: -webkit-linear-gradient(#dde3e7, #92a5b2); background-image: linear-gradient(#dde3e7, #92a5b2); border-bottom: 3px solid #5c7382; padding: 8px; -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; }
.ui-widget-header .ui-dialog-title { padding: 6px 10px; text-shadow: #ced7dc 1px 1px 0; }
.ui-widget-header a.ui-dialog-titlebar-close { position: absolute; top: -8px; right: -15px; width: 30px; height: 30px; z-index: 100000; }
.ui-widget-header a.ui-dialog-titlebar-close { position: absolute; top: -5px; right: -13px; width: 30px; height: 30px; z-index: 100000; }
.ui-widget-header a.ui-state-hover { border-color: transparent; background: transparent; }
.ui-widget-header a.ui-state-hover .ui-icon-closethick { background: url('../images/sprites-32x32-s871d283813.png') 0 -356px no-repeat; }
.ui-widget-header .ui-icon-closethick { background: url('../images/sprites-32x32-s871d283813.png') 0 -396px no-repeat; width: 30px; height: 30px; }
.ui-widget-header a.ui-state-hover .ui-icon-closethick { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -356px no-repeat; }
.ui-widget-header .ui-icon-closethick { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -396px no-repeat; width: 30px; height: 30px; }
.ui-state-hover { cursor: pointer; }
@ -634,10 +634,10 @@ form.member-profile-form #Permissions .optionset li { float: none; width: auto;
.cms .ui-dialog .ss-ui-dialog.ui-dialog-content { padding-top: 0px; }
.ui-dialog { background: url("../images/textures/bg_cms_main_content.png") repeat left top #F0F3F4; border: 3px solid #000 !important; border-radius: 8px; overflow: visible; padding: 0; }
.ui-dialog { background: url("../images/textures/bg_cms_main_content.png") repeat left top #F0F3F4; background-clip: content-box; border: 1px solid #666 !important; -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; overflow: visible; padding: 0; -moz-box-shadow: 0px 0px 30px 10px rgba(0, 0, 0, 0.3); -webkit-box-shadow: 0px 0px 30px 10px rgba(0, 0, 0, 0.3); box-shadow: 0px 0px 30px 10px rgba(0, 0, 0, 0.3); }
.ui-dialog .ui-dialog-titlebar.ui-widget-header { font-size: 14px; padding: 0; border: none; background-color: transparent; background-image: url(../images/textures/cms_content_header.png); background-repeat: repeat; -moz-box-shadow: rgba(107, 120, 123, 0.5) 0 0 4px inset; -webkit-box-shadow: rgba(107, 120, 123, 0.5) 0 0 4px inset; box-shadow: rgba(107, 120, 123, 0.5) 0 0 4px inset; }
.ui-dialog .ui-dialog-titlebar.ui-widget-header .ui-dialog-title { position: absolute; }
.ui-dialog .ui-dialog-content { overflow: auto; }
.ui-dialog .ui-dialog-content { -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; overflow: auto; }
.ui-dialog .ui-dialog-content.loading { background-image: url(../images/spinner.gif); background-position: 50% 50%; background-repeat: no-repeat; }
.ui-dialog .cms-dialog-content { background: url("../images/textures/bg_cms_main_content.png") repeat left top #F0F3F4; padding-bottom: 8px; padding-top: 0px; }
.ui-dialog .cms-dialog-content .Actions { overflow: auto; margin: 8px 0; padding-bottom: 8px; float: right; }
@ -663,7 +663,7 @@ body.cms-dialog { overflow: auto; background: url("../images/textures/bg_cms_mai
.htmleditorfield-dialog .htmleditorfield-from-web button.add-url.ui-state-disabled, .htmleditorfield-dialog .htmleditorfield-from-web button.add-url.ui-state-disabled:hover, .htmleditorfield-dialog .htmleditorfield-from-web button.add-url.ui-state-disabled:active { opacity: 0.35; filter: Alpha(Opacity=35); }
.htmleditorfield-dialog .htmleditorfield-from-web .loading button.add-url .ui-icon { background-image: url(../images/throbber.gif); background-position: 50% 50%; background-repeat: no-repeat; }
.htmleditorfield-dialog .cms-content-header { padding: 0; width: 100%; height: 40px; }
.htmleditorfield-dialog .cms-content-header h3 { padding: 0 8px; margin: 10px; }
.htmleditorfield-dialog .cms-content-header h3 { padding: 3px 8px; margin: 10px; }
.htmleditorfield-dialog .ss-insert-media, .htmleditorfield-dialog .Actions, .htmleditorfield-dialog .ss-insert-link { padding: 8px 16px; }
.htmleditorfield-dialog .ss-insert-media .ui-tabs-panel, .htmleditorfield-dialog .Actions .ui-tabs-panel, .htmleditorfield-dialog .ss-insert-link .ui-tabs-panel { padding: 0; }
.htmleditorfield-dialog .details .file-url { display: block; width: 300px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; }
@ -692,7 +692,7 @@ body.cms-dialog { overflow: auto; background: url("../images/textures/bg_cms_mai
/** -------------------------------------------- Step labels -------------------------------------------- */
.step-label > * { display: inline-block; vertical-align: top; }
.step-label .flyout { height: 18px; font-size: 14px; font-weight: bold; -moz-border-radius-topleft: 3px; -webkit-border-top-left-radius: 3px; border-top-left-radius: 3px; -moz-border-radius-bottomleft: 3px; -webkit-border-bottom-left-radius: 3px; border-bottom-left-radius: 3px; background-color: #667980; padding: 4px 3px 4px 6px; text-align: center; text-shadow: none; color: #fff; }
.step-label .arrow { height: 26px; width: 10px; background: url('../images/sprites-32x32-s871d283813.png') 0 -862px no-repeat; margin-right: 4px; }
.step-label .arrow { height: 26px; width: 10px; background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -862px no-repeat; margin-right: 4px; }
.step-label .title { height: 18px; padding: 4px; }
/** -------------------------------------------- Item Edit Form -------------------------------------------- */
@ -739,10 +739,10 @@ form.import-form label.left { width: 250px; }
/** -------------------------------------------- Buttons for FileUpload -------------------------------------------- */
.ss-uploadfield-item-edit-all .ui-button-text { padding-right: 0; }
.toggle-details-icon { background: url('../images/sprites-32x32-s871d283813.png') 0 -830px no-repeat; }
.ss-uploadfield-item-edit-all .toggle-details-icon { background: url('../images/sprites-32x32-s871d283813.png') 0 -798px no-repeat; display: inline-block; width: 8px; height: 8px; padding-left: 5px; }
.toggle-details-icon.opened { background: url('../images/sprites-32x32-s871d283813.png') 0 -846px no-repeat; }
.ss-uploadfield-item-edit-all .toggle-details-icon.opened { background: url('../images/sprites-32x32-s871d283813.png') 0 -814px no-repeat; }
.toggle-details-icon { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -830px no-repeat; }
.ss-uploadfield-item-edit-all .toggle-details-icon { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -798px no-repeat; display: inline-block; width: 8px; height: 8px; padding-left: 5px; }
.toggle-details-icon.opened { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -846px no-repeat; }
.ss-uploadfield-item-edit-all .toggle-details-icon.opened { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -814px no-repeat; }
/** -------------------------------------------- Hide preview toggle link by default. May be shown in IE7 stylesheet and forced to show with js if needed -------------------------------------------- */
.cms .Actions > .cms-preview-toggle-link, .cms .cms-navigator > .cms-preview-toggle-link { display: none; }
@ -759,7 +759,7 @@ form.import-form label.left { width: 250px; }
.cms .jstree li > .jstree-icon, .TreeDropdownField .treedropdownfield-panel .jstree li > .jstree-icon { cursor: pointer; }
.cms .jstree ins, .TreeDropdownField .treedropdownfield-panel .jstree ins { display: inline-block; text-decoration: none; width: 18px; height: 18px; margin: 0 0 0 0; padding: 0; float: left; }
.cms .jstree a, .TreeDropdownField .treedropdownfield-panel .jstree a { display: inline-block; line-height: 16px; height: 16px; color: black; white-space: nowrap; text-decoration: none; padding: 1px 2px; margin: 0; border: 1px solid #fff; }
.cms .jstree a:focus, .cms .jstree a:active, .cms .jstree a:hover, .TreeDropdownField .treedropdownfield-panel .jstree a:focus, .TreeDropdownField .treedropdownfield-panel .jstree a:active, .TreeDropdownField .treedropdownfield-panel .jstree a:hover { text-decoration: none; cursor: pointer; text-shadow: none; }
.cms .jstree a:focus, .cms .jstree a:active, .cms .jstree a:hover, .TreeDropdownField .treedropdownfield-panel .jstree a:focus, .TreeDropdownField .treedropdownfield-panel .jstree a:active, .TreeDropdownField .treedropdownfield-panel .jstree a:hover { text-decoration: none; cursor: pointer; text-shadow: 1px 1px 1px white; }
.cms .jstree a > ins, .TreeDropdownField .treedropdownfield-panel .jstree a > ins { height: 16px; width: 16px; }
.cms .jstree a > ins.jstree-checkbox, .TreeDropdownField .treedropdownfield-panel .jstree a > ins.jstree-checkbox { height: 19px; }
.cms .jstree a > .jstree-icon, .TreeDropdownField .treedropdownfield-panel .jstree a > .jstree-icon { margin-right: 3px; }
@ -772,8 +772,8 @@ form.import-form label.left { width: 250px; }
.cms .jstree .jstree-wholerow a, .cms .jstree .jstree-wholerow a:hover, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow a, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow a:hover { margin: 0 !important; padding: 0 !important; text-indent: -9999px !important; width: 100%; border-right-width: 0px !important; border-left-width: 0px !important; }
.cms .jstree .jstree-wholerow ins, .cms .jstree .jstree-wholerow span, .cms .jstree .jstree-wholerow input, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow ins, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow span, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow input { display: none !important; }
.cms .jstree .jstree-wholerow-span, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow-span { position: absolute; left: 0; margin: 0px; padding: 0; height: 18px; border-width: 0; padding: 0; z-index: 0; }
.cms .jstree-apple.jstree-focused, .TreeDropdownField .treedropdownfield-panel .jstree-apple.jstree-focused { background: none; }
.cms .jstree-apple.jstree-focused .jstree-apple > ul, .TreeDropdownField .treedropdownfield-panel .jstree-apple.jstree-focused .jstree-apple > ul { background: none; }
.cms .jstree.jstree-focused, .TreeDropdownField .treedropdownfield-panel .jstree.jstree-focused { background: none; }
.cms .jstree.jstree-focused .jstree > ul, .TreeDropdownField .treedropdownfield-panel .jstree.jstree-focused .jstree > ul { background: none; }
.cms a > .jstree-icon, .TreeDropdownField .treedropdownfield-panel a > .jstree-icon { display: none; }
.cms .draggable a > .jstree-icon, .TreeDropdownField .treedropdownfield-panel .draggable a > .jstree-icon { display: block; }
.cms li.jstree-open > ul, .TreeDropdownField .treedropdownfield-panel li.jstree-open > ul { display: block; margin-left: -13px; }
@ -814,38 +814,74 @@ form.import-form label.left { width: 250px; }
.cms #vakata-dragged .jstree-ok, .TreeDropdownField .treedropdownfield-panel #vakata-dragged .jstree-ok { background: green; }
.cms #vakata-dragged .jstree-invalid, .TreeDropdownField .treedropdownfield-panel #vakata-dragged .jstree-invalid { background: red; }
.jstree-apple li, .jstree-apple .jstree-apple ins { background: none; }
.jstree-apple .jstree-unchecked > a > .jstree-checkbox, .jstree-apple .jstree-checked > a > .jstree-checkbox, .jstree-apple .jstree-undetermined > a > .jstree-checkbox { margin-right: 3px; }
.jstree li, .jstree .jstree ins { background: none; }
.jstree .jstree-unchecked > a > .jstree-checkbox, .jstree .jstree-checked > a > .jstree-checkbox, .jstree .jstree-undetermined > a > .jstree-checkbox { margin-right: 3px; }
.tree-holder.jstree-apple, .cms-tree.jstree-apple { /* comment speech bubble - ccs3 only - source: http://nicolasgallagher.com/pure-css-speech-bubbles/demo/ */ }
.tree-holder.jstree-apple li, .cms-tree.jstree-apple li { padding: 0px; clear: left; }
.tree-holder.jstree-apple li.Root strong, .cms-tree.jstree-apple li.Root strong { font-weight: bold; padding-left: 1px; }
.tree-holder.jstree-apple li.Root > a .jstree-icon, .cms-tree.jstree-apple li.Root > a .jstree-icon { background-position: -56px -36px; }
.tree-holder.jstree-apple li.status-deletedonlive .text, .cms-tree.jstree-apple li.status-deletedonlive .text { text-decoration: line-through; }
.tree-holder.jstree-apple li.jstree-checked > a, .tree-holder.jstree-apple li.jstree-checked > a:link, .cms-tree.jstree-apple li.jstree-checked > a, .cms-tree.jstree-apple li.jstree-checked > a:link { background-color: #efe999; }
.tree-holder.jstree-apple li.disabled > a > .jstree-checkbox, .cms-tree.jstree-apple li.disabled > a > .jstree-checkbox { background-position: -57px -54px; }
.tree-holder.jstree-apple li.readonly, .cms-tree.jstree-apple li.readonly { color: #aaa; padding-left: 18px; }
.tree-holder.jstree-apple li.readonly a, .tree-holder.jstree-apple li.readonly a:link, .cms-tree.jstree-apple li.readonly a, .cms-tree.jstree-apple li.readonly a:link { margin: 0; padding: 0; }
.tree-holder.jstree-apple li.readonly .jstree-icon, .cms-tree.jstree-apple li.readonly .jstree-icon { display: none; }
.tree-holder.jstree-apple a, .tree-holder.jstree-apple a:link, .cms-tree.jstree-apple a, .cms-tree.jstree-apple a:link { color: #0073C1; padding: 3px 6px 3px 3px; border: none; display: inline-block; margin-right: 5px; }
.tree-holder.jstree-apple ins, .cms-tree.jstree-apple ins { background-color: transparent; background-image: url(../images/sitetree_ss_default_icons.png); }
.tree-holder.jstree-apple span.badge, .cms-tree.jstree-apple span.badge { clear: both; text-transform: uppercase; display: inline-block; padding: 0px 3px; font-size: 0.75em; line-height: 1em; margin-left: 3px; margin-right: 6px; margin-top: -1px; -moz-border-radius: 2px / 2px; -webkit-border-radius: 2px 2px; border-radius: 2px / 2px; }
.tree-holder.jstree-apple span.badge.status-modified, .tree-holder.jstree-apple span.badge.status-addedtodraft, .cms-tree.jstree-apple span.badge.status-modified, .cms-tree.jstree-apple span.badge.status-addedtodraft { color: #7E7470; border: 1px solid #C9B800; background-color: #FFF0BC; }
.tree-holder.jstree-apple span.badge.status-deletedonlive, .tree-holder.jstree-apple span.badge.status-removedfromdraft, .cms-tree.jstree-apple span.badge.status-deletedonlive, .cms-tree.jstree-apple span.badge.status-removedfromdraft { color: #636363; border: 1px solid #E49393; background-color: #F2DADB; }
.tree-holder.jstree-apple span.badge.status-workflow-approval, .cms-tree.jstree-apple span.badge.status-workflow-approval { color: #56660C; border: 1px solid #7C8816; background-color: #DAE79A; }
.tree-holder.jstree-apple span.comment-count, .cms-tree.jstree-apple span.comment-count { clear: both; position: relative; text-transform: uppercase; display: inline-block; overflow: visible; padding: 0px 3px; font-size: 0.75em; line-height: 1em; margin-left: 3px; margin-right: 6px; -moz-border-radius: 2px / 2px; -webkit-border-radius: 2px 2px; border-radius: 2px / 2px; color: #7E7470; border: 1px solid #C9B800; background-color: #FFF0BC; }
.tree-holder.jstree-apple span.comment-count:before, .cms-tree.jstree-apple span.comment-count:before { content: ""; position: absolute; border-style: solid; display: block; width: 0; bottom: -4px; /* value = - border-top-width - border-bottom-width */ left: 3px; /* controls horizontal position */ border-width: 4px 4px 0; border-color: #C9B800 transparent; }
.tree-holder.jstree-apple span.comment-count:after, .cms-tree.jstree-apple span.comment-count:after { content: ""; position: absolute; border-style: solid; /* reduce the damage in FF3.0 */ display: block; width: 0; bottom: -3px; /* value = - border-top-width - border-bottom-width */ left: 4px; /* value = (:before left) + (:before border-left) - (:after border-left) */ border-width: 3px 3px 0; border-color: #FFF0BC transparent; }
.tree-holder.jstree-apple .jstree-hovered, .cms-tree.jstree-apple .jstree-hovered { text-shadow: none; text-decoration: none; }
.tree-holder.jstree-apple .jstree-closed > ins, .cms-tree.jstree-apple .jstree-closed > ins { background-position: 0 0; }
.tree-holder.jstree-apple .jstree-open > ins, .cms-tree.jstree-apple .jstree-open > ins { background-position: -20px 0; }
.tree-holder.jstree, .cms-tree.jstree { /* comment speech bubble - ccs3 only - source: http://nicolasgallagher.com/pure-css-speech-bubbles/demo/ */ }
.tree-holder.jstree li, .cms-tree.jstree li { padding: 0px; clear: left; }
.tree-holder.jstree li.Root strong, .cms-tree.jstree li.Root strong { font-weight: bold; padding-left: 1px; }
.tree-holder.jstree li.Root > a .jstree-icon, .cms-tree.jstree li.Root > a .jstree-icon { background-position: -56px -36px; }
.tree-holder.jstree li.status-deletedonlive .text, .cms-tree.jstree li.status-deletedonlive .text { text-decoration: line-through; }
.tree-holder.jstree li.jstree-checked > a, .tree-holder.jstree li.jstree-checked > a:link, .cms-tree.jstree li.jstree-checked > a, .cms-tree.jstree li.jstree-checked > a:link { background-color: #efe999; }
.tree-holder.jstree li.disabled > a > .jstree-checkbox, .cms-tree.jstree li.disabled > a > .jstree-checkbox { background-position: -57px -54px; }
.tree-holder.jstree li.readonly, .cms-tree.jstree li.readonly { color: #aaa; padding-left: 18px; }
.tree-holder.jstree li.readonly a, .tree-holder.jstree li.readonly a:link, .cms-tree.jstree li.readonly a, .cms-tree.jstree li.readonly a:link { margin: 0; padding: 0; }
.tree-holder.jstree li.readonly .jstree-icon, .cms-tree.jstree li.readonly .jstree-icon { display: none; }
.tree-holder.jstree a, .tree-holder.jstree a:link, .cms-tree.jstree a, .cms-tree.jstree a:link { color: #0073C1; padding: 3px 6px 3px 3px; border: none; display: inline-block; margin-right: 5px; }
.tree-holder.jstree ins, .cms-tree.jstree ins { background-color: transparent; background-image: url(../images/sitetree_ss_default_icons.png); }
.tree-holder.jstree span.badge, .cms-tree.jstree span.badge { clear: both; text-transform: uppercase; text-shadow: none; display: inline-block; position: relative; padding: 2px 3px 1px; font-size: 0.75em; line-height: 1em; margin-left: 3px; margin-top: -1px; -moz-border-radius: 2px / 2px; -webkit-border-radius: 2px 2px; border-radius: 2px / 2px; }
.tree-holder.jstree span.comment-count, .cms-tree.jstree span.comment-count { clear: both; position: relative; text-transform: uppercase; display: inline-block; overflow: visible; padding: 0px 3px; font-size: 0.75em; line-height: 1em; margin-left: 3px; margin-right: 6px; -moz-border-radius: 2px / 2px; -webkit-border-radius: 2px 2px; border-radius: 2px / 2px; color: #7E7470; border: 1px solid #C9B800; background-color: #FFF0BC; }
.tree-holder.jstree span.comment-count:before, .cms-tree.jstree span.comment-count:before { content: ""; position: absolute; border-style: solid; display: block; width: 0; bottom: -4px; /* value = - border-top-width - border-bottom-width */ left: 3px; /* controls horizontal position */ border-width: 4px 4px 0; border-color: #C9B800 transparent; }
.tree-holder.jstree span.comment-count:after, .cms-tree.jstree span.comment-count:after { content: ""; position: absolute; border-style: solid; /* reduce the damage in FF3.0 */ display: block; width: 0; bottom: -3px; /* value = - border-top-width - border-bottom-width */ left: 4px; /* value = (:before left) + (:before border-left) - (:after border-left) */ border-width: 3px 3px 0; border-color: #FFF0BC transparent; }
.tree-holder.jstree .jstree-hovered, .cms-tree.jstree .jstree-hovered { text-shadow: none; text-decoration: none; }
.tree-holder.jstree .jstree-closed > ins, .cms-tree.jstree .jstree-closed > ins { background-position: 0 0; }
.tree-holder.jstree .jstree-open > ins, .cms-tree.jstree .jstree-open > ins { background-position: -20px 0; }
a .jstree-pageicon { float: left; margin-right: 4px; }
/* ensure status is visible in sidebar */
#cms-content-tools-CMSMain .cms-tree.jstree li { min-width: 159px; }
#cms-content-tools-CMSMain .cms-tree.jstree a { overflow: hidden; display: block; position: relative; }
#cms-content-tools-CMSMain .cms-tree.jstree span.badge { position: absolute; top: 0; right: 0; padding: 7px 9px 6px 5px; margin: 0; max-width: 40%; -moz-transition: max-width 0.75s linear; -o-transition: max-width 0.75s linear; -webkit-transition: max-width 0.75s linear; transition: max-width 0.75s linear; }
#cms-content-tools-CMSMain .cms-tree.jstree span.badge:hover { max-width: 150px; }
a .jstree-pageicon { float: left; margin-right: 4px; position: relative; }
li.class-HomePage > a .jstree-pageicon { background-position: 0 -48px; }
li.class-RedirectorPage > a .jstree-pageicon { background-position: 0 -16px; }
li.class-VirtualPage > a .jstree-pageicon { background-position: 0 -32px; }
li.class-ErrorPage > a .jstree-pageicon { background-position: 0 -112px; }
/* tree status icons and labels */
.cms-tree.jstree .status-modified > a .jstree-pageicon:before, .cms-tree.jstree .status-addedtodraft > a .jstree-pageicon:before, .cms-tree.jstree .status-deletedonlive > a .jstree-pageicon:before, .cms-tree.jstree .status-removedfromdraft > a .jstree-pageicon:before, .cms-tree.jstree .status-workflow-approval > a .jstree-pageicon:before { content: ""; display: block; width: 5px; height: 5px; position: absolute; bottom: 0; right: 0; background: #fce2d0; border: 1px solid #ff9344; border-radius: 100px; box-shadow: 0px 0px 0px 1px #fff; }
.jstree .status-modified > .jstree-hovered, .jstree .status-modified > .jstree-clicked, .cms-tree.jstree span.badge.status-modified, .cms-tree.jstree .status-modified > a .jstree-pageicon:before { background-color: #fce2d0; border-color: #ff7714; }
#cms-content-tools-CMSMain .cms-tree.jstree span.badge.status-modified { box-shadow: 0px 0px 6px 2px #fce2d0; }
.cms-tree.jstree span.badge.status-modified { color: #ff7714; }
.jstree .status-addedtodraft > .jstree-hovered, .jstree .status-addedtodraft > .jstree-clicked, .cms-tree.jstree span.badge.status-addedtodraft, .cms-tree.jstree .status-addedtodraft > a .jstree-pageicon:before { background-color: #f8f4d0; border-color: #e29a00; }
#cms-content-tools-CMSMain .cms-tree.jstree span.badge.status-addedtodraft { box-shadow: 0px 0px 6px 2px #f8f4d0; }
.cms-tree.jstree span.badge.status-addedtodraft { color: #e29a00; }
.jstree .status-deletedonlive > .jstree-hovered, .jstree .status-deletedonlive > .jstree-clicked, .cms-tree.jstree span.badge.status-deletedonlive, .cms-tree.jstree .status-deletedonlive > a .jstree-pageicon:before { background-color: #f9d6dd; border-color: #f0524f; }
#cms-content-tools-CMSMain .cms-tree.jstree span.badge.status-deletedonlive { box-shadow: 0px 0px 6px 2px #f9d6dd; }
.cms-tree.jstree span.badge.status-deletedonlive { color: #f0524f; }
.jstree .status-removedfromdraft > .jstree-hovered, .jstree .status-removedfromdraft > .jstree-clicked, .cms-tree.jstree span.badge.status-removedfromdraft, .cms-tree.jstree .status-removedfromdraft > a .jstree-pageicon:before { background-color: #f9d6dd; border-color: #f0524f; }
#cms-content-tools-CMSMain .cms-tree.jstree span.badge.status-removedfromdraft { box-shadow: 0px 0px 6px 2px #f9d6dd; }
.cms-tree.jstree span.badge.status-removedfromdraft { color: #f0524f; }
.jstree .status-workflow-approval > .jstree-hovered, .jstree .status-workflow-approval > .jstree-clicked, .cms-tree.jstree span.badge.status-workflow-approval, .cms-tree.jstree .status-workflow-approval > a .jstree-pageicon:before { background-color: #d3f2ff; border-color: #0097d7; }
#cms-content-tools-CMSMain .cms-tree.jstree span.badge.status-workflow-approval { box-shadow: 0px 0px 6px 2px #d3f2ff; }
.cms-tree.jstree span.badge.status-workflow-approval { color: #0097d7; }
.cms-tree { visibility: hidden; }
.cms-tree.multiple li > a > .jstree-icon { display: none; }
.cms-tree.multiple li > a > .jstree-icon.jstree-checkbox { display: inline-block; }
@ -866,7 +902,7 @@ li.class-ErrorPage > a .jstree-pageicon { background-position: 0 -112px; }
.cms-logo span { font-weight: bold; font-size: 12px; line-height: 16px; padding: 2px 0; margin-left: 30px; }
.cms-login-status { border-top: 1px solid #19435c; padding: 8px 0 9.6px; line-height: 16px; font-size: 11px; }
.cms-login-status .logout-link { display: inline-block; height: 16px; width: 16px; float: left; margin: 0 8px 0 5px; background: url('../images/sprites-32x32-s871d283813.png') 0 -772px no-repeat; text-indent: -9999em; opacity: 0.9; }
.cms-login-status .logout-link { display: inline-block; height: 16px; width: 16px; float: left; margin: 0 8px 0 5px; background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -772px no-repeat; text-indent: -9999em; opacity: 0.9; }
.cms-login-status .logout-link:hover, .cms-login-status .logout-link:focus { opacity: 1; }
.cms-menu { z-index: 80; background: #b0bec7; width: 160px; -moz-box-shadow: rgba(0, 0, 0, 0.9) 0 0 3px; -webkit-box-shadow: rgba(0, 0, 0, 0.9) 0 0 3px; box-shadow: rgba(0, 0, 0, 0.9) 0 0 3px; }
@ -891,12 +927,12 @@ li.class-ErrorPage > a .jstree-pageicon { background-position: 0 -112px; }
.cms-menu-list li a .icon { display: block; position: absolute; top: 50%; margin-left: 4px; margin-top: -8px; filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); opacity: 0.7; }
.cms-menu-list li a .text { display: block; margin-left: 30px; }
.cms-menu-list li a .toggle-children { display: inline-block; float: right; width: 20px; height: 100%; cursor: pointer; }
.cms-menu-list li a .toggle-children .toggle-children-icon { display: inline-block; width: 8px; height: 8px; background: url('../images/sprites-32x32-s871d283813.png') 0 -798px no-repeat; vertical-align: middle; }
.cms-menu-list li a .toggle-children.opened .toggle-children-icon { background: url('../images/sprites-32x32-s871d283813.png') 0 -814px no-repeat; }
.cms-menu-list li a .toggle-children .toggle-children-icon { display: inline-block; width: 8px; height: 8px; background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -798px no-repeat; vertical-align: middle; }
.cms-menu-list li a .toggle-children.opened .toggle-children-icon { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -814px no-repeat; }
.cms-menu-list li ul li a { border-top: 1px solid #b6c3cb; }
.cms-menu-list li.current a { color: white; text-shadow: #1e5270 0 -1px 0; border-top: 1px solid #55a4d2; border-bottom: 1px solid #236184; background-color: #338DC1; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzMzOGRjMSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzI4NzA5OSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #338dc1), color-stop(100%, #287099)); background-image: -moz-linear-gradient(#338dc1, #287099); background-image: -webkit-linear-gradient(#338dc1, #287099); background-image: linear-gradient(#338dc1, #287099); }
.cms-menu-list li.current a .toggle-children .toggle-children-icon { background: url('../images/sprites-32x32-s871d283813.png') 0 -830px no-repeat; }
.cms-menu-list li.current a .toggle-children.opened .toggle-children-icon { background: url('../images/sprites-32x32-s871d283813.png') 0 -846px no-repeat; }
.cms-menu-list li.current a .toggle-children .toggle-children-icon { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -830px no-repeat; }
.cms-menu-list li.current a .toggle-children.opened .toggle-children-icon { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -846px no-repeat; }
.cms-menu-list li.current ul { border-top: none; display: block; }
.cms-menu-list li.current li { background-color: #287099; }
.cms-menu-list li.current li a { font-size: 11px; padding: 0 10px 0 40px; height: 32px; line-height: 32px; color: #e2f0f7; background: none; border-top: 1px solid #2f81b1; border-bottom: 1px solid #1e5270; }
@ -919,14 +955,14 @@ li.class-ErrorPage > a .jstree-pageicon { background-position: 0 -112px; }
.cms-content-controls.cms-preview-controls { z-index: 1; background: #eceff1; height: 30px; /* should be set in js Layout to match page actions */ padding: 12px 12px; }
.cms-content-controls .icon-view, .cms-content-controls .preview-selector.dropdown a.chzn-single { white-space: nowrap; }
.cms-content-controls .icon-view:before, .cms-content-controls .preview-selector.dropdown a.chzn-single:before { display: inline-block; float: left; content: ''; width: 23px; height: 17px; overflow: hidden; }
.cms-content-controls .icon-auto:before { background: url('../images/sprites-32x32-s871d283813.png') 0 -898px no-repeat; }
.cms-content-controls .icon-desktop:before { background: url('../images/sprites-32x32-s871d283813.png') 0 -925px no-repeat; }
.cms-content-controls .icon-tablet:before { background: url('../images/sprites-32x32-s871d283813.png') 0 -1087px no-repeat; }
.cms-content-controls .icon-mobile:before { background: url('../images/sprites-32x32-s871d283813.png') 0 -1006px no-repeat; }
.cms-content-controls .icon-split:before { background: url('../images/sprites-32x32-s871d283813.png') 0 -1060px no-repeat; }
.cms-content-controls .icon-edit:before { background: url('../images/sprites-32x32-s871d283813.png') 0 -979px no-repeat; }
.cms-content-controls .icon-preview:before { background: url('../images/sprites-32x32-s871d283813.png') 0 -1033px no-repeat; }
.cms-content-controls .icon-window:before { background: url('../images/sprites-32x32-s871d283813.png') 0 -952px no-repeat; }
.cms-content-controls .icon-auto:before { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -898px no-repeat; }
.cms-content-controls .icon-desktop:before { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -925px no-repeat; }
.cms-content-controls .icon-tablet:before { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -1087px no-repeat; }
.cms-content-controls .icon-mobile:before { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -1006px no-repeat; }
.cms-content-controls .icon-split:before { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -1060px no-repeat; }
.cms-content-controls .icon-edit:before { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -979px no-repeat; }
.cms-content-controls .icon-preview:before { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -1033px no-repeat; }
.cms-content-controls .icon-window:before { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -952px no-repeat; }
.cms-content-controls .cms-navigator { width: 100%; }
.cms-content-controls .preview-selector.dropdown a.chzn-single { text-indent: -200px; }
.cms-content-controls .preview-selector { float: right; border-bottom: none; position: relative; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; margin: 3px 0 0 4px; padding: 0; height: 28px; }
@ -1073,10 +1109,10 @@ visible. Added and removed with js in TabSet.js */ /***************************
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a { text-shadow: #fff 0 1px 1px; color: #0073C1; font-size: 13px; font-weight: normal; line-height: 24px; padding: 0 25px 0 10px; /* Arrow */ }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a:hover, .cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a:active { -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; outline: none; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a:hover { text-shadow: #fff 0 10px 10px; color: #005b98; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a:after { background: url('../images/sprites-32x32-s871d283813.png') 0 -26px no-repeat; border-bottom: 0; content: ""; display: inline-block; height: 16px; margin-left: 6px; width: 16px; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a:hover:after { background: url('../images/sprites-32x32-s871d283813.png') 0 0 no-repeat; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li.ui-state-active a:after { background: url('../images/sprites-32x32-s871d283813.png') 0 -78px no-repeat; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li.ui-state-active a:hover:after { background: url('../images/sprites-32x32-s871d283813.png') 0 -52px no-repeat; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a:after { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -26px no-repeat; border-bottom: 0; content: ""; display: inline-block; height: 16px; margin-left: 6px; width: 16px; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a:hover:after { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 0 no-repeat; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li.ui-state-active a:after { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -78px no-repeat; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li.ui-state-active a:hover:after { background: url('../images/sprites-32x32-s47450c5f5b.png') 0 -52px no-repeat; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-panel { overflow: hidden; *zoom: 1; -moz-border-radius-topleft: 3px; -webkit-border-top-left-radius: 3px; border-top-left-radius: 3px; -moz-border-radius-topright: 3px; -webkit-border-top-right-radius: 3px; border-top-right-radius: 3px; -moz-border-radius-bottomleft: 0; -webkit-border-bottom-left-radius: 0; border-bottom-left-radius: 0; -moz-border-radius-bottomright: 0; -webkit-border-bottom-right-radius: 0; border-bottom-right-radius: 0; /* Restyle for smaller area*/ clear: both; display: block; background-color: #ECEFF1; border: 1px solid #ccc; border-bottom: 1px solid #ECEFF1; margin: 0; margin-top: 2px; max-width: 250px; padding: 8px 0 2px; position: absolute; z-index: 1; min-width: 190px; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-panel h3, .cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-panel h4, .cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-panel h5 { font-weight: bold; line-height: 16px; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-panel h3 { font-size: 13px; }
@ -1158,28 +1194,28 @@ green tick icon as a background this is created using compass generated classes
/* Default CMS logo */
.cms-logo a { background-image: url("../images/logo_small@2x.png"); background-size: 22px 22px; }
/* Logout button */
.cms-login-status .logout-link { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -184px; background-size: 30px auto; }
.cms-content-controls .icon-auto:before { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -274px; background-size: 30px auto; }
.cms-content-controls .icon-desktop:before { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -301px; background-size: 30px auto; }
.cms-content-controls .icon-tablet:before { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -436px; background-size: 30px auto; }
.cms-content-controls .icon-mobile:before { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -355px; background-size: 30px auto; }
.cms-content-controls .icon-split:before { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -409px; background-size: 30px auto; }
.cms-content-controls .icon-edit:before { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -328px; background-size: 30px auto; }
.cms-content-controls .icon-preview:before { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -382px; background-size: 30px auto; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a:after { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -26px; background-size: 30px auto; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a:hover:after { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 0; background-size: 30px auto; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li.ui-state-active a:after { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -78px; background-size: 30px auto; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li.ui-state-active a:hover:after { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -52px; background-size: 30px auto; }
.cms-login-status .logout-link { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -184px; background-size: 30px auto; }
.cms-content-controls .icon-auto:before { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -274px; background-size: 30px auto; }
.cms-content-controls .icon-desktop:before { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -301px; background-size: 30px auto; }
.cms-content-controls .icon-tablet:before { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -436px; background-size: 30px auto; }
.cms-content-controls .icon-mobile:before { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -355px; background-size: 30px auto; }
.cms-content-controls .icon-split:before { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -409px; background-size: 30px auto; }
.cms-content-controls .icon-edit:before { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -328px; background-size: 30px auto; }
.cms-content-controls .icon-preview:before { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -382px; background-size: 30px auto; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a:after { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -26px; background-size: 30px auto; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a:hover:after { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 0; background-size: 30px auto; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li.ui-state-active a:after { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -78px; background-size: 30px auto; }
.cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li.ui-state-active a:hover:after { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -52px; background-size: 30px auto; }
/* CMS menu */
.cms-menu-list li a .toggle-children .toggle-children-icon { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -210px; background-size: 30px auto; }
.cms-menu-list li a .toggle-children.opened .toggle-children-icon { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -226px; background-size: 30px auto; }
.cms-menu-list li.current a .toggle-children .toggle-children-icon { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -242px; background-size: 30px auto; }
.cms-menu-list li.current a .toggle-children.opened .toggle-children-icon { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -258px; background-size: 30px auto; }
.cms-menu-list li a .toggle-children .toggle-children-icon { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -210px; background-size: 30px auto; }
.cms-menu-list li a .toggle-children.opened .toggle-children-icon { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -226px; background-size: 30px auto; }
.cms-menu-list li.current a .toggle-children .toggle-children-icon { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -242px; background-size: 30px auto; }
.cms-menu-list li.current a .toggle-children.opened .toggle-children-icon { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -258px; background-size: 30px auto; }
/* Sitetree */
.tree-holder.jstree-apple ins, .cms-tree.jstree-apple ins { background-image: url(../images/sitetree_ss_default_icons@2x.png); background-size: 108px 72px; }
/* UI widget "close" button */
.ui-widget-header a.ui-state-hover .ui-icon-closethick { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -104px; background-size: 30px auto; }
.ui-widget-header .ui-icon-closethick { background-image: url('../images/sprites-32x32-2x-sa271d435b9.png'); background-position: 0 -144px; background-size: 30px auto; }
.ui-widget-header a.ui-state-hover .ui-icon-closethick { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -104px; background-size: 30px auto; }
.ui-widget-header .ui-icon-closethick { background-image: url('../images/sprites-32x32-2x-s6ccfbe50f9.png'); background-position: 0 -144px; background-size: 30px auto; }
/* Tab icons */
.ui-tabs .ui-tabs-nav li.cms-tabset-icon.list a { background-image: url('../images/sprites-64x64-2x-se3e3f47b94.png'); background-position: 0 -150px; background-size: 40px auto; }
.ui-tabs .ui-tabs-nav li.cms-tabset-icon.tree a { background-image: url('../images/sprites-64x64-2x-se3e3f47b94.png'); background-position: 0 -250px; background-size: 40px auto; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -10,7 +10,7 @@ jQuery.noConflict();
// Entwine's 'fromWindow::onresize' does not trigger on IE8. Use synthetic event.
var cb = function() {$('.cms-container').trigger('windowresize');};
// Workaround to avoid IE8 infinite loops when elements are resized as a result of this event
// Workaround to avoid IE8 infinite loops when elements are resized as a result of this event
if($.browser.msie && parseInt($.browser.version, 10) < 9) {
var newWindowWidth = $(window).width(), newWindowHeight = $(window).height();
if(newWindowWidth != windowWidth || newWindowHeight != windowHeight) {
@ -26,7 +26,7 @@ jQuery.noConflict();
// setup jquery.entwine
$.entwine.warningLevel = $.entwine.WARN_LEVEL_BESTPRACTISE;
$.entwine('ss', function($) {
/*
* Handle messages sent via nested iframes
* Messages should be raised via postMessage with an object with the 'type' parameter given.
@ -35,20 +35,20 @@ jQuery.noConflict();
* type should be one of:
* - 'event' - Will trigger the given event (specified by 'event') on the target
* - 'callback' - Will call the given method (specified by 'callback') on the target
*/
*/
$(window).on("message", function(e) {
var target,
event = e.originalEvent,
data = JSON.parse(event.data);
// Reject messages outside of the same origin
if($.path.parseUrl(window.location.href).domain !== $.path.parseUrl(event.origin).domain) return;
// Get target of this action
target = typeof(data.target) === 'undefined'
? $(window)
: $(data.target);
// Determine action
switch(data.type) {
case 'event':
@ -59,18 +59,18 @@ jQuery.noConflict();
break;
}
});
/**
* Position the loading spinner animation below the ss logo
*/
*/
var positionLoadingSpinner = function() {
var offset = 120; // offset from the ss logo
var spinner = $('.ss-loading-screen .loading-animation');
var spinner = $('.ss-loading-screen .loading-animation');
var top = ($(window).height() - spinner.height()) / 2;
spinner.css('top', top + offset);
spinner.show();
};
// apply an select element only when it is ready, ie. when it is rendered into a template
// with css applied and got a width value.
var applyChosen = function(el) {
@ -89,13 +89,13 @@ jQuery.noConflict();
setTimeout(function() {
// Make sure it's visible before applying the ui
el.show();
applyChosen(el); },
applyChosen(el); },
500);
}
};
/**
* Compare URLs, but normalize trailing slashes in
* Compare URLs, but normalize trailing slashes in
* URL to work around routing weirdnesses in SS_HTTPRequest.
* Also normalizes relative URLs by prefixing them with the <base>.
*/
@ -105,11 +105,11 @@ jQuery.noConflict();
url2 = $.path.isAbsoluteUrl(url2) ? url2 : $.path.makeUrlAbsolute(url2, baseUrl);
var url1parts = $.path.parseUrl(url1), url2parts = $.path.parseUrl(url2);
return (
url1parts.pathname.replace(/\/*$/, '') == url2parts.pathname.replace(/\/*$/, '') &&
url1parts.pathname.replace(/\/*$/, '') == url2parts.pathname.replace(/\/*$/, '') &&
url1parts.search == url2parts.search
);
};
$(window).bind('resize', positionLoadingSpinner).trigger('resize');
// global ajax handlers
@ -142,7 +142,7 @@ jQuery.noConflict();
reathenticate = xhr.getResponseHeader('X-Reauthenticate'),
msgType = (xhr.status < 200 || xhr.status > 399) ? 'bad' : 'good',
ignoredMessages = ['OK'];
// Enable reauthenticate dialog if requested
if(reathenticate) {
$('.cms-container').showLoginDialog();
@ -158,14 +158,14 @@ jQuery.noConflict();
/**
* Main LeftAndMain interface with some control panel and an edit form.
*
*
* Events:
* ajaxsubmit - ...
* validate - ...
* aftersubmitform - ...
*/
$('.cms-container').entwine({
/**
* Tracks current panel request.
*/
@ -177,7 +177,7 @@ jQuery.noConflict();
FragmentXHR: {},
StateChangeCount: 0,
/**
* Options for the threeColumnCompressor layout algorithm.
*
@ -198,7 +198,7 @@ jQuery.noConflict();
// Browser detection
if($.browser.msie && parseInt($.browser.version, 10) < 8) {
$('.ss-loading-screen').append(
'<p class="ss-loading-incompat-warning"><span class="notice">' +
'<p class="ss-loading-incompat-warning"><span class="notice">' +
'Your browser is not compatible with the CMS interface. Please use Internet Explorer 8+, Google Chrome or Mozilla Firefox.' +
'</span></p>'
).css('z-index', $('.ss-loading-screen').css('z-index')+1);
@ -207,7 +207,7 @@ jQuery.noConflict();
this._super();
return;
}
// Initialize layouts
this.redraw();
@ -216,7 +216,7 @@ jQuery.noConflict();
$('body').removeClass('loading');
$(window).unbind('resize', positionLoadingSpinner);
this.restoreTabState();
this._super();
},
@ -326,9 +326,9 @@ jQuery.noConflict();
* Proxy around History.pushState() which handles non-HTML5 fallbacks,
* as well as global change tracking. Change tracking needs to be synchronous rather than event/callback
* based because the user needs to be able to abort the action completely.
*
*
* See handleStateChange() for more details.
*
*
* Parameters:
* - {String} url
* - {String} title New window title
@ -343,20 +343,20 @@ jQuery.noConflict();
// Check change tracking (can't use events as we need a way to cancel the current state change)
var contentEls = this._findFragments(data.pjax ? data.pjax.split(',') : ['Content']);
var trackedEls = contentEls.find(':data(changetracker)').add(contentEls.filter(':data(changetracker)'));
if(trackedEls.length) {
var abort = false;
trackedEls.each(function() {
if(!$(this).confirmUnsavedChanges()) abort = true;
});
if(abort) return;
}
// Save tab selections so we can restore them later
this.saveTabState();
if(window.History.enabled) {
$.extend(data, {__forceReferer: forceReferer});
// Active menu item is set based on X-Controller ajax header,
@ -382,31 +382,31 @@ jQuery.noConflict();
/**
* Function: submitForm
*
*
* Parameters:
* {DOMElement} form - The form to be submitted. Needs to be passed
* in to avoid entwine methods/context being removed through replacing the node itself.
* {DOMElement} button - The pressed button (optional)
* {Function} callback - Called in complete() handler of jQuery.ajax()
* {Object} ajaxOptions - Object literal to merge into $.ajax() call
*
*
* Returns:
* (boolean)
*/
submitForm: function(form, button, callback, ajaxOptions) {
var self = this;
// look for save button
if(!button) button = this.find('.Actions :submit[name=action_save]');
// default to first button if none given - simulates browser behaviour
if(!button) button = this.find('.Actions :submit:first');
form.trigger('beforesubmitform');
this.trigger('submitform', {form: form, button: button});
// set button to "submitting" state
$(button).addClass('loading');
// validate if required
var validationResult = form.validate();
if(typeof validationResult!=='undefined' && !validationResult) {
@ -417,12 +417,12 @@ jQuery.noConflict();
return false;
}
// get all data from the form
var formData = form.serializeArray();
// add button action
formData.push({name: $(button).attr('name'), value:'1'});
// Artificial HTTP referer, IE doesn't submit them via ajax.
// Artificial HTTP referer, IE doesn't submit them via ajax.
// Also rewrites anchors to their page counterparts, which is important
// as automatic browser ajax response redirects seem to discard the hash/fragment.
// TODO Replaces trailing slashes added by History after locale (e.g. admin/?locale=en/)
@ -437,7 +437,7 @@ jQuery.noConflict();
// sending back different `X-Pjax` headers and content
jQuery.ajax(jQuery.extend({
headers: {"X-Pjax" : "CurrentForm,Breadcrumbs"},
url: form.attr('action'),
url: form.attr('action'),
data: formData,
type: 'POST',
complete: function() {
@ -453,7 +453,7 @@ jQuery.noConflict();
newContentEls.filter('form').trigger('aftersubmitform', {status: status, xhr: xhr, formData: formData});
}
}, ajaxOptions));
return false;
},
@ -462,21 +462,21 @@ jQuery.noConflict();
* To trigger loading, pass a new URL to window.History.pushState().
* Use loadPanel() as a pushState() wrapper as it provides some additional functionality
* like global changetracking and user aborts.
*
*
* Due to the nature of history management, no callbacks are allowed.
* Use the 'beforestatechange' and 'afterstatechange' events instead,
* or overwrite the beforeLoad() and afterLoad() methods on the
* or overwrite the beforeLoad() and afterLoad() methods on the
* DOM element you're loading the new content into.
* Although you can pass data into pushState(), it shouldn't contain
* Although you can pass data into pushState(), it shouldn't contain
* DOM elements or callback closures.
*
*
* The passed URL should allow reconstructing important interface state
* without additional parameters, in the following use cases:
* - Explicit loading through History.pushState()
* - Implicit loading through browser navigation event triggered by the user (forward or back)
* - Full window refresh without ajax
* For example, a ModelAdmin search event should contain the search terms
* as URL parameters, and the result display should automatically appear
* as URL parameters, and the result display should automatically appear
* if the URL is loaded without ajax.
*/
handleStateChange: function() {
@ -502,22 +502,22 @@ jQuery.noConflict();
// that can be reloaded without reloading the whole window.
if(contentEls.length < fragmentsArr.length) {
fragments = 'Content', fragmentsArr = ['Content'];
contentEls = this._findFragments(fragmentsArr);
contentEls = this._findFragments(fragmentsArr);
}
this.trigger('beforestatechange', {state: state, element: contentEls});
// Set Pjax headers, which can declare a preference for the returned view.
// The actually returned view isn't always decided upon when the request
// is fired, so the server might decide to change it based on its own logic.
headers['X-Pjax'] = fragments;
// Set 'fake' referer - we call pushState() before making the AJAX request, so we have to
// set our own referer here
if (typeof state.data.__forceReferer !== 'undefined') {
// Ensure query string is properly encoded if present
var url = state.data.__forceReferer;
try {
// Prevent double-encoding by attempting to decode
url = decodeURI(url);
@ -528,7 +528,7 @@ jQuery.noConflict();
headers['X-Backurl'] = encodeURI(url);
}
}
contentEls.addClass('loading');
var xhr = $.ajax({
headers: headers,
@ -543,7 +543,7 @@ jQuery.noConflict();
self.trigger('afterstatechange', {data: data, status: status, xhr: xhr, element: els, state: state});
}
});
this.setStateChangeXHR(xhr);
},
@ -633,7 +633,7 @@ jQuery.noConflict();
// Support a full reload
if(xhr.getResponseHeader('X-Reload') && xhr.getResponseHeader('X-ControllerURL')) {
document.location.href = $('base').attr('href').replace(/\/*$/, '')
document.location.href = $('base').attr('href').replace(/\/*$/, '')
+ '/' + xhr.getResponseHeader('X-ControllerURL');
return;
}
@ -651,7 +651,7 @@ jQuery.noConflict();
if(xhr.getResponseHeader('Content-Type').match(/^((text)|(application))\/json[ \t]*;?/i)) {
newFragments = data;
} else {
// Fall back to replacing the content fragment if HTML is returned
var fragment = document.createDocumentFragment();
jQuery.clean( [ data ], document, fragment, [] );
@ -732,9 +732,9 @@ jQuery.noConflict();
},
/**
*
*
* Parameters:
*
*
* Parameters:
* - fragments {Array}
* Returns: jQuery collection
*/
@ -751,14 +751,14 @@ jQuery.noConflict();
/**
* Function: refresh
*
*
* Updates the container based on the current url
*
* Returns: void
*/
refresh: function() {
$(window).trigger('statechange');
$(this).redraw();
},
@ -787,7 +787,7 @@ jQuery.noConflict();
window.sessionStorage.setItem(tabsUrl, JSON.stringify(selectedTabs));
} catch(err) {
if (err.code === DOMException.QUOTA_EXCEEDED_ERR && window.sessionStorage.length === 0) {
// If this fails we ignore the error as the only issue is that it
// If this fails we ignore the error as the only issue is that it
// does not remember the tab state.
// This is a Safari bug which happens when private browsing is enabled.
return;
@ -856,7 +856,7 @@ jQuery.noConflict();
var s = window.sessionStorage;
if(url) {
s.removeItem('tabs-' + url);
s.removeItem('tabs-' + url);
} else {
for(var i=0;i<s.length;i++) {
if(s.key(i).match(/^tabs-/)) s.removeItem(s.key(i));
@ -877,7 +877,7 @@ jQuery.noConflict();
.replace(/#.*/, '')
.replace($('base').attr('href'), '');
},
showLoginDialog: function() {
var tempid = $('body').data('member-tempid'),
dialog = $('.leftandmain-logindialog'),
@ -885,13 +885,13 @@ jQuery.noConflict();
// Force regeneration of any existing dialog
if(dialog.length) dialog.remove();
// Join url params
url = $.path.addSearchParams(url, {
'tempid': tempid,
'BackURL': window.location.href
});
// Show a placeholder for instant feedback. Will be replaced with actual
// form dialog once its loaded.
dialog = $('<div class="leftandmain-logindialog"></div>');
@ -900,12 +900,12 @@ jQuery.noConflict();
$('body').append(dialog);
}
});
// Login dialog page
$('.leftandmain-logindialog').entwine({
onmatch: function() {
this._super();
// Create jQuery dialog
this.ssdialog({
iframeUrl: this.data('url'),
@ -921,7 +921,7 @@ jQuery.noConflict();
},
close: function() {
$('.ui-widget-overlay').removeClass('leftandmain-logindialog-overlay');
}
}
});
},
onunmatch: function() {
@ -952,7 +952,7 @@ jQuery.noConflict();
this.close();
}
});
/**
* Add loading overlay to selected regions in the CMS automatically.
* Not applied to all "*.loading" elements to avoid secondary regions
@ -997,7 +997,7 @@ jQuery.noConflict();
return;
}
var href = this.attr('href'),
var href = this.attr('href'),
url = (href && !href.match(/^#/)) ? href : this.data('href'),
data = {pjax: this.data('pjaxTarget')};
@ -1015,17 +1015,17 @@ jQuery.noConflict();
onclick: function(e) {
$(this).removeClass('ui-button-text-only');
$(this).addClass('ss-ui-button-loading ui-button-text-icons');
var loading = $(this).find(".ss-ui-loading-icon");
if(loading.length < 1) {
loading = $("<span></span>").addClass('ss-ui-loading-icon ui-button-icon-primary ui-icon');
$(this).prepend(loading);
}
loading.show();
var href = this.attr('href'), url = href ? href : this.data('href');
jQuery.ajax({
@ -1033,16 +1033,16 @@ jQuery.noConflict();
// Ensure that form view is loaded (rather than whole "Content" template)
complete: function(xmlhttp, status) {
var msg = (xmlhttp.getResponseHeader('X-Status')) ? xmlhttp.getResponseHeader('X-Status') : xmlhttp.responseText;
try {
if (typeof msg != "undefined" && msg !== null) eval(msg);
}
catch(e) {}
loading.hide();
$(".cms-container").refresh();
$(this).removeClass('ss-ui-button-loading ui-button-text-icons');
$(this).addClass('ui-button-text-only');
},
@ -1073,14 +1073,14 @@ jQuery.noConflict();
dialog = $('<div class="ss-ui-dialog" id="' + id + '" />');
$('body').append(dialog);
}
var extraClass = this.data('popupclass')?this.data('popupclass'):'';
dialog.ssdialog({iframeUrl: this.attr('href'), autoOpen: true, dialogExtraClass: extraClass});
return false;
}
});
/**
* Add styling to all contained buttons, and create buttonsets if required.
*/
@ -1110,20 +1110,20 @@ jQuery.noConflict();
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
// Remove whitespace to avoid gaps with inline elements
this.contents().filter(function() {
return (this.nodeType == 3 && !/\S/.test(this.nodeValue));
this.contents().filter(function() {
return (this.nodeType == 3 && !/\S/.test(this.nodeValue));
}).remove();
// Init buttons if required
this.find('.ss-ui-button').each(function() {
if(!$(this).data('button')) $(this).button();
});
// Mark up buttonsets
this.find('.ss-ui-buttonset').buttonset();
}
});
/**
* Duplicates functionality in DateField.js, but due to using entwine we can match
* the DOM element on creation, rather than onclick - which allows us to decorate
@ -1145,23 +1145,23 @@ jQuery.noConflict();
$(this).datepicker(config);
// // Unfortunately jQuery UI only allows configuration of icon images, not sprites
// this.next('button').button('option', 'icons', {primary : 'ui-icon-calendar'});
this._super();
},
onunmatch: function() {
this._super();
}
});
/**
* Styled dropdown select fields via chosen. Allows things like search and optgroup
* selection support. Rather than manually adding classes to selects we want
* selection support. Rather than manually adding classes to selects we want
* styled, we style everything but the ones we tell it not to.
*
* For the CMS we also need to tell the parent div that his has a select so
* For the CMS we also need to tell the parent div that it has a select so
* we can fix the height cropping.
*/
$('.cms .field.dropdown select, .cms .field select[multiple], .fieldholder-small select.dropdown').entwine({
onmatch: function() {
if(this.is('.no-chzn')) {
@ -1178,20 +1178,20 @@ jQuery.noConflict();
// Apply Chosen
applyChosen(this);
this._super();
},
onunmatch: function() {
this._super();
}
});
$(".cms-panel-layout").entwine({
redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
}
});
/**
* Overload the default GridField behaviour (open a new URL in the browser)
* with the CMS-specific ajax loading.
@ -1209,7 +1209,7 @@ jQuery.noConflict();
/**
* Generic search form in the CMS, often hooked up to a GridField results display.
*/
*/
$('.cms-search-form').entwine({
onsubmit: function(e) {
// Remove empty elements and make the URL prettier
@ -1267,7 +1267,7 @@ jQuery.noConflict();
},
onremove: function() {
if(window.debug) console.log('saving', this.data('url'), this);
// Save the HTML state at the last possible moment.
// Don't store the DOM to avoid memory leaks.
if(!this.data('deferredNoCache')) window._panelDeferredCache[this.data('url')] = this.html();
@ -1326,7 +1326,7 @@ jQuery.noConflict();
if(!this.data('uiTabs')) this.tabs({
active: (activeTab.index() != -1) ? activeTab.index() : 0,
beforeLoad: function(e, ui) {
// Disable automatic ajax loading of tabs without matching DOM elements,
// Disable automatic ajax loading of tabs without matching DOM elements,
// determining if the current URL differs from the tab URL is too error prone.
return false;
},
@ -1347,7 +1347,7 @@ jQuery.noConflict();
}
});
},
/**
* Ensure hash links are prefixed with the current page URL,
* otherwise jQuery interprets them as being external.
@ -1362,7 +1362,7 @@ jQuery.noConflict();
}
});
});
}(jQuery));
var statusMessage = function(text, type) {

View File

@ -0,0 +1,16 @@
// This file was generated by GenerateJavaScriptI18nTask from javascript/lang/src/eo.js.
// See https://github.com/silverstripe/silverstripe-buildtools for details
if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
if(typeof(console) != 'undefined') console.error('Class ss.i18n not defined');
} else {
ss.i18n.addDictionary('eo', {
"LeftAndMain.CONFIRMUNSAVED": "Ĉu vi vere volas navigi for de ĉi tiu paĝo?\n\nAVERTO: Viaj ŝanĝoj ne estas konservitaj.\n\nPremu je Akcepti por daŭrigi, aŭ Nuligi por resti ĉe la aktuala paĝo.",
"LeftAndMain.CONFIRMUNSAVEDSHORT": "AVERTO: Viaj ŝanĝoj ne estas konservitaj.",
"SecurityAdmin.BATCHACTIONSDELETECONFIRM": "Ĉu vi vere volas forigi %s grupojn?",
"ModelAdmin.SAVED": "Konservita",
"ModelAdmin.REALLYDELETE": "Ĉi vi vere volas forigi?",
"ModelAdmin.DELETED": "Forigita",
"ModelAdmin.VALIDATIONERROR": "Validiga eraro",
"LeftAndMain.PAGEWASDELETED": "Ĉi tiu paĝo estas forigita. Por redakti paĝon, elektu ĝin maldekstre."
});
}

View File

@ -0,0 +1,16 @@
// This file was generated by GenerateJavaScriptI18nTask from javascript/lang/src/lt.js.
// See https://github.com/silverstripe/silverstripe-buildtools for details
if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
if(typeof(console) != 'undefined') console.error('Class ss.i18n not defined');
} else {
ss.i18n.addDictionary('lt', {
"LeftAndMain.CONFIRMUNSAVED": "Ar tikrai norite išeiti iš šio puslapio?\n\nDĖMESIO: Jūsų pakeitimai neišsaugoti.\n\nNorėdami tęsti, spauskite OK, jeigu norite likti, spauskite Cancel.",
"LeftAndMain.CONFIRMUNSAVEDSHORT": "DĖMESIO: Jūsų pakeitimai neišsaugoti.",
"SecurityAdmin.BATCHACTIONSDELETECONFIRM": "Ar tikrai norite ištrinti %s grupes?",
"ModelAdmin.SAVED": "Išsaugota",
"ModelAdmin.REALLYDELETE": "Ar tikrai norite ištrinti?",
"ModelAdmin.DELETED": "Ištrinta",
"ModelAdmin.VALIDATIONERROR": "Tikrinimo klaida",
"LeftAndMain.PAGEWASDELETED": "Šis puslapis ištrintas. Norėdami redaguoti puslapį, pasirinkite jį kairėje."
});
}

View File

@ -0,0 +1,16 @@
// This file was generated by GenerateJavaScriptI18nTask from javascript/lang/src/nb.js.
// See https://github.com/silverstripe/silverstripe-buildtools for details
if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
if(typeof(console) != 'undefined') console.error('Class ss.i18n not defined');
} else {
ss.i18n.addDictionary('nb', {
"LeftAndMain.CONFIRMUNSAVED": "Er du sikker på at du vil forlate denne siden?\n\nADVARSEL: Endringene din har ikke blitt lagret.\n\nTrykk OK for å fortsette eller Avbryt for å holde deg på samme side.",
"LeftAndMain.CONFIRMUNSAVEDSHORT": "ADVARSEL: Endringene dine har ikke blitt lagret.",
"SecurityAdmin.BATCHACTIONSDELETECONFIRM": "Vil du virkelig slette %s grupper?",
"ModelAdmin.SAVED": "Lagret",
"ModelAdmin.REALLYDELETE": "Vil du virkelig slette?",
"ModelAdmin.DELETED": "Slettet",
"ModelAdmin.VALIDATIONERROR": "Valideringsfeil",
"LeftAndMain.PAGEWASDELETED": "Denne siden ble slettet. For å redigere en side, velg den fra listen til venstre."
});
}

View File

@ -10,7 +10,7 @@ if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
"ModelAdmin.SAVED": "Opgeslagen",
"ModelAdmin.REALLYDELETE": "Weet u zeker dat u wilt verwijderen?",
"ModelAdmin.DELETED": "Verwijderd",
"ModelAdmin.VALIDATIONERROR": "Validatie fout",
"LeftAndMain.PAGEWASDELETED": "Deze pagina is verwijderd. Om een pagina aan te passen, selecteer pagina aan de linkerkant."
"ModelAdmin.VALIDATIONERROR": "Validatiefout",
"LeftAndMain.PAGEWASDELETED": "Deze pagina is verwijderd. Om een pagina aan te passen, selecteer deze aan de linkerkant."
});
}

View File

@ -0,0 +1,16 @@
// This file was generated by GenerateJavaScriptI18nTask from javascript/lang/src/sl.js.
// See https://github.com/silverstripe/silverstripe-buildtools for details
if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
if(typeof(console) != 'undefined') console.error('Class ss.i18n not defined');
} else {
ss.i18n.addDictionary('sl', {
"LeftAndMain.CONFIRMUNSAVED": "Res želite zapusitit stran?\n\nOPOZORILO: spremembe niso bile shranjene\n\nKliknite OK za nadaljevanje ali Prekliči, da ostanete na trenutni strani.",
"LeftAndMain.CONFIRMUNSAVEDSHORT": "OPOZORILO: spremembe niso bile shranjene.",
"SecurityAdmin.BATCHACTIONSDELETECONFIRM": "Izbrišem %s skupin?",
"ModelAdmin.SAVED": "Shranjeno",
"ModelAdmin.REALLYDELETE": "Izbrišem?",
"ModelAdmin.DELETED": "Izbrisano",
"ModelAdmin.VALIDATIONERROR": "Napaka pri preverjanju",
"LeftAndMain.PAGEWASDELETED": "Stran je bila izbrisana. Za urejanje izberite stran na levi."
});
}

View File

@ -0,0 +1,10 @@
{
"LeftAndMain.CONFIRMUNSAVED": "Är du säker på att du vill lämna denna sida?\n\nVARNING: Dina ändringar har inte sparats.\n\nTryck OK för att lämna sidan eller Avbryt för att stanna på aktuell sida.",
"LeftAndMain.CONFIRMUNSAVEDSHORT": "WARNING: Your changes have not been saved.",
"SecurityAdmin.BATCHACTIONSDELETECONFIRM": "Vill du verkligen radera %s grupper?",
"ModelAdmin.SAVED": "Sparad",
"ModelAdmin.REALLYDELETE": "Vill du verkligen radera?",
"ModelAdmin.DELETED": "Raderad",
"ModelAdmin.VALIDATIONERROR": "Valideringsfel",
"LeftAndMain.PAGEWASDELETED": "Sidan raderades. För att redigera en sida, välj den från menyn till vänster."
}

View File

@ -0,0 +1,16 @@
// This file was generated by GenerateJavaScriptI18nTask from javascript/lang/src/sv.js.
// See https://github.com/silverstripe/silverstripe-buildtools for details
if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
if(typeof(console) != 'undefined') console.error('Class ss.i18n not defined');
} else {
ss.i18n.addDictionary('sv', {
"LeftAndMain.CONFIRMUNSAVED": "Är du säker på att du vill lämna denna sida?\n\nVARNING: Dina ändringar har inte sparats.\n\nTryck OK för att lämna sidan eller Avbryt för att stanna på aktuell sida.",
"LeftAndMain.CONFIRMUNSAVEDSHORT": "WARNING: Your changes have not been saved.",
"SecurityAdmin.BATCHACTIONSDELETECONFIRM": "Vill du verkligen radera %s grupper?",
"ModelAdmin.SAVED": "Sparad",
"ModelAdmin.REALLYDELETE": "Vill du verkligen radera?",
"ModelAdmin.DELETED": "Raderad",
"ModelAdmin.VALIDATIONERROR": "Valideringsfel",
"LeftAndMain.PAGEWASDELETED": "Sidan raderades. För att redigera en sida, välj den från menyn till vänster."
});
}

View File

@ -1313,10 +1313,12 @@ form.member-profile-form {
// most styles should be applied to .cms-dialog instead (which declares the content in the frame)
.ui-dialog {
background: url("../images/textures/bg_cms_main_content.png") repeat left top #F0F3F4;
border: 3px solid #000 !important;
border-radius: $grid-y;
background-clip: content-box;
border: 1px solid #666 !important;
@include border-radius($grid-y);
overflow: visible;
padding: 0;
@include box-shadow(0px 0px 30px 10px rgba(0,0,0,.3));
// Titlebar for pop-up dialog.
.ui-dialog-titlebar.ui-widget-header {
@ -1335,6 +1337,7 @@ form.member-profile-form {
}
.ui-dialog-content {
@include border-radius($grid-y);
overflow: auto; // TODO Replace with proper $.layout grid
&.loading {
@ -1470,7 +1473,7 @@ body.cms-dialog {
width:100%;
height: 40px;
h3{
padding: 0 8px;
padding: 3px 8px;
margin: 10px;
}
}

View File

@ -68,13 +68,13 @@
&:hover {
text-decoration: none;
cursor: pointer;
text-shadow: none;
text-shadow:1px 1px 1px white;
}
> {
ins {
height: 16px;
width: 16px;
&.jstree-checkbox{
&.jstree-checkbox {
height:19px; //Larger to help avoid accidental page loads when trying to click checkboxes
}
}
@ -111,7 +111,7 @@
background: transparent !important;
width: 100%;
}
a, a:hover{
a, a:hover {
margin: 0 !important;
padding: 0 !important;
text-indent: -9999px !important;
@ -138,9 +138,9 @@
}
// Custom styles
.jstree-apple.jstree-focused {
.jstree.jstree-focused {
background: none;
.jstree-apple > ul {
.jstree > ul {
background: none;
}
}
@ -182,7 +182,7 @@
}
}
.jstree-themeroller{
.jstree-themeroller {
a {
padding: 0 2px;
}
@ -276,7 +276,7 @@
min-width: 180px;
*width:180px;
}
ul,li{
ul,li {
margin: 0;
padding: 0 ;
list-style-type: none;
@ -316,7 +316,7 @@
margin-top: 3px;
margin-right: 5px;
}
&.vakata-hover > a{
&.vakata-hover > a {
padding: 1px 10px;
background: #3875d7;
@include background-image(linear-gradient(top, #3875d7 20%, #2a62bc 90%));
@ -326,7 +326,7 @@
}
}
#vakata-contextmenu{
#vakata-contextmenu {
.right {
right: 100%;
left: auto;
@ -345,25 +345,25 @@
@include box-shadow(0 0 10px #CCC);
&.col-2{
width:180px * 2; // 2x the size of the original ul
li{
li {
width:50%;
}
}
&.col-3{
width:180px * 3; // 3x the size of the original ul
li{
li {
width:33%;
}
}
li{
li {
min-width:180px;
float:left;
a{
a {
@include hide-text-overflow;
}
}
}
li{
li {
&.vakata-separator {
min-height: 0;
height: 1px;
@ -409,8 +409,8 @@
}
.jstree-apple {
li, .jstree-apple ins {
.jstree {
li, .jstree ins {
background:none;
}
.jstree-unchecked, .jstree-checked, .jstree-undetermined {
@ -420,9 +420,9 @@
}
}
.tree-holder, .cms-tree{
&.jstree-apple{
li{
.tree-holder, .cms-tree {
&.jstree {
li {
padding: 0px;
clear: left;
&.Root {
@ -440,8 +440,8 @@
text-decoration: line-through;
}
}
&.jstree-checked{
> a, > a:link{
&.jstree-checked {
> a, > a:link {
background-color: $color-cms-batchactions-menu-selected-background;
}
}
@ -478,30 +478,15 @@
span.badge {
clear: both;
text-transform: uppercase;
text-shadow: none;
display: inline-block;
padding: 0px 3px;
position: relative;
padding: 2px 3px 1px;
font-size: 0.75em;
line-height: 1em;
margin-left: 3px;
margin-right: 6px;
margin-top: -1px;
@include border-radius(2px, 2px);
&.status-modified, &.status-addedtodraft {
color: #7E7470;
border: 1px solid #C9B800;
background-color: #FFF0BC;
}
&.status-deletedonlive, &.status-removedfromdraft {
color: #636363;
border: 1px solid #E49393;
background-color: #F2DADB;
}
&.status-workflow-approval {
color: #56660C;
border: 1px solid #7C8816;
background-color: #DAE79A;
}
}
/* comment speech bubble - ccs3 only - source: http://nicolasgallagher.com/pure-css-speech-bubbles/demo/ */
@ -557,12 +542,36 @@
background-position:-20px 0;
}
}
}
}
/* ensure status is visible in sidebar */
#cms-content-tools-CMSMain .cms-tree.jstree {
li {
min-width: 159px;
}
a {
overflow: hidden;
display: block;
position: relative;
}
span.badge {
position: absolute;
top: 0;
right: 0;
padding: 7px 9px 6px 5px;
margin: 0;
max-width: 40%;
@include transition(max-width .75s linear);
}
span.badge:hover {
max-width: 150px;
}
}
a .jstree-pageicon {
float: left;
margin-right: 4px;
position: relative;
li.class-HomePage > &{
background-position: 0 -48px;
}
@ -577,6 +586,43 @@ a .jstree-pageicon {
}
}
/* tree status icons and labels */
%tree-status-icon-before {
content:"";
display: block;
width:5px;
height: 5px;
position: absolute;
bottom: 0;
right: 0;
background: #fce2d0;
border: 1px solid #ff9344;
border-radius: 100px;
box-shadow: 0px 0px 0px 1px #fff;
}
@mixin tree-status-icon($label, $color, $bgColor) {
.cms-tree.jstree .status-#{$label} > a .jstree-pageicon:before {
@extend %tree-status-icon-before;
}
.jstree .status-#{$label} > .jstree-hovered,
.jstree .status-#{$label} > .jstree-clicked,
.cms-tree.jstree span.badge.status-#{$label},
.cms-tree.jstree .status-#{$label} > a .jstree-pageicon:before {
background-color:$bgColor;
border-color:$color;
}
#cms-content-tools-CMSMain .cms-tree.jstree span.badge.status-#{$label} {
box-shadow: 0px 0px 6px 2px $bgColor;
}
.cms-tree.jstree span.badge.status-#{$label} {
color: $color;
}
}
@include tree-status-icon('modified', #ff7714, #fce2d0);
@include tree-status-icon('addedtodraft', #e29a00, #f8f4d0);
@include tree-status-icon('deletedonlive', #f0524f, #f9d6dd);
@include tree-status-icon('removedfromdraft', #f0524f, #f9d6dd);
@include tree-status-icon('workflow-approval', #0097d7, #d3f2ff);
.cms-tree {
visibility: hidden; // enabled by JS to avoid layout glitches
@ -608,7 +654,7 @@ a .jstree-pageicon {
// Show the loading indicator on the page icon rather than the default
// jstree icon (which is only used for its dragging handles)
a.jstree-loading{
a.jstree-loading {
.jstree-icon {
background-image: none !important;
}

View File

@ -38,8 +38,8 @@
& a.ui-dialog-titlebar-close {
position: absolute;
top: -8px;
right: -15px;
top: -5px;
right: -13px;
width: 30px;
height: 30px;
z-index: 100000;

View File

@ -62,7 +62,7 @@ class JSONDataFormatter extends DataFormatter {
}
if($this->relationDepth > 0) {
foreach($obj->has_one() as $relName => $relClass) {
foreach($obj->hasOne() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering
@ -82,7 +82,7 @@ class JSONDataFormatter extends DataFormatter {
));
}
foreach($obj->has_many() as $relName => $relClass) {
foreach($obj->hasMany() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering
@ -103,7 +103,7 @@ class JSONDataFormatter extends DataFormatter {
$serobj->$relName = $innerParts;
}
foreach($obj->many_many() as $relName => $relClass) {
foreach($obj->manyMany() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering

View File

@ -68,7 +68,7 @@ class XMLDataFormatter extends DataFormatter {
}
if($this->relationDepth > 0) {
foreach($obj->has_one() as $relName => $relClass) {
foreach($obj->hasOne() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering
@ -85,7 +85,7 @@ class XMLDataFormatter extends DataFormatter {
. "\"></$relName>\n";
}
foreach($obj->has_many() as $relName => $relClass) {
foreach($obj->hasMany() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering
@ -103,7 +103,7 @@ class XMLDataFormatter extends DataFormatter {
$xml .= "</$relName>\n";
}
foreach($obj->many_many() as $relName => $relClass) {
foreach($obj->manyMany() as $relName => $relClass) {
if(!singleton($relClass)->stat('api_access')) continue;
// Field filtering

View File

@ -438,12 +438,21 @@ class Director implements TemplateGlobalProvider {
public static function absoluteURL($url, $relativeToSiteBase = false) {
if(!isset($_SERVER['REQUEST_URI'])) return false;
//a url of . or ./ is the same as an empty url
if ($url == '.' || $url == './') {
$url = '';
}
if(strpos($url,'/') === false && !$relativeToSiteBase) {
$url = dirname($_SERVER['REQUEST_URI'] . 'x') . '/' . $url;
//if there's no URL we want to force a trailing slash on the link
if (!$url) {
$url = '/';
}
$url = Controller::join_links(dirname($_SERVER['REQUEST_URI'] . 'x'), $url);
}
if(substr($url,0,4) != "http") {
if($url[0] != "/") $url = Director::baseURL() . $url;
if(strpos($url, '/') !== 0) $url = Director::baseURL() . $url;
// Sometimes baseURL() can return a full URL instead of just a path
if(substr($url,0,4) != "http") $url = self::protocolAndHost() . $url;
}
@ -802,14 +811,10 @@ class Director implements TemplateGlobalProvider {
* @param string $destURL - The URL to redirect to
*/
protected static function force_redirect($destURL) {
$response = new SS_HTTPResponse(
"<h1>Your browser is not accepting header redirects</h1>".
"<p>Please <a href=\"$destURL\">click here</a>",
301
);
$response = new SS_HTTPResponse();
$response->redirect($destURL, 301);
HTTP::add_cache_headers($response);
$response->addHeader('Location', $destURL);
// TODO: Use an exception - ATM we can be called from _config.php, before Director#handleRequest's try block
$response->output();

View File

@ -361,7 +361,8 @@ class HTTP {
if(
$body &&
Director::is_https() &&
strstr($_SERVER["HTTP_USER_AGENT"], 'MSIE')==true &&
isset($_SERVER['HTTP_USER_AGENT']) &&
strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')==true &&
strstr($contentDisposition, 'attachment;')==true
) {
// IE6-IE8 have problems saving files when https and no-cache are used

View File

@ -250,7 +250,7 @@ class Config {
* leak through to other instances.
*/
public function __construct() {
$this->cache = new Config_LRU();
$this->cache = new Config_MemCache();
}
public function __clone() {
@ -681,6 +681,7 @@ class Config {
/**
* @package framework
* @subpackage core
* @deprecated 3.2
*/
class Config_LRU {
const SIZE = 1000;
@ -692,6 +693,7 @@ class Config_LRU {
protected $c = 0;
public function __construct() {
Deprecation::notice('3.2', 'Please use Config_MemCache instead', Deprecation::SCOPE_CLASS);
if (version_compare(PHP_VERSION, '5.3.7', '<')) {
// SplFixedArray causes seg faults before PHP 5.3.7
$this->cache = array();
@ -787,6 +789,69 @@ class Config_LRU {
}
}
/**
* @package framework
* @subpackage core
*/
class Config_MemCache {
protected $cache;
protected $i = 0;
protected $c = 0;
protected $tags = array();
public function __construct() {
$this->cache = array();
}
public function set($key, $val, $tags = array()) {
foreach($tags as $t) {
if(!isset($this->tags[$t])) {
$this->tags[$t] = array();
}
$this->tags[$t][$key] = true;
}
$this->cache[$key] = array($val, $tags);
}
private $hit = 0;
private $miss = 0;
public function stats() {
return $this->miss ? ($this->hit / $this->miss) : 0;
}
public function get($key) {
if(isset($this->cache[$key])) {
++$this->hit;
return $this->cache[$key][0];
}
++$this->miss;
return false;
}
public function clean($tag = null) {
if($tag) {
if(isset($this->tags[$tag])) {
foreach($this->tags[$tag] as $k => $dud) {
// Remove the key from everywhere else it is tagged
$ts = $this->cache[$k][1];
foreach($ts as $t) {
unset($this->tags[$t][$k]);
}
unset($this->cache[$k]);
}
unset($this->tags[$tag]);
}
} else {
$this->cache = array();
$this->tags = array();
}
}
}
/**
* @package framework
* @subpackage core

View File

@ -230,15 +230,32 @@ class Convert {
/**
* Converts an XML string to a PHP array
* See http://phpsecurity.readthedocs.org/en/latest/Injection-Attacks.html#xml-external-entity-injection
*
* @uses recursiveXMLToArray()
* @param string
*
* @param string $val
* @param boolean $disableDoctypes Disables the use of DOCTYPE, and will trigger an error if encountered.
* false by default.
* @param boolean $disableExternals Disables the loading of external entities. false by default.
* @return array
*/
public static function xml2array($val) {
$xml = new SimpleXMLElement($val);
return self::recursiveXMLToArray($xml);
public static function xml2array($val, $disableDoctypes = false, $disableExternals = false) {
// Check doctype
if($disableDoctypes && preg_match('/\<\!DOCTYPE.+]\>/', $val)) {
throw new InvalidArgumentException('XML Doctype parsing disabled');
}
// Disable external entity loading
if($disableExternals) $oldVal = libxml_disable_entity_loader($disableExternals);
try {
$xml = new SimpleXMLElement($val);
$result = self::recursiveXMLToArray($xml);
} catch(Exception $ex) {
if($disableExternals) libxml_disable_entity_loader($oldVal);
throw $ex;
}
if($disableExternals) libxml_disable_entity_loader($oldVal);
return $result;
}
/**

View File

@ -85,8 +85,6 @@ require_once 'control/injector/Injector.php';
// Initialise the dependency injector as soon as possible, as it is
// subsequently used by some of the following code
$injector = new Injector(array('locator' => 'SilverStripeServiceConfigurationLocator'));
$injector->registerService(Config::inst());
Injector::set_inst($injector);
///////////////////////////////////////////////////////////////////////////////

View File

@ -65,9 +65,10 @@ class PaginatedList extends SS_ListDecorator {
}
/**
* Set the number of items displayed per page.
* Set the number of items displayed per page. Set to zero to disable paging.
*
* @param int $length
* @return $this
*/
public function setPageLength($length) {
$this->pageLength = $length;
@ -77,7 +78,8 @@ class PaginatedList extends SS_ListDecorator {
/**
* Sets the current page.
*
* @param int $page
* @param int $page Page index beginning with 1
* @return $this
*/
public function setCurrentPage($page) {
$this->pageStart = ($page - 1) * $this->getPageLength();
@ -182,10 +184,11 @@ class PaginatedList extends SS_ListDecorator {
* @return IteratorIterator
*/
public function getIterator() {
if($this->limitItems) {
$pageLength = $this->getPageLength();
if($this->limitItems && $pageLength) {
$tmptList = clone $this->list;
return new IteratorIterator(
$tmptList->limit($this->getPageLength(), $this->getPageStart())
$tmptList->limit($pageLength, $this->getPageStart())
);
} else {
return new IteratorIterator($this->list);
@ -325,14 +328,20 @@ class PaginatedList extends SS_ListDecorator {
* @return int
*/
public function CurrentPage() {
return floor($this->getPageStart() / $this->getPageLength()) + 1;
$pageLength = $this->getPageLength();
return $pageLength
? floor($this->getPageStart() / $pageLength) + 1
: 1;
}
/**
* @return int
*/
public function TotalPages() {
return ceil($this->getTotalItems() / $this->getPageLength());
$pageLength = $this->getPageLength();
return $pageLength
? ceil($this->getTotalItems() / $pageLength)
: min($this->getTotalItems(), 1);
}
/**
@ -372,10 +381,13 @@ class PaginatedList extends SS_ListDecorator {
* @return int
*/
public function LastItem() {
if ($start = $this->getPageStart()) {
return min($start + $this->getPageLength(), $this->getTotalItems());
$pageLength = $this->getPageLength();
if(!$pageLength) {
return $this->getTotalItems();
} elseif ($start = $this->getPageStart()) {
return min($start + $pageLength, $this->getTotalItems());
} else {
return min($this->getPageLength(), $this->getTotalItems());
return min($pageLength, $this->getTotalItems());
}
}

View File

@ -101,7 +101,7 @@ class SS_ClassLoader {
* @return bool
*/
public function classExists($class) {
return class_exists($class, false) || $this->getItemPath($class);
return class_exists($class, false) || interface_exists($class, false) || $this->getItemPath($class);
}
}

View File

@ -113,7 +113,7 @@ Used in side panels and action tabs
.cms table.ss-gridfield-table tr th input.ss-gridfield-sort:-ms-input-placeholder { font-style: italic; color: #ced5d7; }
.cms table.ss-gridfield-table tr th input.ss-gridfield-sort:placeholder { font-style: italic; color: #ced5d7; }
.cms table.ss-gridfield-table tr th input.ss-gridfield-sort:focus { -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; }
.cms table.ss-gridfield-table tr th span.non-sortable { display: block; }
.cms table.ss-gridfield-table tr th span.non-sortable { display: block; padding: 6px 8px; }
.cms table.ss-gridfield-table tr td { border-right: 1px solid rgba(0, 0, 0, 0.1); padding: 8px 8px; color: #666; }
.cms table.ss-gridfield-table tr td.bottom-all { -moz-border-radius-bottomleft: 5px; -webkit-border-bottom-left-radius: 5px; border-bottom-left-radius: 5px; -moz-border-radius-bottomright: 5px; -webkit-border-bottom-right-radius: 5px; border-bottom-right-radius: 5px; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2IwYmVjNyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzk4YWFiNiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b0bec7), color-stop(100%, #98aab6)); background-image: -moz-linear-gradient(#b0bec7, #98aab6); background-image: -webkit-linear-gradient(#b0bec7, #98aab6); background-image: linear-gradient(#b0bec7, #98aab6); padding: 4px 12px; }
.cms table.ss-gridfield-table tr td.bottom-all .datagrid-footer-message { text-align: center; padding-top: 6px; color: white; }

View File

@ -11,6 +11,7 @@
Used in side panels and action tabs
*/
.ss-uploadfield .clear { clear: both; }
.ss-uploadfield .description { margin-left: 0; }
.ss-uploadfield .middleColumn { min-width: 510px; max-width: 600px; width: 100%; margin-left: 0; clear: both; padding: 0; background: #fff; border: 1px solid #b3b3b3; -moz-border-radius: 4px; -webkit-border-radius: 4px; border-radius: 4px; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2VmZWZlZiIvPjxzdG9wIG9mZnNldD0iMTAlIiBzdG9wLWNvbG9yPSIjZmZmZmZmIi8+PHN0b3Agb2Zmc2V0PSI5MCUiIHN0b3AtY29sb3I9IiNmZmZmZmYiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNlZmVmZWYiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2dyYWQpIiAvPjwvc3ZnPiA='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #efefef), color-stop(10%, #ffffff), color-stop(90%, #ffffff), color-stop(100%, #efefef)); background-image: -moz-linear-gradient(#efefef, #ffffff 10%, #ffffff 90%, #efefef); background-image: -webkit-linear-gradient(#efefef, #ffffff 10%, #ffffff 90%, #efefef); background-image: linear-gradient(#efefef, #ffffff 10%, #ffffff 90%, #efefef); }
.ss-uploadfield .ss-uploadfield-item { margin: 0; padding: 15px; overflow: auto; }
.ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-preview { height: 60px; line-height: 60px; width: 80px; text-align: center; font-weight: bold; float: left; overflow: hidden; }

View File

@ -220,9 +220,9 @@ abstract class BulkLoader extends ViewableData {
// using $$includerelations flag as false, so that it only contain $db fields
$spec['fields'] = (array)singleton($this->objectClass)->fieldLabels(false);
$has_ones = singleton($this->objectClass)->has_one();
$has_manys = singleton($this->objectClass)->has_many();
$many_manys = singleton($this->objectClass)->many_many();
$has_ones = singleton($this->objectClass)->hasOne();
$has_manys = singleton($this->objectClass)->hasMany();
$many_manys = singleton($this->objectClass)->manyMany();
$spec['relations'] = (array)$has_ones + (array)$has_manys + (array)$many_manys;

View File

@ -120,7 +120,7 @@ class CsvBulkLoader extends BulkLoader {
$relationObj = $obj->{$this->relationCallbacks[$fieldName]['callback']}($val, $record);
}
if(!$relationObj || !$relationObj->exists()) {
$relationClass = $obj->has_one($relationName);
$relationClass = $obj->hasOneComponent($relationName);
$relationObj = new $relationClass();
//write if we aren't previewing
if (!$preview) $relationObj->write();

View File

@ -110,7 +110,11 @@ class FixtureBlueprint {
// Populate overrides
if($data) foreach($data as $fieldName => $fieldVal) {
// Defer relationship processing
if($obj->many_many($fieldName) || $obj->has_many($fieldName) || $obj->has_one($fieldName)) {
if(
$obj->manyManyComponent($fieldName)
|| $obj->hasManyComponent($fieldName)
|| $obj->hasOneComponent($fieldName)
) {
continue;
}
@ -127,7 +131,7 @@ class FixtureBlueprint {
// Populate all relations
if($data) foreach($data as $fieldName => $fieldVal) {
if($obj->many_many($fieldName) || $obj->has_many($fieldName)) {
if($obj->manyManyComponent($fieldName) || $obj->hasManyComponent($fieldName)) {
$obj->write();
$parsedItems = array();
@ -165,15 +169,15 @@ class FixtureBlueprint {
$parsedItems[] = $this->parseValue($item, $fixtures);
}
if($obj->has_many($fieldName)) {
if($obj->hasManyComponent($fieldName)) {
$obj->getComponents($fieldName)->setByIDList($parsedItems);
} elseif($obj->many_many($fieldName)) {
} elseif($obj->manyManyComponent($fieldName)) {
$obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
}
}
} else {
$hasOneField = preg_replace('/ID$/', '', $fieldName);
if($className = $obj->has_one($hasOneField)) {
if($className = $obj->hasOneComponent($hasOneField)) {
$obj->{$hasOneField.'ID'} = $this->parseValue($fieldVal, $fixtures, $fieldClass);
// Inject class for polymorphic relation
if($className === 'DataObject') {

View File

@ -12,7 +12,7 @@
* $this->get("your/url");
*
* // Submit a form on the page that you get in response
* $this->submitForm("MyForm_ID", array("Email" => "invalid email ^&*&^"));
* $this->submitForm("MyForm_ID", "action_dologin", array("Email" => "invalid email ^&*&^"));
*
* // Validate the content that is returned
* $this->assertExactMatchBySelector("#MyForm_ID p.error", array("That email address is invalid."));

View File

@ -4,7 +4,7 @@ SilverStripe CMS needs to be installed on a web server. Content authors and webs
to access a web-based GUI to do their day-to-day work. Website designers and developers require access to the files on
the server to update templates, website logic, and perform upgrades or maintenance.
Our web-based [PHP installer](/installation) can check if you meet the requirements listed below.
Our web-based [PHP installer](installation/) can check if you meet the requirements listed below.
## Web server software requirements

View File

@ -2,7 +2,7 @@
SilverStripe should be able to be installed on any Linux, Unix or *nix like OS as long as the correct server software is installed and configured (referred to a *nix in this document from herein). It is common that web hosting that you may use for your production SilverStripe application will be *nix based, here you may also want to use *nix locally to ensure how you develop locally mimics closely your production environment.
Is important to ensure you check the [Server Requirements](/Getting_Started/Installation/Server_Requirements) list before acquiring and installing SilverStripe on your *nix server (locally or otherwise).
Is important to ensure you check the [Server Requirements](/getting_started/server_requirements) list before acquiring and installing SilverStripe on your *nix server (locally or otherwise).
At a high level you will need a:
* Web server e.g. Apache, Nginx
@ -12,7 +12,7 @@ At a high level you will need a:
##*nix installation guides on the web
There are a number of good step by step guides covering server setups and installing of SilverStripe on the various flavours of *nix systems.
Note: Many of the following guides simply download SilverStripe as a zipped file. We recommend the use of [Composer](/Getting_Started/Composer/) once you get to the point of installing SilverStripe (though the choice is up to you). Always ensure you get the latest version if you are starting a new project.
Note: Many of the following guides simply download SilverStripe as a zipped file. We recommend the use of [Composer](/getting_started/composer/) once you get to the point of installing SilverStripe (though the choice is up to you). Always ensure you get the latest version if you are starting a new project.
###Known (but not exhaustive) list
* [How To Install Silverstripe on Your VPS](https://www.digitalocean.com/community/tutorials/how-to-install-silverstripe-on-your-vps)

View File

@ -5,7 +5,7 @@ done through [WampServer](http://www.wampserver.com/en/). This can be useful if
want a Microsoft Windows machine with a very similar environment.
Note: Installing on Microsoft's IIS webserver through Microsoft WebPI is likely to be easier, see
[installation-on-windows-pi](windows-pi).
[Windows with Web Platform Installer](other_installation_options/windows_platform_installer).
## Install WAMP

View File

@ -73,11 +73,11 @@ every page on the site, if that's easier.
Please make sure all code inside `*.php` files is wrapped in classes. Due to the way `[api:ManifestBuilder]`
includes all files with this extension, any **procedural code will be executed on every call**. The most common error here
is putting a test.php/phpinfo.php file in the document root. See [datamodel](/topics/datamodel) and [controllers](/topics/controller)
is putting a test.php/phpinfo.php file in the document root. See [datamodel](/developer_guides/data_model_and_orm) and [controllers](/developer_guides/controllers)
for ways how to structure your code.
Also, please check that you have PHP enabled on the webserver, and you're running PHP 5.1 or later.
The web-based [SilverStripe installer](/installation) can help you with this.
The web-based [SilverStripe installer](/getting_started/installation) can help you with this.
## I've got file permission problems during installation

View File

@ -107,4 +107,4 @@ e.g. `/etc/nginx/sites-enabled/mysite`:
include /etc/nginx/silverstripe.conf;
}
For more information on nginx configuration, please see the [nginx installation](nginx) page.
For more information on nginx configuration, please see the [nginx installation](configure_nginx) page.

View File

@ -3,12 +3,12 @@
These instructions show you how to install SilverStripe on any web server.
The best way to install from the source code is to use [Composer](../composer).
Check out our operating system specific guides for [Linux](linux_unix),
[Windows Server](windows-pi) and [Mac OSX](mac-osx).
[Windows Server](windows) and [Mac OSX](mac_osx).
## Installation Steps
* [Download](http://silverstripe.org/download) the installer package
* Make sure the webserver has MySQL and PHP support. See [Server Requirements](server-requirements) for more information.
* Make sure the webserver has MySQL and PHP support. See [Server Requirements](../server_requirements) for more information.
* Unpack the installer somewhere into your web-root. Usually the www folder or similar. Most downloads from SilverStripe
are compressed tarballs. To extract these files you can either do them natively (Unix) or with 7-Zip (Windows)
* Visit your sites domain or IP address in your web browser.
@ -18,7 +18,7 @@ name' and the default login details. Follow the questions and select the *instal
## Issues?
If the above steps don't work for any reason have a read of the [Common Problems](common-problems) section.
If the above steps don't work for any reason have a read of the [Common Problems](common_problems) section.
<div class="notice" markdown="1">
SilverStripe ships with default rewriting rules specific to your web server. Apart from

View File

@ -3,7 +3,7 @@
Composer is a package management tool for PHP that lets you install and upgrade SilverStripe and its modules. Although installing Composer is one extra step, it will give you much more flexibility than just downloading the file from silverstripe.org. This is our recommended way of downloading SilverStripe and managing your code.
For more information about Composer, visit [its website](http://getcomposer.org/).
We also have separate instructions for [installing modules with Composer](/topics/modules).
We also have separate instructions for [installing modules with Composer](/developer_guides/extending/modules).
# Basic usage
@ -36,7 +36,7 @@ If you already have composer installed you can update it by running:
Composer updates regularly, so you should run this command fairly often. These instructions assume you are running the latest version.
## Installing Composer on Windows WAMP
For those that use WAMP as a development environment, [detailed information is available on installing using Composer.](/installation/windows-wamp#install-wamp)
For those that use WAMP as a development environment, [detailed information is available on installing using Composer.](/getting_started/installation/windows)
## Create a new site
@ -108,6 +108,70 @@ So, your deployment process, as it relates to Composer, should be as follows:
* Deploy your project code base, using the deployment tool of your choice.
* Run `composer install --no-dev -o` on your production version.
## Composer managed modules, Git and .gitignore
Modules and themes managed by composer should not be committed with your projects source code. For more details read [Should I commit the dependencies in my vendor directory?](https://getcomposer.org/doc/faqs/should-i-commit-the-dependencies-in-my-vendor-directory.md).
Since SilverStripe modules are installed in to thier own folder, you have to manage your [.gitignore](http://git-scm.com/docs/gitignore) to ensure they are ignored from your repository.
Here is the default SilverStripe [.gitignore](http://git-scm.com/docs/gitignore) with the forum module ignored
assets/*
_ss_environment.php
tools/phing-metadata
silverstripe-cache
.buildpath
.project
.settings
.idea
.DS_Store
vendor/
# Don't include the forum module, as this will be installed with composer
forum
In large projects it can get difficult to manage your [.gitignore](http://git-scm.com/docs/gitignore) and ensure it contains all composer managed modules and themes.
You can automate this with the [SSAutoGitIgnore](https://github.com/guru-digital/SSAutoGitIgnore/) package.
This package will maintain your [.gitignore](http://git-scm.com/docs/gitignore) and ensure it is kept up to date with your composer managed modules without affecting custom ignores. Once installed and setup, it will automatically run every time you install, remove or update modules using composer.
### Installing and enabling the SSAutoGitIgnore package
Include the package in your project by running this command
composer require gdmedia/ss-auto-git-ignore
Edit your composer.json and insert
"scripts": {
"post-update-cmd": "GDM\\SSAutoGitIgnore\\UpdateScript::Go"
}
This will instruct composer to run SSAutoGitIgnore after every update. SSAutoGitIgnore will then ensure composer managed models and themes are correctly added to your [.gitignore](http://git-scm.com/docs/gitignore).
For more information about SSAutoGitIgnore, see the [SSAutoGitIgnore home page](https://github.com/guru-digital/SSAutoGitIgnore/).
For more information about post-updated-cmd and scripts, read the ["Scripts" chapter of the Composer documentation](https://getcomposer.org/doc/articles/scripts.md).
Full example of composer.json with the SSAutoGitIgnore installed and enabled
{
"name": "silverstripe/installer",
"description": "The SilverStripe Framework Installer",
"require": {
"php": ">=5.3.2",
"silverstripe/cms": "3.0.*",
"silverstripe/framework": "3.0.*",
"silverstripe-themes/simple": "*",
"gdmedia/ss-auto-git-ignore": "*"
},
"require-dev": {
"silverstripe/compass": "*",
"silverstripe/docsviewer": "*"
},
"scripts": {
"post-update-cmd": "GDM\\SSAutoGitIgnore\\UpdateScript::Go"
},
"minimum-stability": "dev"
}
# Dev Environments for Contributing Code {#contributing}
So you want to contribute to SilverStripe? Fantastic! You can do this with composer too.
@ -139,7 +203,7 @@ and remove the `@stable` markers from the `silverstripe/cms` and `silverstripe/f
Another `composer update --dev` call will now fetch from the development branch instead.
Note that you can also convert an existing composer project with these steps.
Please read the ["Contributing Code"](/misc/contributing/code) documentation to find out how to
Please read the ["Contributing Code"](/contributing/code) documentation to find out how to
create forks and send pull requests.
# Advanced usage
@ -290,9 +354,9 @@ which triggers their installation into the correct path.
### How do I convert an existing project to Composer?
The easiest way is to follow the [upgrading](/installation/upgrading) instructions
and switch to a newer release. Alternatively, copy the `composer.json` file from
a newer release, and adjust the version settings in the "require" section to your needs.
Copy the `composer.json` file from a newer release, and adjust the
version settings in the "require" section to your needs. Then refer to
the [upgrading documentation](/upgrading).
You'll also need to update your webserver configuration
from there (`.htaccess` or `web.config` files), in order to prevent
web access to the composer-generated files.

View File

@ -93,9 +93,9 @@ This is my `_ss_environment.php` file. I have it placed in `/var`, as each of th
define('SS_DEFAULT_ADMIN_USERNAME', '<email>');
define('SS_DEFAULT_ADMIN_PASSWORD', '<password>');
// This causes errors to be written to the silverstripe.log file in the same directory as this file, so /var.
// Before PHP 5.3.0, you'll need to use dirname(__FILE__) instead of __DIR__
define('SS_ERROR_LOG', __DIR__ . '/silverstripe.log');
// This causes errors to be written to the BASE_PATH/silverstripe.log file.
// Path must be relative to BASE_PATH
define('SS_ERROR_LOG', 'silverstripe.log');
// This is used by sake to know which directory points to which URL
global $_FILE_TO_URL_MAPPING;
@ -119,7 +119,7 @@ This is my `_ss_environment.php` file. I have it placed in `/var`, as each of th
| `SS_ENVIRONMENT_TYPE`| The environment type: dev, test or live.|
| `SS_DEFAULT_ADMIN_USERNAME`| The username of the default admin. This is a user with administrative privileges.|
| `SS_DEFAULT_ADMIN_PASSWORD`| The password of the default admin. This will not be stored in the database.|
| `SS_USE_BASIC_AUTH`| Protect the site with basic auth (good for test sites).<br/>When using CGI/FastCGI with Apache, you will have to add the `RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]` rewrite rule to your `.htaccess` file|
| `SS_USE_BASIC_AUTH`| Protect the site with basic auth (good for test sites).<br/>When using CGI/FastCGI with Apache, you will have to add the `RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]` rewrite rule to your `.htaccess` file|
| `SS_SEND_ALL_EMAILS_TO`| If you set this define, all emails will be redirected to this address.|
| `SS_SEND_ALL_EMAILS_FROM`| If you set this define, all emails will be send from this address.|
| `SS_ERROR_LOG` | |
| `SS_ERROR_LOG` | Relative path to the log file |

View File

@ -117,7 +117,7 @@ Example: `mysite/code/MyClass.php`
To help with namespacing common class names (like Database) it is recommended to use a prefix convention `SS_ClassName` but the filename will remain `ClassName.php`.
See [directory-structure](/topics/directory-structure) for more information.
See [directory structure](directory_structure) for more information.
## Coding Style
@ -149,7 +149,7 @@ When a string is literal (contains no variable substitutions), the apostrophe or
When a literal string itself contains apostrophes, it is permitted to demarcate the string with quotation marks or "double quotes".
:::php
$greeting = "She said 'hello'";
$greeting = "They said 'hello'";
This syntax is preferred over escaping apostrophes as it is much easier to read.
@ -458,5 +458,4 @@ which are licensed under BSD (see [license](http://framework.zend.com/license)).
## Related
* [Topics: CSS](/topics/css)
* [Reference: CMS Architecture](/reference/cms-archirecture)
* [Reference: CMS Architecture](/developer_guides/customising_the_admin_interface/cms_architecture)

View File

@ -178,7 +178,7 @@ would create a new tab called "New Tab", and a single "Author" textfield inside.
</div>
We have added two fields: A simple `[api:TextField]` and a `[api:DateField]`.
There are many more fields available in the default installation, listed in ["form field types"](/developer_guides/forms/fields/common_subclasses).
There are many more fields available in the default installation, listed in ["form field types"](/developer_guides/forms/field_types/common_subclasses).
:::php
return $fields;

View File

@ -9,7 +9,7 @@ This tutorial is deprecated, and has been replaced by Lessons 7, 8, 9, and 10 in
## Overview
This tutorial explores the relationship and management of [DataObjects](/developer_guides/model/dataobject). It builds on the [second tutorial](/tutorials/extending_a_basic_site) where we learnt how to define
This tutorial explores the relationship and management of [DataObjects](api:DataObject). It builds on the [second tutorial](/tutorials/extending_a_basic_site) where we learnt how to define
additional fields on our models, and attach images to them.
## What are we working towards?
@ -65,8 +65,9 @@ Let's create the `Student` and `Project` objects.
The relationships are defined through the `$has_one`
and `$has_many` properties on the objects.
The array keys declares the name of the relationship,
the array values contain the class name (see the ["database structure"](/developer_guides/model/database_structure)
and ["datamodel"](/developer_guides/model/data_model_and_orm) topics for more information).
the array values contain the class name
(see the ["datamodel"](/developer_guides/model/data_model_and_orm)
topic for more information).
As you can see, only the `Project` model extends `Page`,
while `Student` is a plain `DataObject` subclass.

View File

@ -5,7 +5,7 @@ introduction: The tutorials below take a step by step look at how to build a Sil
<div class="alert" markdown="1">
These tutorials are deprecated, and have been replaced by the new [Lessons](http://silverstripe.org/learn/lessons) section.
</div>
[CHIDLREN]
## Video lessons
These include video screencasts, written tutorials and code examples to get you started working with SilverStripe websites.

View File

@ -12,8 +12,8 @@ information.
All data tables in SilverStripe are defined as subclasses of [api:DataObject]. The [api:DataObject] class represents a
single row in a database table, following the ["Active Record"](http://en.wikipedia.org/wiki/Active_record_pattern)
design pattern. Database Columns are is defined as [Data Types](data_types_and_casting) in the static `$db` variable
along with any [relationships](../relations) defined as `$has_one`, `$has_many`, `$many_many` properties on the class.
design pattern. Database Columns are defined as [Data Types](data_types_and_casting) in the static `$db` variable
along with any [relationships](relations) defined as `$has_one`, `$has_many`, `$many_many` properties on the class.
Let's look at a simple example:
@ -222,7 +222,7 @@ Notice that we can step into the loop safely without having to check if `$player
// do something here
}
See the [Lists](../lists) documentation for more information on dealing with [api:SS_List] instances.
See the [Lists](lists) documentation for more information on dealing with [api:SS_List] instances.
## Returning a single DataObject
@ -401,7 +401,7 @@ Remove both Sam and Sig..
'Surname' => 'Minnée',
));
And removing Sig and Sam with that are either age 17 or 74.
And removing Sig and Sam with that are either age 17 or 43.
:::php
$players = Player::get()->exclude(array(
@ -409,7 +409,7 @@ And removing Sig and Sam with that are either age 17 or 74.
'Age' => array(17, 43)
));
// SELECT * FROM Player WHERE ("FirstName" NOT IN ('Sam','Sig) OR "Age" NOT IN ('17', '74));
// SELECT * FROM Player WHERE ("FirstName" NOT IN ('Sam','Sig) OR "Age" NOT IN ('17', '43'));
You can use [SearchFilters](searchfilters) to add additional behavior to your `exclude` command.
@ -512,7 +512,7 @@ whenever a new object is created.
<div class="notice" markdown='1'>
Note: Alternatively you can set defaults directly in the database-schema (rather than the object-model). See
[Data Types and Casting](data-types) for details.
[Data Types and Casting](data_types_and_casting) for details.
</div>
## Subclasses
@ -548,7 +548,7 @@ The data for the following classes would be stored across the following tables:
- LastEdited: Datetime
- Title: Varchar
- Content: Text
NewsArticle:
NewsPage:
- ID: Int
- Summary: Text
@ -558,7 +558,7 @@ Accessing the data is transparent to the developer.
$news = NewsPage::get();
foreach($news as $article) {
echo $news->Title;
echo $article->Title;
}
The way the ORM stores the data is this:
@ -575,7 +575,7 @@ example above, NewsSection didn't have its own data, so an extra table would be
* In all the tables, ID is the primary key. A matching ID number is used for all parts of a particular record:
record #2 in Page refers to the same object as record #2 in `[api:SiteTree]`.
To retrieve a news article, SilverStripe joins the [api:SiteTree], [api:Page] and NewsArticle tables by their ID fields.
To retrieve a news article, SilverStripe joins the [api:SiteTree], [api:Page] and NewsPage tables by their ID fields.
## Related Documentation

View File

@ -204,6 +204,27 @@ The relationship can also be navigated in [templates](../templates).
<% end_if %>
<% end_with %>
To specify multiple $many_manys between the same classes, use the dot notation to distinguish them like below:
:::php
<?php
class Category extends DataObject {
private static $many_many = array(
'Products' => 'Product',
'FeaturedProducts' => 'Product'
);
}
class Product extends DataObject {
private static $belongs_many_many = array(
'Categories' => 'Category.Products',
'FeaturedInCategories' => 'Category.FeaturedProducts'
);
}
## many_many or belongs_many_many?
If you're unsure about whether an object should take on `many_many` or `belongs_many_many`, the best way to think about it is that the object where the relationship will be edited (i.e. via checkboxes) should contain the `many_many`. For instance, in a `many_many` of Product => Categories, the `Product` should contain the `many_many`, because it is much more likely that the user will select Categories for a Product than vice-versa.

View File

@ -54,7 +54,7 @@ Example: Disallow creation of new players if the currently logged-in player is n
Triggered before executing *delete()* on an existing object.
Example: Checking for a specific [permission](/reference/permission) to delete this type of object. It checks if a
Example: Checking for a specific [permission](permissions) to delete this type of object. It checks if a
member is logged in who belongs to a group containing the permission "PLAYER_DELETE".
:::php

View File

@ -103,7 +103,7 @@ Creates a map based on the first two columns of the query result.
## Related Documentation
* [Introduction to the Data Model and ORM](../data_model_and_orm)
* [Introduction to the Data Model and ORM](data_model_and_orm)
## API Documentation

View File

@ -5,7 +5,7 @@ summary: Add Indexes to your Data Model to optimize database queries.
It is sometimes desirable to add indexes to your data model, whether to optimize queries or add a uniqueness constraint
to a field. This is done through the `DataObject::$indexes` map, which maps index names to descriptor arrays that
represent each index. There's several supported notations:
represent each index. There're several supported notations:
:::php
<?php
@ -19,7 +19,7 @@ represent each index. There's several supported notations:
);
}
The `<index-name>` can be an an arbitrary identifier in order to allow for more than one index on a specific database
The `<index-name>` can be an arbitrary identifier in order to allow for more than one index on a specific database
column. The "advanced" notation supports more `<type>` notations. These vary between database drivers, but all of them
support the following:
@ -27,8 +27,8 @@ support the following:
* `unique`: Index plus uniqueness constraint on the value
* `fulltext`: Fulltext content index
In order to use more database specific or complex index notations, we also support raw SQL for as a value in the
`$indexes` definition. Keep in mind this will likely make your code less portable between databases.
In order to use more database specific or complex index notations, we also support raw SQL as a value in the
`$indexes` definition. Keep in mind that using raw SQL is likely to make your code less portable between DBMSs.
**mysite/code/MyTestObject.php**
@ -52,4 +52,4 @@ In order to use more database specific or complex index notations, we also suppo
## API Documentation
* [api:DataObject]
* [api:DataObject]

View File

@ -1,8 +1,8 @@
# Dynamic Default Values
The [api:DataObject::$defaults] array allows you to specify simple static values to be the default value for when a
record is created, but in many situations default values needs to be dynamically calculated. In order to do this, the
`[api:DataObject->populateDefaults()]` method will need to be overloaded.
The [api:DataObject::$defaults] array allows you to specify simple static values to be the default values when a
record is created, but in many situations default values need to be dynamically calculated. In order to do this, the
[api:DataObject->populateDefaults()] method will need to be overloaded.
This method is called whenever a new record is instantiated, and you must be sure to call the method on the parent
object!
@ -33,4 +33,4 @@ methods. For example:
$this->FullTitle = $this->Title;
}
parent::populateDefaults();
}
}

View File

@ -2,7 +2,7 @@
The [api:SS_List] class is designed to return a flat list of records.
These lists can get quite long, and hard to present on a single list.
[Pagination](/howto/pagination) is one way to solve this problem,
[Pagination](/templates/how_tos/pagination) is one way to solve this problem,
by splitting up the list into multiple pages.
In this howto, we present an alternative to pagination:
@ -144,4 +144,5 @@ The final step is the render this into the template using the [api:GroupedList->
## Related
* [Howto: "Pagination"](/howto/pagination)
* [Howto: "Pagination"](/templates/how_tos/pagination)

View File

@ -393,7 +393,7 @@ A page will normally contain some content and potentially a form of some kind. F
SilverStripe log-in form. If you are on such a page, the `$Form` variable will contain the HTML content of the form.
Placing it just below `$Content` is a good default.
You can add your own forms by implementing new form instances (see the [Forms tutorial](../tutorials/forms)).
You can add your own forms by implementing new form instances (see the [Forms tutorial](/tutorials/forms)).
## Related

View File

@ -22,7 +22,7 @@ Requiring assets from the template is restricted compared to the PHP API.
## PHP Requirements API
It is common practice to include most Requirements either in the *init()*-method of your [controller](../controller), or
It is common practice to include most Requirements either in the *init()*-method of your [controller](../controllers/), or
as close to rendering as possible (e.g. in `[api:FormField]`.
:::php
@ -176,17 +176,19 @@ careful when messing with the order of requirements.
By default, SilverStripe includes all Javascript files at the bottom of the page body, unless there's another script
already loaded, then, it's inserted before the first `<script>` tag. If this causes problems, it can be configured.
**mysite/_config/app.yml**
:::yml
Requirements:
write_js_to_body: true
force_js_to_bottom: true
:::php
Requirements::set_force_js_to_bottom(true);
`Requirements.force_js_to_bottom`, will force SilverStripe to write the Javascript to the bottom of the page body, even
if there is an earlier script tag.
If the Javascript files are preferred to be placed in the `<head>` tag rather than in the `<body>` tag,
`Requirements.write_js_to_body` should be set to false.
:::php
Requirements::set_force_js_to_bottom(true);
## API Documentation
* [api:Requirements]
* [api:Requirements]

View File

@ -46,9 +46,10 @@ your entire project for the appropriate `.ss` files located in `template` direct
It will each and prioritize templates in the following priority:
1. mysite (or other name given to site folder)
2. themes
3. modules
4. framework.
2. module-specific themes (e.g. themes/simple_blog)
3. themes (e.g. themes/simple)
4. modules (e.g. blog)
5. framework
<div class="warning">
Whenever you add or remove template files, rebuild the manifest by visiting `http://yoursite.com/?flush=1`. You can

View File

@ -47,6 +47,30 @@ located within the `themes` directory.
![themes:basicfiles.gif](../../_images/basicfiles.gif)
Your theme can also be organised into split folders for each module it caters for.
```
themes
blackcandy
css
style.css
images
templates
Page.ss
Layout
Page.ss
Includes
blackcandy_blog
css
blog.css
images
templates
Layout
BlogHolder.ss
BlogEntry.ss
Includes
```
## Submitting your theme to SilverStripe
If you want to submit your theme to the SilverStripe directory then check

View File

@ -31,4 +31,4 @@ top level menu with a nested second level using the `Menu` loop and a `Children`
## Related
* [Template Syntax](../syntax)
* [Common Variables](../command_variables)
* [Common Variables](../common_variables)

View File

@ -88,6 +88,9 @@ when using custom lists.
$pages = new PaginatedList(Page::get(), $this->request);
$pages->setPageLength(25);
If you set this limit to 0 it will disable paging entirely, effectively causing it to appear as a single page
list.
## Template Variables
| Variable | Description |

View File

@ -39,7 +39,7 @@ routing.
</div>
<div class="alert" markdown="1">
Make sure that after you have modified the `routes.yml` file, that you clear your SilverStripe caches using `flush=1`.
Make sure that after you have modified the `routes.yml` file, that you clear your SilverStripe caches using `?flush=1`.
</div>
**mysite/_config/routes.yml**
@ -70,7 +70,7 @@ Action methods can return one of four main things:
* an array. In this case the values in the array are available in the templates and the controller completes as usual by returning a [api:SS_HTTPResponse] with the body set to the current template.
* `HTML`. SilverStripe will wrap the `HTML` into a `SS_HTTPResponse` and set the status code to 200.
* an [api:SS_HTTPResponse] containing a manually defined `status code` and `body`.
* an [api:SS_HTTPResponse_Exception]. A special type of response which indicates a error. By returning the exception, the execution pipeline can adapt and display any error handlers.
* an [api:SS_HTTPResponse_Exception]. A special type of response which indicates an error. By returning the exception, the execution pipeline can adapt and display any error handlers.
**mysite/code/controllers/TeamController.php**
@ -144,8 +144,8 @@ If a template of that name does not exist, then SilverStripe will fall back to t
Controller actions can use `renderWith` to override this template selection process as in the previous example with
`htmlaction`. `MyCustomTemplate.ss` would be used rather than `TeamsController`.
For more information about templates, inheritance and how to rendering into views, See the
[Templates and Views](templates) documentation.
For more information about templates, inheritance and how to render into views, See the
[Templates and Views](../templates) documentation.
## Link
@ -154,7 +154,7 @@ Each controller should define a `Link()` method. This should be used to avoid ha
**mysite/code/controllers/TeamController.php**
:::php
public function Link($action = null) {
public function Link($action = null) {
return Controller::join_links('teams', $action);
}

View File

@ -135,6 +135,14 @@ start parsing variables and the appropriate controller action AFTER the `//`).
## URL Handlers
<div class="alert" markdown="1">
You **must** use the **$url_handlers** static array described here if your URL
pattern does not use the Controller class's default pattern of
`$Action//$ID/$OtherID`. If you fail to do so, and your pattern has more than
2 parameters, your controller will throw the error "I can't handle sub-URLs of
a *class name* object" with HTTP status 404.
</div>
In the above example the URLs were configured using the [api:Director] rules in the **routes.yml** file. Alternatively
you can specify these in your Controller class via the **$url_handlers** static array. This array is processed by the
[api:RequestHandler] at runtime once the `Controller` has been matched.
@ -154,12 +162,42 @@ This is useful when you want to provide custom actions for the mapping of `teams
);
private static $url_handlers = array(
'staff/$ID/$Name' => 'payroll'
'staff/$ID/$Name' => 'payroll',
'coach/$ID/$Name' => 'payroll'
);
The syntax for the `$url_handlers` array users the same pattern matches as the `YAML` configuration rules.
Now lets consider a more complex example from a real project, where using
**$url_handlers** is mandatory. In this example, the URLs are of the form
`http://example.org/feed/go/`, followed by 5 parameters. The PHP controller
class specifies the URL pattern in `$url_handlers`. Notice that it defines 5
parameters.
:::php
class FeedController extends ContentController {
private static $allowed_actions = array('go');
private static $url_handlers = array(
'go/$UserName/$AuthToken/$Timestamp/$OutputType/$DeleteMode' => 'go'
);
public function go() {
$this->validateUser(
$this->request->param('UserName'),
$this->request->param('AuthToken')
);
/* more processing goes here */
}
The YAML rule, in contrast, is simple. It needs to provide only enough
information for the framework to choose the desired controller.
:::yaml
Director:
rules:
'feed': 'FeedController'
## Links
* [api:Controller] API documentation

View File

@ -45,7 +45,7 @@ In practice, this looks like:
FormAction::create("doSayHello")->setTitle("Say hello")
);
$required = new RequiredFields('Name')
$required = new RequiredFields('Name');
$form = new Form($this, 'HelloForm', $fields, $actions, $required);

View File

@ -0,0 +1,407 @@
# UploadField
## Introduction
The UploadField will let you upload one or multiple files of all types, including images. But that's not all it does - it will also link the uploaded file(s) to an existing relation and let you edit the linked files as well. That makes it flexible enough to sometimes even replace the GridField, like for instance in creating and managing a simple gallery.
## Usage
The field can be used in three ways: To upload a single file into a `has_one` relationship,or allow multiple files into a `has_many` or `many_many` relationship, or to act as a stand
alone uploader into a folder with no underlying relation.
## Validation
Although images are uploaded and stored on the filesystem immediately after selection,the value (or values) of this field will not be written to any related record until the record is saved and successfully validated. However, any invalid records will still persist across form submissions until explicitly removed or replaced by the user.
Care should be taken as invalid files may remain within the filesystem until explicitly removed.
### Single fileupload
The following example adds an UploadField to a page for single fileupload, based on a has_one relation:
```php
class GalleryPage extends Page {
private static $has_one = array(
'SingleImage' => 'Image'
);
function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab(
'Root.Upload',
$uploadField = new UploadField(
$name = 'SingleImage',
$title = 'Upload a single image'
)
);
return $fields;
}
}
```
The UploadField will auto-detect the relation based on it's `name` property, and save it into the GalleyPages' `SingleImageID` field. Setting the `setAllowedMaxFileNumber` to 1 will make sure that only one image can ever be uploaded and linked to the relation.
### Multiple fileupload
Enable multiple fileuploads by using a many_many (or has_many) relation. Again, the `UploadField` will detect the relation based on its $name property value:
```php
class GalleryPage extends Page {
private static $many_many = array(
'GalleryImages' => 'Image'
);
function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab(
'Root.Upload',
$uploadField = new UploadField(
$name = 'GalleryImages',
$title = 'Upload one or more images (max 10 in total)'
)
);
$uploadField->setAllowedMaxFileNumber(10);
return $fields;
}
}
class GalleryPage_Controller extends Page_Controller {
}
```
```php
class GalleryImageExtension extends DataExtension {
private static $belongs_many_many = array('Galleries' => 'GalleryPage);
}
```
```yml
Image:
extensions:
- GalleryImageExtension
```
<div class="notice" markdown='1'>
In order to link both ends of the relationship together it's usually advisable to extend Image with the necessary $has_one, $belongs_to, $has_many or $belongs_many_many. In particular, a DataObject with $has_many Images will not work without this specified explicitly.
</div>
## Configuration
### Overview
The field can either be configured on an instance level with the various getProperty and setProperty functions, or globally by overriding the YAML defaults.
See the [Configuration Reference](uploadfield#configuration-reference) section for possible values.
Example: mysite/_config/uploadfield.yml
```yml
after: framework#uploadfield
---
UploadField:
defaultConfig:
canUpload: false
```
### Set a custom folder
This example will save all uploads in the `/assets/customfolder/` folder. If the folder doesn't exist, it will be created.
```php
$fields->addFieldToTab(
'Root.Upload',
$uploadField = new UploadField(
$name = 'GalleryImages',
$title = 'Please upload one or more images' )
);
$uploadField->setFolderName('customfolder');
```
### Limit the allowed filetypes
`AllowedExtensions` defaults to the `File.allowed_extensions` configuration setting, but can be overwritten for each UploadField:
```php
$uploadField->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif'));
```
Entire groups of file extensions can be specified in order to quickly limit types to known file categories.
```php
$uploadField->setAllowedFileCategories('image', 'doc');
```
This will limit files to the following extensions: bmp gif jpg jpeg pcx tif png alpha als cel icon ico ps doc docx txt rtf xls xlsx pages ppt pptx pps csv html htm xhtml xml pdf.
`AllowedExtensions` can also be set globally via the [YAML configuration](/developer_guides/configuration/configuration/#configuration-yaml-syntax-and-rules), for example you may add the following into your mysite/_config/config.yml:
```yaml
File:
allowed_extensions:
- 7zip
- xzip
```
### Limit the maximum file size
`AllowedMaxFileSize` is by default set to the lower value of the 2 php.ini configurations: `upload_max_filesize` and `post_max_size`. The value is set as bytes.
NOTE: this only sets the configuration for your UploadField, this does NOT change your server upload settings, so if your server is set to only allow 1 MB and you set the UploadField to 2 MB, uploads will not work.
```php
$sizeMB = 2; // 2 MB
$size = $sizeMB * 1024 * 1024; // 2 MB in bytes
$this->getValidator()->setAllowedMaxFileSize($size);
```
You can also specify a default global maximum file size setting in your config for different file types. This is overridden when specifying the max allowed file size on the UploadField instance.
```yaml
Upload_Validator:
default_max_file_size:
'[image]': '1m'
'[doc]': '5m'
'jpeg': 2000
```
### Preview dimensions
Set the dimensions of the image preview. By default the max width is set to 80 and the max height is set to 60.
```php
$uploadField->setPreviewMaxWidth(100);
$uploadField->setPreviewMaxHeight(100);
```
### Disable attachment of existing files
This can force the user to upload a new file, rather than link to the already existing file library
```php
$uploadField->setCanAttachExisting(false);
```
### Disable uploading of new files
Alternatively, you can force the user to only specify already existing files in the file library
```php
$uploadField->setCanUpload(false);
```
### Automatic or manual upload
By default, the UploadField will try to automatically upload all selected files. Setting the `autoUpload` property to false, will present you with a list of selected files that you can then upload manually one by one:
```php
$uploadField->setAutoUpload(false);
```
### Change Detection
The CMS interface will automatically notify the form containing
an UploadField instance of changes, such as a new upload,
or the removal of an existing upload (through a `dirty` event).
The UI can then choose an appropriate response (e.g. highlighting the "save" button). If the UploadField doesn't save into a relation, there's technically no saveable change (the upload has already happened), which is why this feature can be disabled on demand.
```php
$uploadField->setConfig('changeDetection', false);
```
### Build a simple gallery
A gallery most times needs more then simple images. You might want to add a description, or maybe some settings to define a transition effect for each slide.
First create a [DataExtension](/developer_guides/extending/extensions) like this:
```php
class GalleryImage extends DataExtension {
private static $db = array(
'Description' => 'Text'
);
private static $belongs_many_many = array(
'GalleryPage' => 'GalleryPage'
);
}
```
Now register the DataExtension for the Image class in your mysite/_config/config.yml:
```yml
Image:
extensions:
- GalleryImage
```
<div class="notice" markdown='1'>
Note: Although you can subclass the Image class instead of using a DataExtension, this is not advisable. For instance: when using a subclass, the 'From files' button will only return files that were uploaded for that subclass, it won't recognize any other images!
</div>
### Edit uploaded images
By default the UploadField will let you edit the following fields: *Title, Filename, Owner and Folder*. The fileEditFields` configuration setting allows you you alter these settings. One way to go about this is create a `getCustomFields` function in your GalleryImage object like this:
```php
class GalleryImage extends DataExtension {
...
function getCustomFields() {
$fields = new FieldList();
$fields->push(new TextField('Title', 'Title'));
$fields->push(new TextareaField('Description', 'Description'));
return $fields;
}
}
```
Then, in your GalleryPage, tell the UploadField to use this function:
```php
$uploadField->setFileEditFields('getCustomFields');
```
In a similar fashion you can use 'setFileEditActions' to set the actions for the editform, or 'fileEditValidator' to determine the validator (e.g. RequiredFields).
### Configuration Reference
- `setAllowedMaxFileNumber`: (int) php validation of allowedMaxFileNumber only works when a db relation is available, set to null to allow unlimited if record has a has_one and allowedMaxFileNumber is null, it will be set to 1.
- `setAllowedFileExtensions`: (array) List of file extensions allowed.
- `setAllowedFileCategories`: (array|string) List of types of files allowed. May be any of 'image', 'audio', 'mov', 'zip', 'flash', or 'doc'.
- `setAutoUpload`: (boolean) Should the field automatically trigger an upload once a file is selected?
- `setCanAttachExisting`: (boolean|string) Can the user attach existing files from the library. String values are interpreted as permission codes.
- `setCanPreviewFolder`: (boolean|string) Can the user preview the folder files will be saved into? String values are interpreted as permission codes.
- `setCanUpload`: (boolean|string) Can the user upload new files, or just select from existing files. String values are interpreted as permission codes.
- `setDownloadTemplateName`: (string) javascript template used to display already uploaded files, see javascript/UploadField_downloadtemplate.js.
- `setFileEditFields`: (FieldList|string) FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm (Example: 'getCMSFields').
- `setFileEditActions`: (FieldList|string) FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm (Example: 'getCMSActions').
- `setFileEditValidator`: (string) Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm (Example: 'getCMSValidator').
- `setOverwriteWarning`: (boolean) Show a warning when overwriting a file.
- `setPreviewMaxWidth`: (int).
- `setPreviewMaxHeight`: (int).
- `setTemplateFileButtons`: (string) Template name to use for the file buttons.
- `setTemplateFileEdit`: (string) Template name to use for the file edit form.
- `setUploadTemplateName`: (string) javascript template used to display uploading files, see javascript/UploadField_uploadtemplate.js.
- `setCanPreviewFolder`: (boolean|string) Is the upload folder visible to uploading users? String values are interpreted as permission codes.
Certain default values for the above can be configured using the YAML config system.
```yaml
UploadField:
defaultConfig:
autoUpload: true
allowedMaxFileNumber:
canUpload: true
canAttachExisting: 'CMS_ACCESS_AssetAdmin'
canPreviewFolder: true
previewMaxWidth: 80
previewMaxHeight: 60
uploadTemplateName: 'ss-uploadfield-uploadtemplate'
downloadTemplateName: 'ss-uploadfield-downloadtemplate'
overwriteWarning: true # Warning before overwriting existing file (only relevant when Upload: replaceFile is true)
```
The above settings can also be set on a per-instance basis by using `setConfig` with the appropriate key.
The `Upload_Validator` class has configuration options for setting the `default_max_file_size`.
```yaml
Upload_Validator:
default_max_file_size:
'[image]': '1m'
'[doc]': '5m'
'jpeg': 2000
```
You can specify the file extension or the app category (as specified in the `File` class) in square brackets. It supports setting the file size in bytes or using the syntax supported by `File::ini2bytes()`.
You can also configure the underlying `[api:Upload]` class, by using the YAML config system.
```yaml
Upload:
# Globally disables automatic renaming of files and displays a warning before overwriting an existing file
replaceFile: true
uploads_folder: 'Uploads'
```
## Using the UploadField in a frontend form
The UploadField can be used in a frontend form, given that sufficient attention is given to the permissions granted to non-authorised users.
By default Image::canDelete and Image::canEdit do not require admin privileges, so make sure you override the methods in your Image extension class.
For instance, to generate an upload form suitable for saving images into a user-defined gallery the below code could be used:
*In GalleryPage.php:*
```php
<?php
class GalleryPage extends Page {}
class GalleryPage_Controller extends Page_Controller {
private static $allowed_actions = array('Form');
public function Form() {
$fields = new FieldList(
new TextField('Title', 'Title', null, 255),
$field = new UploadField('Images', 'Upload Images')
);
$field->setCanAttachExisting(false); // Block access to SilverStripe assets library
$field->setCanPreviewFolder(false); // Don't show target filesystem folder on upload field
$field->relationAutoSetting = false; // Prevents the form thinking the GalleryPage is the underlying object
$actions = new FieldList(new FormAction('submit', 'Save Images'));
return new Form($this, 'Form', $fields, $actions, null);
}
public function submit($data, Form $form) {
$gallery = new Gallery();
$form->saveInto($gallery);
$gallery->write();
return $this;
}
}
```
*Gallery.php:*
```php
<?php
class Gallery extends DataObject {
private static $db = array(
'Title' => 'Varchar(255)'
);
private static $many_many = array(
'Images' => 'Image'
);
}
```
*ImageExtension.php:*
```php
<?php
class ImageExtension extends DataExtension {
private static $belongs_many_many = array(
'Gallery' => 'Gallery'
);
function canEdit($member) {
// WARNING! This affects permissions on ALL images. Setting this incorrectly can restrict
// access to authorised users or unintentionally give access to unauthorised users if set incorrectly.
return Permission::check('CMS_ACCESS_AssetAdmin');
}
}
```
*mysite/_config/config.yml*
```yml
Image:
extensions:
- ImageExtension
```

View File

@ -1,6 +1,6 @@
# How to add a custom action to a GridField row
In a [GridField](/reference/grid-field) instance each table row can have a
In a [GridField](../field_types/gridfield) instance each table row can have a
number of actions located the end of the row such as edit or delete actions.
Each action is represented as a instance of a specific class
(e.g [api:GridFieldEditButton]) which has been added to the `GridFieldConfig`
@ -79,7 +79,7 @@ below:
While we're working on the code, to add this new action to the `GridField`, add
a new instance of the class to the [api:GridFieldConfig] object. The `GridField`
[Reference](/reference/grid-field) documentation has more information about
[Reference](../field_types/gridfield) documentation has more information about
manipulating the `GridFieldConfig` instance if required.
:::php
@ -142,6 +142,6 @@ message to the user interface indicating a successful message.
## Related
* [GridField Reference](/reference/grid-field)
* [ModelAdmin: A UI driven by GridField](/reference/modeladmin)
* [Tutorial 5: Dataobject Relationship Management](/tutorials/5-dataobject-relationship-management)
* [GridField Reference](/developer_guides/forms/field_types/gridfield)
* [ModelAdmin: A UI driven by GridField](/developer_guides/customising_the_admin_interface/modeladmin)
* [Tutorial 5: Dataobject Relationship Management](/tutorials/dataobject_relationship_management)

View File

@ -16,7 +16,7 @@ throughout the site. Out of the box this includes selecting the current site the
<% with $SiteConfig %>
$Title $AnotherField
<% end_loop %>
<% end_with %>
To access variables in the PHP:
@ -61,12 +61,12 @@ Then activate the extension.
<div class="notice" markdown="1">
After adding the class and the YAML change, make sure to rebuild your database by visiting http://yoursite.com/dev/build.
You may also need to reload the screen with a `flush=1` i.e http://yoursite.com/admin/settings?flush=1.
You may also need to reload the screen with a `?flush=1` i.e http://yoursite.com/admin/settings?flush=1.
</div>
You can define as many extensions for `SiteConfig` as you need. For example, if you're developing a module and what to
You can define as many extensions for `SiteConfig` as you need. For example, if you're developing a module and want to
provide the users a place to configure settings then the `SiteConfig` panel is the place to go it.
## API Documentation
* `[api:SiteConfig]`
* `[api:SiteConfig]`

View File

@ -5,7 +5,7 @@ title: How to Publish a SilverStripe module
If you wish to submit your module to our public directory, you take responsibility for a certain level of code quality,
adherence to conventions, writing documentation, and releasing updates.
SilverStripe uses [Composer](../../getting_started/composer/) to manage module releases and dependencies between
SilverStripe uses [Composer](../../../getting_started/composer/) to manage module releases and dependencies between
modules. If you plan on releasing your module to the public, ensure that you provide a `composer.json` file in the root
of your module containing the meta-data about your module.

View File

@ -309,7 +309,7 @@ equal the class names they manage.
## Related Documentation
* [How to use a FixtureFactory](how_to/fixturefactories/)
* [How to use a FixtureFactory](how_tos/fixturefactories/)
## API Documentation

View File

@ -0,0 +1,29 @@
title: Testing Glossary
<dl>
<dt>Assertion<dd>A predicate statement that must be true when a test runs.
<dt>Behat<dd>A behaviour-driven testing library used with SilverStripe as a higher-level alternative to the `FunctionalTest` API, see <http://behat.org>.
<dt>Test Case<dd>The atomic class type in most unit test frameworks. New unit tests are created by inheriting from the base test case.
<dt>Test Suite<dd>Also known as a 'test group', a composite of test cases, used to collect individual unit tests into packages, allowing all tests to be run at once.
<dt>Fixture<dd>Usually refers to the runtime context of a unit test - the environment and data prerequisites that must be in place in order to run the test and expect a particular outcome. Most unit test frameworks provide methods that can be used to create fixtures for the duration of a test - `setUp` - and clean them up after the test is done - `tearDown`.
<dt>Refactoring<dd>A behavior preserving transformation of code. If you change the code, while keeping the actual functionality the same, it is refactoring. If you change the behavior or add new functionality it's not.
<dt>Smell<dd>A code smell is a symptom of a problem. Usually refers to code that is structured in a way that will lead to problems with maintenance or understanding.
<dt>Spike<dd>A limited and throwaway sketch of code or experiment to get a feel for how long it will take to implement a certain feature, or a possible direction for how that feature might work.
<dt>Test Double<dd>Also known as a 'Substitute'. A general term for a dummy object that replaces a real object with the same interface. Substituting objects is useful when a real object is difficult or impossible to incorporate into a unit test.
**Fake Object**: A substitute object that simply replaces a real object with the same interface, and returns a pre-determined (usually fixed) value from each method.
<dt>Mock Object<dd>A substitute object that mimics the same behavior as a real object (some people think of mocks as "crash test dummy" objects). Mocks differ from other kinds of substitute objects in that they must understand the context of each call to them, setting expectations of which, and what order, methods will be invoked and what parameters will be passed.
<dt>Test-Driven Development (TDD)<dd>A style of programming where tests for a new feature are constructed before any code is written. Code to implement the feature is then written with the aim of making the tests pass. Testing is used to understand the problem space and discover suitable APIs for performing specific actions.
<dt>Behavior Driven Development (BDD)<dd>An extension of the test-driven programming style, where tests are used primarily for describing the specification of how code should perform. In practice, there's little or no technical difference - it all comes down to language. In BDD, the usual terminology is changed to reflect this change of focus, so _Specification_ is used in place of _Test Case_, and _should_ is used in place of _expect_ and _assert_.
</dl>

View File

@ -12,7 +12,7 @@ response and modify the session within a test.
:::php
<?php
class HomePageTest extends SapphireTest {
class HomePageTest extends FunctionalTest {
/**
* Test generation of the view
@ -24,7 +24,7 @@ response and modify the session within a test.
$this->assertEquals(200, $page->getStatusCode());
// We should see a login form
$login = $this->submitForm("#LoginForm", null, array(
$login = $this->submitForm("LoginFormID", null, array(
'Email' => 'test@test.com',
'Password' => 'wrongpassword'
));

View File

@ -12,18 +12,15 @@ SilverStripe uses [PHPUnit](http://www.phpunit.de) for unit tests, and the frame
process of creating and managing tests.
If you're more familiar with unit testing, but want a refresher of some of the concepts and terminology, you can browse
the [Testing Glossary](glossary). To get started now, follow the installation instructions below, and check
[Troubleshooting](testing-guide-troubleshooting) in case you run into any problems.
the [Testing Glossary](testing_glossary). To get started now, follow the installation instructions below.
If you are familiar with PHP coding but new to unit testing then check out Mark's presentation [Getting to Grips with SilverStripe Testing](http://www.slideshare.net/maetl/getting-to-grips-with-silverstripe-testing).
[Why Unit Test?](why-should-i-test) will give you reasons why you should be testing your code.
You should also read over [the PHPUnit manual](http://www.phpunit.de/manual/current/en/). It provides a lot of
fundamental concepts that we build on in this documentation.
Unit tests are not included in the normal SilverStripe downloads so you need to install them through git repositories
([installation instructions](/installation/composer)).
([installation instructions](/getting_started/composer)).
## Install with Composer
@ -108,7 +105,7 @@ All command-line arguments are documented on
### Via the "sake" Wrapper on Command Line
The [sake](/topics/commandline) executable that comes with SilverStripe can trigger a customized
The [sake](/developer_guides/cli/) executable that comes with SilverStripe can trigger a customized
`[api:TestRunner]` class that handles the PHPUnit configuration and output formatting.
While the custom test runner a handy tool, its also more limited than using `phpunit` directly,
particularly around formatting test output.

View File

@ -24,7 +24,7 @@ Append the option and corresponding value to your URL in your browser's address
| URL Variable | | Values | | Description |
| ------------ | | ------ | | ----------- |
| isDev | | 1 | | Put the site into [development mode](/topics/debugging), enabling debugging messages to the browser on a live server. For security, you'll be asked to log in with an administrator log-in. Will persist for the current browser session. |
| isDev | | 1 | | Put the site into [development mode](../), enabling debugging messages to the browser on a live server. For security, you'll be asked to log in with an administrator log-in. Will persist for the current browser session. |
| isTest | | 1 | | See above. |
| debug | | 1 | | Show a collection of debugging information about the director / controller operation |
| debug_request | | 1 | | Show all steps of the request from initial `[api:HTTPRequest]` to `[api:Controller]` to Template Rendering |

View File

@ -3,7 +3,7 @@ summary: Learn how to identify errors in your application and best practice for
# Debugging
SilverStripe can be a large and complex framework to debug, but there are ways to make debugging less painful. In this
guide we show the basics on defining the correct [Environment Type](environment_type) for your application and other
guide we show the basics on defining the correct [Environment Type](environment_types) for your application and other
built-in helpers for dealing with application errors.
[CHILDREN]

View File

@ -22,15 +22,15 @@ will invalidate the cache after a given amount of time has expired (default 10 m
Here are some more complex examples:
:::ss
<% cached 'database', LastEdited %>
<% cached 'database', $LastEdited %>
<!-- that updates every time the record changes. -->
<% end_cached %>
<% cached 'loginblock', CurrentMember.ID %>
<% cached 'loginblock', $CurrentMember.ID %>
<!-- cached unique to the user. i.e for user 2, they will see a different cache to user 1 -->
<% end_cached %>
<% cached 'loginblock', LastEdited, CurrentMember.isAdmin %>
<% cached 'loginblock', $LastEdited, $CurrentMember.isAdmin %>
<!-- recached when block object changes, and if the user is admin -->
<% end_cached %>
@ -53,13 +53,13 @@ user does not influence your template content, you can update this key as below;
Often you want to invalidate a cache when any object in a set of objects change, or when the objects in a relationship
change. To do this, SilverStripe introduces the concept of Aggregates. These calculate and return SQL aggregates
on sets of [api:DataObject]s - the most useful for us being the `Max` aggregate.
on sets of [api:DataObject]s - the most useful for us being the `max` aggregate.
For example, if we have a menu, we want that menu to update whenever _any_ page is edited, but would like to cache it
otherwise. By using aggregates, we do that like this:
:::ss
<% cached 'navigation', List(SiteTree).max(LastEdited), List(SiteTree).count() %>
<% cached 'navigation', $List('SiteTree').max('LastEdited'), $List('SiteTree').count() %>
The cache for this will update whenever a page is added, removed or edited.
@ -67,10 +67,10 @@ If we have a block that shows a list of categories, we can make sure the cache u
or edited
:::ss
<% cached 'categorylist', List(Category).max(LastEdited), List(Category).count() %>
<% cached 'categorylist', $List('Category').max('LastEdited'), $List('Category').count() %>
<div class="notice" markdown="1">
Note the use of both `.max(LastEdited)` and `.count()` - this takes care of both the case where an object has been
Note the use of both `.max('LastEdited')` and `.count()` - this takes care of both the case where an object has been
edited since the cache was last built, and also when an object has been deleted since the cache was last built.
</div>
@ -78,7 +78,7 @@ We can also calculate aggregates on relationships. A block that shows the curren
whenever the relationship `Member::$has_many = array('Favourites' => Favourite')` changes.
:::ss
<% cached 'favourites', CurrentMember.ID, CurrentMember.Favourites.max(LastEdited) %>
<% cached 'favourites', $CurrentMember.ID, $CurrentMember.Favourites.max('LastEdited') %>
## Cache key calculated in controller
@ -100,7 +100,7 @@ extract that logic into the controller.
Then using that function in the cache key:
:::ss
<% cached FavouriteCacheKey %>
<% cached $FavouriteCacheKey %>
## Cache blocks and template changes
@ -118,7 +118,7 @@ data updates.
For instance, if we show some blog statistics, but are happy having them be slightly stale, we could do
:::ss
<% cached 'blogstatistics', Blog.ID %>
<% cached 'blogstatistics', $Blog.ID %>
which will invalidate after the cache lifetime expires. If you need more control than that (cache lifetime is
@ -133,7 +133,7 @@ configurable only on a site-wide basis), you could add a special function to you
and then use it in the cache key
:::ss
<% cached 'blogstatistics', Blog.ID, BlogStatisticsCounter %>
<% cached 'blogstatistics', $Blog.ID, $BlogStatisticsCounter %>
## Cache block conditionals
@ -146,7 +146,7 @@ Following on from the previous example, you might wish to only cache slightly-st
heavy load:
:::ss
<% cached 'blogstatistics', Blog.ID if HighLoad %>
<% cached 'blogstatistics', $Blog.ID if $HighLoad %>
By adding a `HighLoad` function to your `Page_Controller`, you could enable or disable caching dynamically.
@ -155,7 +155,7 @@ To cache the contents of a page for all anonymous users, but dynamically calcula
use something like:
:::ss
<% cached unless CurrentUser %>
<% cached unless $CurrentUser %>
## Uncached
@ -178,10 +178,10 @@ portion dynamic, without having to include any member info in the page's cache k
An example:
:::ss
<% cached LastEdited %>
<% cached $LastEdited %>
Our wonderful site
<% cached Member.ID %>
<% cached $Member.ID %>
Welcome $Member.Name
<% end_cached %>
@ -196,7 +196,7 @@ Cache conditionals and the uncached tag also work in the same nested manner. Sin
could also write the last example as:
:::ss
<% cached LastEdited %>
<% cached $LastEdited %>
Our wonderful site
<% uncached %>
@ -214,7 +214,7 @@ letting you know if you've done this. You can often get around this using aggreg
Failing example:
:::ss
<% cached LastEdited %>
<% cached $LastEdited %>
<% loop $Children %>
<% cached LastEdited %>
@ -227,9 +227,9 @@ Failing example:
Can be re-written as:
:::ss
<% cached LastEdited %>
<% cached $LastEdited %>
<% cached AllChildren.max(LastEdited) %>
<% cached $AllChildren.max('LastEdited') %>
<% loop $Children %>
$Name
<% end_loop %>

View File

@ -17,7 +17,7 @@ Flushing the various manifests is performed through a GET
parameter (`flush=1`). Since this action requires more server resources than normal requests,
executing the action is limited to the following cases when performed via a web request:
* The [environment](/topics/environment-management) is in "dev mode"
* The [environment](../getting_started/environment_management) is in "dev mode"
* A user is logged in with ADMIN permissions
* An error occurs during startup

View File

@ -7,7 +7,7 @@ See our "[Release Process](/misc/release-process#security-releases) on how to re
## SQL Injection
The [coding-conventions](/misc/coding-conventions) help guard against SQL injection attacks but still require developer
The [coding-conventions](/getting_started/coding_conventions) help guard against SQL injection attacks but still require developer
diligence: ensure that any variable you insert into a filter / sort / join clause is either parameterised, or has been
escaped.
@ -95,8 +95,8 @@ result in *double escaping* and alters the actually saved data (e.g. by adding s
### Manual escaping
As a rule of thumb, whenever you're creating SQL queries (or just chunks of SQL) you should use parameterisation,
but there may be cases where you need to take care of escaping yourself. See [coding-conventions](/misc/coding-conventions)
and [datamodel](/topics/datamodel) for ways to parameterise, cast, and convert your data.
but there may be cases where you need to take care of escaping yourself. See [coding-conventions](/getting_started/coding-conventions)
and [datamodel](/developer_guides/model) for ways to parameterise, cast, and convert your data.
* `SQLQuery`
* `DB::query()`

View File

@ -216,7 +216,7 @@ $service->request('service.json', 'GET', null, null, $curlOptions);
## How to's
* [Embed an RSS Feed](how_to/embed_rss)
* [Embed an RSS Feed](how_tos/embed_rss)
## API Documentation

View File

@ -11,7 +11,7 @@ The default output of a [api:SearchContext] is either a [api:SQLQuery] object fo
[api:DataObject] instance.
<div class="notice" markdown="1">
[api:SearchContext] is mainly used by [ModelAdmin](../customising_the_cms/modeladmin).
[api:SearchContext] is mainly used by [ModelAdmin](../customising_the_admin_interface/modeladmin).
</div>
## Usage

View File

@ -80,7 +80,7 @@ not PHP's built-in [date()](http://nz.php.net/manual/en/function.date.php).
### Language Names
SilverStripe comes with a built-in list of common languages, listed by locale and region.
They can be accessed via the `i18n.common_languages` and `i18n.common_locales` [config setting](/topics/configuration).
They can be accessed via the `i18n.common_languages` and `i18n.common_locales` [config setting](/developer_guides/configuration).
In order to add a value, add the following to your `config.yml`:
@ -125,7 +125,7 @@ Date- and time related form fields support i18n ([api:DateField], [api:TimeField
$field->setConfig('dateformat', 'dd. MMMM YYYY'); // sets typical 'de_DE' date format, shows as "23. Juni 1982"
Defaults can be applied globally for all field instances through the `DateField.default_config`
and `TimeField.default_config` [configuration arrays](/topics/configuration).
and `TimeField.default_config` [configuration arrays](/developer_guides/configuration).
If no 'locale' default is set on the field, [api:i18n::get_locale()] will be used.
**Important:** Form fields in the CMS are automatically configured according to the profile settings for the logged-in user (`Member->Locale`, `Member->DateFormat` and `Member->TimeFormat`). This means that in most cases,
@ -408,7 +408,7 @@ The `ss.i18n` object contain a couple functions to help and replace dynamic vari
## Links
* [Help to translate](/misc/contribute/translation) - Instructions for online collaboration to translate core
* [Help to translate](/misc/translation-process) - Instructions for adding translation to your own modules
* [Help to translate](../../contributing/translations) - Instructions for online collaboration to translate core
* [Help to translate](../../contributing/translation_process) - Instructions for adding translation to your own modules
* [http://www.i18nguy.com/](http://www.i18nguy.com/)
* [balbus.tk i18n notes](http://www.balbus.tk/internationalize)

View File

@ -13,7 +13,7 @@ All files, images and folders in the 'assets' directory are stored in the databa
| `Title` | The optional, human-readable title of the file for display only (doesn't apply to folders). |
| `Filename` | The path to the file/folder, relative to the webroot. For example 'assets/images/my-image.jpg', or 'assets/images/' for a folder. |
| `Content` | Typically unused, but handy for a textual representation of files. For example for fulltext indexing of PDF documents. |
| `ShowInSearch` | Whether the file should be shown in search results, defaults to '1'. See ["Tutorial 4 - Site Search"](/tutorials/4-site-search) for enabling search. |
| `ShowInSearch` | Whether the file should be shown in search results, defaults to '1'. See ["Tutorial 4 - Site Search"](/tutorials/site_search) for enabling search. |
| `ParentID` | The ID of the parent Folder that this File/Folder is in. A ParentID of '0' indicates that the File/Folder is in the 'assets' directory. |
| `OwnerID` | The ID of the Member that 'owns' the File/Folder (not related to filesystem permissions). |
@ -37,4 +37,4 @@ You may also notice the 'Sync files' button (highlighted below). This button all
## Upload
Files can be managed through a `FileField` or an `UploadField`. The `[api:FileField]` class provides a simple HTML input with a type of "file", whereas an `[api:UploadField]` provides a much more feature-rich field (including AJAX-based uploads, previews, relationship management and file data management). See [`Reference - UploadField`](/reference/uploadfield) for more information about how to use the `UploadField` class.
Files can be managed through a `FileField` or an `UploadField`. The `[api:FileField]` class provides a simple HTML input with a type of "file", whereas an `[api:UploadField]` provides a much more feature-rich field (including AJAX-based uploads, previews, relationship management and file data management). See [`Reference - UploadField`](/developer_guides/forms/field_types/uploadfield) for more information about how to use the `UploadField` class.

View File

@ -10,7 +10,7 @@ It uses the framework's knowledge about the model to provide sensible defaults,
of lines of code, while still providing a solid base for customization.
<div class="info" markdown="1">
The interface is mainly powered by the [api:GridField] class ([documentation](../forms/fields/gridfield)), which can
The interface is mainly powered by the [api:GridField] class ([documentation](../forms/field_types/gridfield)), which can
also be used in other areas of your application.
</div>
@ -146,7 +146,7 @@ class (see [SearchContext](../search/searchcontext) docs for details).
## Displaying Results
The results are shown in a tabular listing, powered by the [GridField](../forms/fields/gridfield), more specifically
The results are shown in a tabular listing, powered by the [GridField](../forms/field_types/gridfield), more specifically
the [api:GridFieldDataColumns] component. This component looks for a [api:DataObject::$summary_fields] static on your
model class, where you can add or remove columns. To change the title, use [api:DataObject::$field_labels].
@ -320,9 +320,9 @@ To customize the exported columns, create a new method called `getExportFields`
## Related Documentation
* [GridField](../forms/fields/gridfield)
* [GridField](../forms/field_types/gridfield)
* [Permissions](../security/permissions)
* [SeachContext](../search/seachcontext)
* [SearchContext](../search/searchcontext)
## API Documentation

View File

@ -66,7 +66,7 @@ Layout manager will automatically apply algorithms to the children of `.cms-cont
For detailed discussion on available algorithms refer to
[jLayout algorithms](https://github.com/bramstein/jlayout#layout-algorithms).
Our [Howto: Extend the CMS Interface](../howto/extend-cms-interface) has a practical example on how to add a bottom
Our [Howto: Extend the CMS Interface](how_tos/extend_cms_interface) has a practical example on how to add a bottom
panel to the CMS UI.
### Methods
@ -115,6 +115,6 @@ The parameters are as follows:
## Related
* [Reference: CMS Architecture](../reference/cms-architecture)
* [Reference: Preview](../reference/preview)
* [Howto: Extend the CMS Interface](../howto/extend-cms-interface)
* [Reference: CMS Architecture](cms_architecture)
* [Reference: Preview](preview)
* [Howto: Extend the CMS Interface](how_tos/extend_cms_interface)

View File

@ -69,7 +69,7 @@ Note how the configuration happens in different entwine namespaces
}(jQuery));
Load the file in the CMS via setting adding 'mysite/javascript/MyLeftAndMain.Preview.js'
to the `LeftAndMain.extra_requirements_javascript` [configuration value](/topics/configuration)
to the `LeftAndMain.extra_requirements_javascript` [configuration value](../configuration)
:::yml
LeftAndMain:
@ -79,7 +79,7 @@ to the `LeftAndMain.extra_requirements_javascript` [configuration value](/topics
In order to find out which configuration values are available, the source code
is your best reference at the moment - have a look in `framework/admin/javascript/LeftAndMain.Preview.js`.
To understand how layouts are handled in the CMS UI, have a look at the
[CMS Architecture](/reference/cms-architecture) guide.
[CMS Architecture](cms_architecture) guide.
## Enabling preview
@ -146,7 +146,7 @@ You can find out current size by calling:
## Preview modes
Preview modes map to the modes supported by the _threeColumnCompressor_ layout
algorithm, see [layout reference](../reference/layout) for more details. You
algorithm, see [layout reference](cms_layout) for more details. You
can change modes by calling:
```js
@ -184,4 +184,4 @@ previewable content is loaded.
## Related
* [Reference: Layout](../reference/layout)
* [Reference: Layout](cms_layout)

View File

@ -13,7 +13,7 @@ feel familiar to you. This is just a quick run down to get you started
with some special conventions.
For a more practical-oriented approach to CMS customizations, refer to the
[Howto: Extend the CMS Interface](../howto/extend-cms-interface) which builds
[Howto: Extend the CMS Interface](../how_tos/extend_cms_interface) which builds
## Markup and Style Conventions
@ -47,7 +47,7 @@ As there's a whole lot of CSS driving the CMS, we have certain best practives ar
* Use jQuery UI's built-in styles where possible, e.g. `ui-widget` for a generic container, or `ui-state-highlight`
to highlight a specific component. See the [jQuery UI Theming API](http://jqueryui.com/docs/Theming/API) for a full list.
See our [system requirements](../installation/server-requirements) for a list of supported browsers.
See our [system requirements](/getting_started/server_requirements) for a list of supported browsers.
## Templates and Controllers
@ -89,7 +89,7 @@ The various panels and UI components within them are loosely coupled to the layo
attribute. The layout is triggered on the top element and cascades into children, with a `redraw` method defined on
each panel and UI component that needs to update itself as a result of layouting.
Refer to [Layout reference](../reference/layout) for further information.
Refer to [Layout reference](cms_layout) for further information.
## Forms
@ -148,7 +148,7 @@ correctly configured form.
[jQuery.entwine](https://github.com/hafriedlander/jquery.entwine) is a thirdparty library
which allows us to attach behaviour to DOM elements in a flexible and structured mannger.
It replaces the `behaviour.js` library used in previous versions of the CMS interface.
See [Topics: JavaScript](../topics/javascript) for more information on how to use it.
See [JavaScript Development](javascript_development) for more information on how to use it.
In the CMS interface, all entwine rules should be placed in the "ss" entwine namespace.
If you want to call methods defined within these rules outside of entwine logic,
you have to use this namespace, e.g. `$('.cms-menu').entwine('ss').collapse()`.
@ -222,7 +222,7 @@ In order for this to work, the CMS templates declare certain sections as "PJAX f
through a `data-pjax-fragment` attribute. These names correlate to specific
rendering logic in the PHP controllers, through the `[api:PjaxResponseNegotiator]` class.
Through a custom `X-Pjax` HTTP header, the client can declare which view he's expecting,
Through a custom `X-Pjax` HTTP header, the client can declare which view they're expecting,
through identifiers like `CurrentForm` or `Content` (see `[api:LeftAndMain->getResponseNegotiator()]`).
These identifiers are passed to `loadPanel()` via the `pjax` data option.
The HTTP response is a JSON object literal, with template replacements keyed by their Pjax fragment.
@ -433,7 +433,7 @@ The CMS tree for viewing hierarchical structures (mostly pages) is powered
by the [jstree](http://jstree.com) library. It is configured through
`framework/admin/javascript/LeftAndMain.Tree.js`, as well as some
HTML5 metadata generated on its container (see the `data-hints` attribute).
For more information, see the [Howto: Customize the CMS tree](../howto/customize-cms-tree).
For more information, see the [Howto: Customize the CMS tree](../how_tos/customize_cms_tree).
Note that a similar tree logic is also used for the
form fields to select one or more entries from those hierarchies
@ -538,8 +538,8 @@ through the `PjaxResponseNegotiator` class (see above).
## Related
* [Howto: Extend the CMS Interface](../howto/extend-cms-interface)
* [Howto: Customize the CMS tree](../howto/customize-cms-tree)
* [Reference: ModelAdmin](../reference/modeladmin)
* [Reference: Layout](../reference/layout)
* [Topics: Rich Text Editing](../topics/rich-text-editing)
* [Howto: Extend the CMS Interface](how_tos/extend_cms_interface)
* [Howto: Customize the CMS tree](how_tos/customize_cms_tree)
* [ModelAdmin API](api:ModelAdmin)
* [Reference: Layout](cms_layout)
* [Rich Text Editing](/developer_guides/forms/field_types/htmleditorfield)

View File

@ -14,7 +14,7 @@ This how-to will walk you through creation of a "Clean-up" button with two appea
* active: "Clean-up now" green constructive button if the actions can be performed
* netural: "Cleaned" default button if the action does not need to be done
The controller code that goes with this example is listed in [Extend CMS Interface](../reference/extend-cms-interface).
The controller code that goes with this example is listed in [Extend CMS Interface](extend_cms_interface).
## Backend support ##
@ -156,4 +156,4 @@ cases.
## Summary ##
The code presented gives you a fully functioning alternating button, similar to the defaults that come with the the CMS.
These alternating buttons can be used to give user the advantage of visual feedback upon his actions.
These alternating buttons can be used to give user the advantage of visual feedback upon their actions.

View File

@ -27,4 +27,4 @@ more complex fields like `GridField`, `UploadField`
or `DropdownField` with the chosen.js behaviour applied.
Note: For more advanced help text we recommend using
[Custom form field templates](/topics/forms#custom-form-field-templates);
[Custom form field templates](../form_templates);

View File

@ -7,10 +7,10 @@ SilverStripe will automatically create a new `[api:CMSMenuItem]` for it
The most popular extension of LeftAndMain is a `[api:ModelAdmin]` class, so
for a more detailed introduction to creating new `ModelAdmin` interfaces, read
the [ModelAdmin reference](../reference/modeladmin).
the [ModelAdmin reference](../modeladmin).
In this document we'll take the `ProductAdmin` class used in the
[ModelAdmin reference](../reference/modeladmin#setup) and so how we can change
[ModelAdmin reference](../modeladmin#setup) and so how we can change
the menu behaviour by using the `$menu_title` and `$menu_icon` statics to
provide a custom title and icon.
@ -43,7 +43,7 @@ In order to localize the menu title in different languages, use the
the i18n text collection.
For more information on language and translations, please refer to the
[i18n](../reference/ii8n) docs.
[i18n](../../ii8n) docs.
## Adding an external link to the menu
@ -85,7 +85,7 @@ button configuration.
To have the link appear, make sure you add the extension to the `LeftAndMain`
class. For more information about configuring extensions see the
[DataExtension reference](../reference/dataextension).
[extensions reference](../extending/extensions).
:::php
LeftAndMain::add_extension('CustomLeftAndMain')
@ -93,4 +93,4 @@ class. For more information about configuring extensions see the
## Related
* [How to extend the CMS interface](extend-cms-interface)
* [How to extend the CMS interface](extend_cms_interface)

View File

@ -62,7 +62,7 @@ or across page types with common characteristics.
}
}
Now you just need to enable the extension in your [configuration file](/topics/configuration).
Now you just need to enable the extension in your [configuration file](../../configuration).
// mysite/_config/config.yml
LeftAndMain:

View File

@ -55,7 +55,7 @@ with the CMS interface. Paste the following content into a new file called
.bookmarked-link.first {margin-top: 1em;}
Load the new CSS file into the CMS, by setting the `LeftAndMain.extra_requirements_css`
[configuration value](/topics/configuration).
[configuration value](../../configuration).
:::yml
LeftAndMain:
@ -85,7 +85,7 @@ and insert the following code.
}
}
Enable the extension in your [configuration file](/topics/configuration)
Enable the extension in your [configuration file](../../configuration)
:::yml
SiteTree:
@ -114,7 +114,7 @@ Add the following code to a new file `mysite/code/BookmarkedLeftAndMainExtension
}
}
Enable the extension in your [configuration file](/topics/configuration)
Enable the extension in your [configuration file](../../configuration)
:::yml
LeftAndMain:
@ -191,11 +191,11 @@ Empty tabs will be automatically removed from the `FieldList` to prevent clutter
</div>
New actions will need associated controller handlers to work. You can use a
`LeftAndMainExtension` to provide one. Refer to [Controller documentation](../topics/controller)
`LeftAndMainExtension` to provide one. Refer to [Controller documentation](../../controllers)
for instructions on setting up handlers.
To make the actions more user-friendly you can also use alternating buttons as
detailed in the [CMS Alternating Button](../reference/cms-alternating-button)
detailed in the [CMS Alternating Button](cms_alternating_button)
how-to.
## Summary
@ -207,7 +207,7 @@ blocks and concepts for more complex extensions as well.
## Related
* [Reference: CMS Architecture](../reference/cms-architecture)
* [Reference: Layout](../reference/layout)
* [Topics: Rich Text Editing](../topics/rich-text-editing)
* [CMS Alternating Button](../howto/cms-alternating-button)
* [Reference: CMS Architecture](../cms_architecture)
* [Reference: Layout](../cms_layout)
* [Rich Text Editing](/developer_guides/forms/field_types/htmleditorfield)
* [CMS Alternating Button](cms_alternating_button)

View File

@ -64,14 +64,14 @@ a `tests/` folder, unless tests are executed.
The `[api:SS_TemplateManifest]` class builds a manifest of all templates present in a directory,
in both modules and themes. Templates in `tests/` folders are automatically excluded.
The chapter on [template inheritance](../templates/template-inheritance) provides more details
The chapter on [template inheritance](../templates/template_inheritance) provides more details
on its operation.
## Config Manifest
The `[api:SS_ConfigManifest]` loads builds a manifest of configuration items,
for both PHP and YAML. It also takes care of ordering and merging configuration fragments.
The chapter on [configuration](/topics/configuration) has more details.
The chapter on [configuration](../configuration) has more details.
## Flushing

View File

@ -103,14 +103,14 @@ before handing control off to SilverStripe's own `main.php`.
## Routing and Request Handling
The `main.php` script relies on `[api:Director]` to work out which [controller](../controllers) should handle this request. It parses the URL, matching it to one of a number of patterns,
The `main.php` script relies on `[api:Director]` to work out which [controller](../controllers/) should handle this request. It parses the URL, matching it to one of a number of patterns,
and determines the controller, action and any argument to be used ([Routing](../controllers/routing)).
* Creates a `[api:SS_HTTPRequest]` object containing all request and environment information
* The [session](../cookies_and_sessions/sessions) holds an abstraction of PHP session
* Instantiates a [controller](../Controllers) object
* Instantiates a [controller](../controllers/) object
* The `[api:Injector]` is first referenced, and asks the registered
[RequestFilter](../controller/request_filters)
[RequestFilter](../controllers/requestfilters)
to pre-process the request object (see below)
* The `Controller` executes the actual business logic and populates an `[api:SS_HTTPResponse]`
* The `Controller` can optionally hand off control to further nested controllers
@ -125,7 +125,7 @@ The framework provides the ability to hook into the request both before and
after it is handled to allow binding custom logic. This can be used
to transform or filter request data, instanciate helpers, execute global logic,
or even short-circuit execution (e.g. to enforce custom authentication schemes).
The ["Request Filters" documentation](../controller/request_filters) shows you how.
The ["Request Filters" documentation](../controllers/requestfilters) shows you how.
## Flushing Manifests

View File

@ -35,14 +35,14 @@ Thanks to Rutger de Jong for reporting.
Severity: Moderate
Autologin tokens (remember me and reset password) are stored in the database as a plain text.
If attacker obtained the database he would be able to gain access to accounts that have requested a password change, or have "remember me" enabled.
If attacker obtained the database they would be able to gain access to accounts that have requested a password change, or have "remember me" enabled.
### Security: Privilege escalation through profile form
Severity: Moderate
A logged-in CMS user can gain additional privileges by crafting a request
to his/her profile form which resets another user's password.
to their profile form which resets another user's password.
This method can potentially be used by CSRF attacks as well.
Thanks to Nathaniel Carew (Sense of Security) for reporting.

View File

@ -0,0 +1,71 @@
# 3.1.11
# Overview
This release resolves a high level security issue in the SiteTree class, as well as
the CMS controller classes which act on these objects during creation.
This release also resolves an issue affecting GridField on sites running in
an environment with Suhosin enabled.
## Upgrading
### SiteTree::canCreate Permissions
Any user code which overrides the `SiteTree::canCreate` method should be investigated to
ensure it continues to work correctly. In particular, a second parameter may now be passed
to this method in order to determine if page creation is allowed in any given context, whether
it be at the root level, or as a child of a parent page.
The creation of pages at the root level is now corrected to follow the rules specified
by the SiteConfig, which in turn has been updated to ensure only valid CMS users are
granted this permission (when applicable).
The creation of pages beneath parent pages will now inherit from the ability to edit
this parent page.
User code which is not updated, but relies on the old implementation of SiteTree::canCreate will
now assume creation at the top level.
For example see the below code as an example
E.g.
:::php
<?php
class SingletonPage extends Page {
public function canCreate($member) {
if(static::get()->count()) return false;
$context = func_num_args() > 1 ? func_get_arg(1) : array();
return parent::canCreate($member, $context);
}
}
For more information on the reason for this change please see the security announcement below.
## Security
* 2015-03-11 [3df41e1](https://github.com/silverstripe/silverstripe-cms/commit/3df41e1) Fix SiteTree / SiteConfig permissions (Damian Mooyman) - See announcement [ss-2015-008](http://www.silverstripe.org/software/download/security-releases/ss-2015-008-sitetree-creation-permission-vulnerability)
### Bugfixes
* 2015-03-09 [1770fab](https://github.com/silverstripe/sapphire/commit/1770fab) Fix gridfield generating invalid session keys (Damian Mooyman)
* 2015-03-05 [87adc44](https://github.com/silverstripe/sapphire/commit/87adc44) Fix serialised stateid exceeding request length (Damian Mooyman)
* 2015-03-04 [eb35f26](https://github.com/silverstripe/sapphire/commit/eb35f26) Corrected padding on non-sortable columns. (Sam Minnee)
* 2015-03-03 [6e0afd5](https://github.com/silverstripe/sapphire/commit/6e0afd5) Prevent unnecessary call to config system which doesn't exist yet (micmania1)
* 2015-03-03 [4709b90](https://github.com/silverstripe/sapphire/commit/4709b90) UploadField description alignment (Loz Calver)
* 2015-03-02 [f234301](https://github.com/silverstripe/sapphire/commit/f234301) DataQuery::applyRelation using incorrect foreign key (fixes #3954) (Loz Calver)
* 2015-03-02 [f9d493d](https://github.com/silverstripe/sapphire/commit/f9d493d) Fixes case insensitive search for postgres databases (Jean-Fabien Barrois)
* 2015-02-27 [4c5a07e](https://github.com/silverstripe/sapphire/commit/4c5a07e) Updated docs (Michael Strong)
* 2015-02-25 [3a7e24a](https://github.com/silverstripe/sapphire/commit/3a7e24a) Unable to access a list of all many_many_extraFields (Loz Calver)
* 2015-02-13 [998c055](https://github.com/silverstripe/sapphire/commit/998c055) Misleading error message in SSViewer (Loz Calver)
* 2015-02-10 [bbe2799](https://github.com/silverstripe/sapphire/commit/bbe2799) Use correct query when searching for items managed by a tree dropdown field #3173 (Jean-Fabien Barrois)
* 2015-01-13 [ab24ed3](https://github.com/silverstripe/sapphire/commit/ab24ed3) Use i18n_plural_name() instead of plural_name() (Elvinas L.)
* 2014-11-17 [a142ffd](https://github.com/silverstripe/silverstripe-cms/commit/a142ffd) VirtualPages use correct casting for 'virtual' database fields (Loz Calver)
## Changelog
* [framework](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.1.11)
* [cms](https://github.com/silverstripe/silverstripe-cms/releases/tag/3.1.11)
* [installer](https://github.com/silverstripe/silverstripe-installer/releases/tag/3.1.11)

View File

@ -0,0 +1,43 @@
# 3.1.12
# Overview
This security release resolves some XSS and an XML vulnerability in the Framework.
## Upgrading
If your code relies on `Convert::xml2array` there are some important things to consider with regards to
certain vulnerabilities. In this release additional options have been added to this method to assist
users in guarding against these risks, although each option has been turned off by default.
Please refer to http://phpsecurity.readthedocs.org/en/latest/Injection-Attacks.html#xml-external-entity-injection
on details of some of the specific reasons behind the need for these changes and how you can guard
against them in your code.
Specifically this method has these two new parameters:
* The `$disableDoctypes` parameter has been added to disallow parsing of XML content containing
a <!DOCTYPE > header, which may potentially contain unguarded or recursive entity definitions.
* The `$disableExternals` parameter allows XML parsing to ignore any externally referenced
dependency within the file, ensuring that injected XML is unable to invoke data from potentially
hazardous sources.
## Security
* 2015-03-20 [ee9bddb](https://github.com/silverstripe/sapphire/commit/ee9bddb) Fix SS-2015-010 (Damian Mooyman) - See announcement [ss-2015-010](http://www.silverstripe.org/software/download/security-releases/ss-2015-010-xss-in-directorforce-redirect)
* 2015-03-20 [7f983c2](https://github.com/silverstripe/sapphire/commit/7f983c2) Fix SS-2014-017 (Damian Mooyman) - See announcement [ss-2014-017](http://www.silverstripe.org/software/download/security-releases/ss-2014-017-xml-quadratic-blowup-attack)
* 2015-03-20 [604c328](https://github.com/silverstripe/sapphire/commit/604c328) Fixed XSS vulnerability relating to rewrite_hash (Christopher Pitt) - See announcements [ss-2014-015](http://www.silverstripe.org/software/download/security-releases/ss-2014-015-ie-requests-not-properly-behaving-with-rewritehashlinks), [ss-2015-009](http://www.silverstripe.org/software/download/security-releases/ss-2015-009-xss-in-rewritten-hash-links)
### Bugfixes
* 2015-03-18 [b34c236](https://github.com/silverstripe/sapphire/commit/b34c236) Fix joins on tables containing "select" being mistaken for sub-selects Fix PHPDoc on SQLQuery::addFrom and SQLQuery::setFrom Fixes #3965 (Damian Mooyman)
* 2015-03-11 [a61c08d](https://github.com/silverstripe/sapphire/commit/a61c08d) Security::$default_message_set Config value unusable (Loz Calver)
* 2015-03-10 [9651889](https://github.com/silverstripe/sapphire/commit/9651889) Fix yaml generation to conform to version 1.1, accepted by transifex (Damian Mooyman)
* 2015-02-25 [f5f41b2](https://github.com/silverstripe/sapphire/commit/f5f41b2) Ensuring custom CMS validator uses Object-&gt;hasMethod() to respect extension decorator pattern. (Patrick Nelson)
* 2015-01-13 [9da7e90](https://github.com/silverstripe/silverstripe-cms/commit/9da7e90) . Missing translation entity (Elvinas L.)
## Changelog
* [framework](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.1.12)
* [cms](https://github.com/silverstripe/silverstripe-cms/releases/tag/3.1.12)
* [installer](https://github.com/silverstripe/silverstripe-installer/releases/tag/3.1.12)

View File

@ -607,9 +607,7 @@ when using deprecated functionality (through the new `Deprecation` class).
* 2012-04-12 [e9dc610](https://github.com/silverstripe/sapphire/commit/e9dc610) API-CHANGE: new GridFieldFooter component (Julian Seidenberg)
* 2012-04-10 [9888f98](https://github.com/silverstripe/silverstripe-cms/commit/9888f98) ENHANCMENT: Link pages in reports to cms edit (Andrew O'Neil)
* 2012-04-10 [1516934](https://github.com/silverstripe/silverstripe-cms/commit/1516934) Revert "BUGFIX: SSF-168 fixing rendering issue in Chrome, which displays extra control at the bottom of the window in a report that is of a certain length" (Julian Seidenberg)
* 2012-04-06 [797d526](https://github.com/silverstripe/sapphire/commit/797d526) For png images with transparency, the imagesaveaplpha() needs to be set to true on the source image in order for
he alpha to be preserved when using the modifier methods. (jmwohl)
* 2012-04-06 [797d526](https://github.com/silverstripe/sapphire/commit/797d526) For png images with transparency, the imagesaveaplpha() needs to be set to true on the source image in order for the alpha to be preserved when using the modifier methods. (jmwohl)
* 2012-04-05 [e76913f](https://github.com/silverstripe/sapphire/commit/e76913f) API-CHANGE: adding a default option of null to the $args argument in DataExtension::add_to_class. The args argument isn't used anywhere in the class and adding a third argument to every call to this function is tedious. (Julian Seidenberg)
* 2012-04-04 [5826b36](https://github.com/silverstripe/sapphire/commit/5826b36) ENHACEMENT: SSF-168 updated the font for titles on print stylesheets (Felipe Skroski)
* 2012-04-04 [349a04d](https://github.com/silverstripe/silverstripe-cms/commit/349a04d) API-CHANGE: SSF-168 changing the API/code-conventions for excluding specific reports. get_reports method now returns an ArrayList instead of an array of SS_Reports. (Julian Seidenberg)

View File

@ -22,7 +22,7 @@ Thanks to Rutger de Jong for reporting.
Severity: Moderate
A logged-in CMS user can gain additional privileges by crafting a request
to his/her profile form which resets another user's password.
to their profile form which resets another user's password.
This method can potentially be used by CSRF attacks as well.
Thanks to Nathaniel Carew (Sense of Security) for reporting.

View File

@ -25,7 +25,7 @@ API changes related to the below security patch:
Severity: Moderate
Autologin tokens (remember me and reset password) are stored in the database as a plain text.
If attacker obtained the database he would be able to gain access to accounts that have requested a password change, or have "remember me" enabled.
If attacker obtained the database they would be able to gain access to accounts that have requested a password change, or have "remember me" enabled.
## Changelog

View File

@ -0,0 +1,28 @@
# 3.1.11-rc1
# Overview
This release contains fixes to resolve issues in 3.1.10 running in a Suhosin environment.
Failure to update may result in unpredictable behaviour of the GridField control in affected sites.
### Bugfixes
* 2015-03-09 [1770fab](https://github.com/silverstripe/sapphire/commit/1770fab) Fix gridfield generating invalid session keys (Damian Mooyman)
* 2015-03-05 [87adc44](https://github.com/silverstripe/sapphire/commit/87adc44) Fix serialised stateid exceeding request length (Damian Mooyman)
* 2015-03-04 [eb35f26](https://github.com/silverstripe/sapphire/commit/eb35f26) Corrected padding on non-sortable columns. (Sam Minnee)
* 2015-03-03 [6e0afd5](https://github.com/silverstripe/sapphire/commit/6e0afd5) Prevent unnecessary call to config system which doesn't exist yet (micmania1)
* 2015-03-03 [4709b90](https://github.com/silverstripe/sapphire/commit/4709b90) UploadField description alignment (Loz Calver)
* 2015-03-02 [f234301](https://github.com/silverstripe/sapphire/commit/f234301) DataQuery::applyRelation using incorrect foreign key (fixes #3954) (Loz Calver)
* 2015-03-02 [f9d493d](https://github.com/silverstripe/sapphire/commit/f9d493d) Fixes case insensitive search for postgres databases (Jean-Fabien Barrois)
* 2015-02-27 [4c5a07e](https://github.com/silverstripe/sapphire/commit/4c5a07e) Updated docs (Michael Strong)
* 2015-02-25 [3a7e24a](https://github.com/silverstripe/sapphire/commit/3a7e24a) Unable to access a list of all many_many_extraFields (Loz Calver)
* 2015-02-13 [998c055](https://github.com/silverstripe/sapphire/commit/998c055) Misleading error message in SSViewer (Loz Calver)
* 2015-02-10 [bbe2799](https://github.com/silverstripe/sapphire/commit/bbe2799) Use correct query when searching for items managed by a tree dropdown field #3173 (Jean-Fabien Barrois)
* 2015-01-13 [ab24ed3](https://github.com/silverstripe/sapphire/commit/ab24ed3) . Use i18n_plural_name() instead of plural_name() (Elvinas L.)
* 2014-11-17 [a142ffd](https://github.com/silverstripe/silverstripe-cms/commit/a142ffd) VirtualPages use correct casting for 'virtual' database fields (Loz Calver)
## Changelog
* [framework](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.1.11-rc1)
* [cms](https://github.com/silverstripe/silverstripe-cms/releases/tag/3.1.11-rc1)
* [installer](https://github.com/silverstripe/silverstripe-installer/releases/tag/3.1.11-rc1)

View File

@ -6,7 +6,7 @@ The SilverStripe core modules (`framework` and `cms`), as well as some of the mo
git version control. SilverStripe hosts its modules on [github.com/silverstripe](http://github.com/silverstripe) and [github.com/silverstripe-labs](http://github.com/silverstripe-labs). After [installing git](http://help.github.com/git-installation-redirect) and creating a [free github.com account](https://github.com/signup/free), you can "fork" a module,
which creates a copy that you can commit to (see github's [guide to "forking"](http://help.github.com/forking/)).
For other modules, our [module list on silverstripe.org](http://silverstripe.org/modules) lists the repository locations, typically using a version control system like "git" or "[subversion](subversion)".
For other modules, our [add-ons site](http://addons.silverstripe.org/add-ons) lists the repository locations, typically using the version control system like "git".
<div class="hint" markdown="1">
Note: By supplying code to the SilverStripe core team in patches, tickets and pull requests, you agree to assign copyright of that code to SilverStripe Limited, on the condition that SilverStripe Limited releases that code under the BSD license.
@ -18,7 +18,7 @@ We ask for this so that the ownership in the license is clear and unambiguous, a
_**NOTE:** The commands on this page assume that you are branching from `4.0`, at the time of writing this is the pre-release branch._
1. Install the project through composer. The process is described in detail in "[Installation through Composer](../../installation/composer#contributing)".
1. Install the project through composer. The process is described in detail in "[Installation through Composer](../getting_started/composer#contributing)".
composer create-project --keep-vcs --dev silverstripe/installer ./my/website/folder 4.0.x-dev
@ -63,7 +63,43 @@ _**NOTE:** The commands on this page assume that you are branching from `4.0`, a
8. Issue pull request on GitHub. Visit your forked respoistory on GitHub.com and click the "Create Pull Request" button nex tot the new branch.
The core team is then responsible for reviewing patches and deciding if they will make it into core. If
there are any problems they will follow up with you, so please ensure they have a way to contact you!
there are any problems they will follow up with you, so please ensure they have a way to contact you!
### The Pull Request Process
Once your pull request is issued, it's not the end of the road. A [core committer](/contributing/core_committers/) will most likely have some questions for you and may ask you to make some changes depending on discussions you have.
If you've been naughty and not adhered to the coding conventions, expect a few requests to make changes so your code is in-line.
If your change is particularly significant, it may be referred to the [mailing list](https://groups.google.com/forum/#!forum/silverstripe-dev) for further community discussion.
A core committer will also "label" your PR using the labels defined in GitHub, these are to correctly classify and help find your work at a later date.
#### GitHub Labels
The current GitHub labels are grouped into 5 sections:
1. Changes - These are designed to signal what kind of change they are and how they fit into the [Semantic Versioning](http://semver.org/) schema
2. Impact - What impact does this bug/issue/fix have, does it break a feature completely, is it just a side effect or is it trivial and not a bit problem (but a bit annoying)
3. Effort - How much effort is required to fix this issue?
4. Type - What aspect of the system the PR/issue covers
5. Feedback - Are we waiting on feedback, if so who from? Typically used for issues that are likely to take a while to have feedback given
| Label | Purpose |
| ----- | ------- |
| change/major | A change for the next major release (eg: 4.0) |
| change/minor | A change for the next minor release (eg: 3.x) |
| change/patch | A change for the next patch release (eg: 3.1.x) |
| impact/critical | Broken functionality for which no work around can be produced |
| impact/high | Broken functionality but can be mitigated by other non-core code changes |
| impact/medium | Unexpected behaviour but does not break functionality |
| impact/low | A nuisance but doesn't break any functionality (typos, etc) |
| effort/easy | Someone with limited SilverStripe experience could resolve |
| effort/medium | Someone with a good understanding of SilverStripe could resolve |
| effort/hard | Only an expert with SilverStripe could resolve |
| type/docs | A docs change |
| type/frontend | A change to front-end (CSS, HTML, etc) |
| feedback-required/core-team | Core team members need to give an in-depth consideration |
| feedback-required/author | This issue is awaiting feedback from the original author of the PR |
### Workflow Diagram ###
@ -75,7 +111,7 @@ If you aren't familiar with git and GitHub, try reading the ["GitHub bootcamp do
We also found the [free online git book](http://git-scm.com/book/) and the [git crash course](http://gitref.org/) useful.
If you're familiar with it, here's the short version of what you need to know. Once you fork and download the code:
* **Don't develop on the master branch.** Always create a development branch specific to "the issue" you're working on (mostly on our [bugtracker](/misc/contributing/issues)). Name it by issue number and description. For example, if you're working on Issue #100, a `DataObject::get_one()` bugfix, your development branch should be called 100-dataobject-get-one. If you decide to work on another issue mid-stream, create a new branch for that issue--don't work on both in one branch.
* **Don't develop on the master branch.** Always create a development branch specific to "the issue" you're working on (on our [GitHub repository's issues](https://github.com/silverstripe/silverstripe-framework/issues)). Name it by issue number and description. For example, if you're working on Issue #100, a `DataObject::get_one()` bugfix, your development branch should be called 100-dataobject-get-one. If you decide to work on another issue mid-stream, create a new branch for that issue--don't work on both in one branch.
* **Do not merge the upstream master** with your development branch; *rebase* your branch on top of the upstream master.
@ -86,7 +122,7 @@ If you're familiar with it, here's the short version of what you need to know. O
* **Choose the correct branch**: Assume the current release is 3.0.3, and 3.1.0 is in beta state.
Most pull requests should go against the `3.1.x-dev` *pre-release branch*, only critical bugfixes
against the `3.0.x-dev` *release branch*. If you're changing an API or introducing a major feature,
the pull request should go against `master` (read more about our [release process](/misc/release-process)). Branches are periodically merged "upwards" (3.0 into 3.1, 3.1 into master).
the pull request should go against `master` (read more about our [release process](release_process)). Branches are periodically merged "upwards" (3.0 into 3.1, 3.1 into master).
### Editing files directly on GitHub.com
@ -96,11 +132,11 @@ After you have edited the file, GitHub will offer to create a pull request for y
## Check List
* Adhere to our [coding conventions](/misc/coding-conventions)
* Adhere to our [coding conventions](/getting_started/coding_conventions)
* If your patch is extensive, discuss it first on the [silverstripe-dev google group](https://groups.google.com/group/silverstripe-dev) (ideally before doing any serious coding)
* When working on existing tickets, provide status updates through ticket comments
* Check your patches against the "master" branch, as well as the latest release branch
* Write [unit tests](/topics/testing)
* Write [unit tests](../developer_guides/testing/unit_testing)
* Write [Behat integration tests](https://github.com/silverstripe-labs/silverstripe-behat-extension) for any interface changes
* Describe specifics on how to test the effects of the patch
* It's better to submit multiple patches with separate bits of functionality than a big patch containing lots of
@ -110,7 +146,7 @@ changes
[API documentation](http://api.silverstripe.org/3.1/) for good examples.
* Check and update documentation on [doc.silverstripe.org](http://doc.silverstripe.org). Check for any references to functionality deprecated or extended through your patch. Documentation changes should be included in the patch.
* If you get stuck, please post to the [forum](http://silverstripe.org/forum) or for deeper core problems, to the [core mailinglist](https://groups.google.com/forum/#!forum/silverstripe-dev)
* When working with the CMS, please read the ["CMS Architecture Guide"](/reference/cms-architecture) first
* When working with the CMS, please read the ["CMS Architecture Guide"](cms_architecture) first
## Commit Messages

View File

@ -9,7 +9,7 @@ only have time for a partial translation or quick review work - our system accom
same language.
The content for UI elements (button labels, field titles) and instruction texts shown in the CMS and elsewhere is
stored in the PHP code for a module (see [i18n](/topics/i18n)). All content can be extracted as a "language file", and
stored in the PHP code for a module (see [i18n](../developer_guides/i18n)). All content can be extracted as a "language file", and
uploaded to an online translation editor interface. SilverStripe is already translated in over 60 languages, and we're
relying on native speakers to keep these up to date, and of course add new languages.
@ -83,13 +83,13 @@ dropdown which automatically includes all found translations (based on the files
### I've found a piece of untranslatable text
It is entirely possible that we missed certain strings in preparing Silverstripe for translation-support. If you're
technically minded, please read [i18n](/topics/i18n) on how to make it translatable. Otherwise just post your findings
technically minded, please read [i18n](../developer_guides/i18n) on how to make it translatable. Otherwise just post your findings
to the forum.
### How do I add my own module?
Once you've built a translation-enabled module, you can run the "textcollector" on your local installation for this
specific module (see [i18n](/topics/i18n)). This should find all calls to `_t()` in php and template files, and generate
specific module (see [i18n](../developer_guides/i18n)). This should find all calls to `_t()` in php and template files, and generate
a new lang file with the default locale (path: <mymodule>/lang/en.yml). Upload this file to the online translation
tool, and wait for your translators to do their magic!
@ -119,7 +119,7 @@ translators.
### I'm seeing lots of duplicated translations, what should I do?
For now, please translate all duplications - sometimes they might be intentional, but mostly the developer just didn't
know his phrase was already translated. Please contact us about any duplicates that might be worth merging.
know their phrase was already translated. Please contact us about any duplicates that might be worth merging.
### What happened to translate.silverstripe.org?
@ -128,7 +128,7 @@ This was a custom-built online translation tool serving us well for a couple of
were migrated. Unfortunately, the ownership of individual translations couldn't be migrated.
As the new tool doesn't support the PHP format used in SilverStripe 2.x, this means that we no longer have a working
translation tool for PHP files. Please edit the PHP files directly and [send us pull requests](/misc/contributing).
translation tool for PHP files. Please edit the PHP files directly and [send us pull requests](/contributing).
This also applies for any modules staying compatible with SilverStripe 2.x.
@ -140,7 +140,7 @@ board if you have specific comments on a translation.
## Related
* [i18n](/developer_guids/i18n): Developer-level documentation of Silverstripe's i18n capabilities
* [translation-process](translation-process): Information about managing translations for the core team and/or module maintainers.
* [i18n](/developer_guides/i18n): Developer-level documentation of Silverstripe's i18n capabilities
* [Translation Process](translation_process): Information about managing translations for the core team and/or module maintainers.
* [translatable](https://github.com/silverstripe/silverstripe-translatable): DataObject-interface powering the website-content translations
* ["Translatable ModelAdmin" module](http://silverstripe.org/translatablemodeladmin-module/): An extension which allows translations of DataObjects inside ModelAdmin

View File

@ -4,14 +4,14 @@ summary: Implement SilverStripe's internationalization system in your own module
# Implementing Internationalization
To find out about how to assist with translating SilverStripe from a users point of view, see the
[Contributing Translations page](translation).
[Contributing Translations page](/contributing/translations).
## Set up your own module for localization
### Collecting translatable text
As a first step, you can automatically collect all translatable text in your module through the `i18nTextCollector`
task. See [i18n](/topics/i18n#collecting-text) for more details.
task. See [i18n](../developer_guides/i18n#collecting-text) for more details.
### Import master files
@ -127,7 +127,7 @@ files back into the JS files SilverStripe can actually read. This requires an in
# Related
* [i18n](/topics/i18n): Developer-level documentation of Silverstripe's i18n capabilities
* [contributing/translation](contributing/translation): Information for translators looking to contribute translations of the SilverStripe UI.
* [i18n](/developer_guides/i18n/): Developer-level documentation of Silverstripe's i18n capabilities
* [Contributing Translations](/contributing/translations): Information for translators looking to contribute translations of the SilverStripe UI.
* [translatable](https://github.com/silverstripe/silverstripe-translatable): DataObject-interface powering the website-content translations
* ["Translatable ModelAdmin" module](http://silverstripe.org/translatablemodeladmin-module/): An extension which allows translations of DataObjects inside ModelAdmin

View File

@ -24,7 +24,7 @@ With great power comes great responsibility, so we have agreed on certain expect
* Treat issues according to our [issue guidelines](issues_and_bugs)
* Don't commit directly to core, raise pull requests instead (except trivial fixes)
* Only merge code you have tested and fully understand. If in doubt, ask for a second opinion.
* Ensure contributions have appropriate [test coverage](/topics/testing), are documented, and pass our [coding conventions](/getting_started/coding-conventions)
* Ensure contributions have appropriate [test coverage](../developer_guides/testing), are documented, and pass our [coding conventions](/getting_started/coding_conventions)
* Keep the codebase "releasable" at all times (check our [release process](release_process))
* API changes and non-trivial features should not be merged into release branches.
* API changes on master should not be merged until they have the buy-in of at least two core committers (or better, through the [core mailing list](https://groups.google.com/forum/#!forum/silverstripe-dev))

View File

@ -830,7 +830,7 @@ class File extends DataObject {
'js' => _t('File.JsType', 'Javascript file'),
'css' => _t('File.CssType', 'CSS file'),
'html' => _t('File.HtmlType', 'HTML file'),
'htm' => _t('File.HtlType', 'HTML file')
'htm' => _t('File.HtmlType', 'HTML file')
);
$ext = $this->getExtension();

View File

@ -43,14 +43,14 @@ class ImagickBackend extends Imagick implements Image_Backend {
/**
* set_default_quality
*
* @deprecated 3.2 Use the "IMagickBackend.default_quality" config setting instead
* @deprecated 3.2 Use the "ImagickBackend.default_quality" config setting instead
* @param int $quality
* @return void
*/
public static function set_default_quality($quality) {
Deprecation::notice('3.2', 'Use the "IMagickBackend.default_quality" config setting instead');
Deprecation::notice('3.2', 'Use the "ImagickBackend.default_quality" config setting instead');
if(is_numeric($quality) && (int) $quality >= 0 && (int) $quality <= 100) {
config::inst()->update('IMagickBackend', 'default_quality', (int) $quality);
Config::inst()->update('ImagickBackend', 'default_quality', (int) $quality);
}
}

View File

@ -305,6 +305,16 @@ class Upload extends Controller {
*/
class Upload_Validator {
/**
* Contains a list of the max file sizes shared by
* all upload fields. This is then duplicated into the
* "allowedMaxFileSize" instance property on construct.
*
* @config
* @var array
*/
private static $default_max_file_size = array();
/**
* Information about the temporary file produced
* by the PHP-runtime.
@ -360,22 +370,46 @@ class Upload_Validator {
* @return int Filesize in bytes
*/
public function getAllowedMaxFileSize($ext = null) {
// Check if there is any defined instance max file sizes
if (empty($this->allowedMaxFileSize)) {
// Set default max file sizes if there isn't
$fileSize = Config::inst()->get('Upload_Validator', 'default_max_file_size');
if (isset($fileSize)) {
$this->setAllowedMaxFileSize($fileSize);
} else {
// When no default is present, use maximum set by PHP
$maxUpload = File::ini2bytes(ini_get('upload_max_filesize'));
$maxPost = File::ini2bytes(ini_get('post_max_size'));
$this->setAllowedMaxFileSize(min($maxUpload, $maxPost));
}
}
$ext = strtolower($ext);
if(isset($ext) && isset($this->allowedMaxFileSize[$ext])) {
return $this->allowedMaxFileSize[$ext];
if ($ext) {
if (isset($this->allowedMaxFileSize[$ext])) {
return $this->allowedMaxFileSize[$ext];
}
$category = File::get_app_category($ext);
if ($category && isset($this->allowedMaxFileSize['[' . $category . ']'])) {
return $this->allowedMaxFileSize['[' . $category . ']'];
}
return false;
} else {
return (isset($this->allowedMaxFileSize['*'])) ? $this->allowedMaxFileSize['*'] : false;
}
}
/**
* Set filesize maximums (in bytes).
* Set filesize maximums (in bytes or INI format).
* Automatically converts extensions to lowercase
* for easier matching.
*
* Example:
* <code>
* array('*' => 200, 'jpg' => 1000)
* array('*' => 200, 'jpg' => 1000, '[doc]' => '5m')
* </code>
*
* @param array|int $rules
@ -384,7 +418,22 @@ class Upload_Validator {
if(is_array($rules) && count($rules)) {
// make sure all extensions are lowercase
$rules = array_change_key_case($rules, CASE_LOWER);
$this->allowedMaxFileSize = $rules;
$finalRules = array();
$tmpSize = 0;
foreach ($rules as $rule => $value) {
if (is_numeric($value)) {
$tmpSize = $value;
} else {
$tmpSize = File::ini2bytes($value);
}
$finalRules[$rule] = (int)$tmpSize;
}
$this->allowedMaxFileSize = $finalRules;
} elseif(is_string($rules)) {
$this->allowedMaxFileSize['*'] = File::ini2bytes($rules);
} elseif((int) $rules > 0) {
$this->allowedMaxFileSize['*'] = (int)$rules;
}

View File

@ -109,7 +109,7 @@ class FileField extends FormField {
if($this->relationAutoSetting) {
// assume that the file is connected via a has-one
$hasOnes = $record->has_one($this->name);
$hasOnes = $record->hasOne($this->name);
// try to create a file matching the relation
$file = (is_string($hasOnes)) ? Object::create($hasOnes) : new $fileClass();
} else if($record instanceof File) {

View File

@ -93,8 +93,8 @@ class FormScaffolder extends Object {
}
// add has_one relation fields
if($this->obj->has_one()) {
foreach($this->obj->has_one() as $relationship => $component) {
if($this->obj->hasOne()) {
foreach($this->obj->hasOne() as $relationship => $component) {
if($this->restrictFields && !in_array($relationship, $this->restrictFields)) continue;
$fieldName = $component === 'DataObject'
? $relationship // Polymorphic has_one field is composite, so don't refer to ID subfield
@ -118,10 +118,10 @@ class FormScaffolder extends Object {
// only add relational fields if an ID is present
if($this->obj->ID) {
// add has_many relation fields
if($this->obj->has_many()
if($this->obj->hasMany()
&& ($this->includeRelations === true || isset($this->includeRelations['has_many']))) {
foreach($this->obj->has_many() as $relationship => $component) {
foreach($this->obj->hasMany() as $relationship => $component) {
if($this->tabbed) {
$relationTab = $fields->findOrMakeTab(
"Root.$relationship",
@ -145,10 +145,10 @@ class FormScaffolder extends Object {
}
}
if($this->obj->many_many()
if($this->obj->manyMany()
&& ($this->includeRelations === true || isset($this->includeRelations['many_many']))) {
foreach($this->obj->many_many() as $relationship => $component) {
foreach($this->obj->manyMany() as $relationship => $component) {
if($this->tabbed) {
$relationTab = $fields->findOrMakeTab(
"Root.$relationship",

View File

@ -88,5 +88,12 @@ class GroupedDropdownField extends DropdownField {
return 'groupeddropdown dropdown';
}
/**
* @todo Implement DropdownField::validate() with group validation support
*/
public function validate(Validator $validator) {
return true;
}
}

View File

@ -453,7 +453,6 @@ class TreeDropdownField extends FormField {
$this->labelField
));
}
$res = DataObject::get($this->sourceObject)->filterAny($filters);
}
@ -463,18 +462,18 @@ class TreeDropdownField extends FormField {
if ($row->ParentID) $parents[$row->ParentID] = true;
$this->searchIds[$row->ID] = true;
}
$sourceObject = $this->sourceObject;
while (!empty($parents)) {
$idsClause = DB::placeholders($parents);
$res = DB::prepared_query(
"SELECT \"ParentID\", \"ID\" FROM \"{$this->sourceObject}\" WHERE \"ID\" in ($idsClause)",
array_keys($parents)
);
$items = $sourceObject::get()
->filter("ID",array_keys($parents));
$parents = array();
foreach($res as $row) {
if ($row['ParentID']) $parents[$row['ParentID']] = true;
$this->searchIds[$row['ID']] = true;
$this->searchExpanded[$row['ID']] = true;
foreach($items as $item) {
if ($item->ParentID) $parents[$item->ParentID] = true;
$this->searchIds[$item->ID] = true;
$this->searchExpanded[$item->ID] = true;
}
}
}

View File

@ -502,7 +502,7 @@ class UploadField extends FileField {
if($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
// has_many or many_many
$relation->setByIDList($idList);
} elseif($record->has_one($fieldname)) {
} elseif($record->hasOneComponent($fieldname)) {
// has_one
$record->{"{$fieldname}ID"} = $idList ? reset($idList) : 0;
}
@ -590,7 +590,7 @@ class UploadField extends FileField {
if(empty($allowedMaxFileNumber)) {
$record = $this->getRecord();
$name = $this->getName();
if($record && $record->has_one($name)) {
if($record && $record->hasOneComponent($name)) {
return 1; // Default for has_one
} else {
return null; // Default for has_many and many_many

View File

@ -841,7 +841,8 @@ class GridField_FormAction extends FormAction {
'args' => $this->args,
);
$id = md5(serialize($state));
// Ensure $id doesn't contain only numeric characters
$id = 'gf_'.substr(md5(serialize($state)), 0, 8);
Session::set($id, $state);
$actionData['StateID'] = $id;

View File

@ -95,7 +95,7 @@ class GridFieldDetailForm implements GridField_URLHandler {
// if no validator has been set on the GridField and the record has a
// CMS validator, use that.
if(!$this->getValidator() && method_exists($record, 'getCMSValidator')) {
if(!$this->getValidator() && (method_exists($record, 'getCMSValidator') || $record instanceof Object && $record->hasMethod('getCMSValidator'))) {
$this->setValidator($record->getCMSValidator());
}
@ -188,6 +188,7 @@ class GridFieldDetailForm implements GridField_URLHandler {
*/
public function setItemEditFormCallback(Closure $cb) {
$this->itemEditFormCallback = $cb;
return $this;
}
/**

View File

@ -155,7 +155,9 @@ class GridFieldExportButton implements GridField_HTMLProvider, GridField_ActionP
$fileData .= "\n";
}
$item->destroy();
if ($item->hasMethod('destroy')) {
$item->destroy();
}
}
return $fileData;

View File

@ -136,6 +136,12 @@
$('.ss-gridfield .action').entwine({
onclick: function(e){
var filterState='show'; //filterstate should equal current state.
// If the button is disabled, do nothing.
if (this.button('option', 'disabled')) {
e.preventDefault();
return;
}
if(this.hasClass('ss-gridfield-button-close') || !(this.closest('.ss-gridfield').hasClass('show-filter'))){
filterState='hidden';
@ -146,6 +152,34 @@
}
});
/**
* Don't allow users to submit empty values in grid field auto complete inputs.
*/
$('.ss-gridfield .add-existing-autocompleter').entwine({
onbuttoncreate: function () {
var self = this;
this.toggleDisabled();
this.find('input[type="text"]').on('keyup', function () {
self.toggleDisabled();
});
},
onunmatch: function () {
this.find('input[type="text"]').off('keyup');
},
toggleDisabled: function () {
var $button = this.find('.ss-ui-button'),
$input = this.find('input[type="text"]'),
inputHasValue = $input.val() !== '',
buttonDisabled = $button.is(':disabled');
if ((inputHasValue && buttonDisabled) || (!inputHasValue && !buttonDisabled)) {
$button.button("option", "disabled", !buttonDisabled);
}
}
});
// Covers both tabular delete button, and the button on the detail form
$('.ss-gridfield .col-buttons .action.gridfield-button-delete, .cms-edit-form .Actions button.action.action-delete').entwine({
onclick: function(e){

41
javascript/lang/src/sv.js Normal file
View File

@ -0,0 +1,41 @@
{
"VALIDATOR.FIELDREQUIRED": "Var god fyll i \"%s\", det är obligatoriskt.",
"HASMANYFILEFIELD.UPLOADING": "Laddar upp... %s",
"TABLEFIELD.DELETECONFIRMMESSAGE": "Vill du verkligen radera detta?",
"LOADING": "laddar...",
"UNIQUEFIELD.SUGGESTED": "Ändrade värde till '%s' : %s",
"UNIQUEFIELD.ENTERNEWVALUE": "Du måste fylla i ett nytt värde för detta fält",
"UNIQUEFIELD.CANNOTLEAVEEMPTY": "Detta fält kan inte lämnas tomt",
"RESTRICTEDTEXTFIELD.CHARCANTBEUSED": "Tecknet '%s' kan inte användas i detta fält",
"UPDATEURL.CONFIRM": "Vill du att URL:en ändras till:\n\n%s/\n\nKlicka OK för att ändra URL:en, klicka Avbryt för att lämna den som:\n\n%s",
"UPDATEURL.CONFIRMURLCHANGED": "URL:en har ändrats till\n'%s'",
"FILEIFRAMEFIELD.DELETEFILE": "Radera fil",
"FILEIFRAMEFIELD.UNATTACHFILE": "Avlänka fil",
"FILEIFRAMEFIELD.DELETEIMAGE": "Radera bild",
"FILEIFRAMEFIELD.CONFIRMDELETE": "Vill du verkligen radera denna fil?",
"LeftAndMain.IncompatBrowserWarning": "Din webbläsare är inte kompatibel med detta CMS. Var god använd Internet Explorer 7+, Google Chrome 10+ eller Mozilla Firefox 3.5+.",
"GRIDFIELD.ERRORINTRANSACTION": "Ett fel uppstod när data hämtades från servern.\nVar god försök igen senare.",
"HtmlEditorField.SelectAnchor": "Välj ett ankare",
"UploadField.ConfirmDelete": "Är du säker på att du vill radera denna fil från servern?",
"UploadField.PHP_MAXFILESIZE": "Filen överskrider upload_max_filesize (php-ini-direktiv)",
"UploadField.HTML_MAXFILESIZE": "Filen överskrider MAX_FILE_SIZE (HTML form-direktiv)",
"UploadField.ONLYPARTIALUPLOADED": "Filen laddas bara upp delvis",
"UploadField.NOFILEUPLOADED": "Ingen fil laddades upp",
"UploadField.NOTMPFOLDER": "Tillfällig mapp saknas",
"UploadField.WRITEFAILED": "Kunde inte skriva filen",
"UploadField.STOPEDBYEXTENSION": "Uppladdning stoppades av otillåten filtyp",
"UploadField.TOOLARGE": "Filen är för stor",
"UploadField.TOOSMALL": "Filen är för liten",
"UploadField.INVALIDEXTENSION": "Filtypen tillåts inte",
"UploadField.MAXNUMBEROFFILESSIMPLE": "Maximalt antal filer överstiget",
"UploadField.UPLOADEDBYTES": "Antalet uppladdade bytes överstiger filstorleken",
"UploadField.EMPTYRESULT": "Tomt uppladdningsresultat",
"UploadField.LOADING": "Laddar ...",
"UploadField.Editing": "Redigerar ...",
"UploadField.Uploaded": "Uppladdad",
"UploadField.OVERWRITEWARNING": "Fil med samma namn existerar redan",
"TreeDropdownField.ENTERTOSEARCH": "Tryck Enter för att söka",
"TreeDropdownField.OpenLink": "Öppna",
"TreeDropdownField.FieldTitle": "Välj",
"TreeDropdownField.SearchFieldTitle": "Välj eller Sök"
}

47
javascript/lang/sv.js Normal file
View File

@ -0,0 +1,47 @@
// This file was generated by GenerateJavaScriptI18nTask from javascript/lang/src/sv.js.
// See https://github.com/silverstripe/silverstripe-buildtools for details
if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
if(typeof(console) != 'undefined') console.error('Class ss.i18n not defined');
} else {
ss.i18n.addDictionary('sv', {
"VALIDATOR.FIELDREQUIRED": "Var god fyll i \"%s\", det är obligatoriskt.",
"HASMANYFILEFIELD.UPLOADING": "Laddar upp... %s",
"TABLEFIELD.DELETECONFIRMMESSAGE": "Vill du verkligen radera detta?",
"LOADING": "laddar...",
"UNIQUEFIELD.SUGGESTED": "Ändrade värde till '%s' : %s",
"UNIQUEFIELD.ENTERNEWVALUE": "Du måste fylla i ett nytt värde för detta fält",
"UNIQUEFIELD.CANNOTLEAVEEMPTY": "Detta fält kan inte lämnas tomt",
"RESTRICTEDTEXTFIELD.CHARCANTBEUSED": "Tecknet '%s' kan inte användas i detta fält",
"UPDATEURL.CONFIRM": "Vill du att URL:en ändras till:\n\n%s/\n\nKlicka OK för att ändra URL:en, klicka Avbryt för att lämna den som:\n\n%s",
"UPDATEURL.CONFIRMURLCHANGED": "URL:en har ändrats till\n'%s'",
"FILEIFRAMEFIELD.DELETEFILE": "Radera fil",
"FILEIFRAMEFIELD.UNATTACHFILE": "Avlänka fil",
"FILEIFRAMEFIELD.DELETEIMAGE": "Radera bild",
"FILEIFRAMEFIELD.CONFIRMDELETE": "Vill du verkligen radera denna fil?",
"LeftAndMain.IncompatBrowserWarning": "Din webbläsare är inte kompatibel med detta CMS. Var god använd Internet Explorer 7+, Google Chrome 10+ eller Mozilla Firefox 3.5+.",
"GRIDFIELD.ERRORINTRANSACTION": "Ett fel uppstod när data hämtades från servern.\nVar god försök igen senare.",
"HtmlEditorField.SelectAnchor": "Välj ett ankare",
"UploadField.ConfirmDelete": "Är du säker på att du vill radera denna fil från servern?",
"UploadField.PHP_MAXFILESIZE": "Filen överskrider upload_max_filesize (php-ini-direktiv)",
"UploadField.HTML_MAXFILESIZE": "Filen överskrider MAX_FILE_SIZE (HTML form-direktiv)",
"UploadField.ONLYPARTIALUPLOADED": "Filen laddas bara upp delvis",
"UploadField.NOFILEUPLOADED": "Ingen fil laddades upp",
"UploadField.NOTMPFOLDER": "Tillfällig mapp saknas",
"UploadField.WRITEFAILED": "Kunde inte skriva filen",
"UploadField.STOPEDBYEXTENSION": "Uppladdning stoppades av otillåten filtyp",
"UploadField.TOOLARGE": "Filen är för stor",
"UploadField.TOOSMALL": "Filen är för liten",
"UploadField.INVALIDEXTENSION": "Filtypen tillåts inte",
"UploadField.MAXNUMBEROFFILESSIMPLE": "Maximalt antal filer överstiget",
"UploadField.UPLOADEDBYTES": "Antalet uppladdade bytes överstiger filstorleken",
"UploadField.EMPTYRESULT": "Tomt uppladdningsresultat",
"UploadField.LOADING": "Laddar ...",
"UploadField.Editing": "Redigerar ...",
"UploadField.Uploaded": "Uppladdad",
"UploadField.OVERWRITEWARNING": "Fil med samma namn existerar redan",
"TreeDropdownField.ENTERTOSEARCH": "Tryck Enter för att söka",
"TreeDropdownField.OpenLink": "Öppna",
"TreeDropdownField.FieldTitle": "Välj",
"TreeDropdownField.SearchFieldTitle": "Välj eller Sök"
});
}

View File

@ -235,7 +235,7 @@ lt:
SINGULARNAME: Grupė
Sort: 'Rūšiavimo tvarka'
has_many_Permissions: Leidimai
many_many_Members: Nariai
many_many_Members: Vartotojai
GroupImportForm:
Help1: '<p>Importuoti vieną ar kelias grupes <em>CSV</em> formatu (kableliu atskirtos reikšmės). <small><a href="#" class="toggle-advanced">Rodyti detalesnį aprašymą</a></small></p>'
Help2: "<div class=\"advanced\">\n<h4>Detalesnis aprašymas</h4>\n<ul>\n<li>Galimi stulpeliai: <em>%s</em></li>\n<li>Esamos grupės yra surišamos su jų unikalia <em>Code</em> reikšme ir atnaujinamos duomenimis iš importuojamos bylos</li>\n<li>Grupių hierarchija gali būti sukurta naudojant <em>ParentCode</em> stulpelį.</li>\n<li>Leidimų gali būti priskirti naudojant <em>PermissionCode</em> stulpelį. Esami leidimai nebus pakeisti.</li>\n</ul>\n</div>"
@ -350,15 +350,15 @@ lt:
NEWPASSWORD: 'Naujas slaptažodis'
NoPassword: 'Šis vartotojas neturi slaptažodžio.'
PASSWORD: Slaptažodis
PLURALNAME: Nariai
PLURALNAME: Vartotojai
REMEMBERME: 'Prisiminti jungiantis kitą kartą?'
SINGULARNAME: Narys
SINGULARNAME: Vartotojas
SUBJECTPASSWORDCHANGED: 'Jūsų slaptažodis pakeistas'
SUBJECTPASSWORDRESET: 'Slaptažodžio atstatymo nuoroda'
SURNAME: Pavardė
TIMEFORMAT: 'Laiko formatas'
VALIDATIONMEMBEREXISTS: 'Tokį e. paštą jau naudoja kitas narys.'
ValidationIdentifierFailed: 'Nepavyko perrašyti nario #{id} su tuo pačiu atpažinimo kodu ({name} = {value}))'
VALIDATIONMEMBEREXISTS: 'Vartotojas šiuo el. pašto adresu %s jau egzistuoja.'
ValidationIdentifierFailed: 'Nepavyko atnaujinti vartotojo #{id} duomenų su atpažinimo kodu ({name} = {value})'
WELCOMEBACK: 'Sveiki, {firstname}'
YOUROLDPASSWORD: 'Jūsų senas slaptažodis'
belongs_many_many_Groups: Grupės
@ -488,7 +488,7 @@ lt:
GROUPNAME: 'Grupės pavadinimas'
IMPORTGROUPS: 'Importuoti grupes'
IMPORTUSERS: 'Importuoti vartotojus'
MEMBERS: Nariai
MEMBERS: Vartotojai
MENUTITLE: Saugumas
MemberListCaution: 'Dėmesio: Pašalinus vartotojus iš šio sąrašo, jie bus pašalinti ir iš visų grupių, bei duomenų bazės.'
NEWGROUP: 'Nauja grupė'

View File

@ -1,6 +1,7 @@
sv:
AssetAdmin:
NEWFOLDER: Ny mapp
SHOWALLOWEDEXTS: 'Visa tillåtna filtyper'
AssetTableField:
CREATED: 'Först uppladdad'
DIM: Dimensioner
@ -67,6 +68,8 @@ sv:
ACCESSALLINTERFACES: 'Tillgång till alla CMS-sektioner'
ACCESSALLINTERFACESHELP: 'Ersätter mer specifika behörighetsinställningar.'
SAVE: Spara
CMSPageHistoryController_versions_ss:
PREVIEW: 'Förhandsgranska sida'
CMSProfileController:
MENUTITLE: 'Min Profil'
ChangePasswordEmail_ss:
@ -87,6 +90,8 @@ sv:
FOURTH: fjärde
SECOND: andra
THIRD: tredje
CurrencyField:
CURRENCYSYMBOL: $
DataObject:
PLURALNAME: 'Dataobjekt'
SINGULARNAME: 'Dataobjekt'
@ -96,9 +101,12 @@ sv:
HOUR: timme
HOURS: timmar
LessThanMinuteAgo: 'mindre än en minut'
MIN: minut
MINS: minuter
MONTH: månad
MONTHS: månader
SEC: sek
SEC: sekund
SECS: sekunder
TIMEDIFFAGO: '{difference} sen'
TIMEDIFFIN: 'om {difference}'
YEAR: år
@ -121,37 +129,39 @@ sv:
Enum:
ANY: Vilken som helst
File:
AviType: 'AVI videofil'
AviType: 'AVI-videofil'
Content: Innehåll
CssType: 'CSS fil'
DmgType: 'Apple skivavbild'
DocType: 'Word dokument'
CssType: 'CSS-fil'
DmgType: 'Apple-skivavbild'
DocType: 'Word-dokument'
Filename: Filnamn
GifType: 'GIF bild - bra för diagram'
GzType: 'GZIP packad fil'
HtlType: 'HTML fil'
HtmlType: 'HTML fil'
GifType: 'GIF-bild - bra för diagram'
GzType: 'GZIP-packad fil'
HtlType: 'HTML-fil'
HtmlType: 'HTML-fil'
INVALIDEXTENSION: 'Filändelsen tillåts inte (tillåtna: {extensions})'
INVALIDEXTENSIONSHORT: 'Filändelsen tillåts inte'
IcoType: 'Icon bild'
JpgType: 'JPEG bild - bra för fotografier'
JsType: 'Javascript fil'
Mp3Type: 'MP3 ljudfil'
MpgType: 'MPEG videofil'
IcoType: 'Ikonbild'
JpgType: 'JPEG-bild - bra för fotografier'
JsType: 'Javascript-fil'
Mp3Type: 'MP3-ljudfil'
MpgType: 'MPEG-videofil'
NOFILESIZE: 'Filstorleken är noll bytes'
NOVALIDUPLOAD: 'Filen är inte giltig för uppladdning'
Name: Namn
PLURALNAME: Filer
PdfType: 'Adobe Acrobat PDF fil'
PngType: 'PNG bild - bra allmänt format'
PdfType: 'Adobe Acrobat PDF-fil'
PngType: 'PNG-bild - bra allmänt format'
SINGULARNAME: Fil
TOOLARGE: 'Filen är för stor, max {size} tillåts'
TOOLARGESHORT: 'Filstorlek överskriden {size}'
TiffType: 'Tiff bildformat'
Title: Titel
WavType: 'WAV ljudfil'
WavType: 'WAV-ljudfil'
XlsType: 'Excel kalkylblad'
ZipType: 'ZIP packad fil'
ZipType: 'ZIP-packad fil'
Filesystem:
SYNCRESULTS: 'Synkning komplett: {createdcount} artiklar skapades, {deletedcount} artiklar raderades'
Folder:
PLURALNAME: Mappar
SINGULARNAME: Mapp
@ -161,14 +171,19 @@ sv:
TEXT2: 'Återställningslänk för lösenord'
TEXT3: för
Form:
CSRF_FAILED_MESSAGE: "Ett tekniskt fel uppstod. Var god klicka på bakåt-knappen,\n⇥⇥⇥⇥⇥ladda om webbläsaren och försök igen."
FIELDISREQUIRED: '{name} är obligatoriskt'
SubmitBtnLabel: Kör
VALIDATIONCREDITNUMBER: 'Kontrollera att du angav kortnummret {number} rätt'
VALIDATIONNOTUNIQUE: 'Det angivna värdet är inte unikt'
VALIDATIONPASSWORDSDONTMATCH: 'Lösenorden stämmer inte överrens '
VALIDATIONPASSWORDSNOTEMPTY: 'Lösenordfältet får inte vara tomt'
VALIDATIONSTRONGPASSWORD: 'Lösenord måste innehålla minst en siffra och en bokstav.'
VALIDATOR: Validator
VALIDCURRENCY: 'Var vänlig ange en korrekt valuta'
CSRF_EXPIRED_MESSAGE: 'Din session har upphört. Var god och skicka in formuläret på nytt.'
FormField:
Example: 't.ex. %s'
NONE: ingen
GridAction:
DELETE_DESCRIPTION: Radera
@ -191,6 +206,7 @@ sv:
ResetFilter: Rensa
GridFieldAction_Delete:
DeletePermissionsFailure: 'Rättighet för att radera saknas'
EditPermissionsFailure: 'Rättigheter för avlänkning saknas'
GridFieldDetailForm:
CancelBtn: Avbryt
Create: Skapa
@ -198,6 +214,7 @@ sv:
DeletePermissionsFailure: 'Rättighet för att radera saknas'
Deleted: 'Raderade %s %s'
Save: Spara
Saved: 'Sparade {name} {link}'
GridFieldEditButton_ss:
EDIT: Redigera
GridFieldItemEditView:
@ -209,9 +226,11 @@ sv:
DefaultGroupTitleContentAuthors: 'Författare'
Description: Beskrivning
GroupReminder: 'Om du väljer en förälder till gruppen så kommer gruppen ärva alla förälderns roller'
HierarchyPermsError: 'Den överordnade gruppen "%s" kan inte ges priviligerad tillgång (adminrättigheter krävs)'
Locked: 'Låst?'
NoRoles: 'Inga roller fun'
PLURALNAME: Grupper
Parent: 'Överordnad grupp'
RolesAddEditLink: 'Hantera roller'
SINGULARNAME: Grupp
Sort: 'Sorteringsordning'
@ -250,6 +269,7 @@ sv:
FindInFolder: 'Hitta i mapp'
IMAGEALT: 'Alternativ text (alt)'
IMAGEALTTEXT: 'Alternativ text (alt) - visas om bilden inte kan visas'
IMAGEALTTEXTDESC: 'Visas för skärmläsare eller om bilden inte kan visas'
IMAGEDIMENSIONS: Dimensioner
IMAGEHEIGHTPX: Höjd
IMAGETITLE: 'Titel text (tooltip) - för ytterligare information om bilden'
@ -319,8 +339,10 @@ sv:
EMAIL: E-post
EMPTYNEWPASSWORD: 'Det nya lösenordet kan inte vara tomt, vänligen försök igen'
ENTEREMAIL: 'Ange en e-postadress för att få en återställningslänk för lösenordet.'
ERRORLOCKEDOUT2: 'Ditt konto har tillfälligt stängs av på grund av för många misslyckade inloggningsförsök. Försök igen om {count} minuter.'
ERRORNEWPASSWORD: 'Du har angett ditt nya lösenord annorlunda, försök igen'
ERRORPASSWORDNOTMATCH: 'Ditt nuvarande lösenord matchar inte, var god försök igen'
ERRORWRONGCRED: 'Antingen e-postadressen eller lösenordet är fel. Försök igen.'
FIRSTNAME: 'Förnamn'
INTERFACELANG: 'Gränssnittsspråk'
INVALIDNEWPASSWORD: 'Vi kunde inte godkänna det lösenordet: {password}'
@ -412,6 +434,10 @@ sv:
Pagination:
Page: Sida
View: Visa
PasswordValidator:
LOWCHARSTRENGTH: 'Var god och stärk ditt lösenord genom att lägga till något av följande tecken: %s'
PREVPASSWORD: 'Du har redan använt samma lösenord tidigare, var god och välj ett nytt lösenord'
TOOSHORT: 'Lösenordet är för kort, det måste innehålla %s eller fler tecken.'
Permission:
AdminGroup: Administratör
CMS_ACCESS_CATEGORY: 'CMS-åtkomst'
@ -431,6 +457,7 @@ sv:
Title: Rollnamn
PermissionRoleCode:
PLURALNAME: 'Koder för rollrättigheter'
PermsError: 'Koden "%s" kan inte ges privilegierad tillgång (adminrättigheter krävs)'
SINGULARNAME: 'Kodför rollrättigheter'
Permissions:
PERMISSIONS_CATEGORY: 'Roller och åtkomsträttigheter'
@ -446,6 +473,7 @@ sv:
ERRORPASSWORDPERMISSION: 'Du måste vara inloggad för att kunna ändra ditt lösenord!'
LOGGEDOUT: 'Du har blivit utloggad. Om du vill logga in igen anger du dina uppgifter nedan.'
LOGIN: 'Logga in'
LOSTPASSWORDHEADER: 'Bortglömt lösenord'
NOTEPAGESECURED: 'Den här sidan är låst. Fyll i dina uppgifter nedan så skickar vi dig vidare.'
NOTERESETLINKINVALID: '<p>Återställningslänk för lösenord är felaktig eller för gammal.</p><p>Du kan begära en ny <a href="{link1}">här</a> eller ändra ditt lösenord när du <a href="{link2}">loggat in</a>.</p>'
NOTERESETPASSWORD: 'Ange din e-postadress så skickar vi en länk med vilken du kan återställa ditt lösenord'
@ -474,8 +502,21 @@ sv:
FileFieldLabel: 'CSV-fil <small>(Tillåtna filtyper: *.csv)</small>'
SilverStripeNavigator:
Auto: Auto
ChangeViewMode: 'Ändra visningsläge'
Desktop: Stationär
DualWindowView: 'Delat fönster'
Edit: Ändra
EditView: 'Redigeringsläge'
Mobile: Mobil
PreviewState: 'Förhandsgranskningsstatus'
PreviewView: 'Förhandsgranskningsläge'
Responsive: Responsiv
SplitView: 'Uppdelat läge'
Tablet: Platta
ViewDeviceWidth: 'Välj bredd på förhandsgranskning'
Width: bredd
SiteTree:
TABMAIN: Allmän
TableListField:
CSVEXPORT: 'Exportera till CSV'
Print: Skriv ut

View File

@ -78,7 +78,7 @@ class DataDifferencer extends ViewableData {
$fields = array_keys($this->toRecord->toMap());
}
$hasOnes = array_merge($this->fromRecord->has_one(), $this->toRecord->has_one());
$hasOnes = array_merge($this->fromRecord->hasOne(), $this->toRecord->hasOne());
// Loop through properties
foreach($fields as $field) {

View File

@ -547,10 +547,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// DO NOT copy has_many relations, because copying the relation would result in us changing the has_one
// relation on the other side of this relation to point at the copy and no longer the original (being a
// has_one, it can only point at one thing at a time). So, all relations except has_many can and are copied
if ($sourceObject->has_one()) foreach($sourceObject->has_one() as $name => $type) {
if ($sourceObject->hasOne()) foreach($sourceObject->hasOne() as $name => $type) {
$this->duplicateRelations($sourceObject, $destinationObject, $name);
}
if ($sourceObject->many_many()) foreach($sourceObject->many_many() as $name => $type) {
if ($sourceObject->manyMany()) foreach($sourceObject->manyMany() as $name => $type) {
//many_many include belongs_many_many
$this->duplicateRelations($sourceObject, $destinationObject, $name);
}
@ -666,24 +666,24 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if($this->class == 'DataObject') return;
// Set up accessors for joined items
if($manyMany = $this->many_many()) {
if($manyMany = $this->manyMany()) {
foreach($manyMany as $relationship => $class) {
$this->addWrapperMethod($relationship, 'getManyManyComponents');
}
}
if($hasMany = $this->has_many()) {
if($hasMany = $this->hasMany()) {
foreach($hasMany as $relationship => $class) {
$this->addWrapperMethod($relationship, 'getComponents');
}
}
if($hasOne = $this->has_one()) {
if($hasOne = $this->hasOne()) {
foreach($hasOne as $relationship => $class) {
$this->addWrapperMethod($relationship, 'getComponent');
}
}
if($belongsTo = $this->belongs_to()) foreach(array_keys($belongsTo) as $relationship) {
if($belongsTo = $this->belongsTo()) foreach(array_keys($belongsTo) as $relationship) {
$this->addWrapperMethod($relationship, 'getComponent');
}
}
@ -972,7 +972,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// merge relations
if($includeRelations) {
if($manyMany = $this->many_many()) {
if($manyMany = $this->manyMany()) {
foreach($manyMany as $relationship => $class) {
$leftComponents = $leftObj->getManyManyComponents($relationship);
$rightComponents = $rightObj->getManyManyComponents($relationship);
@ -983,7 +983,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
if($hasMany = $this->has_many()) {
if($hasMany = $this->hasMany()) {
foreach($hasMany as $relationship => $class) {
$leftComponents = $leftObj->getComponents($relationship);
$rightComponents = $rightObj->getComponents($relationship);
@ -995,7 +995,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
if($hasOne = $this->has_one()) {
if($hasOne = $this->hasOne()) {
foreach($hasOne as $relationship => $class) {
$leftComponent = $leftObj->getComponent($relationship);
$rightComponent = $rightObj->getComponent($relationship);
@ -1130,7 +1130,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->$fieldName = $fieldValue;
}
// Set many-many defaults with an array of ids
if(is_array($fieldValue) && $this->many_many($fieldName)) {
if(is_array($fieldValue) && $this->manyManyComponent($fieldName)) {
$manyManyJoin = $this->$fieldName();
$manyManyJoin->setByIdList($fieldValue);
}
@ -1497,7 +1497,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $this->components[$componentName];
}
if($class = $this->has_one($componentName)) {
if($class = $this->hasOneComponent($componentName)) {
$joinField = $componentName . 'ID';
$joinID = $this->getField($joinField);
@ -1514,7 +1514,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(empty($component)) {
$component = $this->model->$class->newObject();
}
} elseif($class = $this->belongs_to($componentName)) {
} elseif($class = $this->belongsToComponent($componentName)) {
$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
$joinID = $this->ID;
@ -1553,18 +1553,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Returns a one-to-many relation as a HasManyList
*
* @param string $componentName Name of the component
* @param string $filter A filter to be inserted into the WHERE clause
* @param string|array $sort A sort expression to be inserted into the ORDER BY clause. If omitted, the static
* field $default_sort on the component class will be used.
* @param string|null $filter Deprecated. A filter to be inserted into the WHERE clause
* @param string|null|array $sort Deprecated. A sort expression to be inserted into the ORDER BY clause. If omitted,
* the static field $default_sort on the component class will be used.
* @param string $join Deprecated, use leftJoin($table, $joinClause) instead
* @param string|array $limit A limit expression to be inserted into the LIMIT clause
* @param string|null|array $limit Deprecated. A limit expression to be inserted into the LIMIT clause
*
* @return HasManyList The components of the one-to-many relationship.
*/
public function getComponents($componentName, $filter = "", $sort = "", $join = "", $limit = null) {
public function getComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
$result = null;
if(!$componentClass = $this->has_many($componentName)) {
if(!$componentClass = $this->hasManyComponent($componentName)) {
user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName'"
. " on class '$this->class'", E_USER_ERROR);
}
@ -1575,6 +1575,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
);
}
if($filter !== null || $sort !== null || $limit !== null) {
Deprecation::notice('3.2', 'The $filter, $sort and $limit parameters for DataObject::getComponents()
have been deprecated. Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
}
// If we haven't been written yet, we can't save these relations, so use a list that handles this case
if(!$this->ID) {
if(!isset($this->unsavedRelations[$componentName])) {
@ -1647,7 +1652,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
// Extract relation from current object
$remoteClass = $this->$type($component, false);
if($type === 'has_many') {
$remoteClass = $this->hasManyComponent($component, false);
} else {
$remoteClass = $this->belongsToComponent($component, false);
}
if(empty($remoteClass)) {
throw new Exception("Unknown $type component '$component' on class '$this->class'");
}
@ -1716,8 +1726,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*
* @todo Implement query-params
*/
public function getManyManyComponents($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
public function getManyManyComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
list($parentClass, $componentClass, $parentField, $componentField, $table)
= $this->manyManyComponent($componentName);
if($filter !== null || $sort !== null || $join !== null || $limit !== null) {
Deprecation::notice('3.2', 'The $filter, $sort, $join and $limit parameters for
DataObject::getManyManyComponents() have been deprecated.
Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
}
// If we haven't been written yet, we can't save these relations, so use a list that handles this case
if(!$this->ID) {
@ -1730,7 +1747,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$result = ManyManyList::create(
$componentClass, $table, $componentField, $parentField,
$this->many_many_extraFields($componentName)
$this->manyManyExtraFieldsForComponent($componentName)
);
if($this->model) $result->setDataModel($this->model);
@ -1743,64 +1760,91 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
->limit($limit);
}
/**
* @deprecated 4.0 Method has been replaced by hasOne() and hasOneComponent()
* @param string $component
* @return array|null
*/
public function has_one($component = null) {
if($component) {
Deprecation::notice('3.2', 'Please use hasOneComponent() instead');
return $this->hasOneComponent($component);
}
Deprecation::notice('3.2', 'Please use hasOne() instead');
return $this->hasOne();
}
/**
* Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and
* their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
*
* @param string $component Name of component
*
* @return string|array The class of the one-to-one component, or an array of all one-to-one components and their
* classes.
* @param string $component Deprecated - Name of component
* @return string|array The class of the one-to-one component, or an array of all one-to-one components and
* their classes.
*/
public function has_one($component = null) {
$classes = ClassInfo::ancestry($this);
foreach($classes as $class) {
// Wait until after we reach DataObject
if(in_array($class, array('Object', 'ViewableData', 'DataObject'))) continue;
if($component) {
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
if(isset($hasOne[$component])) {
return $hasOne[$component];
}
} else {
$newItems = (array)Config::inst()->get($class, 'has_one', Config::UNINHERITED);
// Validate the data
foreach($newItems as $k => $v) {
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
user_error("$class::\$has_one has a bad entry: "
. var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a"
. " relationship name, and the map value should be the data class to join to.", E_USER_ERROR);
}
}
$items = isset($items) ? array_merge($newItems, (array)$items) : $newItems;
}
public function hasOne($component = null) {
if($component) {
Deprecation::notice(
'3.2',
'Please use DataObject::hasOneComponent() instead of passing a component name to hasOne()',
Deprecation::SCOPE_GLOBAL
);
return $this->hasOneComponent($component);
}
return isset($items) ? $items : null;
return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
}
/**
* Return data for a specific has_one component.
* @param string $component
* @return string|null
*/
public function hasOneComponent($component) {
$hasOnes = (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
if(isset($hasOnes[$component])) {
return $hasOnes[$component];
}
}
/**
* @deprecated 4.0 Method has been replaced by belongsTo() and belongsToComponent()
* @param string $component
* @param bool $classOnly
* @return array|null
*/
public function belongs_to($component = null, $classOnly = true) {
if($component) {
Deprecation::notice('3.2', 'Please use belongsToComponent() instead');
return $this->belongsToComponent($component, $classOnly);
}
Deprecation::notice('3.2', 'Please use belongsTo() instead');
return $this->belongsTo(null, $classOnly);
}
/**
* Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
* their class name will be returned.
*
* @param string $component
* @param string $component - Name of component
* @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
* the field data stripped off. It defaults to TRUE.
* @return string|array
*/
public function belongs_to($component = null, $classOnly = true) {
$belongsTo = $this->config()->belongs_to;
public function belongsTo($component = null, $classOnly = true) {
if($component) {
if($belongsTo && array_key_exists($component, $belongsTo)) {
$belongsTo = $belongsTo[$component];
} else {
return false;
}
Deprecation::notice(
'3.2',
'Please use DataObject::belongsToComponent() instead of passing a component name to belongsTo()',
Deprecation::SCOPE_GLOBAL
);
return $this->belongsToComponent($component, $classOnly);
}
$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
if($belongsTo && $classOnly) {
return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
} else {
@ -1808,9 +1852,28 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
/**
* Return data for a specific belongs_to component.
* @param string $component
* @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
* the field data stripped off. It defaults to TRUE.
* @return string|false
*/
public function belongsToComponent($component, $classOnly = true) {
$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
if($belongsTo && array_key_exists($component, $belongsTo)) {
$belongsTo = $belongsTo[$component];
} else {
return false;
}
return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $belongsTo) : $belongsTo;
}
/**
* Return all of the database fields defined in self::$db and all the parent classes.
* Doesn't include any fields specified by self::$has_one. Use $this->has_one() to get these fields
* Doesn't include any fields specified by self::$has_one. Use $this->hasOne() to get these fields
*
* @param string $fieldName Limit the output to a specific field name
* @return array The database fields
@ -1837,15 +1900,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $dbItems[$fieldName];
}
} else {
// Validate the data
foreach($dbItems as $k => $v) {
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
user_error("$class::\$db has a bad entry: "
. var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a"
. " property name, and the map value should be the property type.", E_USER_ERROR);
}
}
$items = isset($items) ? array_merge((array) $items, $dbItems) : $dbItems;
}
}
@ -1853,26 +1907,42 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $items;
}
/**
* @deprecated 4.0 Method has been replaced by hasMany() and hasManyComponent()
* @param string $component
* @param bool $classOnly
* @return array|null
*/
public function has_many($component = null, $classOnly = true) {
if($component) {
Deprecation::notice('3.2', 'Please use hasManyComponent() instead');
return $this->hasManyComponent($component, $classOnly);
}
Deprecation::notice('3.2', 'Please use hasMany() instead');
return $this->hasMany(null, $classOnly);
}
/**
* Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
* relationships and their classes will be returned.
*
* @param string $component Name of component
* @param string $component Deprecated - Name of component
* @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
* the field data stripped off. It defaults to TRUE.
* @return string|array
* @return string|array|false
*/
public function has_many($component = null, $classOnly = true) {
$hasMany = $this->config()->has_many;
public function hasMany($component = null, $classOnly = true) {
if($component) {
if($hasMany && array_key_exists($component, $hasMany)) {
$hasMany = $hasMany[$component];
} else {
return false;
}
Deprecation::notice(
'3.2',
'Please use DataObject::hasManyComponent() instead of passing a component name to hasMany()',
Deprecation::SCOPE_GLOBAL
);
return $this->hasManyComponent($component, $classOnly);
}
$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
if($hasMany && $classOnly) {
return preg_replace('/(.+)?\..+/', '$1', $hasMany);
} else {
@ -1880,92 +1950,123 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
/**
* Return data for a specific has_many component.
* @param string $component
* @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
* the field data stripped off. It defaults to TRUE.
* @return string|false
*/
public function hasManyComponent($component, $classOnly = true) {
$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
if($hasMany && array_key_exists($component, $hasMany)) {
$hasMany = $hasMany[$component];
} else {
return false;
}
return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany;
}
/**
* @deprecated 4.0 Method has been replaced by manyManyExtraFields() and
* manyManyExtraFieldsForComponent()
* @param string $component
* @return array
*/
public function many_many_extraFields($component = null) {
if($component) {
Deprecation::notice('3.2', 'Please use manyManyExtraFieldsForComponent() instead');
return $this->manyManyExtraFieldsForComponent($component);
}
Deprecation::notice('3.2', 'Please use manyManyExtraFields() instead');
return $this->manyManyExtraFields();
}
/**
* Return the many-to-many extra fields specification.
*
* If you don't specify a component name, it returns all
* extra fields for all components available.
*
* @param string $component Name of component
* @return array
* @param string $component Deprecated - Name of component
* @return array|null
*/
public function many_many_extraFields($component = null) {
$classes = ClassInfo::ancestry($this);
public function manyManyExtraFields($component = null) {
if($component) {
Deprecation::notice(
'3.2',
'Please use DataObject::manyManyExtraFieldsForComponent() instead of passing a component name
to manyManyExtraFields()',
Deprecation::SCOPE_GLOBAL
);
return $this->manyManyExtraFieldsForComponent($component);
}
foreach($classes as $class) {
if(in_array($class, array('ViewableData', 'Object', 'DataObject'))) continue;
return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
}
/**
* Return the many-to-many extra fields specification for a specific component.
* @param string $component
* @return array|null
*/
public function manyManyExtraFieldsForComponent($component) {
// Get all many_many_extraFields defined in this class or parent classes
$extraFields = (array)Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
// Extra fields are immediately available
if(isset($extraFields[$component])) {
return $extraFields[$component];
}
// Check this class' belongs_many_manys to see if any of their reverse associations contain extra fields
$manyMany = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
if($candidate) {
$relationName = null;
// Extract class and relation name from dot-notation
if(strpos($candidate, '.') !== false) {
list($candidate, $relationName) = explode('.', $candidate, 2);
}
// Find extra fields for one component
if($component) {
$SNG_class = singleton($class);
$extraFields = $SNG_class->stat('many_many_extraFields');
// If we've not already found the relation name from dot notation, we need to find a relation that points
// back to this class. As there's no dot-notation, there can only be one relation pointing to this class,
// so it's safe to assume that it's the correct one
if(!$relationName) {
$candidateManyManys = (array)Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
// Extra fields are immediately available on this class
if(isset($extraFields[$component])) {
return $extraFields[$component];
}
$manyMany = $SNG_class->stat('many_many');
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
if($candidate) {
$SNG_candidate = singleton($candidate);
$candidateManyMany = $SNG_candidate->stat('belongs_many_many');
// Find the relation given the class
if($candidateManyMany) foreach($candidateManyMany as $relation => $relatedClass) {
if($relatedClass == $class) {
$relationName = $relation;
break;
}
}
if($relationName) {
$extraFields = $SNG_candidate->stat('many_many_extraFields');
if(isset($extraFields[$relationName])) {
return $extraFields[$relationName];
}
foreach($candidateManyManys as $relation => $relatedClass) {
if($relatedClass === $this->class) {
$relationName = $relation;
}
}
}
$manyMany = $SNG_class->stat('belongs_many_many');
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
if($candidate) {
$SNG_candidate = singleton($candidate);
$candidateManyMany = $SNG_candidate->stat('many_many');
// Find the relation given the class
if($candidateManyMany) foreach($candidateManyMany as $relation => $relatedClass) {
if($relatedClass == $class) {
$relationName = $relation;
}
}
$extraFields = $SNG_candidate->stat('many_many_extraFields');
if(isset($extraFields[$relationName])) {
return $extraFields[$relationName];
}
}
} else {
// Find all the extra fields for all components
$newItems = eval("return (array){$class}::\$many_many_extraFields;");
foreach($newItems as $k => $v) {
if(!is_array($v)) {
user_error(
"$class::\$many_many_extraFields has a bad entry: "
. var_export($k, true) . " => " . var_export($v, true)
. ". Each many_many_extraFields entry should map to a field specification array.",
E_USER_ERROR
);
}
}
return isset($items) ? array_merge($newItems, $items) : $newItems;
// If we've found a matching relation on the target class, see if we can find extra fields for it
$extraFields = (array)Config::inst()->get($candidate, 'many_many_extraFields', Config::UNINHERITED);
if(isset($extraFields[$relationName])) {
return $extraFields[$relationName];
}
}
return isset($items) ? $items : null;
}
/**
* @deprecated 4.0 Method has been renamed to manyMany()
* @param string $component
* @return array|null
*/
public function many_many($component = null) {
if($component) {
Deprecation::notice('3.2', 'Please use manyManyComponent() instead');
return $this->manyManyComponent($component);
}
Deprecation::notice('3.2', 'Please use manyMany() instead');
return $this->manyMany();
}
/**
@ -1973,76 +2074,93 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* The return value is an array of (parentclass, childclass). If $component is null, then all many-many
* components are returned.
*
* @param string $component Name of component
*
* @return array An array of (parentclass, childclass), or an array of all many-many components
* @see DataObject::manyManyComponent()
* @param string $component Deprecated - Name of component
* @return array|null An array of (parentclass, childclass), or an array of all many-many components
*/
public function many_many($component = null) {
$classes = ClassInfo::ancestry($this);
foreach($classes as $class) {
// Wait until after we reach DataObject
if(in_array($class, array('ViewableData', 'Object', 'DataObject'))) continue;
if($component) {
$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
// Try many_many
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
if($candidate) {
$parentField = $class . "ID";
$childField = ($class == $candidate) ? "ChildID" : $candidate . "ID";
return array($class, $candidate, $parentField, $childField, "{$class}_$component");
}
// Try belongs_many_many
$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
$candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
if($candidate) {
$childField = $candidate . "ID";
// We need to find the inverse component name
$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
if(!$otherManyMany) {
user_error("Inverse component of $candidate not found ({$this->class})", E_USER_ERROR);
}
foreach($otherManyMany as $inverseComponentName => $candidateClass) {
if($candidateClass == $class || is_subclass_of($class, $candidateClass)) {
$parentField = ($class == $candidate) ? "ChildID" : $candidateClass . "ID";
return array($class, $candidate, $parentField, $childField,
"{$candidate}_$inverseComponentName");
}
}
user_error("Orphaned \$belongs_many_many value for $this->class.$component", E_USER_ERROR);
}
} else {
$newItems = (array)Config::inst()->get($class, 'many_many', Config::UNINHERITED);
// Validate the data
foreach($newItems as $k => $v) {
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
user_error("$class::\$many_many has a bad entry: "
. var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a"
. " relationship name, and the map value should be the data class to join to.", E_USER_ERROR);
}
}
$items = isset($items) ? array_merge($newItems, $items) : $newItems;
$newItems = (array)Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
// Validate the data
foreach($newItems as $k => $v) {
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
user_error("$class::\$belongs_many_many has a bad entry: "
. var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a"
. " relationship name, and the map value should be the data class to join to.", E_USER_ERROR);
}
}
$items = isset($items) ? array_merge($newItems, $items) : $newItems;
}
public function manyMany($component = null) {
if($component) {
Deprecation::notice(
'3.2',
'Please use DataObject::manyManyComponent() instead of passing a component name to manyMany()',
Deprecation::SCOPE_GLOBAL
);
return $this->manyManyComponent($component);
}
return isset($items) ? $items : null;
$manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
$belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
$items = array_merge($manyManys, $belongsManyManys);
return $items;
}
/**
* Return information about a specific many_many component. Returns a numeric array of:
* array(
* <classname>, The class that relation is defined in e.g. "Product"
* <candidateName>, The target class of the relation e.g. "Category"
* <parentField>, The field name pointing to <classname>'s table e.g. "ProductID"
* <childField>, The field name pointing to <candidatename>'s table e.g. "CategoryID"
* <joinTable> The join table between the two classes e.g. "Product_Categories"
* )
* @param string $component The component name
* @return array|null
*/
public function manyManyComponent($component) {
$classes = $this->getClassAncestry();
foreach($classes as $class) {
$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
// Check if the component is defined in many_many on this class
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
if($candidate) {
$parentField = $class . "ID";
$childField = ($class == $candidate) ? "ChildID" : $candidate . "ID";
return array($class, $candidate, $parentField, $childField, "{$class}_$component");
}
// Check if the component is defined in belongs_many_many on this class
$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
$candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
if($candidate) {
// Extract class and relation name from dot-notation
if(strpos($candidate, '.') !== false) {
list($candidate, $relationName) = explode('.', $candidate, 2);
}
$childField = $candidate . "ID";
// We need to find the inverse component name
$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
if(!$otherManyMany) {
throw new LogicException("Inverse component of $candidate not found ({$this->class})");
}
// If we've got a relation name (extracted from dot-notation), we can already work out
// the join table and candidate class name...
if(isset($relationName) && isset($otherManyMany[$relationName])) {
$candidateClass = $otherManyMany[$relationName];
$joinTable = "{$candidate}_{$relationName}";
} else {
// ... otherwise, we need to loop over the many_manys and find a relation that
// matches up to this class
foreach($otherManyMany as $inverseComponentName => $candidateClass) {
if($candidateClass == $class || is_subclass_of($class, $candidateClass)) {
$joinTable = "{$candidate}_{$inverseComponentName}";
break;
}
}
}
// If we could work out the join table, we've got all the info we need
if(isset($joinTable)) {
$parentField = ($class == $candidate) ? "ChildID" : $candidateClass . "ID";
return array($class, $candidate, $parentField, $childField, $joinTable);
}
throw new LogicException("Orphaned \$belongs_many_many value for $this->class.$component");
}
}
}
/**
@ -2534,7 +2652,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return (
array_key_exists($field, $this->record)
|| $this->db($field)
|| (substr($field,-2) == 'ID') && $this->has_one(substr($field,0, -2))
|| (substr($field,-2) == 'ID') && $this->hasOneComponent(substr($field,0, -2))
|| $this->hasMethod("get{$field}")
);
}
@ -2623,7 +2741,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
if(Permission::checkMember($member, "ADMIN")) return true;
if($this->many_many('Can' . $perm)) {
if($this->manyManyComponent('Can' . $perm)) {
if($this->ParentID && $this->SecurityType == 'Inherit') {
if(!($p = $this->Parent)) {
return false;
@ -2807,12 +2925,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $obj;
// Special case for has_one relationships
} else if(preg_match('/ID$/', $fieldName) && $this->has_one(substr($fieldName,0,-2))) {
} else if(preg_match('/ID$/', $fieldName) && $this->hasOneComponent(substr($fieldName,0,-2))) {
$val = $this->$fieldName;
return DBField::create_field('ForeignKey', $val, $fieldName, $this);
// has_one for polymorphic relations do not end in ID
} else if(($type = $this->has_one($fieldName)) && ($type === 'DataObject')) {
} else if(($type = $this->hasOneComponent($fieldName)) && ($type === 'DataObject')) {
$val = $this->$fieldName();
return DBField::create_field('PolymorphicForeignKey', $val, $fieldName, $this);
@ -2910,16 +3028,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return String
*/
public function getReverseAssociation($className) {
if (is_array($this->many_many())) {
$many_many = array_flip($this->many_many());
if (is_array($this->manyMany())) {
$many_many = array_flip($this->manyMany());
if (array_key_exists($className, $many_many)) return $many_many[$className];
}
if (is_array($this->has_many())) {
$has_many = array_flip($this->has_many());
if (is_array($this->hasMany())) {
$has_many = array_flip($this->hasMany());
if (array_key_exists($className, $has_many)) return $has_many[$className];
}
if (is_array($this->has_one())) {
$has_one = array_flip($this->has_one());
if (is_array($this->hasOne())) {
$has_one = array_flip($this->hasOne());
if (array_key_exists($className, $has_one)) return $has_one[$className];
}
@ -3191,6 +3309,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$indexes = $this->databaseIndexes();
// Validate relationship configuration
$this->validateModelDefinitions();
if($fields) {
$hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class));
DB::require_table($this->class, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'),
@ -3227,6 +3348,42 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->extend('augmentDatabase', $dummy);
}
/**
* Validate that the configured relations for this class use the correct syntaxes
* @throws LogicException
*/
protected function validateModelDefinitions() {
$modelDefinitions = array(
'db' => Config::inst()->get($this->class, 'db', Config::UNINHERITED),
'has_one' => Config::inst()->get($this->class, 'has_one', Config::UNINHERITED),
'has_many' => Config::inst()->get($this->class, 'has_many', Config::UNINHERITED),
'belongs_to' => Config::inst()->get($this->class, 'belongs_to', Config::UNINHERITED),
'many_many' => Config::inst()->get($this->class, 'many_many', Config::UNINHERITED),
'belongs_many_many' => Config::inst()->get($this->class, 'belongs_many_many', Config::UNINHERITED),
'many_many_extraFields' => Config::inst()->get($this->class, 'many_many_extraFields', Config::UNINHERITED)
);
foreach($modelDefinitions as $defType => $relations) {
if( ! $relations) continue;
foreach($relations as $k => $v) {
if($defType === 'many_many_extraFields') {
if(!is_array($v)) {
throw new LogicException("$this->class::\$many_many_extraFields has a bad entry: "
. var_export($k, true) . " => " . var_export($v, true)
. ". Each many_many_extraFields entry should map to a field specification array.");
}
} else {
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
throw new LogicException("$this->class::$defType has a bad entry: "
. var_export($k, true). " => " . var_export($v, true) . ". Each map key should be a
relationship name, and the map value should be the data class to join to.");
}
}
}
}
}
/**
* Add default records to database. This function is called whenever the
* database is built, after the database tables have all been created. Overload
@ -3783,7 +3940,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function hasValue($field, $arguments = null, $cache = true) {
// has_one fields should not use dbObject to check if a value is given
if(!$this->has_one($field) && ($obj = $this->dbObject($field))) {
if(!$this->hasOneComponent($field) && ($obj = $this->dbObject($field))) {
return $obj->exists();
} else {
return parent::hasValue($field, $arguments, $cache);

View File

@ -645,10 +645,9 @@ class DataQuery {
foreach($relation as $rel) {
$model = singleton($modelClass);
if ($component = $model->has_one($rel)) {
if ($component = $model->hasOneComponent($rel)) {
if(!$this->query->isJoinedTo($component)) {
$has_one = array_flip($model->has_one());
$foreignKey = $has_one[$component];
$foreignKey = $rel;
$realModelClass = ClassInfo::table_for_object_field($modelClass, "{$foreignKey}ID");
$this->query->addLeftJoin($component,
"\"$component\".\"ID\" = \"{$realModelClass}\".\"{$foreignKey}ID\"");
@ -669,7 +668,7 @@ class DataQuery {
}
$modelClass = $component;
} elseif ($component = $model->has_many($rel)) {
} elseif ($component = $model->hasManyComponent($rel)) {
if(!$this->query->isJoinedTo($component)) {
$ancestry = $model->getClassAncestry();
$foreignKey = $model->getRemoteJoinField($rel);
@ -691,7 +690,7 @@ class DataQuery {
}
$modelClass = $component;
} elseif ($component = $model->many_many($rel)) {
} elseif ($component = $model->manyManyComponent($rel)) {
list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component;
$parentBaseClass = ClassInfo::baseDataClass($parentClass);
$componentBaseClass = ClassInfo::baseDataClass($componentClass);

View File

@ -28,7 +28,7 @@ class ForeignKey extends Int {
public function scaffoldFormField($title = null, $params = null) {
$relationName = substr($this->name,0,-2);
$hasOneClass = $this->object->has_one($relationName);
$hasOneClass = $this->object->hasOneComponent($relationName);
if($hasOneClass && singleton($hasOneClass) instanceof Image) {
$field = new UploadField($relationName, $title);

View File

@ -17,6 +17,7 @@ abstract class StringField extends DBField {
*/
private static $casting = array(
"LimitCharacters" => "Text",
"LimitCharactersToClosestWord" => "Text",
'LimitWordCount' => 'Text',
'LimitWordCountXML' => 'HTMLText',
"LowerCase" => "Text",
@ -116,7 +117,6 @@ abstract class StringField extends DBField {
*/
public function LimitCharacters($limit = 20, $add = '...') {
$value = trim($this->value);
if($this->stat('escape_type') == 'xml') {
$value = strip_tags($value);
$value = html_entity_decode($value, ENT_COMPAT, 'UTF-8');
@ -126,11 +126,40 @@ abstract class StringField extends DBField {
} else {
$value = (mb_strlen($value) > $limit) ? mb_substr($value, 0, $limit) . $add : $value;
}
return $value;
}
/**
* Limit this field's content by a number of characters and truncate
* the field to the closest complete word. All HTML tags are stripped
* from the field.
*
* @param int $limit Number of characters to limit by
* @param string $add Ellipsis to add to the end of truncated string
* @return string
*/
public function LimitCharactersToClosestWord($limit = 20, $add = '...') {
// Strip HTML tags if they exist in the field
$this->value = strip_tags($this->value);
// Determine if value exceeds limit before limiting characters
$exceedsLimit = mb_strlen($this->value) > $limit;
// Limit to character limit
$value = $this->LimitCharacters($limit, '');
// If value exceeds limit, strip punctuation off the end to the last space and apply ellipsis
if($exceedsLimit) {
$value = html_entity_decode($value, ENT_COMPAT, 'UTF-8');
$value = rtrim(mb_substr($value, 0, mb_strrpos($value, " ")), "/[\.,-\/#!$%\^&\*;:{}=\-_`~()]\s") . $add;
$value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8');
}
return $value;
}
/**
* Limit this field's content by a number of words.
*

View File

@ -271,8 +271,13 @@ abstract class SQLConditionalExpression extends SQLExpression {
$filter = "(" . implode(") AND (", $join['filter']) . ")";
}
$table = strpos(strtoupper($join['table']), 'SELECT') ? $join['table'] : "\"" . $join['table'] . "\"";
$aliasClause = ($alias != $join['table']) ? " AS \"$alias\"" : "";
// Ensure tables are quoted, unless the table is actually a sub-select
$table = preg_match('/\bSELECT\b/i', $join['table'])
? $join['table']
: "\"{$join['table']}\"";
$aliasClause = ($alias != $join['table'])
? " AS \"{$alias}\""
: "";
$joins[$alias] = strtoupper($join['type']) . " JOIN " . $table . "$aliasClause ON $filter";
if(!empty($join['parameters'])) {
$parameters = array_merge($parameters, $join['parameters']);

View File

@ -580,6 +580,7 @@ $gf_grid_x: 16px;
span.non-sortable {
display:block;
padding: 6px 8px;
}
}

View File

@ -10,6 +10,10 @@
clear: both;
}
.description {
margin-left: 0;
}
.middleColumn {
// TODO .middleColumn styling should probably be theme specific (eg cms ui will look different than blackcandy)
// so we should move this style into the cms and black candy files

View File

@ -228,7 +228,7 @@ abstract class SearchFilter extends Object {
* @return DataQuery
*/
protected function applyMany(DataQuery $query) {
throw new InvalidArgumentException(get_class($this) . "can't be used to filter by a list of items.");
throw new InvalidArgumentException(get_class($this) . " can't be used to filter by a list of items.");
}
/**
@ -264,7 +264,7 @@ abstract class SearchFilter extends Object {
* @return DataQuery
*/
protected function excludeMany(DataQuery $query) {
throw new InvalidArgumentException(get_class($this) . "can't be used to filter by a list of items.");
throw new InvalidArgumentException(get_class($this) . " can't be used to filter by a list of items.");
}
/**

View File

@ -50,9 +50,19 @@ class BasicAuth {
$isRunningTests = (class_exists('SapphireTest', false) && SapphireTest::is_running_test());
if(!Security::database_is_ready() || (Director::is_cli() && !$isRunningTests)) return true;
/*
* Enable HTTP Basic authentication workaround for PHP running in CGI mode with Apache
* Depending on server configuration the auth header may be in HTTP_AUTHORIZATION or
* REDIRECT_HTTP_AUTHORIZATION
*
* The follow rewrite rule must be in the sites .htaccess file to enable this workaround
* RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
*/
$authHeader = (isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] :
(isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) ? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] : null));
$matches = array();
if (isset($_SERVER['HTTP_AUTHORIZATION']) &&
preg_match('/Basic\s+(.*)$/i', $_SERVER['HTTP_AUTHORIZATION'], $matches)) {
if ($authHeader &&
preg_match('/Basic\s+(.*)$/i', $authHeader, $matches)) {
list($name, $password) = explode(':', base64_decode($matches[1]));
$_SERVER['PHP_AUTH_USER'] = strip_tags($name);
$_SERVER['PHP_AUTH_PW'] = strip_tags($password);

View File

@ -120,6 +120,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
'Salt',
'NumVisit'
);
/**
* @config
* @var Array See {@link set_title_columns()}
@ -230,7 +231,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
public static function default_admin() {
// Check if set
if(!Security::has_default_admin()) return null;
// Find or create ADMIN group
singleton('Group')->requireDefaultRecords();
$adminGroup = Permission::get_groups_by_permission('ADMIN')->First();
@ -513,37 +514,44 @@ class Member extends DataObject implements TemplateGlobalProvider {
// Don't bother trying this multiple times
self::$_already_tried_to_auto_log_in = true;
if(strpos(Cookie::get('alc_enc'), ':') && !Session::get("loggedInAs")) {
list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
if(strpos(Cookie::get('alc_enc'), ':') === false
|| Session::get("loggedInAs")
|| !Security::database_is_ready()
) {
return;
}
$member = DataObject::get_by_id("Member", $uid);
list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
// check if autologin token matches
if($member) {
$hash = $member->encryptWithUserSettings($token);
if(!$member->RememberLoginToken || $member->RememberLoginToken !== $hash) {
$member = null;
}
$member = DataObject::get_by_id("Member", $uid);
// check if autologin token matches
if($member) {
$hash = $member->encryptWithUserSettings($token);
if(!$member->RememberLoginToken || $member->RememberLoginToken !== $hash) {
$member = null;
}
}
if($member) {
self::session_regenerate_id();
Session::set("loggedInAs", $member->ID);
// This lets apache rules detect whether the user has logged in
if(Member::config()->login_marker_cookie) {
Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
}
if($member) {
self::session_regenerate_id();
Session::set("loggedInAs", $member->ID);
// This lets apache rules detect whether the user has logged in
if(Member::config()->login_marker_cookie) {
Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
}
$generator = new RandomGenerator();
$token = $generator->randomToken('sha1');
$hash = $member->encryptWithUserSettings($token);
$member->RememberLoginToken = $hash;
Cookie::set('alc_enc', $member->ID . ':' . $token, 90, null, null, false, true);
$generator = new RandomGenerator();
$token = $generator->randomToken('sha1');
$hash = $member->encryptWithUserSettings($token);
$member->RememberLoginToken = $hash;
Cookie::set('alc_enc', $member->ID . ':' . $token, 90, null, null, false, true);
$member->write();
$member->NumVisit++;
$member->write();
// Audit logging hook
$member->extend('memberAutoLoggedIn');
}
// Audit logging hook
$member->extend('memberAutoLoggedIn');
}
}
@ -1442,8 +1450,8 @@ class Member extends DataObject implements TemplateGlobalProvider {
if(!($member && $member->exists())) return false;
// If the requesting member is not an admin, but has access to manage members,
// he still can't edit other members with ADMIN permission.
// This is a bit weak, strictly speaking he shouldn't be allowed to
// they still can't edit other members with ADMIN permission.
// This is a bit weak, strictly speaking they shouldn't be allowed to
// perform any action that could change the password on a member
// with "higher" permissions than himself, but thats hard to determine.
if(!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) return false;

View File

@ -227,7 +227,7 @@ JS;
return $this->controller->redirect(Director::absoluteBaseURL() . Security::config()->default_login_dest);
}
// Redirect the user to the page where he came from
// Redirect the user to the page where they came from
$member = Member::currentUser();
if($member) {
$firstname = Convert::raw2xml($member->FirstName);

View File

@ -269,7 +269,7 @@ class PermissionCheckboxSetField extends FormField {
$permission->delete();
}
if($fieldname && $record && ($record->has_many($fieldname) || $record->many_many($fieldname))) {
if($fieldname && $record && ($record->hasManyComponent($fieldname) || $record->manyManyComponent($fieldname))) {
if(!$record->ID) $record->write(); // We need a record ID to write permissions

View File

@ -97,9 +97,10 @@ class Security extends Controller implements TemplateGlobalProvider {
/**
* Default message set used in permission failures.
*
* @config
* @var array|string
*/
private static $default_message_set = '';
private static $default_message_set;
/**
* Random secure token, can be used as a crypto key internally.
@ -198,9 +199,6 @@ class Security extends Controller implements TemplateGlobalProvider {
* If you pass an array, you can use the
* following keys:
* - default: The default message
* - logInAgain: The message to show
* if the user has just
* logged out and the
* - alreadyLoggedIn: The message to
* show if the user
* is already logged
@ -231,8 +229,8 @@ class Security extends Controller implements TemplateGlobalProvider {
} else {
// Prepare the messageSet provided
if(!$messageSet) {
if(self::$default_message_set) {
$messageSet = self::$default_message_set;
if($configMessageSet = static::config()->get('default_message_set')) {
$messageSet = $configMessageSet;
} else {
$messageSet = array(
'default' => _t(
@ -246,11 +244,6 @@ class Security extends Controller implements TemplateGlobalProvider {
. "can access that page, you can log in again below.",
"%s will be replaced with a link to log in."
),
'logInAgain' => _t(
'Security.LOGGEDOUT',
"You have been logged out. If you would like to log in again, enter "
. "your credentials below."
)
);
}
@ -420,6 +413,20 @@ class Security extends Controller implements TemplateGlobalProvider {
if($result instanceof SS_HTTPResponse) return $result;
}
}
// If arriving on the login page already logged in, with no security error, and a ReturnURL then redirect
// back. The login message check is neccesary to prevent infinite loops where BackURL links to
// an action that triggers Security::permissionFailure.
// This step is necessary in cases such as automatic redirection where a user is authenticated
// upon landing on an SSL secured site and is automatically logged in, or some other case
// where the user has permissions to continue but is not given the option.
if($this->request->requestVar('BackURL')
&& !$this->getLoginMessage()
&& ($member = Member::currentUser())
&& $member->exists()
) {
return $this->redirectBack();
}
}
/**
@ -530,6 +537,7 @@ class Security extends Controller implements TemplateGlobalProvider {
// Finally, customise the controller to add any form messages and the form.
$customisedController = $controller->customise(array(
"Content" => $message,
"Message" => $message,
"MessageType" => $messageType,
"Form" => $content,
@ -579,7 +587,8 @@ class Security extends Controller implements TemplateGlobalProvider {
* @return Form Returns the lost password form
*/
public function LostPasswordForm() {
return MemberLoginForm::create( $this,
return MemberLoginForm::create(
$this,
'LostPasswordForm',
new FieldList(
new EmailField('Email', _t('Member.EMAIL', 'Email'))

View File

@ -97,6 +97,16 @@ class DirectorTest extends SapphireTest {
$_SERVER['REQUEST_URI'] = "$rootURL/mysite/sub-page/";
Config::inst()->update('Director', 'alternate_base_url', '/mysite/');
//test empty URL
$this->assertEquals("$rootURL/mysite/sub-page/", Director::absoluteURL(''));
//test absolute - /
$this->assertEquals("$rootURL/", Director::absoluteURL('/'));
//test relative
$this->assertEquals("$rootURL/mysite/sub-page/", Director::absoluteURL('./'));
$this->assertEquals("$rootURL/mysite/sub-page/", Director::absoluteURL('.'));
// Test already absolute url
$this->assertEquals($rootURL, Director::absoluteURL($rootURL));
$this->assertEquals($rootURL, Director::absoluteURL($rootURL, true));
@ -135,8 +145,10 @@ class DirectorTest extends SapphireTest {
// absolute base URLs - you should end them in a /
Config::inst()->update('Director', 'alternate_base_url', 'http://www.example.org/');
$_SERVER['REQUEST_URI'] = "http://www.example.org/";
$this->assertEquals('http://www.example.org/', Director::baseURL());
$this->assertEquals('http://www.example.org/', Director::absoluteBaseURL());
$this->assertEquals('http://www.example.org/', Director::absoluteURL(''));
$this->assertEquals('http://www.example.org/subfolder/test', Director::absoluteURL('subfolder/test'));
// Setting it to false restores functionality

View File

@ -160,6 +160,26 @@ class HTTPTest extends FunctionalTest {
*/
public function testAbsoluteURLsAttributes() {
$this->withBaseURL('http://www.silverstripe.org/', function($test){
//empty links
$test->assertEquals(
'<a href="http://www.silverstripe.org/">test</a>',
HTTP::absoluteURLs('<a href="">test</a>')
);
$test->assertEquals(
'<a href="http://www.silverstripe.org/">test</a>',
HTTP::absoluteURLs('<a href="/">test</a>')
);
//relative
$test->assertEquals(
'<a href="http://www.silverstripe.org/">test</a>',
HTTP::absoluteURLs('<a href="./">test</a>')
);
$test->assertEquals(
'<a href="http://www.silverstripe.org/">test</a>',
HTTP::absoluteURLs('<a href=".">test</a>')
);
// links
$test->assertEquals(

View File

@ -6,6 +6,14 @@
*/
class ClassInfoTest extends SapphireTest {
protected $extraDataObjects = array(
'ClassInfoTest_BaseClass',
'ClassInfoTest_ChildClass',
'ClassInfoTest_GrandChildClass',
'ClassInfoTest_BaseDataClass',
'ClassInfoTest_NoFields',
);
public function testExists() {
$this->assertTrue(ClassInfo::exists('Object'));
$this->assertTrue(ClassInfo::exists('ClassInfoTest'));
@ -146,7 +154,7 @@ class ClassInfoTest extends SapphireTest {
* @subpackage tests
*/
class ClassInfoTest_BaseClass extends DataObject {
class ClassInfoTest_BaseClass extends DataObject implements TestOnly {
}
@ -173,7 +181,7 @@ class ClassInfoTest_GrandChildClass extends ClassInfoTest_ChildClass {
* @subpackage tests
*/
class ClassInfoTest_BaseDataClass extends DataObject {
class ClassInfoTest_BaseDataClass extends DataObject implements TestOnly {
private static $db = array(
'Title' => 'Varchar'

View File

@ -254,54 +254,92 @@ class ConfigTest extends SapphireTest {
$this->markTestIncomplete();
}
public function testLRUDiscarding() {
$cache = new ConfigTest_Config_LRU();
public function testCacheCleaning() {
$cache = new ConfigTest_Config_MemCache();
for ($i = 0; $i < 1000; $i++) $cache->set($i, $i);
$this->assertEquals(1000, count($cache->cache));
$cache->clean();
$this->assertEquals(0, count($cache->cache), 'Clean clears all items');
$this->assertFalse($cache->get(1), 'Clean clears all items');
$cache->set(1, 1, array('Foo'));
$this->assertEquals(1, count($cache->cache));
$this->assertEquals(1, count($cache->tags));
$cache->clean('Foo');
$this->assertEquals(0, count($cache->tags), 'Clean items with matching tag');
$this->assertFalse($cache->get(1), 'Clean items with matching tag');
$cache->set(1, 1, array('Foo', 'Bar'));
$this->assertEquals(2, count($cache->tags));
$this->assertEquals(1, count($cache->cache));
$cache->clean('Bar');
$this->assertEquals(1, count($cache->tags));
$this->assertEquals(0, count($cache->cache), 'Clean items with any single matching tag');
$this->assertFalse($cache->get(1), 'Clean items with any single matching tag');
}
public function testLRUDiscarding() {
$depSettings = Deprecation::dump_settings();
Deprecation::restore_settings(array(
'level' => false,
'version' => false,
'moduleVersions' => false,
));
$cache = new ConfigTest_Config_LRU();
for ($i = 0; $i < Config_LRU::SIZE*2; $i++) $cache->set($i, $i);
$this->assertEquals(
Config_LRU::SIZE, count($cache->indexing),
'Homogenous usage gives exact discarding'
);
$cache = new ConfigTest_Config_LRU();
for ($i = 0; $i < Config_LRU::SIZE; $i++) $cache->set($i, $i);
for ($i = 0; $i < Config_LRU::SIZE; $i++) $cache->set(-1, -1);
$this->assertLessThan(
Config_LRU::SIZE, count($cache->indexing),
'Heterogenous usage gives sufficient discarding'
);
Deprecation::restore_settings($depSettings);
}
public function testLRUCleaning() {
$depSettings = Deprecation::dump_settings();
Deprecation::restore_settings(array(
'level' => false,
'version' => false,
'moduleVersions' => false,
));
$cache = new ConfigTest_Config_LRU();
for ($i = 0; $i < Config_LRU::SIZE; $i++) $cache->set($i, $i);
$this->assertEquals(Config_LRU::SIZE, count($cache->indexing));
$cache->clean();
$this->assertEquals(0, count($cache->indexing), 'Clean clears all items');
$this->assertFalse($cache->get(1), 'Clean clears all items');
$cache->set(1, 1, array('Foo'));
$this->assertEquals(1, count($cache->indexing));
$cache->clean('Foo');
$this->assertEquals(0, count($cache->indexing), 'Clean items with matching tag');
$this->assertFalse($cache->get(1), 'Clean items with matching tag');
$cache->set(1, 1, array('Foo', 'Bar'));
$this->assertEquals(1, count($cache->indexing));
$cache->clean('Bar');
$this->assertEquals(0, count($cache->indexing), 'Clean items with any single matching tag');
$this->assertFalse($cache->get(1), 'Clean items with any single matching tag');
Deprecation::restore_settings($depSettings);
}
}
class ConfigTest_Config_LRU extends Config_LRU implements TestOnly {
public $cache;
public $indexing;
}
class ConfigTest_Config_MemCache extends Config_MemCache implements TestOnly {
public $cache;
public $tags;
}

View File

@ -280,4 +280,55 @@ PHP
Convert::raw2json($value)
);
}
public function testXML2Array() {
// Ensure an XML file at risk of entity expansion can be avoided safely
$inputXML = <<<XML
<?xml version="1.0"?>
<!DOCTYPE results [<!ENTITY long "SOME_SUPER_LONG_STRING">]>
<results>
<result>Now include &long; lots of times to expand the in-memory size of this XML structure</result>
<result>&long;&long;&long;</result>
</results>
XML
;
try {
Convert::xml2array($inputXML, true);
} catch(Exception $ex) {}
$this->assertTrue(
isset($ex)
&& $ex instanceof InvalidArgumentException
&& $ex->getMessage() === 'XML Doctype parsing disabled'
);
// Test without doctype validation
$expected = array(
'result' => array(
"Now include SOME_SUPER_LONG_STRING lots of times to expand the in-memory size of this XML structure",
array(
'long' => array(
array(
'long' => 'SOME_SUPER_LONG_STRING'
),
array(
'long' => 'SOME_SUPER_LONG_STRING'
),
array(
'long' => 'SOME_SUPER_LONG_STRING'
)
)
)
)
);
$result = Convert::xml2array($inputXML, false, true);
$this->assertEquals(
$expected,
$result
);
$result = Convert::xml2array($inputXML, false, false);
$this->assertEquals(
$expected,
$result
);
}
}

View File

@ -28,7 +28,7 @@ class FixtureBlueprintTest extends SapphireTest {
$obj = $blueprint->createObject(
'one',
array(
'ManyMany' =>
'ManyManyRelation' =>
array(
array(
"=>FixtureFactoryTest_DataObjectRelation.relation1" => array(),
@ -48,18 +48,18 @@ class FixtureBlueprintTest extends SapphireTest {
)
);
$this->assertEquals(2, $obj->ManyMany()->Count());
$this->assertNotNull($obj->ManyMany()->find('ID', $relation1->ID));
$this->assertNotNull($obj->ManyMany()->find('ID', $relation2->ID));
$this->assertEquals(2, $obj->ManyManyRelation()->Count());
$this->assertNotNull($obj->ManyManyRelation()->find('ID', $relation1->ID));
$this->assertNotNull($obj->ManyManyRelation()->find('ID', $relation2->ID));
$this->assertEquals(
array('Label' => 'This is a label for relation 1'),
$obj->ManyMany()->getExtraData('ManyMany', $relation1->ID)
$obj->ManyManyRelation()->getExtraData('ManyManyRelation', $relation1->ID)
);
$this->assertEquals(
array('Label' => 'This is a label for relation 2'),
$obj->ManyMany()->getExtraData('ManyMany', $relation2->ID)
$obj->ManyManyRelation()->getExtraData('ManyManyRelation', $relation2->ID)
);
}
@ -92,7 +92,7 @@ class FixtureBlueprintTest extends SapphireTest {
$obj = $blueprint->createObject(
'one',
array(
'ManyMany' =>
'ManyManyRelation' =>
'=>FixtureFactoryTest_DataObjectRelation.relation1,' .
'=>FixtureFactoryTest_DataObjectRelation.relation2'
),
@ -104,9 +104,9 @@ class FixtureBlueprintTest extends SapphireTest {
)
);
$this->assertEquals(2, $obj->ManyMany()->Count());
$this->assertNotNull($obj->ManyMany()->find('ID', $relation1->ID));
$this->assertNotNull($obj->ManyMany()->find('ID', $relation2->ID));
$this->assertEquals(2, $obj->ManyManyRelation()->Count());
$this->assertNotNull($obj->ManyManyRelation()->find('ID', $relation1->ID));
$this->assertNotNull($obj->ManyManyRelation()->find('ID', $relation2->ID));
}
/**
@ -119,7 +119,7 @@ class FixtureBlueprintTest extends SapphireTest {
$obj = $blueprint->createObject(
'one',
array(
'ManyMany' => '=>UnknownClass.relation1'
'ManyManyRelation' => '=>UnknownClass.relation1'
),
array(
'FixtureFactoryTest_DataObjectRelation' => array(
@ -139,7 +139,7 @@ class FixtureBlueprintTest extends SapphireTest {
$obj = $blueprint->createObject(
'one',
array(
'ManyMany' => '=>FixtureFactoryTest_DataObjectRelation.unknown_identifier'
'ManyManyRelation' => '=>FixtureFactoryTest_DataObjectRelation.unknown_identifier'
),
array(
'FixtureFactoryTest_DataObjectRelation' => array(
@ -163,7 +163,7 @@ class FixtureBlueprintTest extends SapphireTest {
$obj = $blueprint->createObject(
'one',
array(
'ManyMany' => 'FixtureFactoryTest_DataObjectRelation.relation1'
'ManyManyRelation' => 'FixtureFactoryTest_DataObjectRelation.relation1'
),
array(
'FixtureFactoryTest_DataObjectRelation' => array(

View File

@ -163,11 +163,11 @@ class FixtureFactoryTest_DataObject extends DataObject implements TestOnly {
);
private static $many_many = array(
"ManyMany" => "FixtureFactoryTest_DataObjectRelation"
"ManyManyRelation" => "FixtureFactoryTest_DataObjectRelation"
);
private static $many_many_extraFields = array(
"ManyMany" => array(
"ManyManyRelation" => array(
"Label" => "Varchar"
)
);

View File

@ -83,16 +83,101 @@ class UploadTest extends SapphireTest {
'extension' => 'txt',
'error' => UPLOAD_ERR_OK,
);
$v = new UploadTest_Validator();
$v->setAllowedMaxFileSize(array('txt' => 10));
// test upload into default folder
$u1 = new Upload();
$v = new UploadTest_Validator();
$v->setAllowedMaxFileSize(array('txt' => 10));
$u1->setValidator($v);
$result = $u1->load($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
$v->setAllowedMaxFileSize(array('[doc]' => 10));
$u1->setValidator($v);
$result = $u1->load($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
$v->setAllowedMaxFileSize(array('txt' => 200000));
$u1->setValidator($v);
$result = $u1->load($tmpFile);
$this->assertTrue($result, 'Load failed with setting max file size');
// check max file size set by app category
$tmpFileName = 'UploadTest-testUpload.jpg';
$tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
file_put_contents($tmpFilePath, $tmpFileContent . $tmpFileContent);
$tmpFile = array(
'name' => $tmpFileName,
'type' => 'image/jpeg',
'size' => filesize($tmpFilePath),
'tmp_name' => $tmpFilePath,
'extension' => 'jpg',
'error' => UPLOAD_ERR_OK,
);
$v->setAllowedMaxFileSize(array('[image]' => '40k'));
$u1->setValidator($v);
$result = $u1->load($tmpFile);
$this->assertTrue($result, 'Load failed with setting max file size');
$v->setAllowedMaxFileSize(array('[image]' => '1k'));
$u1->setValidator($v);
$result = $u1->load($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
$v->setAllowedMaxFileSize(array('[image]' => 1000));
$u1->setValidator($v);
$result = $u1->load($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
}
public function testGetAllowedMaxFileSize() {
Config::nest();
// Check the max file size uses the config values
$configMaxFileSizes = array(
'[image]' => '1k',
'txt' => 1000
);
Config::inst()->update('Upload_Validator', 'default_max_file_size', $configMaxFileSizes);
$v = new UploadTest_Validator();
$retrievedSize = $v->getAllowedMaxFileSize('[image]');
$this->assertEquals(1024, $retrievedSize, 'Max file size check on default values failed (config category set check)');
$retrievedSize = $v->getAllowedMaxFileSize('txt');
$this->assertEquals(1000, $retrievedSize, 'Max file size check on default values failed (config extension set check)');
// Check instance values for max file size
$maxFileSizes = array(
'[doc]' => 2000,
'txt' => '4k'
);
$v = new UploadTest_Validator();
$v->setAllowedMaxFileSize($maxFileSizes);
$retrievedSize = $v->getAllowedMaxFileSize('[doc]');
$this->assertEquals(2000, $retrievedSize, 'Max file size check on instance values failed (instance category set check)');
// Check that the instance values overwrote the default values
// ie. The max file size will not exist for [image]
$retrievedSize = $v->getAllowedMaxFileSize('[image]');
$this->assertFalse($retrievedSize, 'Max file size check on instance values failed (config overridden check)');
// Check a category that has not been set before
$retrievedSize = $v->getAllowedMaxFileSize('[zip]');
$this->assertFalse($retrievedSize, 'Max file size check on instance values failed (category not set check)');
// Check a file extension that has not been set before
$retrievedSize = $v->getAllowedMaxFileSize('mp3');
$this->assertFalse($retrievedSize, 'Max file size check on instance values failed (extension not set check)');
$retrievedSize = $v->getAllowedMaxFileSize('txt');
$this->assertEquals(4096, $retrievedSize, 'Max file size check on instance values failed (instance extension set check)');
Config::unnest();
}
public function testAllowedSizeOnFileWithNoExtension() {

View File

@ -0,0 +1,107 @@
<?php
/**
* @package framework
* @subpackage tests
*/
class TreeDropdownFieldTest extends SapphireTest {
protected static $fixture_file = 'TreeDropdownFieldTest.yml';
public function testTreeSearch(){
$field = new TreeDropdownField('TestTree', 'Test tree', 'Folder');
// case insensitive search against keyword 'sub' for folders
$request = new SS_HTTPRequest('GET','url',array('search'=>'sub'));
$tree = $field->tree($request);
$folder1 = $this->objFromFixture('Folder','folder1');
$folder1Subfolder1 = $this->objFromFixture('Folder','folder1-subfolder1');
$parser = new CSSContentParser($tree);
$cssPath = 'ul.tree li#selector-TestTree-'.$folder1->ID.' li#selector-TestTree-'.$folder1Subfolder1->ID.' a span.item';
$firstResult = $parser->getBySelector($cssPath);
$this->assertEquals(
(string)$firstResult[0],
$folder1Subfolder1->Name,
$folder1Subfolder1->Name.' is found, nested under '.$folder1->Name
);
$subfolder = $this->objFromFixture('Folder','subfolder');
$cssPath = 'ul.tree li#selector-TestTree-'.$subfolder->ID.' a span.item';
$secondResult = $parser->getBySelector($cssPath);
$this->assertEquals(
(string)$secondResult[0],
$subfolder->Name,
$subfolder->Name.' is found at root level'
);
// other folders which don't contain the keyword 'sub' are not returned in search results
$folder2 = $this->objFromFixture('Folder','folder2');
$cssPath = 'ul.tree li#selector-TestTree-'.$folder2->ID.' a span.item';
$noResult = $parser->getBySelector($cssPath);
$this->assertEquals(
$noResult,
array(),
$folder2.' is not found'
);
$field = new TreeDropdownField('TestTree', 'Test tree', 'File');
// case insensitive search against keyword 'sub' for files
$request = new SS_HTTPRequest('GET','url',array('search'=>'sub'));
$tree = $field->tree($request);
$parser = new CSSContentParser($tree);
// Even if we used File as the source object, folders are still returned because Folder is a File
$cssPath = 'ul.tree li#selector-TestTree-'.$folder1->ID.' li#selector-TestTree-'.$folder1Subfolder1->ID.' a span.item';
$firstResult = $parser->getBySelector($cssPath);
$this->assertEquals(
(string)$firstResult[0],
$folder1Subfolder1->Name,
$folder1Subfolder1->Name.' is found, nested under '.$folder1->Name
);
// Looking for two files with 'sub' in their name, both under the same folder
$file1 = $this->objFromFixture('File','subfolderfile1');
$file2 = $this->objFromFixture('File','subfolderfile2');
$cssPath = 'ul.tree li#selector-TestTree-'.$subfolder->ID.' li#selector-TestTree-'.$file1->ID.' a';
$firstResult = $parser->getBySelector($cssPath);
$this->assertGreaterThan(
0,
count($firstResult),
$file1->Name.' with ID '.$file1->ID.' is in search results'
);
$this->assertEquals(
(string)$firstResult[0],
$file1->Name,
$file1->Name.' is found nested under '.$subfolder->Name
);
$cssPath = 'ul.tree li#selector-TestTree-'.$subfolder->ID.' li#selector-TestTree-'.$file2->ID.' a';
$secondResult = $parser->getBySelector($cssPath);
$this->assertGreaterThan(
0,
count($secondResult),
$file2->Name.' with ID '.$file2->ID.' is in search results'
);
$this->assertEquals(
(string)$secondResult[0],
$file2->Name,
$file2->Name.' is found nested under '.$subfolder->Name
);
// other files which don't include 'sub' are not returned in search results
$file3 = $this->objFromFixture('File','asdf');
$cssPath = 'ul.tree li#selector-TestTree-'.$file3->ID;
$noResult = $parser->getBySelector($cssPath);
$this->assertEquals(
$noResult,
array(),
$file3->Name.' is not found'
);
}
}

View File

@ -0,0 +1,25 @@
Folder:
subfolder:
Name: FileTest-subfolder
folder1:
Name: FileTest-folder1
folder2:
Name: FileTest-folder2
folder1-subfolder1:
Name: FileTest-folder1-subfolder1
ParentID: =>Folder.folder1
File:
asdf:
Filename: assets/FileTest.txt
subfolderfile1:
Filename: assets/FileTest-subfolder/TestFile1InSubfolder.txt
Name: TestFile1InSubfolder
ParentID: =>Folder.subfolder
subfolderfile2:
Filename: assets/FileTest-subfolder/TestFile2InSubfolder.txt
Name: TestFile2InSubfolder
ParentID: =>Folder.subfolder
file1-folder1:
Filename: assets/FileTest-folder1/File1.txt
Name: File1.txt
ParentID: =>Folder.folder1

View File

@ -32,7 +32,7 @@ class GridFieldEditButtonTest extends SapphireTest {
// Check that there are content
$this->assertEquals(3, count($content->getBySelector('.ss-gridfield-item')));
// Make sure that there are edit links, even though the user doesn't have "edit" permissions
// (he can still view the records)
// (they can still view the records)
$this->assertEquals(2, count($content->getBySelector('.edit-link')),
'Edit links should show when not logged in.');
}

View File

@ -37,7 +37,7 @@ class CompositeDBFieldTest extends SapphireTest {
}
}
class CompositeDBFieldTest_DataObject extends DataObject {
class CompositeDBFieldTest_DataObject extends DataObject implements TestOnly {
private static $db = array(
'Title' => 'Text',
'MyMoney' => 'Money',

View File

@ -19,7 +19,12 @@ class DataListTest extends SapphireTest {
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
'DataObjectTest_ExtendedTeamComment',
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'DataObjectTest\NamespacedClass',
'DataObjectTest_Company',
'DataObjectTest_Fan',
);
public function testFilterDataObjectByCreatedDate() {

View File

@ -22,8 +22,12 @@ class DataObjectLazyLoadingTest extends SapphireTest {
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'VersionedTest_DataObject',
'VersionedTest_Subclass'
'VersionedTest_Subclass',
'VersionedLazy_DataObject',
'VersionedLazySub_DataObject',
);
public function testQueriedColumnsID() {
@ -403,7 +407,7 @@ class DataObjectLazyLoadingTest extends SapphireTest {
/** Additional classes for versioned lazy loading testing */
class VersionedLazy_DataObject extends DataObject {
class VersionedLazy_DataObject extends DataObject implements TestOnly {
private static $db = array(
"PageName" => "Varchar"
);

View File

@ -17,9 +17,15 @@ class DataObjectTest extends SapphireTest {
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'DataObjectTest\NamespacedClass',
'DataObjectTest\RelationClass',
'DataObjectTest_ExtendedTeamComment'
'DataObjectTest_ExtendedTeamComment',
'DataObjectTest_Company',
'DataObjectTest_Staff',
'DataObjectTest_CEO',
'DataObjectTest_Fan',
);
public function testDb() {
@ -1056,6 +1062,86 @@ class DataObjectTest extends SapphireTest {
);
}
protected function makeAccessible($object, $method) {
$reflectionMethod = new ReflectionMethod($object, $method);
$reflectionMethod->setAccessible(true);
return $reflectionMethod;
}
public function testValidateModelDefinitionsFailsWithArray() {
Config::nest();
$object = new DataObjectTest_Team;
$method = $this->makeAccessible($object, 'validateModelDefinitions');
Config::inst()->update('DataObjectTest_Team', 'has_one', array('NotValid' => array('NoArraysAllowed')));
$this->setExpectedException('LogicException');
try {
$method->invoke($object);
} catch(Exception $e) {
Config::unnest(); // Catch the exception so we can unnest config before failing the test
throw $e;
}
}
public function testValidateModelDefinitionsFailsWithIntKey() {
Config::nest();
$object = new DataObjectTest_Team;
$method = $this->makeAccessible($object, 'validateModelDefinitions');
Config::inst()->update('DataObjectTest_Team', 'has_many', array(12 => 'DataObjectTest_Player'));
$this->setExpectedException('LogicException');
try {
$method->invoke($object);
} catch(Exception $e) {
Config::unnest(); // Catch the exception so we can unnest config before failing the test
throw $e;
}
}
public function testValidateModelDefinitionsFailsWithIntValue() {
Config::nest();
$object = new DataObjectTest_Team;
$method = $this->makeAccessible($object, 'validateModelDefinitions');
Config::inst()->update('DataObjectTest_Team', 'many_many', array('Players' => 12));
$this->setExpectedException('LogicException');
try {
$method->invoke($object);
} catch(Exception $e) {
Config::unnest(); // Catch the exception so we can unnest config before failing the test
throw $e;
}
}
/**
* many_many_extraFields is allowed to have an array value, so shouldn't throw an exception
*/
public function testValidateModelDefinitionsPassesWithExtraFields() {
Config::nest();
$object = new DataObjectTest_Team;
$method = $this->makeAccessible($object, 'validateModelDefinitions');
Config::inst()->update('DataObjectTest_Team', 'many_many_extraFields',
array('Relations' => array('Price' => 'Int')));
try {
$method->invoke($object);
} catch(Exception $e) {
Config::unnest();
$this->fail('Exception should not be thrown');
throw $e;
}
Config::unnest();
}
public function testNewClassInstance() {
$dataObject = $this->objFromFixture('DataObjectTest_Team', 'team1');
$changedDO = $dataObject->newClassInstance('DataObjectTest_SubTeam');
@ -1074,18 +1160,85 @@ class DataObjectTest extends SapphireTest {
$this->assertEquals($changedDO->ClassName, 'DataObjectTest_SubTeam');
}
public function testMultipleManyManyWithSameClass() {
$team = $this->objFromFixture('DataObjectTest_Team', 'team1');
$sponsors = $team->Sponsors();
$equipmentSuppliers = $team->EquipmentSuppliers();
// Check that DataObject::many_many() works as expected
list($class, $targetClass, $parentField, $childField, $joinTable) = $team->manyManyComponent('Sponsors');
$this->assertEquals('DataObjectTest_Team', $class,
'DataObject::many_many() didn\'t find the correct base class');
$this->assertEquals('DataObjectTest_EquipmentCompany', $targetClass,
'DataObject::many_many() didn\'t find the correct target class for the relation');
$this->assertEquals('DataObjectTest_EquipmentCompany_SponsoredTeams', $joinTable,
'DataObject::many_many() didn\'t find the correct relation table');
// Check that ManyManyList still works
$this->assertEquals(2, $sponsors->count(), 'Rows are missing from relation');
$this->assertEquals(1, $equipmentSuppliers->count(), 'Rows are missing from relation');
// Check everything works when no relation is present
$teamWithoutSponsor = $this->objFromFixture('DataObjectTest_Team', 'team3');
$this->assertInstanceOf('ManyManyList', $teamWithoutSponsor->Sponsors());
$this->assertEquals(0, $teamWithoutSponsor->Sponsors()->count());
// Check many_many_extraFields still works
$equipmentCompany = $this->objFromFixture('DataObjectTest_EquipmentCompany', 'equipmentcompany1');
$equipmentCompany->SponsoredTeams()->add($teamWithoutSponsor, array('SponsorFee' => 1000));
$sponsoredTeams = $equipmentCompany->SponsoredTeams();
$this->assertEquals(1000, $sponsoredTeams->byID($teamWithoutSponsor->ID)->SponsorFee,
'Data from many_many_extraFields was not stored/extracted correctly');
// Check subclasses correctly inherit multiple many_manys
$subTeam = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
$this->assertEquals(2, $subTeam->Sponsors()->count(),
'Child class did not inherit multiple many_manys');
$this->assertEquals(1, $subTeam->EquipmentSuppliers()->count(),
'Child class did not inherit multiple many_manys');
// Team 2 has one EquipmentCompany sponsor and one SubEquipmentCompany
$team2 = $this->objFromFixture('DataObjectTest_Team', 'team2');
$this->assertEquals(2, $team2->Sponsors()->count(),
'Child class did not inherit multiple belongs_many_manys');
// Check many_many_extraFields also works from the belongs_many_many side
$sponsors = $team2->Sponsors();
$sponsors->add($equipmentCompany, array('SponsorFee' => 750));
$this->assertEquals(750, $sponsors->byID($equipmentCompany->ID)->SponsorFee,
'Data from many_many_extraFields was not stored/extracted correctly');
$subEquipmentCompany = $this->objFromFixture('DataObjectTest_SubEquipmentCompany', 'subequipmentcompany1');
$subTeam->Sponsors()->add($subEquipmentCompany, array('SponsorFee' => 1200));
$this->assertEquals(1200, $subTeam->Sponsors()->byID($subEquipmentCompany->ID)->SponsorFee,
'Data from inherited many_many_extraFields was not stored/extracted correctly');
}
public function testManyManyExtraFields() {
$player = $this->objFromFixture('DataObjectTest_Player', 'player1');
$team = $this->objFromFixture('DataObjectTest_Team', 'team1');
// Get all extra fields
$teamExtraFields = $team->manyManyExtraFields();
$this->assertEquals(array(
'Players' => array('Position' => 'Varchar(100)')
), $teamExtraFields);
// Ensure fields from parent classes are included
$subTeam = singleton('DataObjectTest_SubTeam');
$teamExtraFields = $subTeam->manyManyExtraFields();
$this->assertEquals(array(
'Players' => array('Position' => 'Varchar(100)'),
'FormerPlayers' => array('Position' => 'Varchar(100)')
), $teamExtraFields);
// Extra fields are immediately available on the Team class (defined in $many_many_extraFields)
$teamExtraFields = $team->many_many_extraFields('Players');
$teamExtraFields = $team->manyManyExtraFieldsForComponent('Players');
$this->assertEquals($teamExtraFields, array(
'Position' => 'Varchar(100)'
));
// We'll have to go through the relation to get the extra fields on Player
$playerExtraFields = $player->many_many_extraFields('Teams');
$playerExtraFields = $player->manyManyExtraFieldsForComponent('Teams');
$this->assertEquals($playerExtraFields, array(
'Position' => 'Varchar(100)'
));
@ -1115,7 +1268,7 @@ class DataObjectTest extends SapphireTest {
// Check that ordering a many-many relation by an aggregate column doesn't fail
$player = $this->objFromFixture('DataObjectTest_Player', 'player2');
$player->Teams("", "count(DISTINCT \"DataObjectTest_Team_Players\".\"DataObjectTest_PlayerID\") DESC");
$player->Teams()->sort("count(DISTINCT \"DataObjectTest_Team_Players\".\"DataObjectTest_PlayerID\") DESC");
}
/**
@ -1268,13 +1421,13 @@ class DataObjectTest extends SapphireTest {
'CurrentStaff' => 'DataObjectTest_Staff',
'PreviousStaff' => 'DataObjectTest_Staff'
),
$company->has_many(),
$company->hasMany(),
'has_many strips field name data by default.'
);
$this->assertEquals (
'DataObjectTest_Staff',
$company->has_many('CurrentStaff'),
$company->hasManyComponent('CurrentStaff'),
'has_many strips field name data by default on single relationships.'
);
@ -1283,13 +1436,13 @@ class DataObjectTest extends SapphireTest {
'CurrentStaff' => 'DataObjectTest_Staff.CurrentCompany',
'PreviousStaff' => 'DataObjectTest_Staff.PreviousCompany'
),
$company->has_many(null, false),
$company->hasMany(null, false),
'has_many returns field name data when $classOnly is false.'
);
$this->assertEquals (
'DataObjectTest_Staff.CurrentCompany',
$company->has_many('CurrentStaff', false),
$company->hasManyComponent('CurrentStaff', false),
'has_many returns field name data on single records when $classOnly is false.'
);
}
@ -1545,6 +1698,11 @@ class DataObjectTest_Team extends DataObject implements TestOnly {
)
);
private static $belongs_many_many = array(
'Sponsors' => 'DataObjectTest_EquipmentCompany.SponsoredTeams',
'EquipmentSuppliers' => 'DataObjectTest_EquipmentCompany.EquipmentCustomers'
);
private static $summary_fields = array(
'Title' => 'Custom Title',
'Title.UpperCase' => 'Title',
@ -1606,6 +1764,16 @@ class DataObjectTest_SubTeam extends DataObjectTest_Team implements TestOnly {
private static $has_one = array(
"ParentTeam" => 'DataObjectTest_Team',
);
private static $many_many = array(
'FormerPlayers' => 'DataObjectTest_Player'
);
private static $many_many_extraFields = array(
'FormerPlayers' => array(
'Position' => 'Varchar(100)'
)
);
}
class OtherSubclassWithSameField extends DataObjectTest_Team implements TestOnly {
private static $db = array(
@ -1652,7 +1820,7 @@ class DataObjectTest_ValidatedObject extends DataObject implements TestOnly {
}
}
class DataObjectTest_Company extends DataObject {
class DataObjectTest_Company extends DataObject implements TestOnly {
private static $db = array(
'Name' => 'Varchar'
@ -1670,7 +1838,26 @@ class DataObjectTest_Company extends DataObject {
);
}
class DataObjectTest_Staff extends DataObject {
class DataObjectTest_EquipmentCompany extends DataObjectTest_Company implements TestOnly {
private static $many_many = array(
'SponsoredTeams' => 'DataObjectTest_Team',
'EquipmentCustomers' => 'DataObjectTest_Team'
);
private static $many_many_extraFields = array(
'SponsoredTeams' => array(
'SponsorFee' => 'Int'
)
);
}
class DataObjectTest_SubEquipmentCompany extends DataObjectTest_EquipmentCompany implements TestOnly {
private static $db = array(
'SubclassDatabaseField' => 'Varchar'
);
}
class DataObjectTest_Staff extends DataObject implements TestOnly {
private static $has_one = array (
'CurrentCompany' => 'DataObjectTest_Company',
'PreviousCompany' => 'DataObjectTest_Company'
@ -1685,7 +1872,7 @@ class DataObjectTest_CEO extends DataObjectTest_Staff {
);
}
class DataObjectTest_TeamComment extends DataObject {
class DataObjectTest_TeamComment extends DataObject implements TestOnly {
private static $db = array(
'Name' => 'Varchar',
'Comment' => 'Text'
@ -1697,7 +1884,7 @@ class DataObjectTest_TeamComment extends DataObject {
}
class DataObjectTest_Fan extends DataObject {
class DataObjectTest_Fan extends DataObject implements TestOnly {
private static $db = array(
'Name' => 'Varchar(255)'

View File

@ -1,68 +1,82 @@
DataObjectTest_EquipmentCompany:
equipmentcompany1:
Name: Company corp
equipmentcompany2:
Name: 'Team co.'
DataObjectTest_SubEquipmentCompany:
subequipmentcompany1:
Name: John Smith and co
DataObjectTest_Team:
team1:
Title: Team 1
team2:
Title: Team 2
team3:
Title: Team 3
team1:
Title: Team 1
Sponsors: =>DataObjectTest_EquipmentCompany.equipmentcompany1,=>DataObjectTest_EquipmentCompany.equipmentcompany2
EquipmentSuppliers: =>DataObjectTest_EquipmentCompany.equipmentcompany2
team2:
Title: Team 2
Sponsors: =>DataObjectTest_EquipmentCompany.equipmentcompany2,=>DataObjectTest_SubEquipmentCompany.subequipmentcompany1
EquipmentSuppliers: =>DataObjectTest_EquipmentCompany.equipmentcompany1,=>DataObjectTest_EquipmentCompany.equipmentcompany2
team3:
Title: Team 3
DataObjectTest_Player:
captain1:
FirstName: Captain
ShirtNumber: 007
FavouriteTeam: =>DataObjectTest_Team.team1
Teams: =>DataObjectTest_Team.team1
IsRetired: 1
captain2:
FirstName: Captain 2
Teams: =>DataObjectTest_Team.team2
player1:
FirstName: Player 1
player2:
FirstName: Player 2
Teams: =>DataObjectTest_Team.team1,=>DataObjectTest_Team.team2
captain1:
FirstName: Captain
ShirtNumber: 007
FavouriteTeam: =>DataObjectTest_Team.team1
Teams: =>DataObjectTest_Team.team1
IsRetired: 1
captain2:
FirstName: Captain 2
Teams: =>DataObjectTest_Team.team2
player1:
FirstName: Player 1
player2:
FirstName: Player 2
Teams: =>DataObjectTest_Team.team1,=>DataObjectTest_Team.team2
DataObjectTest_SubTeam:
subteam1:
Title: Subteam 1
SubclassDatabaseField: Subclassed 1
ExtendedDatabaseField: Extended 1
ParentTeam: =>DataObjectTest_Team.team1
subteam2_with_player_relation:
Title: Subteam 2
SubclassDatabaseField: Subclassed 2
ExtendedHasOneRelationship: =>DataObjectTest_Player.player1
subteam3_with_empty_fields:
Title: Subteam 3
subteam1:
Title: Subteam 1
SubclassDatabaseField: Subclassed 1
ExtendedDatabaseField: Extended 1
ParentTeam: =>DataObjectTest_Team.team1
Sponsors: =>DataObjectTest_EquipmentCompany.equipmentcompany1,=>DataObjectTest_EquipmentCompany.equipmentcompany2
EquipmentSuppliers: =>DataObjectTest_EquipmentCompany.equipmentcompany1
subteam2_with_player_relation:
Title: Subteam 2
SubclassDatabaseField: Subclassed 2
ExtendedHasOneRelationship: =>DataObjectTest_Player.player1
subteam3_with_empty_fields:
Title: Subteam 3
DataObjectTest_TeamComment:
comment1:
Name: Joe
Comment: This is a team comment by Joe
Team: =>DataObjectTest_Team.team1
comment2:
Name: Bob
Comment: This is a team comment by Bob
Team: =>DataObjectTest_Team.team1
comment3:
Name: Phil
Comment: Phil is a unique guy, and comments on team2
Team: =>DataObjectTest_Team.team2
comment1:
Name: Joe
Comment: This is a team comment by Joe
Team: =>DataObjectTest_Team.team1
comment2:
Name: Bob
Comment: This is a team comment by Bob
Team: =>DataObjectTest_Team.team1
comment3:
Name: Phil
Comment: Phil is a unique guy, and comments on team2
Team: =>DataObjectTest_Team.team2
DataObjectTest_Fan:
fan1:
Name: Damian
Favourite: =>DataObjectTest_Team.team1
fan2:
Name: Stephen
Favourite: =>DataObjectTest_Player.player1
SecondFavourite: =>DataObjectTest_Team.team2
fan3:
Name: Richard
Favourite: =>DataObjectTest_Team.team1
fan4:
Name: Mitch
Favourite: =>DataObjectTest_SubTeam.subteam1
fan1:
Name: Damian
Favourite: =>DataObjectTest_Team.team1
fan2:
Name: Stephen
Favourite: =>DataObjectTest_Player.player1
SecondFavourite: =>DataObjectTest_Team.team2
fan3:
Name: Richard
Favourite: =>DataObjectTest_Team.team1
fan4:
Name: Mitch
Favourite: =>DataObjectTest_SubTeam.subteam1
DataObjectTest_Company:
company1:
Name: Company corp
Owner: =>DataObjectTest_Player.player1
company1:
Name: 'Team co.'
Owner: =>DataObjectTest_Player.player2
company1:
Name: Company corp
Owner: =>DataObjectTest_Player.player1
company1:
Name: 'Team co.'
Owner: =>DataObjectTest_Player.player2

View File

@ -49,6 +49,19 @@ class DataQueryTest extends SapphireTest {
$dq->sql($parameters));
}
public function testApplyRelation() {
// Test applyRelation with two has_ones pointing to the same class
$dq = new DataQuery('DataQueryTest_B');
$dq->applyRelation('TestC');
$this->assertTrue($dq->query()->isJoinedTo('DataQueryTest_C'));
$this->assertContains('"DataQueryTest_C"."ID" = "DataQueryTest_B"."TestCID"', $dq->sql());
$dq = new DataQuery('DataQueryTest_B');
$dq->applyRelation('TestCTwo');
$this->assertTrue($dq->query()->isJoinedTo('DataQueryTest_C'));
$this->assertContains('"DataQueryTest_C"."ID" = "DataQueryTest_B"."TestCTwoID"', $dq->sql());
}
public function testApplyReplationDeepInheretence() {
$newDQ = new DataQuery('DataQueryTest_E');
//apply a relation to a relation from an ancestor class
@ -257,6 +270,7 @@ class DataQueryTest_B extends DataObject implements TestOnly {
private static $has_one = array(
'TestC' => 'DataQueryTest_C',
'TestCTwo' => 'DataQueryTest_C',
);
}
@ -273,7 +287,8 @@ class DataQueryTest_C extends DataObject implements TestOnly {
private static $has_many = array(
'TestAs' => 'DataQueryTest_A',
'TestBs' => 'DataQueryTest_B',
'TestBs' => 'DataQueryTest_B.TestC',
'TestBsTwo' => 'DataQueryTest_B.TestCTwo',
);
private static $many_many = array(

View File

@ -9,6 +9,7 @@ class HasManyListTest extends SapphireTest {
'DataObjectTest_Team',
'DataObjectTest_SubTeam',
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
);
public function testRelationshipEmptyOnNewRecords() {

View File

@ -64,6 +64,10 @@ class PaginatedListTest extends SapphireTest {
$this->assertEquals(10, $list->CurrentPage());
$this->assertEquals(90, $list->getPageStart());
// Test disabled paging
$list->setPageLength(0);
$this->assertEquals(1, $list->CurrentPage());
}
public function testGetIterator() {
@ -93,6 +97,20 @@ class PaginatedListTest extends SapphireTest {
$list->setCurrentPage(999);
$this->assertDOSEquals(array(), $list->getIterator());
// Test disabled paging
$list->setPageLength(0);
$list->setCurrentPage(1);
$this->assertDOSEquals(
array(
array('Num' => 1),
array('Num' => 2),
array('Num' => 3),
array('Num' => 4),
array('Num' => 5)
), $list->getIterator()
);
// Test with dataobjectset
$players = DataObjectTest_Player::get();
$list = new PaginatedList($players);
$list->setPageLength(1);
@ -127,6 +145,13 @@ class PaginatedListTest extends SapphireTest {
array('PageNum' => 4),
);
$this->assertDOSEquals($expectLimited, $list->Pages(3));
// Disable paging
$list->setPageLength(0);
$expectAll = array(
array('PageNum' => 1, 'CurrentBool' => true),
);
$this->assertDOSEquals($expectAll, $list->Pages());
}
public function testPaginationSummary() {
@ -148,6 +173,13 @@ class PaginatedListTest extends SapphireTest {
array('PageNum' => 25),
);
$this->assertDOSEquals($expect, $list->PaginationSummary(4));
// Disable paging
$list->setPageLength(0);
$expect = array(
array('PageNum' => 1, 'CurrentBool' => true)
);
$this->assertDOSEquals($expect, $list->PaginationSummary(4));
}
public function testLimitItems() {
@ -170,6 +202,10 @@ class PaginatedListTest extends SapphireTest {
$this->assertEquals(2, $list->CurrentPage());
$list->setPageStart(40);
$this->assertEquals(5, $list->CurrentPage());
// Disable paging
$list->setPageLength(0);
$this->assertEquals(1, $list->CurrentPage());
}
public function testTotalPages() {
@ -183,6 +219,13 @@ class PaginatedListTest extends SapphireTest {
$list->setTotalItems(5);
$this->assertEquals(5, $list->TotalPages());
// Disable paging
$list->setPageLength(0);
$this->assertEquals(1, $list->TotalPages());
$list->setTotalItems(0);
$this->assertEquals(0, $list->TotalPages());
}
public function testMoreThanOnePage() {
@ -194,6 +237,10 @@ class PaginatedListTest extends SapphireTest {
$list->setTotalItems(2);
$this->assertTrue($list->MoreThanOnePage());
// Disable paging
$list->setPageLength(0);
$this->assertFalse($list->MoreThanOnePage());
}
public function testNotFirstPage() {
@ -230,6 +277,10 @@ class PaginatedListTest extends SapphireTest {
$this->assertEquals(20, $list->LastItem());
$list->setCurrentPage(3);
$this->assertEquals(25, $list->LastItem());
// Disable paging
$list->setPageLength(0);
$this->assertEquals(25, $list->LastItem());
}
public function testFirstLink() {
@ -242,6 +293,10 @@ class PaginatedListTest extends SapphireTest {
$list->setPageLength(10);
$list->setTotalItems(100);
$this->assertContains('start=90', $list->LastLink());
// Disable paging
$list->setPageLength(0);
$this->assertContains('start=0', $list->LastLink());
}
public function testNextLink() {
@ -257,6 +312,11 @@ class PaginatedListTest extends SapphireTest {
$this->assertContains('start=40', $list->NextLink());
$list->setCurrentPage(5);
$this->assertNull($list->NextLink());
// Disable paging
$list->setCurrentPage(1);
$list->setPageLength(0);
$this->assertNull($list->NextLink());
}
public function testPrevLink() {
@ -270,6 +330,10 @@ class PaginatedListTest extends SapphireTest {
$this->assertContains('start=10', $list->PrevLink());
$list->setCurrentPage(5);
$this->assertContains('start=30', $list->PrevLink());
// Disable paging
$list->setPageLength(0);
$this->assertNull($list->PrevLink());
}
}

View File

@ -19,7 +19,7 @@ class PolymorphicHasManyListTest extends SapphireTest {
'DataObjectTest_Team',
'DataObjectTest_SubTeam',
'DataObjectTest_Player',
'DataObjectTest_Fan'
'DataObjectTest_Fan',
);
public function testRelationshipEmptyOnNewRecords() {

View File

@ -21,6 +21,35 @@ class TextTest extends SapphireTest {
}
}
/**
* Test {@link Text->LimitCharactersToClosestWord()}
*/
public function testLimitCharactersToClosestWord() {
$cases = array(
/* Standard words limited, ellipsis added if truncated */
'Lorem ipsum dolor sit amet' => 'Lorem ipsum dolor sit...',
/* Complete words less than the character limit don't get truncated, ellipsis not added */
'Lorem ipsum' => 'Lorem ipsum',
'Lorem' => 'Lorem',
'' => '', // No words produces nothing!
/* HTML tags get stripped out, leaving the raw text */
'<p>Lorem ipsum dolor sit amet</p>' => 'Lorem ipsum dolor sit...',
'<p><span>Lorem ipsum dolor sit amet</span></p>' => 'Lorem ipsum dolor sit...',
'<p>Lorem ipsum</p>' => 'Lorem ipsum',
/* HTML entities are treated as a single character */
'Lorem &amp; ipsum dolor sit amet' => 'Lorem &amp; ipsum dolor...'
);
foreach($cases as $originalValue => $expectedValue) {
$textObj = new Text('Test');
$textObj->setValue($originalValue);
$this->assertEquals($expectedValue, $textObj->LimitCharactersToClosestWord(24));
}
}
/**
* Test {@link Text->LimitWordCount()}
*/

View File

@ -1,78 +1,78 @@
Permission:
admin:
Code: ADMIN
security-admin:
Code: CMS_ACCESS_SecurityAdmin
admin:
Code: ADMIN
security-admin:
Code: CMS_ACCESS_SecurityAdmin
Group:
admingroup:
Title: Admin
Code: admin
Permissions: =>Permission.admin
securityadminsgroup:
Title: securityadminsgroup
Code: securityadminsgroup
Permissions: =>Permission.security-admin
staffgroup:
Title: staffgroup
Code: staffgroup
managementgroup:
Title: managementgroup
Code: managementgroup
Parent: =>Group.staffgroup
accountinggroup:
Title: accountinggroup
Code: accountinggroup
Parent: =>Group.staffgroup
ceogroup:
Title: ceogroup
Code: ceogroup
Parent: =>Group.managementgroup
memberlessgroup:
Title: Memberless Group
code: memberless
admingroup:
Title: Admin
Code: admin
Permissions: =>Permission.admin
securityadminsgroup:
Title: securityadminsgroup
Code: securityadminsgroup
Permissions: =>Permission.security-admin
staffgroup:
Title: staffgroup
Code: staffgroup
managementgroup:
Title: managementgroup
Code: managementgroup
Parent: =>Group.staffgroup
accountinggroup:
Title: accountinggroup
Code: accountinggroup
Parent: =>Group.staffgroup
ceogroup:
Title: ceogroup
Code: ceogroup
Parent: =>Group.managementgroup
memberlessgroup:
Title: Memberless Group
code: memberless
Member:
admin:
FirstName: Admin
Email: admin@silverstripe.com
Groups: =>Group.admingroup
other-admin:
FirstName: OtherAdmin
Email: other-admin@silverstripe.com
Groups: =>Group.admingroup
test:
FirstName: Test
Surname: User
Email: sam@silverstripe.com
Password: 1nitialPassword
PasswordExpiry: 2030-01-01
Groups: =>Group.securityadminsgroup
expiredpassword:
FirstName: Test
Surname: User
Email: expired@silverstripe.com
Password: 1nitialPassword
PasswordExpiry: 2006-01-01
noexpiry:
FirstName: Test
Surname: User
Email: noexpiry@silverstripe.com
Password: 1nitialPassword
staffmember:
Email: staffmember@test.com
Groups: =>Group.staffgroup
managementmember:
Email: managementmember@test.com
Groups: =>Group.managementgroup
accountingmember:
Email: accountingmember@test.com
Groups: =>Group.accountinggroup
ceomember:
Email: ceomember@test.com
Groups: =>Group.ceogroup
grouplessmember:
FirstName: Groupless Member
noformatmember:
Email: noformat@test.com
delocalemember:
Email: delocalemember@test.com
Locale: de_DE
admin:
FirstName: Admin
Email: admin@silverstripe.com
Groups: =>Group.admingroup
other-admin:
FirstName: OtherAdmin
Email: other-admin@silverstripe.com
Groups: =>Group.admingroup
test:
FirstName: Test
Surname: User
Email: sam@silverstripe.com
Password: 1nitialPassword
PasswordExpiry: 2030-01-01
Groups: =>Group.securityadminsgroup
expiredpassword:
FirstName: Test
Surname: User
Email: expired@silverstripe.com
Password: 1nitialPassword
PasswordExpiry: 2006-01-01
noexpiry:
FirstName: Test
Surname: User
Email: noexpiry@silverstripe.com
Password: 1nitialPassword
staffmember:
Email: staffmember@test.com
Groups: =>Group.staffgroup
managementmember:
Email: managementmember@test.com
Groups: =>Group.managementgroup
accountingmember:
Email: accountingmember@test.com
Groups: =>Group.accountinggroup
ceomember:
Email: ceomember@test.com
Groups: =>Group.ceogroup
grouplessmember:
FirstName: Groupless Member
noformatmember:
Email: noformat@test.com
delocalemember:
Email: delocalemember@test.com
Locale: de_DE

View File

@ -46,7 +46,7 @@ class PermissionCheckboxSetFieldTest extends SapphireTest {
$this->assertEquals($group->Permissions()->Count(), 0, 'The tested group has no permissions');
$this->assertEquals($untouchable->Permissions()->Count(), 1, 'The other group has one permission');
$this->assertEquals($untouchable->Permissions("\"Code\"='ADMIN'")->Count(), 1,
$this->assertEquals($untouchable->Permissions()->where("\"Code\"='ADMIN'")->Count(), 1,
'The other group has ADMIN permission');
$this->assertEquals(DataObject::get('Permission')->Count(), $baseCount, 'There are no orphaned permissions');
@ -62,14 +62,14 @@ class PermissionCheckboxSetFieldTest extends SapphireTest {
$untouchable->flushCache();
$this->assertEquals($group->Permissions()->Count(), 2,
'The tested group has two permissions permission');
$this->assertEquals($group->Permissions("\"Code\"='ADMIN'")->Count(), 1,
$this->assertEquals($group->Permissions()->where("\"Code\"='ADMIN'")->Count(), 1,
'The tested group has ADMIN permission');
$this->assertEquals($group->Permissions("\"Code\"='NON-ADMIN'")->Count(), 1,
$this->assertEquals($group->Permissions()->where("\"Code\"='NON-ADMIN'")->Count(), 1,
'The tested group has CMS_ACCESS_AssetAdmin permission');
$this->assertEquals($untouchable->Permissions()->Count(), 1,
'The other group has one permission');
$this->assertEquals($untouchable->Permissions("\"Code\"='ADMIN'")->Count(), 1,
$this->assertEquals($untouchable->Permissions()->where("\"Code\"='ADMIN'")->Count(), 1,
'The other group has ADMIN permission');
$this->assertEquals(DataObject::get('Permission')->Count(), $baseCount+2,
@ -85,12 +85,12 @@ class PermissionCheckboxSetFieldTest extends SapphireTest {
$untouchable->flushCache();
$this->assertEquals($group->Permissions()->Count(), 1,
'The tested group has 1 permission');
$this->assertEquals($group->Permissions("\"Code\"='ADMIN'")->Count(), 1,
$this->assertEquals($group->Permissions()->where("\"Code\"='ADMIN'")->Count(), 1,
'The tested group has ADMIN permission');
$this->assertEquals($untouchable->Permissions()->Count(), 1,
'The other group has one permission');
$this->assertEquals($untouchable->Permissions("\"Code\"='ADMIN'")->Count(), 1,
$this->assertEquals($untouchable->Permissions()->where("\"Code\"='ADMIN'")->Count(), 1,
'The other group has ADMIN permission');
$this->assertEquals(DataObject::get('Permission')->Count(), $baseCount+1,

View File

@ -74,6 +74,104 @@ class SecurityTest extends FunctionalTest {
$this->autoFollowRedirection = true;
}
public function testPermissionFailureSetsCorrectFormMessages() {
Config::nest();
// Controller that doesn't attempt redirections
$controller = new SecurityTest_NullController();
$controller->response = new SS_HTTPResponse();
Security::permissionFailure($controller, array('default' => 'Oops, not allowed'));
$this->assertEquals('Oops, not allowed', Session::get('Security.Message.message'));
// Test that config values are used correctly
Config::inst()->update('Security', 'default_message_set', 'stringvalue');
Security::permissionFailure($controller);
$this->assertEquals('stringvalue', Session::get('Security.Message.message'),
'Default permission failure message value was not present');
Config::inst()->remove('Security', 'default_message_set');
Config::inst()->update('Security', 'default_message_set', array('default' => 'arrayvalue'));
Security::permissionFailure($controller);
$this->assertEquals('arrayvalue', Session::get('Security.Message.message'),
'Default permission failure message value was not present');
// Test that non-default messages work.
// NOTE: we inspect the response body here as the session message has already
// been fetched and output as part of it, so has been removed from the session
$this->logInWithPermission('EDITOR');
Config::inst()->update('Security', 'default_message_set',
array('default' => 'default', 'alreadyLoggedIn' => 'You are already logged in!'));
Security::permissionFailure($controller);
$this->assertContains('You are already logged in!', $controller->response->getBody(),
'Custom permission failure message was ignored');
Security::permissionFailure($controller,
array('default' => 'default', 'alreadyLoggedIn' => 'One-off failure message'));
$this->assertContains('One-off failure message', $controller->response->getBody(),
"Message set passed to Security::permissionFailure() didn't override Config values");
Config::unnest();
}
/**
* Follow all redirects recursively
*
* @param string $url
* @param int $limit Max number of requests
* @return SS_HTTPResponse
*/
protected function getRecursive($url, $limit = 10) {
$this->cssParser = null;
$response = $this->mainSession->get($url);
while(--$limit > 0 && $response instanceof SS_HTTPResponse && $response->getHeader('Location')) {
$response = $this->mainSession->followRedirection();
}
return $response;
}
public function testAutomaticRedirectionOnLogin() {
// BackURL with permission error (not authenticated) should not redirect
if($member = Member::currentUser()) $member->logOut();
$response = $this->getRecursive('SecurityTest_SecuredController');
$this->assertContains(Convert::raw2xml("That page is secured."), $response->getBody());
$this->assertContains('<input type="submit" name="action_dologin"', $response->getBody());
// Non-logged in user should not be redirected, but instead shown the login form
// No message/context is available as the user has not attempted to view the secured controller
$response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/');
$this->assertNotContains(Convert::raw2xml("That page is secured."), $response->getBody());
$this->assertNotContains(Convert::raw2xml("You don't have access to this page"), $response->getBody());
$this->assertContains('<input type="submit" name="action_dologin"', $response->getBody());
// BackURL with permission error (wrong permissions) should not redirect
$this->logInAs('grouplessmember');
$response = $this->getRecursive('SecurityTest_SecuredController');
$this->assertContains(Convert::raw2xml("You don't have access to this page"), $response->getBody());
$this->assertContains(
'<input type="submit" name="action_logout" value="Log in as someone else"',
$response->getBody()
);
// Directly accessing this page should attempt to follow the BackURL, but stop when it encounters the error
$response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/');
$this->assertContains(Convert::raw2xml("You don't have access to this page"), $response->getBody());
$this->assertContains(
'<input type="submit" name="action_logout" value="Log in as someone else"',
$response->getBody()
);
// Check correctly logged in admin doesn't generate the same errors
$this->logInAs('admin');
$response = $this->getRecursive('SecurityTest_SecuredController');
$this->assertContains(Convert::raw2xml("Success"), $response->getBody());
// Directly accessing this page should attempt to follow the BackURL and succeed
$response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/');
$this->assertContains(Convert::raw2xml("Success"), $response->getBody());
}
public function testLogInAsSomeoneElse() {
$member = DataObject::get_one('Member');
@ -517,3 +615,11 @@ class SecurityTest_SecuredController extends Controller implements TestOnly {
return 'Success';
}
}
class SecurityTest_NullController extends Controller implements TestOnly {
public function redirect($url, $code = 302) {
// NOOP
}
}

View File

@ -49,7 +49,7 @@ class YamlFixtureTest extends SapphireTest {
$factory->getId("YamlFixtureTest_DataObject", "testobject1")
);
$this->assertTrue(
$object1->ManyMany()->Count() == 2,
$object1->ManyManyRelation()->Count() == 2,
"Should be two items in this relationship"
);
$this->assertGreaterThan(0, $factory->getId("YamlFixtureTest_DataObject", "testobject2"));
@ -58,7 +58,7 @@ class YamlFixtureTest extends SapphireTest {
$factory->getId("YamlFixtureTest_DataObject", "testobject2")
);
$this->assertTrue(
$object2->ManyMany()->Count() == 1,
$object2->ManyManyRelation()->Count() == 1,
"Should be one item in this relationship"
);
}
@ -79,7 +79,7 @@ class YamlFixtureTest_DataObject extends DataObject implements TestOnly {
"Name" => "Varchar"
);
private static $many_many = array(
"ManyMany" => "YamlFixtureTest_DataObjectRelation"
"ManyManyRelation" => "YamlFixtureTest_DataObjectRelation"
);
}

View File

@ -6,7 +6,7 @@ YamlFixtureTest_DataObjectRelation:
YamlFixtureTest_DataObject:
testobject1:
Name: TestObject1
ManyMany: =>YamlFixtureTest_DataObjectRelation.relation1,=>YamlFixtureTest_DataObjectRelation.relation2
ManyManyRelation: =>YamlFixtureTest_DataObjectRelation.relation1,=>YamlFixtureTest_DataObjectRelation.relation2
testobject2:
Name: TestObject2
ManyMany: =>YamlFixtureTest_DataObjectRelation.relation1
ManyManyRelation: =>YamlFixtureTest_DataObjectRelation.relation1

View File

@ -1,6 +1,11 @@
<?php
class SSViewerTest extends SapphireTest {
protected $extraDataObjects = array(
'SSViewerTest_Object',
);
public function setUp() {
parent::setUp();
Config::inst()->update('SSViewer', 'source_file_comments', false);
@ -1109,9 +1114,11 @@ after')
}
public function testRewriteHashlinks() {
$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
Config::inst()->update('SSViewer', 'rewrite_hash_links', true);
$_SERVER['REQUEST_URI'] = 'http://path/to/file?foo"onclick="alert(\'xss\')""';
// Emulate SSViewer::process()
$base = Convert::raw2att($_SERVER['REQUEST_URI']);
@ -1122,6 +1129,8 @@ after')
<html>
<head><% base_tag %></head>
<body>
<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>
$ExternalInsertedLink
<a class="inline" href="#anchor">InlineLink</a>
$InsertedLink
<svg><use xlink:href="#sprite"></use></svg>
@ -1130,15 +1139,24 @@ after')
$tmpl = new SSViewer($tmplFile);
$obj = new ViewableData();
$obj->InsertedLink = '<a class="inserted" href="#anchor">InsertedLink</a>';
$obj->ExternalInsertedLink = '<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>';
$result = $tmpl->process($obj);
$this->assertContains(
'<a class="inserted" href="' . $base . '#anchor">InsertedLink</a>',
$result
);
$this->assertContains(
'<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>',
$result
);
$this->assertContains(
'<a class="inline" href="' . $base . '#anchor">InlineLink</a>',
$result
);
$this->assertContains(
'<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>',
$result
);
$this->assertContains(
'<svg><use xlink:href="#sprite"></use></svg>',
$result,
@ -1171,7 +1189,7 @@ after')
$obj->InsertedLink = '<a class="inserted" href="#anchor">InsertedLink</a>';
$result = $tmpl->process($obj);
$this->assertContains(
'<a class="inserted" href="<?php echo strip_tags(',
'<a class="inserted" href="<?php echo Convert::raw2att(',
$result
);
// TODO Fix inline links in PHP mode
@ -1509,7 +1527,7 @@ class SSViewerTest_Controller extends Controller {
}
class SSViewerTest_Object extends DataObject {
class SSViewerTest_Object extends DataObject implements TestOnly {
public $number = null;

View File

@ -156,6 +156,25 @@ class ViewableDataTest extends SapphireTest {
$this->assertEquals($uncastedData, $castedData->getValue(), 'Casted and uncasted strings are not equal.');
}
public function testCaching() {
$objCached = new ViewableDataTest_Cached();
$objNotCached = new ViewableDataTest_NotCached();
$objCached->Test = 'AAA';
$objNotCached->Test = 'AAA';
$this->assertEquals('AAA', $objCached->obj('Test', null, true, true));
$this->assertEquals('AAA', $objNotCached->obj('Test', null, true, true));
$objCached->Test = 'BBB';
$objNotCached->Test = 'BBB';
// Cached data must be always the same
$this->assertEquals('AAA', $objCached->obj('Test', null, true, true));
$this->assertEquals('BBB', $objNotCached->obj('Test', null, true, true));
}
}
/**#@+
@ -253,4 +272,17 @@ class ViewableDataTest_NoCastingInformation extends ViewableData {
}
}
class ViewableDataTest_Cached extends ViewableData {
public $Test;
}
class ViewableDataTest_NotCached extends ViewableData {
public $Test;
protected function objCacheGet($key) {
// Disable caching
return null;
}
}
/**#@-*/

View File

@ -10,7 +10,7 @@ jQuery(function($){
nextText: 'Neste&raquo;',
currentText: 'I dag',
monthNames: ['januar','februar','mars','april','mai','juni','juli','august','september','oktober','november','desember'],
monthNamesShort: ['jan.','feb.','mar.','apr.','mai','juni','juli','aug.','sep.','okt.','nov.','des.'],
monthNamesShort: ['jan.','feb.','mars','apr.','mai','juni','juli','aug.','sep.','okt.','nov.','des.'],
dayNames: ['søndag','mandag','tirsdag','onsdag','torsdag','fredag','lørdag'],
dayNamesShort: ['søn','man','tir','ons','tor','fre','lør'],
dayNamesMin: ['Sø','Ma','Ti','On','To','Fr','Lø'],

View File

@ -4675,7 +4675,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
$text = preg_replace(
'/(<a[^>]+href *= *)"#/i',
'\\1"\' . (Config::inst()->get(\'SSViewer\', \'rewrite_hash_links\') ?' .
' strip_tags( $_SERVER[\'REQUEST_URI\'] ) : "") .
' Convert::raw2att( $_SERVER[\'REQUEST_URI\'] ) : "") .
\'#',
$text
);

View File

@ -1129,7 +1129,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
$text = preg_replace(
'/(<a[^>]+href *= *)"#/i',
'\\1"\' . (Config::inst()->get(\'SSViewer\', \'rewrite_hash_links\') ?' .
' strip_tags( $_SERVER[\'REQUEST_URI\'] ) : "") .
' Convert::raw2att( $_SERVER[\'REQUEST_URI\'] ) : "") .
\'#',
$text
);

View File

@ -1115,9 +1115,9 @@ class SSViewer implements Flushable {
if($this->rewriteHashlinks && $rewrite) {
if(strpos($output, '<base') !== false) {
if($rewrite === 'php') {
$thisURLRelativeToBase = "<?php echo strip_tags(\$_SERVER['REQUEST_URI']); ?>";
$thisURLRelativeToBase = "<?php echo Convert::raw2att(\$_SERVER['REQUEST_URI']); ?>";
} else {
$thisURLRelativeToBase = strip_tags($_SERVER['REQUEST_URI']);
$thisURLRelativeToBase = Convert::raw2att($_SERVER['REQUEST_URI']);
}
$output = preg_replace('/(<a[^>]+href *= *)"#/i', '\\1"' . $thisURLRelativeToBase . '#', $output);

View File

@ -345,6 +345,38 @@ class ViewableData extends Object implements IteratorAggregate {
"ViewableData::renderWith(): unexpected $template->class object, expected an SSViewer instance"
);
}
/**
* Generate the cache name for a field
*
* @param string $fieldName Name of field
* @param array $arguments List of optional arguments given
*/
protected function objCacheName($fieldName, $arguments) {
return $arguments
? $fieldName . ":" . implode(',', $arguments)
: $fieldName;
}
/**
* Get a cached value from the field cache
*
* @param string $key Cache key
* @return mixed
*/
protected function objCacheGet($key) {
if(isset($this->objCache[$key])) return $this->objCache[$key];
}
/**
* Store a value in the field cache
*
* @param string $key Cache key
* @param mixed $value
*/
protected function objCacheSet($key, $value) {
$this->objCache[$key] = $value;
}
/**
* Get the value of a field on this object, automatically inserting the value into any available casting objects
@ -354,12 +386,14 @@ class ViewableData extends Object implements IteratorAggregate {
* @param array $arguments
* @param bool $forceReturnedObject if TRUE, the value will ALWAYS be casted to an object before being returned,
* even if there is no explicit casting information
* @param bool $cache Cache this object
* @param string $cacheName a custom cache name
*/
public function obj($fieldName, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
if(!$cacheName) $cacheName = $arguments ? $fieldName . implode(',', $arguments) : $fieldName;
if(!isset($this->objCache[$cacheName])) {
if(!$cacheName && $cache) $cacheName = $this->objCacheName($fieldName, $arguments);
$value = $cache ? $this->objCacheGet($cacheName) : null;
if(!isset($value)) {
// HACK: Don't call the deprecated FormField::Name() method
$methodIsAllowed = true;
if($this instanceof FormField && $fieldName == 'Name') $methodIsAllowed = false;
@ -381,9 +415,7 @@ class ViewableData extends Object implements IteratorAggregate {
$value = $valueObject;
}
if($cache) $this->objCache[$cacheName] = $value;
} else {
$value = $this->objCache[$cacheName];
if($cache) $this->objCacheSet($cacheName, $value);
}
if(!is_object($value) && $forceReturnedObject) {
@ -668,7 +700,14 @@ class ViewableData_Debugger extends ViewableData {
$this->object = $object;
parent::__construct();
}
/**
* @return string The rendered debugger
*/
public function __toString() {
return $this->forTemplate();
}
/**
* Return debugging information, as XHTML. If a field name is passed, it will show debugging information on that
* field, otherwise it will show information on all methods and fields.