Merge remote-tracking branch 'origin/3.1'

Conflicts:
	.travis.yml
This commit is contained in:
Ingo Schommer 2013-05-31 18:08:59 +02:00
commit 88536998b9
78 changed files with 2396 additions and 1219 deletions

View File

@ -6,11 +6,9 @@ UploadField:
allowedMaxFileNumber: allowedMaxFileNumber:
canUpload: true canUpload: true
canAttachExisting: 'CMS_ACCESS_AssetAdmin' canAttachExisting: 'CMS_ACCESS_AssetAdmin'
replaceExistingFile: false canPreviewFolder: true
previewMaxWidth: 80 previewMaxWidth: 80
previewMaxHeight: 60 previewMaxHeight: 60
uploadTemplateName: 'ss-uploadfield-uploadtemplate' uploadTemplateName: 'ss-uploadfield-uploadtemplate'
downloadTemplateName: 'ss-uploadfield-downloadtemplate' downloadTemplateName: 'ss-uploadfield-downloadtemplate'
fileEditFields: overwriteWarning: true # Warning before overwriting existing file (only relevant when Upload: replaceFile is true)
fileEditActions:
fileEditValidator:

View File

@ -6,7 +6,7 @@ HtmlEditorConfig::get('cms')->setOptions(array(
'mode' => 'none', // initialized through LeftAndMain.EditFor.js logic 'mode' => 'none', // initialized through LeftAndMain.EditFor.js logic
'body_class' => 'typography', 'body_class' => 'typography',
'document_base_url' => Director::absoluteBaseURL(), 'document_base_url' => isset($_SERVER['HTTP_HOST']) ? Director::absoluteBaseURL() : null,
'cleanup_callback' => "sapphiremce_cleanup", 'cleanup_callback' => "sapphiremce_cleanup",

View File

@ -152,6 +152,15 @@ class LeftAndMain extends Controller implements PermissionProvider {
*/ */
private static $extra_requirements_themedCss = array(); private static $extra_requirements_themedCss = array();
/**
* If true, call a keepalive ping every 5 minutes from the CMS interface,
* to ensure that the session never dies.
*
* @config
* @var boolean
*/
private static $session_keepalive_ping = true;
/** /**
* @var PjaxResponseNegotiator * @var PjaxResponseNegotiator
*/ */
@ -327,28 +336,30 @@ class LeftAndMain extends Controller implements PermissionProvider {
HTMLEditorField::include_js(); HTMLEditorField::include_js();
Requirements::combine_files( $leftAndMainIncludes = array_unique(array_merge(
'leftandmain.js', array(
array_unique(array_merge( FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Layout.js',
array( FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Layout.js', FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.ActionTabSet.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.js', FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Panel.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.ActionTabSet.js', FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Tree.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Panel.js', FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Content.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Tree.js', FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.EditForm.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Ping.js', FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Menu.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Content.js', FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Preview.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.EditForm.js', FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.BatchActions.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Menu.js', FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.FieldHelp.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Preview.js', FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.TreeDropdownField.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.BatchActions.js', ),
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.FieldHelp.js', Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang', true, true),
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.TreeDropdownField.js', Requirements::add_i18n_javascript(FRAMEWORK_ADMIN_DIR . '/javascript/lang', true, true)
), ));
Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang', true, true),
Requirements::add_i18n_javascript(FRAMEWORK_ADMIN_DIR . '/javascript/lang', true, true) if($this->config()->session_keepalive_ping) {
)) $leftAndMainIncludes[] = FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Ping.js';
); }
Requirements::combine_files('leftandmain.js', $leftAndMainIncludes);
// TODO Confuses jQuery.ondemand through document.write() // TODO Confuses jQuery.ondemand through document.write()
if (Director::isDev()) { if (Director::isDev()) {
@ -381,7 +392,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
if($extraCss) foreach($extraCss as $file => $config) { if($extraCss) foreach($extraCss as $file => $config) {
Requirements::css($file, isset($config['media']) ? $config['media'] : null); Requirements::css($file, isset($config['media']) ? $config['media'] : null);
} }
$extraThemedCss = $this->stat('extra_requirements_themedcss'); $extraThemedCss = $this->stat('extra_requirements_themedCss');
if($extraThemedCss) foreach ($extraThemedCss as $file => $config) { if($extraThemedCss) foreach ($extraThemedCss as $file => $config) {
Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null); Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null);
} }
@ -1687,8 +1698,8 @@ class LeftAndMain extends Controller implements PermissionProvider {
* @param $media String Comma-separated list of media-types (e.g. "screen,projector") * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
*/ */
public static function require_themed_css($name, $media = null) { public static function require_themed_css($name, $media = null) {
Deprecation::notice('3.2', 'Use "LeftAndMain.extra_requirements_css" config setting instead'); Deprecation::notice('3.2', 'Use "LeftAndMain.extra_requirements_themedCss" config setting instead');
Config::inst()->update('LeftAndMain', 'extra_requirements_css', array($name => array('media' => $media))); Config::inst()->update('LeftAndMain', 'extra_requirements_themedCss', array($name => array('media' => $media)));
} }
} }
@ -1840,7 +1851,12 @@ class LeftAndMain_TreeNode extends ViewableData {
$classes = $this->obj->CMSTreeClasses(); $classes = $this->obj->CMSTreeClasses();
if($this->isCurrent) $classes .= " current"; if($this->isCurrent) $classes .= " current";
$flags = $this->obj->hasMethod('getStatusFlags') ? $this->obj->getStatusFlags() : false; $flags = $this->obj->hasMethod('getStatusFlags') ? $this->obj->getStatusFlags() : false;
if($flags) $classes .= ' ' . implode(' ', array_keys($flags)); if ($flags) {
$statuses = array_keys($flags);
foreach ($statuses as $s) {
$classes .= ' status-' . $s;
}
}
return $classes; return $classes;
} }

View File

@ -129,8 +129,8 @@ body, html { font-size: 12px; line-height: 16px; font-family: Arial, sans-serif;
.ui-widget-header .ui-dialog-title { padding: 6px 0; text-shadow: #ced7dc 1px 1px 0; } .ui-widget-header .ui-dialog-title { padding: 6px 0; 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: -8px; right: -15px; 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 { border-color: transparent; background: transparent; }
.ui-widget-header a.ui-state-hover .ui-icon-closethick { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -216px no-repeat; } .ui-widget-header a.ui-state-hover .ui-icon-closethick { background: url('../images/sprites-32x32-s972e408b05.png') 0 -216px no-repeat; }
.ui-widget-header .ui-icon-closethick { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -318px no-repeat; width: 30px; height: 30px; } .ui-widget-header .ui-icon-closethick { background: url('../images/sprites-32x32-s972e408b05.png') 0 -292px no-repeat; width: 30px; height: 30px; }
.ui-state-hover { cursor: pointer; } .ui-state-hover { cursor: pointer; }
@ -632,8 +632,9 @@ body.cms-dialog { overflow: auto; background: url("../images/textures/bg_cms_mai
.htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb #RemoteURL label { position: absolute; left: 8px; top: 13px; font-weight: normal; color: #888; } .htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb #RemoteURL label { position: absolute; left: 8px; top: 13px; font-weight: normal; color: #888; }
.htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb #RemoteURL .middleColumn { margin-left: 0; } .htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb #RemoteURL .middleColumn { margin-left: 0; }
.htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb #RemoteURL input.remoteurl { padding-left: 40px; } .htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb #RemoteURL input.remoteurl { padding-left: 40px; }
.htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url { margin-top: 13px; padding-top: 15px; overflow: hidden; *zoom: 1; border: none; background: none; opacity: 0.8; cursor: hand; } .htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url { margin-top: 5px; padding-top: 15px; overflow: hidden; *zoom: 1; border: none; background: none; opacity: 0.8; cursor: hand; }
.htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url .btn-icon-addMedia { width: 20px; height: 20px; } .htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url .btn-icon-addMedia { width: 20px; height: 20px; }
.htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url .ui-button-text { margin-left: 10px; margin-top: -5px; line-height: 20px; float: left; }
.htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url:hover, .htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url:active { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; opacity: 1; } .htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url:hover, .htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url:active { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; opacity: 1; }
.htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url.ui-state-disabled, .htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url.ui-state-disabled:hover, .htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url.ui-state-disabled:active { opacity: 0.35; filter: Alpha(Opacity=35); } .htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url.ui-state-disabled, .htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url.ui-state-disabled:hover, .htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb button.add-url.ui-state-disabled:active { opacity: 0.35; filter: Alpha(Opacity=35); }
.htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb .loading button.add-url .ui-icon { background-image: url(../images/throbber.gif); background-position: 50% 50%; background-repeat: no-repeat; } .htmleditorfield-dialog #MediaFormInsertMediaTabs_FromWeb .loading button.add-url .ui-icon { background-image: url(../images/throbber.gif); background-position: 50% 50%; background-repeat: no-repeat; }
@ -644,6 +645,7 @@ body.cms-dialog { overflow: auto; background: url("../images/textures/bg_cms_mai
.htmleditorfield-dialog .details .cms-file-info .field { border: none; -webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0); -moz-box-shadow: 0 0 0 rgba(0, 0, 0, 0); box-shadow: 0 0 0 rgba(0, 0, 0, 0); } .htmleditorfield-dialog .details .cms-file-info .field { border: none; -webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0); -moz-box-shadow: 0 0 0 rgba(0, 0, 0, 0); box-shadow: 0 0 0 rgba(0, 0, 0, 0); }
.htmleditorfield-dialog .details .field { border-bottom: 1px solid rgba(201, 205, 206, 0.8); -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); } .htmleditorfield-dialog .details .field { border-bottom: 1px solid rgba(201, 205, 206, 0.8); -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); }
.htmleditorfield-dialog .details .field.last { border-bottom: none; -webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0); -moz-box-shadow: 0 0 0 rgba(0, 0, 0, 0); box-shadow: 0 0 0 rgba(0, 0, 0, 0); margin-bottom: 0; } .htmleditorfield-dialog .details .field.last { border-bottom: none; -webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0); -moz-box-shadow: 0 0 0 rgba(0, 0, 0, 0); box-shadow: 0 0 0 rgba(0, 0, 0, 0); margin-bottom: 0; }
.htmleditorfield-dialog .CompositeField .text select { margin: 5px 0 0 0; }
.htmleditorfield-linkform .step2 { margin-bottom: 16px; } .htmleditorfield-linkform .step2 { margin-bottom: 16px; }
@ -664,7 +666,7 @@ body.cms-dialog { overflow: auto; background: url("../images/textures/bg_cms_mai
/** -------------------------------------------- Step labels -------------------------------------------- */ /** -------------------------------------------- Step labels -------------------------------------------- */
.step-label > * { display: inline-block; vertical-align: top; } .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 .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-sf6890c994e.png') 0 -256px no-repeat; margin-right: 4px; } .step-label .arrow { height: 26px; width: 10px; background: url('../images/sprites-32x32-s972e408b05.png') 0 -256px no-repeat; margin-right: 4px; }
.step-label .title { height: 18px; padding: 4px; } .step-label .title { height: 18px; padding: 4px; }
/** -------------------------------------------- Item Edit Form -------------------------------------------- */ /** -------------------------------------------- Item Edit Form -------------------------------------------- */
@ -710,10 +712,10 @@ form.import-form label.left { width: 250px; }
/** -------------------------------------------- Buttons for FileUpload -------------------------------------------- */ /** -------------------------------------------- Buttons for FileUpload -------------------------------------------- */
.ss-uploadfield-item-edit-all .ui-button-text { padding-right: 0; } .ss-uploadfield-item-edit-all .ui-button-text { padding-right: 0; }
.toggle-details-icon { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -433px no-repeat; } .toggle-details-icon { background: url('../images/sprites-32x32-s972e408b05.png') 0 -407px no-repeat; }
.ss-uploadfield-item-edit-all .toggle-details-icon { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -375px no-repeat; display: inline-block; width: 8px; height: 8px; padding-left: 5px; } .ss-uploadfield-item-edit-all .toggle-details-icon { background: url('../images/sprites-32x32-s972e408b05.png') 0 -349px no-repeat; display: inline-block; width: 8px; height: 8px; padding-left: 5px; }
.toggle-details-icon.opened { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -1121px no-repeat; } .toggle-details-icon.opened { background: url('../images/sprites-32x32-s972e408b05.png') 0 -1121px no-repeat; }
.ss-uploadfield-item-edit-all .toggle-details-icon.opened { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -359px no-repeat; } .ss-uploadfield-item-edit-all .toggle-details-icon.opened { background: url('../images/sprites-32x32-s972e408b05.png') 0 -333px no-repeat; }
/** -------------------------------------------- Hide preview toggle link by default. May be shown in IE7 stylesheet and forced to show with js if needed -------------------------------------------- */ /** -------------------------------------------- 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; } .cms .Actions > .cms-preview-toggle-link, .cms .cms-navigator > .cms-preview-toggle-link { display: none; }
@ -736,7 +738,7 @@ form.import-form label.left { width: 250px; }
.cms .jstree .jstree-wholerow-real, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow-real { position: relative; z-index: 1; } .cms .jstree .jstree-wholerow-real, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow-real { position: relative; z-index: 1; }
.cms .jstree .jstree-wholerow-real li, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow-real li { cursor: pointer; } .cms .jstree .jstree-wholerow-real li, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow-real li { cursor: pointer; }
.cms .jstree .jstree-wholerow-real a, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow-real a { border-left-color: transparent !important; border-right-color: transparent !important; } .cms .jstree .jstree-wholerow-real a, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow-real a { border-left-color: transparent !important; border-right-color: transparent !important; }
.cms .jstree .jstree-wholerow, .cms .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow, .TreeDropdownField .treedropdownfield-panel .cms .jstree .jstree-wholerow, .cms .jstree .jstree-wholerow ul, .cms .jstree .jstree-wholerow li, .cms .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow ul, .TreeDropdownField .treedropdownfield-panel .cms .jstree .jstree-wholerow ul, .cms .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow li, .TreeDropdownField .treedropdownfield-panel .cms .jstree .jstree-wholerow li, .cms .jstree .jstree-wholerow a, .cms .jstree .jstree-wholerow a:hover, .cms .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow a, .TreeDropdownField .treedropdownfield-panel .cms .jstree .jstree-wholerow a, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow ul, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow li, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow a, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow a:hover { margin: 0 !important; padding: 0 !important; } .cms .jstree .jstree-wholerow, .cms .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow, .TreeDropdownField .treedropdownfield-panel .cms .jstree .jstree-wholerow, .cms .jstree .jstree-wholerow ul, .cms .jstree .jstree-wholerow li, .cms .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow ul, .TreeDropdownField .treedropdownfield-panel .cms .jstree .jstree-wholerow ul, .cms .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow li, .TreeDropdownField .treedropdownfield-panel .cms .jstree .jstree-wholerow li, .cms .jstree .jstree-wholerow a, .cms .jstree .jstree-wholerow a:hover, .cms .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow a, .TreeDropdownField .treedropdownfield-panel .cms .jstree .jstree-wholerow a, .cms .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow a:hover, .TreeDropdownField .treedropdownfield-panel .cms .jstree .jstree-wholerow a:hover, .TreeDropdownField .treedropdownfield-panel .cms .jstree .jstree-wholerow, .cms .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow, .TreeDropdownField .treedropdownfield-panel .cms .jstree .jstree-wholerow ul, .cms .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow ul, .TreeDropdownField .treedropdownfield-panel .cms .jstree .jstree-wholerow li, .cms .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow li, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow ul, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow li, .TreeDropdownField .treedropdownfield-panel .cms .jstree .jstree-wholerow a, .cms .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow a, .TreeDropdownField .treedropdownfield-panel .cms .jstree .jstree-wholerow a:hover, .cms .TreeDropdownField .treedropdownfield-panel .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; }
.cms .jstree .jstree-wholerow, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow { position: relative; z-index: 0; height: 0; background: transparent !important; } .cms .jstree .jstree-wholerow, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow { position: relative; z-index: 0; height: 0; background: transparent !important; }
.cms .jstree .jstree-wholerow ul, .cms .jstree .jstree-wholerow li, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow ul, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow li { background: transparent !important; width: 100%; } .cms .jstree .jstree-wholerow ul, .cms .jstree .jstree-wholerow li, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow ul, .TreeDropdownField .treedropdownfield-panel .jstree .jstree-wholerow li { background: transparent !important; width: 100%; }
.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 { text-indent: -9999px !important; width: 100%; border-right-width: 0px !important; border-left-width: 0px !important; } .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 { text-indent: -9999px !important; width: 100%; border-right-width: 0px !important; border-left-width: 0px !important; }
@ -755,7 +757,7 @@ form.import-form label.left { width: 250px; }
.cms .jstree-themeroller a, .TreeDropdownField .treedropdownfield-panel .jstree-themeroller a { padding: 0 2px; } .cms .jstree-themeroller a, .TreeDropdownField .treedropdownfield-panel .jstree-themeroller a { padding: 0 2px; }
.cms .jstree-themeroller .ui-icon, .TreeDropdownField .treedropdownfield-panel .jstree-themeroller .ui-icon { overflow: visible; } .cms .jstree-themeroller .ui-icon, .TreeDropdownField .treedropdownfield-panel .jstree-themeroller .ui-icon { overflow: visible; }
.cms .jstree-themeroller .jstree-no-icon, .TreeDropdownField .treedropdownfield-panel .jstree-themeroller .jstree-no-icon { display: none; } .cms .jstree-themeroller .jstree-no-icon, .TreeDropdownField .treedropdownfield-panel .jstree-themeroller .jstree-no-icon { display: none; }
.cms #jstree-marker, .cms .TreeDropdownField .treedropdownfield-panel #jstree-marker, .TreeDropdownField .treedropdownfield-panel .cms #jstree-marker, .cms #jstree-marker-line, .cms .TreeDropdownField .treedropdownfield-panel #jstree-marker-line, .TreeDropdownField .treedropdownfield-panel .cms #jstree-marker-line, .TreeDropdownField .treedropdownfield-panel #jstree-marker, .TreeDropdownField .treedropdownfield-panel #jstree-marker-line { padding: 0; margin: 0; overflow: hidden; position: absolute; top: -30px; background-repeat: no-repeat; display: none; } .cms #jstree-marker, .cms .TreeDropdownField .treedropdownfield-panel #jstree-marker, .TreeDropdownField .treedropdownfield-panel .cms #jstree-marker, .cms #jstree-marker-line, .cms .TreeDropdownField .treedropdownfield-panel #jstree-marker-line, .TreeDropdownField .treedropdownfield-panel .cms #jstree-marker-line, .TreeDropdownField .treedropdownfield-panel .cms #jstree-marker, .cms .TreeDropdownField .treedropdownfield-panel #jstree-marker, .TreeDropdownField .treedropdownfield-panel #jstree-marker, .TreeDropdownField .treedropdownfield-panel .cms #jstree-marker-line, .cms .TreeDropdownField .treedropdownfield-panel #jstree-marker-line, .TreeDropdownField .treedropdownfield-panel #jstree-marker-line { padding: 0; margin: 0; overflow: hidden; position: absolute; top: -30px; background-repeat: no-repeat; display: none; }
.cms #jstree-marker, .TreeDropdownField .treedropdownfield-panel #jstree-marker { line-height: 10px; font-size: 12px; height: 12px; width: 8px; z-index: 10001; background-color: transparent; text-shadow: 1px 1px 1px white; color: black; } .cms #jstree-marker, .TreeDropdownField .treedropdownfield-panel #jstree-marker { line-height: 10px; font-size: 12px; height: 12px; width: 8px; z-index: 10001; background-color: transparent; text-shadow: 1px 1px 1px white; color: black; }
.cms #jstree-marker-line, .TreeDropdownField .treedropdownfield-panel #jstree-marker-line { line-height: 0%; font-size: 1px; height: 1px; width: 100px; z-index: 10000; background-color: #456c43; cursor: pointer; border: 1px solid #eeeeee; border-left: 0; -moz-box-shadow: 0px 0px 2px #666; -webkit-box-shadow: 0px 0px 2px #666; box-shadow: 0px 0px 2px #666; -moz-border-radius: 1px; border-radius: 1px; -webkit-border-radius: 1px; } .cms #jstree-marker-line, .TreeDropdownField .treedropdownfield-panel #jstree-marker-line { line-height: 0%; font-size: 1px; height: 1px; width: 100px; z-index: 10000; background-color: #456c43; cursor: pointer; border: 1px solid #eeeeee; border-left: 0; -moz-box-shadow: 0px 0px 2px #666; -webkit-box-shadow: 0px 0px 2px #666; box-shadow: 0px 0px 2px #666; -moz-border-radius: 1px; border-radius: 1px; -webkit-border-radius: 1px; }
.cms #vakata-contextmenu, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu { display: block; visibility: hidden; left: 0; top: -200px; position: absolute; margin: 0; padding: 0; min-width: 180px; background: #FFF; border: 1px solid silver; z-index: 10000; *width: 180px; -webkit-box-shadow: 0 0 10px #cccccc; -moz-box-shadow: 0 0 10px #cccccc; box-shadow: 0 0 10px #cccccc; } .cms #vakata-contextmenu, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu { display: block; visibility: hidden; left: 0; top: -200px; position: absolute; margin: 0; padding: 0; min-width: 180px; background: #FFF; border: 1px solid silver; z-index: 10000; *width: 180px; -webkit-box-shadow: 0 0 10px #cccccc; -moz-box-shadow: 0 0 10px #cccccc; box-shadow: 0 0 10px #cccccc; }
@ -786,7 +788,7 @@ form.import-form label.left { width: 250px; }
.tree-holder.jstree-apple li, .cms-tree.jstree-apple li { padding: 0px; clear: left; } .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 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.Root > a .jstree-icon, .cms-tree.jstree-apple li.Root > a .jstree-icon { background-position: -56px -36px; }
.tree-holder.jstree-apple li.deletedonlive .text, .cms-tree.jstree-apple li.deletedonlive .text { text-decoration: line-through; } .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.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.readonly, .cms-tree.jstree-apple li.readonly { color: #aaaaaa; padding-left: 18px; } .tree-holder.jstree-apple li.readonly, .cms-tree.jstree-apple li.readonly { color: #aaaaaa; 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 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; }
@ -794,11 +796,11 @@ form.import-form label.left { width: 250px; }
.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 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 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; -webkit-border-radius: 2px 2px; -moz-border-radius: 2px / 2px; border-radius: 2px / 2px; } .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; -webkit-border-radius: 2px 2px; -moz-border-radius: 2px / 2px; border-radius: 2px / 2px; }
.tree-holder.jstree-apple span.badge.modified, .tree-holder.jstree-apple span.badge.addedtodraft, .cms-tree.jstree-apple span.badge.modified, .cms-tree.jstree-apple span.badge.addedtodraft { color: #7E7470; border: 1px solid #C9B800; background-color: #FFF0BC; } .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.deletedonlive, .tree-holder.jstree-apple span.badge.removedfromdraft, .cms-tree.jstree-apple span.badge.deletedonlive, .cms-tree.jstree-apple span.badge.removedfromdraft { color: #636363; border: 1px solid #E49393; background-color: #F2DADB; } .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.workflow-approval, .cms-tree.jstree-apple span.badge.workflow-approval { color: #56660C; border: 1px solid #7C8816; background-color: #DAE79A; } .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; -webkit-border-radius: 2px 2px; -moz-border-radius: 2px / 2px; border-radius: 2px / 2px; color: #7E7470; border: 1px solid #C9B800; background-color: #FFF0BC; } .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; -webkit-border-radius: 2px 2px; -moz-border-radius: 2px / 2px; border-radius: 2px / 2px; color: #7E7470; border: 1px solid #C9B800; background-color: #FFF0BC; }
.tree-holder.jstree-apple span.comment-count span.comment-count:before, .tree-holder.jstree-apple span.comment-count span.comment-count:after, .cms-tree.jstree-apple span.comment-count span.comment-count:before, .cms-tree.jstree-apple span.comment-count span.comment-count:after { content: ""; position: absolute; border-style: solid; /* reduce the damage in FF3.0 */ display: block; width: 0; } .tree-holder.jstree-apple span.comment-count span.comment-count:before, .tree-holder.jstree-apple span.comment-count .cms-tree.jstree-apple span.comment-count:before, .cms-tree.jstree-apple .tree-holder.jstree-apple span.comment-count span.comment-count:before, .tree-holder.jstree-apple span.comment-count span.comment-count:after, .tree-holder.jstree-apple span.comment-count .cms-tree.jstree-apple span.comment-count:after, .cms-tree.jstree-apple .tree-holder.jstree-apple span.comment-count span.comment-count:after, .cms-tree.jstree-apple span.comment-count .tree-holder.jstree-apple span.comment-count:before, .tree-holder.jstree-apple .cms-tree.jstree-apple span.comment-count span.comment-count:before, .cms-tree.jstree-apple span.comment-count span.comment-count:before, .cms-tree.jstree-apple span.comment-count .tree-holder.jstree-apple span.comment-count:after, .tree-holder.jstree-apple .cms-tree.jstree-apple span.comment-count span.comment-count:after, .cms-tree.jstree-apple span.comment-count span.comment-count:after { content: ""; position: absolute; border-style: solid; /* reduce the damage in FF3.0 */ display: block; width: 0; }
.tree-holder.jstree-apple span.comment-count:before, .cms-tree.jstree-apple span.comment-count:before { 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:before, .cms-tree.jstree-apple span.comment-count:before { 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 { 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 span.comment-count:after, .cms-tree.jstree-apple span.comment-count:after { 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-hovered, .cms-tree.jstree-apple .jstree-hovered { text-shadow: none; text-decoration: none; }
@ -830,7 +832,8 @@ 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-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 { 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-sf6890c994e.png') 0 -292px no-repeat; text-indent: -9999em; } .cms-login-status .logout-link { display: inline-block; height: 16px; width: 16px; float: left; margin: 0 8px 0 5px; background: url('../images/sprites-32x32-s972e408b05.png') 0 -1095px 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; -webkit-box-shadow: rgba(0, 0, 0, 0.9) 0 0 3px; -moz-box-shadow: rgba(0, 0, 0, 0.9) 0 0 3px; box-shadow: rgba(0, 0, 0, 0.9) 0 0 3px; } .cms-menu { z-index: 80; background: #b0bec7; width: 160px; -webkit-box-shadow: rgba(0, 0, 0, 0.9) 0 0 3px; -moz-box-shadow: rgba(0, 0, 0, 0.9) 0 0 3px; box-shadow: rgba(0, 0, 0, 0.9) 0 0 3px; }
.cms-menu a { text-decoration: none; } .cms-menu a { text-decoration: none; }
@ -854,12 +857,12 @@ li.class-ErrorPage > a .jstree-pageicon { background-position: 0 -112px; }
.cms-menu-list li a .icon { display: inline-block; float: left; margin: 4px 10px 0 4px; filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); opacity: 0.7; } .cms-menu-list li a .icon { display: inline-block; float: left; margin: 4px 10px 0 4px; filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); opacity: 0.7; }
.cms-menu-list li a .text { display: inline-block; float: left; } .cms-menu-list li a .text { display: inline-block; float: left; }
.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 { 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-sf6890c994e.png') 0 -375px no-repeat; vertical-align: middle; } .cms-menu-list li a .toggle-children .toggle-children-icon { display: inline-block; width: 8px; height: 8px; background: url('../images/sprites-32x32-s972e408b05.png') 0 -349px no-repeat; vertical-align: middle; }
.cms-menu-list li a .toggle-children.opened .toggle-children-icon { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -359px no-repeat; } .cms-menu-list li a .toggle-children.opened .toggle-children-icon { background: url('../images/sprites-32x32-s972e408b05.png') 0 -333px no-repeat; }
.cms-menu-list li ul li a { border-top: 1px solid #b6c3cb; } .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+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+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: -webkit-linear-gradient(#338dc1, #287099); background-image: -moz-linear-gradient(#338dc1, #287099); background-image: -o-linear-gradient(#338dc1, #287099); background-image: linear-gradient(#338dc1, #287099); } .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+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+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: -webkit-linear-gradient(#338dc1, #287099); background-image: -moz-linear-gradient(#338dc1, #287099); background-image: -o-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-sf6890c994e.png') 0 -433px no-repeat; } .cms-menu-list li.current a .toggle-children .toggle-children-icon { background: url('../images/sprites-32x32-s972e408b05.png') 0 -407px no-repeat; }
.cms-menu-list li.current a .toggle-children.opened .toggle-children-icon { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -1121px no-repeat; } .cms-menu-list li.current a .toggle-children.opened .toggle-children-icon { background: url('../images/sprites-32x32-s972e408b05.png') 0 -1121px no-repeat; }
.cms-menu-list li.current ul { border-top: none; display: block; } .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 { 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; } .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; }
@ -882,14 +885,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.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, .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-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-sf6890c994e.png') 0 -108px no-repeat; } .cms-content-controls .icon-auto:before { background: url('../images/sprites-32x32-s972e408b05.png') 0 -108px no-repeat; }
.cms-content-controls .icon-desktop:before { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -81px no-repeat; } .cms-content-controls .icon-desktop:before { background: url('../images/sprites-32x32-s972e408b05.png') 0 -81px no-repeat; }
.cms-content-controls .icon-tablet:before { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -162px no-repeat; } .cms-content-controls .icon-tablet:before { background: url('../images/sprites-32x32-s972e408b05.png') 0 -162px no-repeat; }
.cms-content-controls .icon-mobile:before { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -189px no-repeat; } .cms-content-controls .icon-mobile:before { background: url('../images/sprites-32x32-s972e408b05.png') 0 -189px no-repeat; }
.cms-content-controls .icon-split:before { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -135px no-repeat; } .cms-content-controls .icon-split:before { background: url('../images/sprites-32x32-s972e408b05.png') 0 -135px no-repeat; }
.cms-content-controls .icon-edit:before { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -27px no-repeat; } .cms-content-controls .icon-edit:before { background: url('../images/sprites-32x32-s972e408b05.png') 0 -27px no-repeat; }
.cms-content-controls .icon-preview:before { background: url('../images/sprites-32x32-sf6890c994e.png') 0 0 no-repeat; } .cms-content-controls .icon-preview:before { background: url('../images/sprites-32x32-s972e408b05.png') 0 0 no-repeat; }
.cms-content-controls .icon-window:before { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -54px no-repeat; } .cms-content-controls .icon-window:before { background: url('../images/sprites-32x32-s972e408b05.png') 0 -54px no-repeat; }
.cms-content-controls .cms-navigator { width: 100%; } .cms-content-controls .cms-navigator { width: 100%; }
.cms-content-controls .preview-selector.dropdown a.chzn-single { text-indent: -200px; } .cms-content-controls .preview-selector.dropdown a.chzn-single { text-indent: -200px; }
.cms-content-controls .preview-selector { float: right; border-bottom: none; position: relative; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; margin: 3px 0 0 4px; padding: 0; height: 28px; } .cms-content-controls .preview-selector { float: right; border-bottom: none; position: relative; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; margin: 3px 0 0 4px; padding: 0; height: 28px; }
@ -1036,10 +1039,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: white 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 { text-shadow: white 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 { -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; outline: none; } .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 { -webkit-box-shadow: none; -moz-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: white 0 10px 10px; color: #005b98; } .cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a:hover { text-shadow: white 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-sf6890c994e.png') 0 -1189px 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:after { background: url('../images/sprites-32x32-s972e408b05.png') 0 -1189px 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-sf6890c994e.png') 0 -1163px no-repeat; } .cms .ss-ui-action-tabset.action-menus.ss-tabset ul.ui-tabs-nav li a:hover:after { background: url('../images/sprites-32x32-s972e408b05.png') 0 -1163px 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-sf6890c994e.png') 0 -1215px 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-s972e408b05.png') 0 -1215px 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-sf6890c994e.png') 0 -1137px 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-s972e408b05.png') 0 -1137px 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 { 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, .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; } .cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-panel h3 { font-size: 13px; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

BIN
admin/images/sprites-32x32/logout.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 260 B

View File

@ -92,7 +92,8 @@
// Ensure the first validation error is visible // Ensure the first validation error is visible
var firstTabWithErrors = this.find('.message.validation:first').closest('.tab'); var firstTabWithErrors = this.find('.message.validation:first').closest('.tab');
$('.cms-container').clearCurrentTabState(); // clear state to avoid override later on $('.cms-container').clearCurrentTabState(); // clear state to avoid override later on
firstTabWithErrors.closest('.tabset').tabs('select', firstTabWithErrors.attr('id')); this.redraw();
firstTabWithErrors.closest('.cms-tabset').tabs('select', firstTabWithErrors.attr('id'));
} }
this._super(); this._super();

View File

@ -453,6 +453,9 @@
if(navi) navi.style.display = 'none'; if(navi) navi.style.display = 'none';
var naviMsg = doc.getElementById('SilverStripeNavigatorMessage'); var naviMsg = doc.getElementById('SilverStripeNavigatorMessage');
if(naviMsg) naviMsg.style.display = 'none'; if(naviMsg) naviMsg.style.display = 'none';
// Trigger extensions.
this.trigger('afterIframeAdjustedForPreview', [ doc ]);
} }
}); });

View File

@ -291,6 +291,10 @@
node.attr(attrName, newNode.attr(attrName)); node.attr(attrName, newNode.attr(attrName));
}); });
// To avoid conflicting classes when the node gets its content replaced (see below)
// Filter out all previous status flags if they are not in the class property of the new node
origClasses = origClasses.replace(/status-[^\s]*/, '');
// Replace inner content // Replace inner content
var origChildren = node.children('ul').detach(); var origChildren = node.children('ul').detach();
node.addClass(origClasses).html(newNode.html()).append(origChildren); node.addClass(origClasses).html(newNode.html()).append(origChildren);

View File

@ -821,15 +821,18 @@ jQuery.noConflict();
$('.cms-content .Actions').entwine({ $('.cms-content .Actions').entwine({
onmatch: function() { onmatch: function() {
this.find('.ss-ui-button').click(function() { this.find('.ss-ui-button').click(function() {
var form = this.form; var form = this.form;
// forms don't natively store the button they've been triggered with
if(form) { // forms don't natively store the button they've been triggered with
form.clickedButton = this; if(form) {
// Reset the clicked button shortly after the onsubmit handlers form.clickedButton = this;
// have fired on the form // Reset the clicked button shortly after the onsubmit handlers
setTimeout(function() {form.clickedButton = null;}, 10); // have fired on the form
} setTimeout(function() {
}); form.clickedButton = null;
}, 10);
}
});
this.redraw(); this.redraw();
this._super(); this._super();
@ -942,34 +945,46 @@ jQuery.noConflict();
* Generic search form in the CMS, often hooked up to a GridField results display. * Generic search form in the CMS, often hooked up to a GridField results display.
*/ */
$('.cms-search-form').entwine({ $('.cms-search-form').entwine({
onsubmit: function(e) {
onsubmit: function() {
// Remove empty elements and make the URL prettier // Remove empty elements and make the URL prettier
var nonEmptyInputs = this.find(':input:not(:submit)').filter(function() { var nonEmptyInputs,
url;
nonEmptyInputs = this.find(':input:not(:submit)').filter(function() {
// Use fieldValue() from jQuery.form plugin rather than jQuery.val(), // Use fieldValue() from jQuery.form plugin rather than jQuery.val(),
// as it handles checkbox values more consistently // as it handles checkbox values more consistently
var vals = $.grep($(this).fieldValue(), function(val) { return (val);}); var vals = $.grep($(this).fieldValue(), function(val) { return (val);});
return (vals.length); return (vals.length);
}); });
var url = this.attr('action');
if(nonEmptyInputs.length) url = $.path.addSearchParams(url, nonEmptyInputs.serialize()); url = this.attr('action');
if(nonEmptyInputs.length) {
url = $.path.addSearchParams(url, nonEmptyInputs.serialize());
}
var container = this.closest('.cms-container'); var container = this.closest('.cms-container');
container.find('.cms-edit-form').tabs('select',0); //always switch to the first tab (list view) when searching container.find('.cms-edit-form').tabs('select',0); //always switch to the first tab (list view) when searching
container.loadPanel(url); container.loadPanel(url, "", {}, true);
return false; return false;
},
/**
* Resets are processed on the serverside, so need to trigger a submit.
*/
onreset: function(e) {
this.clearForm();
this.submit();
} }
}); });
/**
* Reset button handler. IE8 does not bubble reset events to
*/
$(".cms-search-form button[type=reset]").entwine({
onclick: function(e) {
e.preventDefault();
var form = $(this).parents('form');
form.clearForm();
form.submit();
}
})
/** /**
* Allows to lazy load a panel, by leaving it empty * Allows to lazy load a panel, by leaving it empty
* and declaring a URL to load its content via a 'url' HTML5 data attribute. * and declaring a URL to load its content via a 'url' HTML5 data attribute.

View File

@ -79,6 +79,10 @@
margin: 0 8px 0 5px; margin: 0 8px 0 5px;
background: sprite($sprites32, logout) no-repeat; background: sprite($sprites32, logout) no-repeat;
text-indent: -9999em; text-indent: -9999em;
opacity:0.9;
&:hover, &:focus{
opacity:1;
}
} }
} }

View File

@ -1354,7 +1354,7 @@ body.cms-dialog {
} }
} }
button.add-url{ button.add-url{
margin-top:13px; margin-top:5px;
padding-top:15px; padding-top:15px;
@include clearfix; @include clearfix;
border:none; border:none;
@ -1365,6 +1365,12 @@ body.cms-dialog {
width:20px; width:20px;
height:20px; height:20px;
} }
.ui-button-text{
margin-left:10px;
margin-top:-5px;
line-height:20px;
float:left;
}
&:hover, &:active{ &:hover, &:active{
border:none; border:none;
@include box-shadow-none; @include box-shadow-none;
@ -1419,6 +1425,14 @@ body.cms-dialog {
} }
} }
} }
.CompositeField{
.text{
select{
margin: 5px 0 0 0;
}
}
}
} }
.htmleditorfield-linkform { .htmleditorfield-linkform {

View File

@ -408,7 +408,7 @@
background-position: -56px -36px; background-position: -56px -36px;
} }
} }
&.deletedonlive { &.status-deletedonlive {
.text { .text {
text-decoration: line-through; text-decoration: line-through;
} }
@ -455,17 +455,17 @@
margin-top: -1px; margin-top: -1px;
@include border-radius(2px, 2px); @include border-radius(2px, 2px);
&.modified, &.addedtodraft { &.status-modified, &.status-addedtodraft {
color: #7E7470; color: #7E7470;
border: 1px solid #C9B800; border: 1px solid #C9B800;
background-color: #FFF0BC; background-color: #FFF0BC;
} }
&.deletedonlive, &.removedfromdraft { &.status-deletedonlive, &.status-removedfromdraft {
color: #636363; color: #636363;
border: 1px solid #E49393; border: 1px solid #E49393;
background-color: #F2DADB; background-color: #F2DADB;
} }
&.workflow-approval { &.status-workflow-approval {
color: #56660C; color: #56660C;
border: 1px solid #7C8816; border: 1px solid #7C8816;
background-color: #DAE79A; background-color: #DAE79A;

View File

@ -68,10 +68,43 @@ global $databaseConfig;
// We don't have a session in cli-script, but this prevents errors // We don't have a session in cli-script, but this prevents errors
$_SESSION = null; $_SESSION = null;
// Connect to database
require_once("model/DB.php"); require_once("model/DB.php");
// Connect to database
if(!isset($databaseConfig) || !isset($databaseConfig['database']) || !$databaseConfig['database']) {
echo "\nPlease configure your database connection details. You can do this by creating a file
called _ss_environment.php in either of the following locations:\n\n";
echo " - " . BASE_PATH ."_ss_environment.php\n - " . dirname(BASE_PATH) . "_ss_environment.php\n\n";
echo <<<ENVCONTENT
Put the following content into this file:
--------------------------------------------------
<?php
/* Change this from 'dev' to 'live' for a production environment. */
define('SS_ENVIRONMENT_TYPE', 'dev');
/* This defines a default database user */
define('SS_DATABASE_SERVER', 'localhost');
define('SS_DATABASE_USERNAME', '<user>');
define('SS_DATABASE_PASSWORD', '<password>');
define('SS_DATABASE_NAME', '<database>');
--------------------------------------------------
Once you have done that, run 'composer install' or './framework/sake dev/build' to create
an empty database.
For more information, please read this page in our docs:
http://doc.silverstripe.org/framework/en/topics/environment-management
ENVCONTENT;
exit(1);
}
DB::connect($databaseConfig); DB::connect($databaseConfig);
// Get the request URL from the querystring arguments // Get the request URL from the querystring arguments
$url = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : null; $url = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : null;
if(!$url) { if(!$url) {

View File

@ -451,6 +451,9 @@ class SS_HTTPRequest implements ArrayAccess {
$shiftCount = sizeof($patternParts); $shiftCount = sizeof($patternParts);
} }
// Filter out any "empty" matching parts - either from an initial / or a trailing /
$patternParts = array_values(array_filter($patternParts));
$arguments = array(); $arguments = array();
foreach($patternParts as $i => $part) { foreach($patternParts as $i => $part) {
$part = trim($part); $part = trim($part);

View File

@ -137,6 +137,24 @@ class Session {
if($data instanceof Session) $data = $data->inst_getAll(); if($data instanceof Session) $data = $data->inst_getAll();
$this->data = $data; $this->data = $data;
if (isset($_SERVER['HTTP_USER_AGENT'])) {
$ua = $_SERVER['HTTP_USER_AGENT'];
} else {
$ua = '';
}
if (isset($this->data['HTTP_USER_AGENT'])) {
if ($this->data['HTTP_USER_AGENT'] != $ua) {
// Funny business detected!
$this->inst_clearAll();
Session::destroy();
Session::start();
}
}
$this->inst_set('HTTP_USER_AGENT', $ua);
} }
/** /**
@ -517,6 +535,13 @@ class Session {
// There's nothing we can do about this, because it's an operating system function! // There's nothing we can do about this, because it's an operating system function!
if($sid) session_id($sid); if($sid) session_id($sid);
@session_start(); @session_start();
}
// Modify the timeout behaviour so it's the *inactive* time before the session expires.
// By default it's the total session lifetime
if($timeout && !headers_sent()) {
setcookie(session_name(), session_id(), time()+$timeout, $path, $domain ? $domain : null, $secure, true);
} }
} }
@ -528,21 +553,26 @@ class Session {
public static function destroy($removeCookie = true) { public static function destroy($removeCookie = true) {
if(session_id()) { if(session_id()) {
if($removeCookie) { if($removeCookie) {
$path = Config::inst()->get('cookie_path'); $path = Config::inst()->get('Session', 'cookie_path');
if(!$path) $path = Director::baseURL(); if(!$path) $path = Director::baseURL();
$domain = Config::inst()->get('cookie_domain'); $domain = Config::inst()->get('Session', 'cookie_domain');
$secure = Config::inst()->get('cookie_secure'); $secure = Config::inst()->get('Session', 'cookie_secure');
if($domain) { if($domain) {
setcookie(session_name(), '', null, $path, $domain, $secure, true); setcookie(session_name(), '', null, $path, $domain, $secure, true);
} }
else { else {
setcookie(session_name(), '', null, $path, null, $secure, true); setcookie(session_name(), '', null, $path, null, $secure, true);
} }
unset($_COOKIE[session_name()]); unset($_COOKIE[session_name()]);
} }
session_destroy(); session_destroy();
// Clean up the superglobal - session_destroy does not do it.
// http://nz1.php.net/manual/en/function.session-destroy.php
unset($_SESSION);
} }
} }

View File

@ -142,6 +142,9 @@ if(!isset($_SERVER['HTTP_HOST'])) {
if($_GET) stripslashes_recursively($_GET); if($_GET) stripslashes_recursively($_GET);
if($_POST) stripslashes_recursively($_POST); if($_POST) stripslashes_recursively($_POST);
if($_COOKIE) stripslashes_recursively($_COOKIE); if($_COOKIE) stripslashes_recursively($_COOKIE);
// No more magic_quotes!
trigger_error('get_magic_quotes_gpc support is being removed from Silverstripe. Please set this to off in ' .
' your php.ini and see http://php.net/manual/en/security.magicquotes.php', E_USER_WARNING);
} }
/** /**

View File

@ -746,8 +746,8 @@ abstract class Object {
} }
} else { } else {
// Please do not change the exception code number below. // Please do not change the exception code number below.
$class = get_class($this);
throw new Exception("Object->__call(): the method '$method' does not exist on '$this->class'", 2175); throw new Exception("Object->__call(): the method '$method' does not exist on '$class'", 2175);
} }
} }

View File

@ -52,9 +52,9 @@ body.cms.ss-uploadfield-edit-iframe .fieldholder-small label, .composite.ss-asse
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions { position: absolute; top: 0; right: 0; left: 0; z-index: 0; color: #f00; font-size: 14px; } .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions { position: absolute; top: 0; right: 0; left: 0; z-index: 0; color: #f00; font-size: 14px; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button { background: none; border: 0; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; text-shadow: none; color: white; float: right; } .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button { background: none; border: 0; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; text-shadow: none; color: white; float: right; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-delete { display: none; } .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-delete { display: none; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel { -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; border-left: 1px solid rgba(255, 255, 255, 0.2); margin-top: 0px; cursor: pointer; opacity: 0.9; } .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel, .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-overwrite-warning { -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; border-left: 1px solid rgba(255, 255, 255, 0.2); margin-top: 0px; cursor: pointer; opacity: 0.9; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel:hover { opacity: 1; } .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel:hover, .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-overwrite-warning:hover { opacity: 1; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel .ui-icon { display: block; margin: 0; position: realtive; top: 8px; } .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-cancel .ui-icon, .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-overwrite-warning .ui-icon { display: block; margin: 0; position: realtive; top: 8px; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit { opacity: 0.9; padding-top: 1px; padding-bottom: 0; height: 100%; -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; } .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit { opacity: 0.9; padding-top: 1px; padding-bottom: 0; height: 100%; -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit.ui-state-hover { background: none; opacity: 1; } .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit.ui-state-hover { background: none; opacity: 1; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit.ui-state-hover span.toggle-details { opacity: 1; } .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-actions .ss-ui-button.ss-uploadfield-item-edit.ui-state-hover span.toggle-details { opacity: 1; }
@ -68,17 +68,17 @@ body.cms.ss-uploadfield-edit-iframe .fieldholder-small label, .composite.ss-asse
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-progress .ss-uploadfield-item-progressbarvalue { width: 0; background: #60b3dd url(../images/progressbar_blue.gif) repeat left center; } .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-progress .ss-uploadfield-item-progressbarvalue { width: 0; background: #60b3dd url(../images/progressbar_blue.gif) repeat left center; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-editform { /* don't use display none, for it will break jQuery('iframe').contents().height() */ height: 0; overflow: hidden; clear: both; } .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-editform { /* don't use display none, for it will break jQuery('iframe').contents().height() */ height: 0; overflow: hidden; clear: both; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-editform iframe { width: 100%; } .ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-editform iframe { width: 100%; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info { float: left; margin: 10px 0 0; } .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info { float: left; margin: 25px 0 0; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info { margin: 10px 0px 0 20px; } .ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info { margin: 10px 0px 0 20px; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info label { font-size: 18px; line-height: 30px; padding: 8px 16px; margin-right: 0px; } .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-info label { font-size: 18px; line-height: 30px; padding: 0px 16px; margin-right: 0px; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-fromcomputer { /*position: relative; */ overflow: hidden; display: block; } .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-fromcomputer { /*position: relative; */ overflow: hidden; display: block; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-fromcomputer .btn-icon-drive-upload-large { background: url(../images/drive-upload-large.png) no-repeat 0px -4px; width: 32px; height: 32px; margin-top: -12px; } .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-fromcomputer .btn-icon-drive-upload-large { background: url(../images/drive-upload-white.png) no-repeat 0px 0px; width: 32px; height: 32px; margin-top: -12px; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-uploador { float: left; font-weight: bold; font-size: 22px; padding: 0 20px; line-height: 70px; margin-top: 4px; display: none; } .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-uploador { float: left; font-weight: bold; font-size: 22px; padding: 0 20px; line-height: 70px; margin-top: 10px; display: none; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-uploador { font-size: 18px; margin-top: 0; } .ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-uploador { font-size: 18px; margin-top: -5px; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone { margin-top: 9px; -webkit-border-radius: 13px; -moz-border-radius: 13px; -ms-border-radius: 13px; -o-border-radius: 13px; border-radius: 13px; -webkit-box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; -moz-box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; border: 2px dashed gray; background: #d4dbe0; display: none; height: 54px; width: 360px; float: left; text-align: left; } .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone { margin-top: 9px; padding: 8px 0; -webkit-border-radius: 13px; -moz-border-radius: 13px; -ms-border-radius: 13px; -o-border-radius: 13px; border-radius: 13px; -webkit-box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; -moz-box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; box-shadow: rgba(128, 128, 128, 0.4) 0 0 4px 0 inset, 0 1px 0 #fafafa; border: 2px dashed gray; background: #d4dbe0; display: none; height: 54px; min-width: 280px; float: left; text-align: center; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone.active.hover { -webkit-box-shadow: rgba(255, 255, 255, 0.6) 0 0 3px 2px inset; -moz-box-shadow: rgba(255, 255, 255, 0.6) 0 0 3px 2px inset; box-shadow: rgba(255, 255, 255, 0.6) 0 0 3px 2px inset; } .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone.active.hover { -webkit-box-shadow: rgba(255, 255, 255, 0.6) 0 0 3px 2px inset; -moz-box-shadow: rgba(255, 255, 255, 0.6) 0 0 3px 2px inset; box-shadow: rgba(255, 255, 255, 0.6) 0 0 3px 2px inset; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div { color: #5e5e5e; text-shadow: 0px 1px 0px #fff; background: url("../images/upload.png") 0 10px no-repeat; z-index: 1; padding: 6px 48px 0; line-height: 25px; font-size: 20px; font-weight: bold; display: inline-block; margin-left: 83px; } .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div { color: #5e5e5e; text-shadow: 0px 1px 0px #fff; background: url("../images/upload.png") 0 10px no-repeat; z-index: 1; padding: 6px 48px 0; line-height: 25px; font-size: 20px; font-weight: bold; display: inline-block; }
.ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div span { display: block; font-size: 12px; z-index: -1; margin-top: -3px; } .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div span { display: block; font-size: 12px; z-index: -1; margin-top: -3px; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone { height: 54px; width: 277px; overflow: hidden; } .ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone { height: 54px; min-width: 250px; overflow: hidden; padding: 0; margin-top: 2px; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div { background-position: 0 11px; padding-top: 21px; margin-left: 33px; } .ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div { background-position: 0 11px; padding-top: 21px; margin-left: 33px; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div span { height: 38px; font-size: 18px; line-height: 18px; } .ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-dropzone div span { height: 38px; font-size: 18px; line-height: 18px; }

View File

@ -44,11 +44,11 @@ Used in side panels and action tabs
.cms table.ss-gridfield-table tbody td.col-listChildrenLink .list-children-link { background: transparent url(../images/sitetree_ss_default_icons.png) no-repeat 3px -4px; display: block; } .cms table.ss-gridfield-table tbody td.col-listChildrenLink .list-children-link { background: transparent url(../images/sitetree_ss_default_icons.png) no-repeat 3px -4px; display: block; }
.cms table.ss-gridfield-table tbody td.col-getTreeTitle span.item { color: #0073c1; } .cms table.ss-gridfield-table tbody td.col-getTreeTitle span.item { color: #0073c1; }
.cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge { clear: both; text-transform: uppercase; display: inline-block; padding: 0px 3px; font-size: 0.75em; line-height: 1em; margin-left: 10px; margin-right: 6px; margin-top: -1px; -webkit-border-radius: 2px 2px; -moz-border-radius: 2px / 2px; border-radius: 2px / 2px; } .cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge { clear: both; text-transform: uppercase; display: inline-block; padding: 0px 3px; font-size: 0.75em; line-height: 1em; margin-left: 10px; margin-right: 6px; margin-top: -1px; -webkit-border-radius: 2px 2px; -moz-border-radius: 2px / 2px; border-radius: 2px / 2px; }
.cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge.modified { color: #7E7470; border: 1px solid #C9B800; background-color: #FFF0BC; } .cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge.status-modified { color: #7E7470; border: 1px solid #C9B800; background-color: #FFF0BC; }
.cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge.addedtodraft { color: #7E7470; border: 1px solid #C9B800; background-color: #FFF0BC; } .cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge.status-addedtodraft { color: #7E7470; border: 1px solid #C9B800; background-color: #FFF0BC; }
.cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge.deletedonlive { color: #636363; border: 1px solid #E49393; background-color: #F2DADB; } .cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge.status-deletedonlive { color: #636363; border: 1px solid #E49393; background-color: #F2DADB; }
.cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge.removedfromdraft { color: #636363; border: 1px solid #E49393; background-color: #F2DADB; } .cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge.status-removedfromdraft { color: #636363; border: 1px solid #E49393; background-color: #F2DADB; }
.cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge.workflow-approval { color: #56660C; border: 1px solid #7C8816; background-color: #DAE79A; } .cms table.ss-gridfield-table tbody td.col-getTreeTitle span.badge.status-workflow-approval { color: #56660C; border: 1px solid #7C8816; background-color: #DAE79A; }
.cms table.ss-gridfield-table tbody td button { border: none; background: none; margin: 0 0 0 2px; padding: 1px 0; width: auto; text-shadow: none; } .cms table.ss-gridfield-table tbody td button { border: none; background: none; margin: 0 0 0 2px; padding: 1px 0; width: auto; text-shadow: none; }
.cms table.ss-gridfield-table tbody td button.ui-state-hover { background: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } .cms table.ss-gridfield-table tbody td button.ui-state-hover { background: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; }
.cms table.ss-gridfield-table tbody td button.ui-state-active { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } .cms table.ss-gridfield-table tbody td button.ui-state-active { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; }

View File

@ -1,4 +1,4 @@
div.TreeDropdownField { width: 400px; background: #fff; border: 1px solid #aaa; cursor: pointer; overflow: hidden; } div.TreeDropdownField { width: 400px; background: #fff; border: 1px solid #aaa; cursor: pointer; overflow: visible; position: relative; }
div.TreeDropdownField input { border: none; background: none; padding: 0; margin: 0; } div.TreeDropdownField input { border: none; background: none; padding: 0; margin: 0; }
div.TreeDropdownField .treedropdownfield-title { float: left; padding: 7px; width: 90%; line-height: 16px; overflow: hidden; outline: none; } div.TreeDropdownField .treedropdownfield-title { float: left; padding: 7px; width: 90%; line-height: 16px; overflow: hidden; outline: none; }
div.TreeDropdownField .treedropdownfield-panel { clear: left; position: absolute; overflow: auto; display: none; cursor: default; border: 1px solid #aaa; border-top: none; margin: 1px 0 0 -1px; /* account for border on container div */ max-height: 200px; background-color: #fff; z-index: 50; -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); -o-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); } div.TreeDropdownField .treedropdownfield-panel { clear: left; position: absolute; overflow: auto; display: none; cursor: default; border: 1px solid #aaa; border-top: none; margin: 1px 0 0 -1px; /* account for border on container div */ max-height: 200px; background-color: #fff; z-index: 50; -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); -o-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); }

View File

@ -13,30 +13,31 @@ Used in side panels and action tabs
.ss-uploadfield .clear { clear: both; } .ss-uploadfield .clear { clear: both; }
.ss-insert-media .ss-uploadfield { margin-top: 20px; } .ss-insert-media .ss-uploadfield { margin-top: 20px; }
.ss-insert-media .ss-uploadfield h4 { float: left; } .ss-insert-media .ss-uploadfield h4 { float: left; }
.ss-uploadfield .middleColumn { width: 526px; padding: 0; background: #fff; border: 1px solid #b3b3b3; -webkit-border-radius: 4px; -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; 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: -webkit-linear-gradient(#efefef, #ffffff 10%, #ffffff 90%, #efefef); background-image: -moz-linear-gradient(#efefef, #ffffff 10%, #ffffff 90%, #efefef); background-image: -o-linear-gradient(#efefef, #ffffff 10%, #ffffff 90%, #efefef); background-image: linear-gradient(#efefef, #ffffff 10%, #ffffff 90%, #efefef); } .ss-insert-media .ss-uploadfield.from-CMS .nolabel.treedropdown .middleColumn { margin-left: 184px; }
.ss-uploadfield .middleColumn { width: 510px; padding: 0; background: #fff; border: 1px solid #b3b3b3; -webkit-border-radius: 4px; -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; 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: -webkit-linear-gradient(#efefef, #ffffff 10%, #ffffff 90%, #efefef); background-image: -moz-linear-gradient(#efefef, #ffffff 10%, #ffffff 90%, #efefef); background-image: -o-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 { 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; } .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; }
.ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-preview.ss-uploadfield-dropzone { -webkit-box-shadow: gray 0 0 4px 0 inset; -moz-box-shadow: gray 0 0 4px 0 inset; box-shadow: gray 0 0 4px 0 inset; border: 2px dashed gray; background: #d0d3d5; display: none; margin-right: 15px; } .ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-preview.ss-uploadfield-dropzone { -webkit-box-shadow: gray 0 0 4px 0 inset; -moz-box-shadow: gray 0 0 4px 0 inset; box-shadow: gray 0 0 4px 0 inset; border: 2px dashed gray; background: #d0d3d5; display: none; margin-right: 15px; }
.ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info { float: left; margin-left: 15px; } .ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info { margin-left: 95px; }
.ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info .ss-uploadfield-item-name { display: block; line-height: 13px; height: 26px; margin: 0; text-align: left; } .ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info .ss-uploadfield-item-name { display: block; line-height: 13px; height: 26px; margin: 0; text-align: left; }
.ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info .ss-uploadfield-item-name b { font-weight: bold; padding: 0 5px 0 0; } .ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info .ss-uploadfield-item-name .name { max-width: 240px; font-weight: bold; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; display: inline; float: left; }
.ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info .ss-uploadfield-item-name .name { font-size: 11px; color: #848484; width: 290px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; display: inline; float: left; } .ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info .ss-uploadfield-item-name .size { color: #848484; padding: 0 0 0 5px; display: inline; float: left; }
.ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info .ss-uploadfield-item-name .ss-uploadfield-item-status { float: right; padding: 0 0 0 5px; width: 100px; text-align: right; } .ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info .ss-uploadfield-item-name .ss-uploadfield-item-status { float: right; padding: 0 0 0 5px; text-align: right; max-width: 75%; }
.ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info .ss-uploadfield-item-name .ss-uploadfield-item-status.ui-state-error-text { color: red; font-weight: bold; } .ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info .ss-uploadfield-item-name .ss-uploadfield-item-status.ui-state-error-text { color: red; font-weight: bold; width: 150px; }
.ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info .ss-uploadfield-item-name .ss-uploadfield-item-status.ui-state-warning-text { color: #b7a403; } .ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info .ss-uploadfield-item-name .ss-uploadfield-item-status.ui-state-warning-text { color: #b7a403; }
.ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info .ss-uploadfield-item-name .ss-uploadfield-item-status.ui-state-success-text { color: #1f9433; } .ss-uploadfield .ss-uploadfield-item .ss-uploadfield-item-info .ss-uploadfield-item-name .ss-uploadfield-item-status.ui-state-success-text { color: #1f9433; }
.ss-uploadfield .ss-ui-button { display: block; float: left; margin: 0 10px 0 0; } .ss-uploadfield .ss-ui-button { display: block; float: left; margin: 0 10px 6px 0; }
.ss-uploadfield .ss-ui-button.ss-uploadfield-fromcomputer { position: relative; overflow: hidden; } .ss-uploadfield .ss-ui-button.ss-uploadfield-fromcomputer { position: relative; overflow: hidden; }
.ss-uploadfield .ss-uploadfield-files { margin: 0; padding: 0; overflow: auto; position: relative; } .ss-uploadfield .ss-uploadfield-files { margin: 0; padding: 0; overflow: auto; position: relative; }
.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item.ui-state-error { border: 0; border-bottom: 1px solid #b3b3b3; background: none; color: #444444; } .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item.ui-state-error { border: 0; border-bottom: 1px solid #b3b3b3; background: none; color: #444444; }
.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item:last-child, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item.ui-state-error:last-child { border-bottom: 0; } .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item:last-child, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item.ui-state-error:last-child { border-bottom: 0; }
.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-actions { height: 28px; margin: 6px 0 0; position: relative; } .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-actions { min-height: 28px; overflow: hidden; margin: 6px 0 -6px 0; position: relative; }
.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-progress { position: absolute; left: 0; right: 42px; width: auto; margin: 11px 0 0; height: 15px; } .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-progress { position: absolute; left: 0; right: 42px; width: auto; margin: 11px 0 0; height: 15px; }
.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-progress div { -webkit-border-radius: 25px; -moz-border-radius: 25px; -ms-border-radius: 25px; -o-border-radius: 25px; border-radius: 25px; height: 13px; padding: 0; margin: 0; overflow: hidden; } .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-progress div { -webkit-border-radius: 25px; -moz-border-radius: 25px; -ms-border-radius: 25px; -o-border-radius: 25px; border-radius: 25px; height: 13px; padding: 0; margin: 0; overflow: hidden; }
.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-progressbar { border: 1px solid gray; background-color: #92a6b3; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #92a6b3), color-stop(11%, #90aab8), color-stop(22%, #96b1bf), color-stop(33%, #9eb4c1), color-stop(44%, #a7bac7), color-stop(100%, #c1d5dc)); background-image: -webkit-linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); background-image: -moz-linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); background-image: -o-linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); background-image: linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); } .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-progressbar { border: 1px solid gray; background-color: #92a6b3; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #92a6b3), color-stop(11%, #90aab8), color-stop(22%, #96b1bf), color-stop(33%, #9eb4c1), color-stop(44%, #a7bac7), color-stop(100%, #c1d5dc)); background-image: -webkit-linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); background-image: -moz-linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); background-image: -o-linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); background-image: linear-gradient(top, #92a6b3 0%, #90aab8 11%, #96b1bf 22%, #9eb4c1 33%, #a7bac7 44%, #c1d5dc 100%); }
.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-progressbarvalue { border: 0; width: 0%; background: #60b3dd url(../images/progressbar_blue.gif) repeat-x left center; } .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-progressbarvalue { border: 0; width: 0%; background: #60b3dd url(../images/progressbar_blue.gif) repeat-x left center; }
.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-cancel, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-start { position: absolute; top: 10px; right: 0; } .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-cancel, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-start { position: absolute; top: 10px; right: 0; }
.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-cancel button, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-start button { display: block; overflow: hidden; text-indent: -9999px; padding: 0; margin: 0; border: 0; width: 16px; height: 16px; cursor: pointer; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; background: none; position: relative; } .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-cancel button, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-start button { display: block; overflow: hidden; text-indent: -9999px; padding: 0; margin: 0; border: 0; width: 16px; height: 16px; cursor: pointer; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; position: relative; }
.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-cancel button span, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-start button span { position: absolute; left: 0; top: 0; margin: 0; } .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-cancel button span, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-start button span { position: absolute; left: 0; top: 0; margin: 0; }
.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-cancel button span.ui-button-text, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-start button span.ui-button-text { display: none; } .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-cancel button span.ui-button-text, .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-start button span.ui-button-text { display: none; }
.ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-start { right: 20px; } .ss-uploadfield .ss-uploadfield-files .ss-uploadfield-item-start { right: 20px; }

View File

@ -860,6 +860,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
} }
$this->cache_generatedMembers[$permCode]->logIn(); $this->cache_generatedMembers[$permCode]->logIn();
return $this->cache_generatedMembers[$permCode]->ID;
} }
/** /**

View File

@ -92,6 +92,10 @@ class TestRunner extends Controller {
Config::inst()->pushConfigStaticManifest(new SS_ConfigStaticManifest( Config::inst()->pushConfigStaticManifest(new SS_ConfigStaticManifest(
BASE_PATH, true, isset($_GET['flush']) BASE_PATH, true, isset($_GET['flush'])
)); ));
// Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
// (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
DataObject::clear_classname_spec_cache();
} }
public function init() { public function init() {

View File

@ -31,6 +31,8 @@ if (function_exists('session_start')) {
/** /**
* Include _ss_environment.php file * Include _ss_environment.php file
*/ */
$usingEnv = false;
$envFileExists = false;
//define the name of the environment file //define the name of the environment file
$envFile = '_ss_environment.php'; $envFile = '_ss_environment.php';
//define the dirs to start scanning from (have to add the trailing slash) //define the dirs to start scanning from (have to add the trailing slash)
@ -53,8 +55,10 @@ foreach ($dirsToCheck as $dir) {
if (@is_readable($dir)) { if (@is_readable($dir)) {
//if the file exists, then we include it, set relevant vars and break out //if the file exists, then we include it, set relevant vars and break out
if (file_exists($dir . $envFile)) { if (file_exists($dir . $envFile)) {
define('SS_ENVIRONMENT_FILE', $dir . $envFile); include_once($dir . $envFile);
include_once(SS_ENVIRONMENT_FILE); $envFileExists = true;
//legacy variable assignment
$usingEnv = true;
//break out of BOTH loops because we found the $envFile //break out of BOTH loops because we found the $envFile
break(2); break(2);
} }
@ -418,6 +422,7 @@ class InstallRequirements {
$this->requireModule(FRAMEWORK_NAME, array("File permissions", FRAMEWORK_NAME . "/ directory exists?")); $this->requireModule(FRAMEWORK_NAME, array("File permissions", FRAMEWORK_NAME . "/ directory exists?"));
if($isApache) { if($isApache) {
$this->checkApacheVersion(array("Webserver Configuration", "Webserver is not Apache 1.x", "SilverStripe requires Apache version 2 or greater", $webserver));
$this->requireWriteable('.htaccess', array("File permissions", "Is the .htaccess file writeable?", null)); $this->requireWriteable('.htaccess', array("File permissions", "Is the .htaccess file writeable?", null));
} elseif($isIIS) { } elseif($isIIS) {
$this->requireWriteable('web.config', array("File permissions", "Is the web.config file writeable?", null)); $this->requireWriteable('web.config', array("File permissions", "Is the web.config file writeable?", null));
@ -493,9 +498,9 @@ class InstallRequirements {
$this->suggestClass('tidy', array('PHP Configuration', 'tidy support', 'Tidy provides a library of code to clean up your html. SilverStripe will operate fine without tidy but HTMLCleaner will not be effective.')); $this->suggestClass('tidy', array('PHP Configuration', 'tidy support', 'Tidy provides a library of code to clean up your html. SilverStripe will operate fine without tidy but HTMLCleaner will not be effective.'));
$this->suggestPHPSetting('asp_tags', array(false,0,''), array('PHP Configuration', 'asp_tags option', 'This should be turned off as it can cause issues with SilverStripe')); $this->suggestPHPSetting('asp_tags', array(false), array('PHP Configuration', 'asp_tags option', 'This should be turned off as it can cause issues with SilverStripe'));
$this->suggestPHPSetting('magic_quotes_gpc', array(false,0,''), array('PHP Configuration', 'magic_quotes_gpc option', 'This should be turned off, as it can cause issues with cookies. More specifically, unserializing data stored in cookies.')); $this->requirePHPSetting('magic_quotes_gpc', array(false), array('PHP Configuration', 'magic_quotes_gpc option', 'This should be turned off, as it can cause issues with cookies. More specifically, unserializing data stored in cookies.'));
$this->suggestPHPSetting('display_errors', array(false,0,''), array('PHP Configuration', 'display_errors option', 'Unless you\'re in a development environment, this should be turned off, as it can expose sensitive data to website users.')); $this->suggestPHPSetting('display_errors', array(false), array('PHP Configuration', 'display_errors option', 'Unless you\'re in a development environment, this should be turned off, as it can expose sensitive data to website users.'));
// Check memory allocation // Check memory allocation
$this->requireMemory(32*1024*1024, 64*1024*1024, array("PHP Configuration", "Memory allocation (PHP config option 'memory_limit')", "SilverStripe needs a minimum of 32M allocated to PHP, but recommends 64M.", ini_get("memory_limit"))); $this->requireMemory(32*1024*1024, 64*1024*1024, array("PHP Configuration", "Memory allocation (PHP config option 'memory_limit')", "SilverStripe needs a minimum of 32M allocated to PHP, but recommends 64M.", ini_get("memory_limit")));
@ -513,6 +518,16 @@ class InstallRequirements {
} }
} }
function requirePHPSetting($settingName, $settingValues, $testDetails) {
$this->testing($testDetails);
$val = ini_get($settingName);
if(!in_array($val, $settingValues) && $val != $settingValues) {
$testDetails[2] = "$settingName is set to '$val' in php.ini. $testDetails[2]";
$this->error($testDetails);
}
}
function suggestClass($class, $testDetails) { function suggestClass($class, $testDetails) {
$this->testing($testDetails); $this->testing($testDetails);
@ -673,6 +688,17 @@ class InstallRequirements {
else return true; else return true;
} }
function checkApacheVersion($testDetails) {
$this->testing($testDetails);
$is1pointx = preg_match('#Apache[/ ]1\.#', $testDetails[3]);
if($is1pointx) {
$this->error($testDetails);
}
return true;
}
function requirePHPVersion($recommendedVersion, $requiredVersion, $testDetails) { function requirePHPVersion($recommendedVersion, $requiredVersion, $testDetails) {
$this->testing($testDetails); $this->testing($testDetails);
@ -1350,16 +1376,15 @@ HTML;
ErrorDocument 404 /assets/error-404.html ErrorDocument 404 /assets/error-404.html
ErrorDocument 500 /assets/error-500.html ErrorDocument 500 /assets/error-500.html
<IfModule mod_alias.c>
RedirectMatch 403 /silverstripe-cache(/|$)
RedirectMatch 403 /vendor(/|$)
RedirectMatch 403 /composer\.(json|lock)
</IfModule>
<IfModule mod_rewrite.c> <IfModule mod_rewrite.c>
SetEnv HTTP_MOD_REWRITE On SetEnv HTTP_MOD_REWRITE On
RewriteEngine On RewriteEngine On
$baseClause $baseClause
RewriteRule ^vendor(/|$) - [F,L,NC]
RewriteRule silverstripe-cache(/|$) - [F,L,NC]
RewriteRule composer\.(json|lock) - [F,L,NC]
RewriteCond %{REQUEST_URI} ^(.*)$ RewriteCond %{REQUEST_URI} ^(.*)$
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !\.php$ RewriteCond %{REQUEST_URI} !\.php$

View File

@ -389,33 +389,44 @@ you can enable those warnings and future-proof your code already.
### Other ### Other
* `TableListField`, `ComplexTableField`, `TableField`, `HasOneComplexTableField`, `HasManyComplexTableField` and `ManyManyComplexTableField` have been removed from the core and placed into a module called "legacytablefields" located at https://github.com/silverstripe-labs/legacytablefields * `TableListField`, `ComplexTableField`, `TableField`, `HasOneComplexTableField`, `HasManyComplexTableField`
* `prototype.js` and `behaviour.js` have been removed from the core, they are no longer used. If you have custom code relying on these two libraries, please update your code to include the files yourself and `ManyManyComplexTableField` have been removed from the core and placed into a module called
* Removed `SiteTree.MetaKeywords` since they are irrelevant in terms of SEO ([seomoz article](http://www.mattcutts.com/blog/keywords-meta-tag-in-web-search/)) and general page informancy "legacytablefields" located at https://github.com/silverstripe-labs/legacytablefields
* `prototype.js` and `behaviour.js` have been removed from the core, they are no longer used. If you
have custom code relying on these two libraries, please update your code to include the files yourself
* Removed `SiteTree.MetaKeywords` since they are irrelevant in terms of SEO
([seomoz article](http://www.mattcutts.com/blog/keywords-meta-tag-in-web-search/)) and general page informancy
* Removed `SiteTree.MetaTitle` as a means to customize the window title, use `SiteTree.Title` instead * Removed `SiteTree.MetaTitle` as a means to customize the window title, use `SiteTree.Title` instead
* Deprecated `Profiler` class, use third-party solutions like [xhprof](https://github.com/facebook/xhprof/) * Deprecated `Profiler` class, use third-party solutions like [xhprof](https://github.com/facebook/xhprof/)
* Removed defunct or unnecessary debug GET parameters: * Removed defunct or unnecessary debug GET parameters:
`debug_profile`, `debug_memory`, `profile_trace`, `debug_javascript`, `debug_behaviour` `debug_profile`, `debug_memory`, `profile_trace`, `debug_javascript`, `debug_behaviour`
* Removed `Member_ProfileForm`, use `CMSProfileController` instead * Removed `Member_ProfileForm`, use `CMSProfileController` instead
* `SiteTree::$nested_urls` enabled by default. To disable, call `SiteTree::disable_nested_urls()`. * `SiteTree::$nested_urls` enabled by default. To disable, call `SiteTree::disable_nested_urls()`.
* Removed CMS permission checks from `File->canEdit()` and `File->canDelete()`. If you have unsecured controllers relying on these permissions, please override them through a `DataExtension`. * Removed CMS permission checks from `File->canEdit()` and `File->canDelete()`. If you have unsecured
* Moved email bounce handling to new ["emailbouncehandler" module](https://github.com/silverstripe-labs/silverstripe-emailbouncehandler), controllers relying on these permissions, please override them through a `DataExtension`.
* Moved email bounce handling to new
["emailbouncehandler" module](https://github.com/silverstripe-labs/silverstripe-emailbouncehandler),
including `Email_BounceHandler` and `Email_BounceRecord` classes, including `Email_BounceHandler` and `Email_BounceRecord` classes,
as well as the `Member->Bounced` property. as well as the `Member->Bounced` property.
* Deprecated global email methods `htmlEmail()` and `plaintextEmail`, as well as various email helper methods like `encodeMultipart()`. Use the `Email` API, or the `Mailer` class where applicable. * Deprecated global email methods `htmlEmail()` and `plaintextEmail`, as well as various email helper
methods like `encodeMultipart()`. Use the `Email` API, or the `Mailer` class where applicable.
* Removed non-functional `$inlineImages` option for sending emails * Removed non-functional `$inlineImages` option for sending emails
* Removed support for keyed arrays in `SelectionGroup`, use new `SelectionGroup_Item` object * Removed support for keyed arrays in `SelectionGroup`, use new `SelectionGroup_Item` object
to populate the list instead (see [API docs](api:SelectionGroup)). to populate the list instead (see [API docs](api:SelectionGroup)).
* `FormField->setDescription()` now renders in a `<span class="description">` by default, rather than a `title` attribute * Removed `Form->Name()`: Use getName() * `FormField->setDescription()` now renders in a `<span class="description">` by default, rather
than a `title` attribute * Removed `Form->Name()`: Use getName()
* Removed `FormField->setContainerFieldSet()`: Use setContainerFieldList() * Removed `FormField->setContainerFieldSet()`: Use setContainerFieldList()
* Removed `FormField->rootFieldSet()`: Use rootFieldList() * Removed `FormField->rootFieldSet()`: Use rootFieldList()
* Removed `Group::map()`: Use DataList::("Group")->map() * Removed `Group::map()`: Use DataList::("Group")->map()
* Removed `Member->generateAutologinHash()`: Tokens are no longer saved directly into the database in plaintext. Use the return value of the Member::generateAutologinTokenAndHash to get the token * Removed `Member->generateAutologinHash()`: Tokens are no longer saved directly into the database in
plaintext. Use the return value of the Member::generateAutologinTokenAndHash to get the token
* Removed `Member->sendInfo()`: use Member_ChangePasswordEmail or Member_ForgotPasswordEmail directly * Removed `Member->sendInfo()`: use Member_ChangePasswordEmail or Member_ForgotPasswordEmail directly
* Removed `SQLMap::map()`: Use DataList::("Member")->map() * Removed `SQLMap::map()`: Use DataList::("Member")->map()
* Removed `SQLMap::mapInGroups()`: Use Member::map_in_groups() * Removed `SQLMap::mapInGroups()`: Use Member::map_in_groups()
* Removed `PasswordEncryptor::register()/unregister()`: Use config system instead * Removed `PasswordEncryptor::register()/unregister()`: Use config system instead
* Methods on DataList and ArrayList that used to both modify the existing list & return a new version now just return a new version. Make sure you change statements like `$list->filter(...)` to $`list = $list->filter(...)` for these methods: * Methods on DataList and ArrayList that used to both modify the existing list & return a new version
now just return a new version. Make sure you change statements like `$list->filter(...)` to
$`list = $list->filter(...)` for these methods:
- `ArrayList#reverse` - `ArrayList#reverse`
- `ArrayList#sort` - `ArrayList#sort`
- `ArrayList#filter` - `ArrayList#filter`
@ -430,12 +441,15 @@ you can enable those warnings and future-proof your code already.
- `DataList#find` - `DataList#find`
- `DataList#byIDs` - `DataList#byIDs`
- `DataList#reverse` - `DataList#reverse`
* `DataList#dataQuery` has been changed to return a clone of the query, and so can't be used to modify the list's query directly. Use `DataList#alterDataQuery` instead to modify dataQuery in a safe manner. * `DataList#dataQuery` has been changed to return a clone of the query, and so can't be used to modify the
list's query directly. Use `DataList#alterDataQuery` instead to modify dataQuery in a safe manner.
* `ScheduledTask`, `QuarterHourlyTask`, `HourlyTask`, `DailyTask`, `MonthlyTask`, `WeeklyTask` and * `ScheduledTask`, `QuarterHourlyTask`, `HourlyTask`, `DailyTask`, `MonthlyTask`, `WeeklyTask` and
`YearlyTask` are deprecated, please extend from `BuildTask` or `CliController`, `YearlyTask` are deprecated, please extend from `BuildTask` or `CliController`,
and invoke them in self-defined frequencies through Unix cronjobs etc. and invoke them in self-defined frequencies through Unix cronjobs etc.
* `i18n::$common_locales` and `i18n::$common_languages` are now accessed via the Config API, and contain associative rather than indexed arrays. * `i18n::$common_locales` and `i18n::$common_languages` are now accessed via the Config API, and contain
Before: `array('de_DE' => array('German', 'Deutsch'))`, after: `array('de_DE' => array('name' => 'German', 'native' => 'Deutsch'))`. associative rather than indexed arrays.
Before: `array('de_DE' => array('German', 'Deutsch'))`,
After: `array('de_DE' => array('name' => 'German', 'native' => 'Deutsch'))`.
* `SSViewer::current_custom_theme()` has been replaced with the `SSViewer.theme_enabled` configuration setting. * `SSViewer::current_custom_theme()` has been replaced with the `SSViewer.theme_enabled` configuration setting.
Please use it to toggle theme behaviour rather than relying on the custom theme being set in the Please use it to toggle theme behaviour rather than relying on the custom theme being set in the
(now deprecated) `SSViewer::set_theme()` call. (now deprecated) `SSViewer::set_theme()` call.
@ -443,7 +457,8 @@ you can enable those warnings and future-proof your code already.
formatting hints as placeholders and description text below the field itself. formatting hints as placeholders and description text below the field itself.
If you change the date/time format of those fields, you need to adjust the hints. If you change the date/time format of those fields, you need to adjust the hints.
To remove the hints, use `setDescription(null)` and `setAttribute('placeholder', null)`. To remove the hints, use `setDescription(null)` and `setAttribute('placeholder', null)`.
* Changed the way FreeStrings in `SSTemplateParser` are recognized, they will now also break on inequality operators (`<`, `>`). If you use inequality operators in free strings in comparisions like * Changed the way FreeStrings in `SSTemplateParser` are recognized, they will now also break on inequality
operators (`<`, `>`). If you use inequality operators in free strings in comparisions like
`<% if Some<String == Some>Other>String %>...<% end_if %>` `<% if Some<String == Some>Other>String %>...<% end_if %>`
@ -458,3 +473,5 @@ you can enable those warnings and future-proof your code already.
* `Object` now has `beforeExtending` and `afterExtending` to inject behaviour around method extension. * `Object` now has `beforeExtending` and `afterExtending` to inject behaviour around method extension.
`DataObject` also has `beforeUpdateCMSFields` to insert fields between automatic scaffolding and extension `DataObject` also has `beforeUpdateCMSFields` to insert fields between automatic scaffolding and extension
by `updateCMSFields`. See the [DataExtension Reference](/reference/dataextension) for more information. by `updateCMSFields`. See the [DataExtension Reference](/reference/dataextension) for more information.
* Magic quotes is now deprecated. Will trigger user_error on live sites, as well as an error on new installs
* Support for Apache 1.x is removed.

View File

@ -58,19 +58,19 @@ every page on the site, if that's easier.
## I can see unparsed PHP output in my browser ## I can see unparsed PHP output in my browser
Please make sure all code inside `*.php` files is wrapped in classes. Due to the way `[api:ManifestBuilder]` 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**. Most common error here 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](/topics/datamodel) and [controllers](/topics/controller)
for ways how to structure your code. for ways how to structure your code.
Also, please check that you have PHP enabled on the webserver, and you're at least running PHP 5.1. 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](/installation) can help you with this.
## I've got file permission problems during installation ## I've got file permission problems during installation
The php installer needs to be able write files during installation, which should be restricted again afterwards. It The php installer needs to be able to write files during installation, which should be restricted again afterwards. It
needs to create/have write-access to: needs to create/have write-access to:
* The main installation directory (for creating .htaccess file and assets directory) * The main installation directory (for creating .htaccess file and assets directory)
* The mysite folder (to create _config.php) * The mysite folder (to create _config.php)
* After the install, the assets directory is the only directory that needs write access. * After the install, the assets directory is the only directory that needs write access.
* Image thumbnails will not show in the CMS if permission is not given * Image thumbnails will not show in the CMS if permission is not given

View File

@ -2,5 +2,5 @@
The best way to install from source is to use [Composer](composer). The best way to install from source is to use [Composer](composer).
The original instructions on this page have been removed, as they are obsolete and misleading. Please use Composer The original instructions on this page have been removed, as they were obsolete and misleading. Please use Composer
instead. instead.

View File

@ -11,7 +11,7 @@ SilverStripe is a web application. This means that you will need to have a webse
## Getting the code ## Getting the code
The best way to get SilverStripe is with [Composer](composer). Composer is a package management tool for PHP that The best way to get SilverStripe is to [install with Composer](composer). 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. 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.
Other ways to get SilverStripe: Other ways to get SilverStripe:

View File

@ -7,7 +7,7 @@ webserver on OSX we suggest using [MAMP](http://www.mamp.info/en/index.php) or u
to manage your packages. to manage your packages.
If you want to use the default OSX PHP version then you will need to recompile your own versions of PHP with GD. Providing instructions If you want to use the default OSX PHP version then you will need to recompile your own versions of PHP with GD. Providing instructions
for how to recompile PHP is beyond the scope of our documentation but try a online search. for how to recompile PHP is beyond the scope of our documentation but try an online search.
## Installing MAMP ## Installing MAMP
@ -34,4 +34,4 @@ won't come along.
Open your web browser and go to `http://localhost:8888/silverstripe/`. Enter your database details - by default with MAMP its user `root` and password `root` and select your account details. Click "Check Details". Open your web browser and go to `http://localhost:8888/silverstripe/`. Enter your database details - by default with MAMP its user `root` and password `root` and select your account details. Click "Check Details".
Once everything is sorted hit Install! and Voila you have SilverStripe installed Once everything is sorted hit "Install!" and Voila, you have SilverStripe installed

View File

@ -25,7 +25,7 @@ Our web-based [PHP installer](/installation) can check if you meet the requireme
* SQL Server 2008+ (requires ["mssql" module](http://silverstripe.org/microsoft-sql-server-database/)) * SQL Server 2008+ (requires ["mssql" module](http://silverstripe.org/microsoft-sql-server-database/))
* Support for [Oracle](http://www.silverstripe.org/oracle-database-module/) and [SQLite](http://silverstripe.org/sqlite-database/) is not commercially supported, but is under development by our open source community. * Support for [Oracle](http://www.silverstripe.org/oracle-database-module/) and [SQLite](http://silverstripe.org/sqlite-database/) is not commercially supported, but is under development by our open source community.
* One of the following web server products: * One of the following web server products:
* Apache 1.3+ with mod_rewrite and "AllowOverride All" set * Apache 2.0+ with mod_rewrite and "AllowOverride All" set
* IIS 7+ * IIS 7+
* Support for Lighttpd, IIS 6, and other web servers may work if you are familiar with configuring those products. * Support for Lighttpd, IIS 6, and other web servers may work if you are familiar with configuring those products.
* We recommend enabling content compression (for example with mod_deflate) to speed up the delivery of HTML, CSS, and JavaScript. * We recommend enabling content compression (for example with mod_deflate) to speed up the delivery of HTML, CSS, and JavaScript.
@ -36,7 +36,7 @@ Our web-based [PHP installer](/installation) can check if you meet the requireme
## Web server hardware requirements ## Web server hardware requirements
Hardware requirements vary widely depending on the traffic to your website, the complexity its logic (i.e., PHP), and its size (i.e., database.) By default, all pages are dynamic, and thus access both the database and execute PHP code to generate. SilverStripe can cache full pages and segments of templates to dramatically increase performance. Hardware requirements vary widely depending on the traffic to your website, the complexity of its logic (i.e., PHP), and its size (i.e., database.) By default, all pages are dynamic, and thus access both the database and execute PHP code to generate. SilverStripe can cache full pages and segments of templates to dramatically increase performance.
A typical website page on a conservative single CPU machine (e.g., Intel 2Ghz) takes roughly 300ms to generate. This comfortably allows over a million page views per month. Caching and other optimisations can improve this by a factor of ten or even one hundred times. SilverStripe CMS can be used in multiple-server architectures to improve scalability and redunancy. A typical website page on a conservative single CPU machine (e.g., Intel 2Ghz) takes roughly 300ms to generate. This comfortably allows over a million page views per month. Caching and other optimisations can improve this by a factor of ten or even one hundred times. SilverStripe CMS can be used in multiple-server architectures to improve scalability and redunancy.

View File

@ -1,13 +1,13 @@
# Upgrading # Upgrading
Usually an update or upgrade your SilverStripe installation just means Usually an update or upgrade to your SilverStripe installation just means
overwriting files and updating your database-schema. overwriting files and updating your database-schema.
See our [upgrade notes and changelogs](/changelogs) for release-specific information. See our [upgrade notes and changelogs](/changelogs) for release-specific information.
## Process ## Process
* Check if any modules (e.g. blog or forum) in your installation are compatible and need to be upgraded as well * Check if any modules (e.g. blog or forum) in your installation are incompatible and need to be upgraded as well
* Backup your database content * Backup your database content
* Backup your webroot files * Backup your webroot files
* Download the new release and uncompress it to a temporary folder * Download the new release and uncompress it to a temporary folder
@ -38,4 +38,4 @@ How easy will it be to update my project? It's a fair question, and sometimes a
* [Release Announcements](http://groups.google.com/group/silverstripe-announce/) * [Release Announcements](http://groups.google.com/group/silverstripe-announce/)
* [Blog posts about releases on silverstripe.org](http://silverstripe.org/blog/tag/release) * [Blog posts about releases on silverstripe.org](http://silverstripe.org/blog/tag/release)
* [/misc/release-process](Release Process) * [/misc/release-process](Release Process)

View File

@ -32,7 +32,7 @@ If the above steps don't work for any reason have a read of the [Common Problems
### Yaml ### Yaml
For the reasons explained in [security](/topics/security) Yaml files are blocked by default by the .htaccess file For the reasons explained in [security](/topics/security), Yaml files are blocked by default by the .htaccess file
provided by the SilverStripe installer module. provided by the SilverStripe installer module.
To allow serving yaml files from a specific directory, add code like this to an .htaccess file in that directory To allow serving yaml files from a specific directory, add code like this to an .htaccess file in that directory

View File

@ -218,7 +218,7 @@ Most of the time, it's caused by a loaded PHP extension that is broken.
**Q: I get the error "HTTP Error 500.0 - Internal Server Error - C:\Program Files (x86)\PHP\php-cgi.exe - The FastCGI process exceeded configured request timeout"** **Q: I get the error "HTTP Error 500.0 - Internal Server Error - C:\Program Files (x86)\PHP\php-cgi.exe - The FastCGI process exceeded configured request timeout"**
**A:** A script is running for a long time, such as a unit test. To fix this, you'll need to increase the request timeout to a higher value in both IIS and PHP. Refer to the IIS FastCGI configuration documentation and PHP configuration parts of this document for where to change the appropriate values. **A:** A script has been running for a long time, such as a unit test. To fix this, you'll need to increase the request timeout to a higher value in both IIS and PHP. Refer to the IIS FastCGI configuration documentation and PHP configuration parts of this document for where to change the appropriate values.
**Q: I cannot access SQL Server from a remote server, such as a development environment** **Q: I cannot access SQL Server from a remote server, such as a development environment**

View File

@ -44,7 +44,7 @@ going into the *alpha* cycle. Preview releases are named *A.B.C-pr1* (and furthe
So far, major releases have happened every couple of years. Most new releases are *minor version number* or *micro So far, major releases have happened every couple of years. Most new releases are *minor version number* or *micro
version number* increments. version number* increments.
So far, we only had one major release, from the *1.x* to the *2.x* line. So far, we have had two major releases; from the *1.x* to the *2.x* line and from the *2.x* to the *3.x* line.
### Minor releases ### Minor releases

View File

@ -10,8 +10,19 @@ like for instance in creating and managing a simple gallery.
## Usage ## Usage
The field can be used in two ways: To upload a single file into a `has_one` relationship, The field can be used in three ways: To upload a single file into a `has_one` relationship,
or allow multiple files into a fixed folder (or 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 ### Single fileupload
@ -42,13 +53,13 @@ based on a has_one relation:
The UploadField will autodetect the relation based on it's `name` property, and The UploadField will autodetect the relation based on it's `name` property, and
save it into the GalleyPages' `SingleImageID` field. Setting the save it into the GalleyPages' `SingleImageID` field. Setting the
`allowedMaxFileNumber` to 1 will make sure that only one image can ever be `setAllowedMaxFileNumber` to 1 will make sure that only one image can ever be
uploaded and linked to the relation. uploaded and linked to the relation.
### Multiple fileupload ### Multiple fileupload
Enable multiple fileuploads by using a many_many relation. Again, the Enable multiple fileuploads by using a many_many (or has_many) relation. Again,
UploadField will detect the relation based on its $name property value: the `UploadField` will detect the relation based on its $name property value:
:::php :::php
class GalleryPage extends Page { class GalleryPage extends Page {
@ -68,22 +79,33 @@ UploadField will detect the relation based on its $name property value:
$title = 'Upload one or more images (max 10 in total)' $title = 'Upload one or more images (max 10 in total)'
) )
); );
$uploadField->setConfig('allowedMaxFileNumber', 10); $uploadField->setAllowedMaxFileNumber(10);
return $fields; return $fields;
} }
} }
class GalleryPage_Controller extends Page_Controller { class GalleryPage_Controller extends Page_Controller {
} }
WARNING: Currently the UploadField doesn't fully support has_many relations, so use a many_many relation instead! class GalleryImageExtension extends DataExtension {
private static $belongs_many_many = array('Galleries' => 'GalleryPage);
}
Image::add_extension('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 ## Configuration
### Overview ### Overview
The field can either be configured on an instance level through `setConfig(<key>, <value>)`, The field can either be configured on an instance level with the various
or globally by overriding the YAML defaults. See the [Configuration Reference](uploadfield#configuration-reference) section for possible values. 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 Example: mysite/_config/uploadfield.yml
@ -93,7 +115,6 @@ Example: mysite/_config/uploadfield.yml
defaultConfig: defaultConfig:
canUpload: false canUpload: false
### Set a custom folder ### Set a custom folder
This example will save all uploads in the `/assets/customfolder/` folder. If This example will save all uploads in the `/assets/customfolder/` folder. If
@ -108,13 +129,22 @@ the folder doesn't exist, it will be created.
); );
$uploadField->setFolderName('customfolder'); $uploadField->setFolderName('customfolder');
## Limit the allowed filetypes ### Limit the allowed filetypes
`AllowedExtensions` defaults to the `File.allowed_extensions` configuration setting, `AllowedExtensions` defaults to the `File.allowed_extensions` configuration setting,
but can be overwritten for each UploadField: but can be overwritten for each UploadField:
:::php :::php
$uploadField->getValidator()->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif')); $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
// 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'
$uploadField->setAllowedFileCategories('image', 'doc');
### Limit the maximum file size ### Limit the maximum file size
@ -135,8 +165,24 @@ Set the dimensions of the image preview. By default the max width is set to 80
and the max height is set to 60. and the max height is set to 60.
:::php :::php
$uploadField->setConfig('previewMaxWidth', 100); $uploadField->setPreviewMaxWidth(100);
$uploadField->setConfig('previewMaxHeight', 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 librarry
:::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 ### Automatic or manual upload
@ -145,8 +191,8 @@ Setting the `autoUpload` property to false, will present you with a list of
selected files that you can then upload manually one by one: selected files that you can then upload manually one by one:
:::php :::php
$uploadField->setConfig('autoUpload', false); $uploadField->setAutoUpload(false);
### Build a simple gallery ### Build a simple gallery
A gallery most times needs more then simple images. You might want to add a A gallery most times needs more then simple images. You might want to add a
@ -171,8 +217,14 @@ Now register the DataExtension for the Image class in your _config.php:
:::php :::php
Image::add_extension('GalleryImage'); Image::add_extension('GalleryImage');
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 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 ### Edit uploaded images
By default the UploadField will let you edit the following fields: *Title, By default the UploadField will let you edit the following fields: *Title,
@ -195,40 +247,128 @@ you you alter these settings. One way to go about this is create a
Then, in your GalleryPage, tell the UploadField to use this function: Then, in your GalleryPage, tell the UploadField to use this function:
:::php :::php
$uploadField->setConfig('fileEditFields', 'getCustomFields'); $uploadField->setFileEditFields('getCustomFields');
In a similar fashion you can use 'fileEditActions' to set the actions for the In a similar fashion you can use 'setFileEditActions' to set the actions for the
editform, or 'fileEditValidator' to determine the validator (eg RequiredFields). editform, or 'fileEditValidator' to determine the validator (eg RequiredFields).
### Configuration Reference ### Configuration Reference
- `autoUpload`: (boolean) - `setAllowedMaxFileNumber`: (int) php validation of allowedMaxFileNumber
- `allowedMaxFileNumber`: (int) php validation of allowedMaxFileNumber
only works when a db relation is available, set to null to allow 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 unlimited if record has a has_one and allowedMaxFileNumber is null, it will be set to 1
- `canUpload`: (boolean) Can the user upload new files, or just select from existing files. - `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. String values are interpreted as permission codes.
- `previewMaxWidth`: (int) - `setCanPreviewFolder`: (boolean|string) Can the user preview the folder files will be saved into?
- `previewMaxHeight`: (int) String values are interpreted as permission codes.
- `uploadTemplateName`: (string) javascript template used to display uploading - `setCanUpload`: (boolean|string) Can the user upload new files, or just select from existing files.
files, see javascript/UploadField_uploadtemplate.js String values are interpreted as permission codes.
- `downloadTemplateName`: (string) javascript template used to display already - `setDownloadTemplateName`: (string) javascript template used to display already
uploaded files, see javascript/UploadField_downloadtemplate.js uploaded files, see javascript/UploadField_downloadtemplate.js
- `fileEditFields`: (FieldList|string) FieldList $fields or string $name - `setFileEditFields`: (FieldList|string) FieldList $fields or string $name
(of a method on File to provide a fields) for the EditForm (Example: 'getCMSFields') (of a method on File to provide a fields) for the EditForm (Example: 'getCMSFields')
- `fileEditActions`: (FieldList|string) FieldList $actions or string $name - `setFileEditActions`: (FieldList|string) FieldList $actions or string $name
(of a method on File to provide a actions) for the EditForm (Example: 'getCMSActions') (of a method on File to provide a actions) for the EditForm (Example: 'getCMSActions')
- `fileEditValidator`: (string) Validator (eg RequiredFields) or string $name - `setFileEditValidator`: (string) Validator (eg RequiredFields) or string $name
(of a method on File to provide a Validator) for the EditForm (Example: 'getCMSValidator') (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.
You can also configure the underlying `[api:Upload]` class, by using the YAML config system. You can also configure the underlying `[api:Upload]` class, by using the YAML config system.
:::yaml :::yaml
Upload: Upload:
# Globally disables automatic renaming of files # Globally disables automatic renaming of files and displays a warning before overwriting an existing file
replaceFile: true replaceFile: true
uploads_folder: 'Uploads'
## TODO: Using the UploadField in a frontend form ## Using the UploadField in a frontend form
*At this moment the UploadField not yet fully supports being used on a frontend The UploadField can be used in a frontend form, given that sufficient attention is given
form.* 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:
:::php
// In GalleryPage.php
class GalleryPage extends Page {}
class GalleryPage_Controller extends Page_Controller {
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;
}
}
// In Gallery.php
class Gallery extends DataObject {
private static $db = array(
'Title' => 'Varchar(255)'
);
private static $many_many = array(
'Images' => 'Image'
);
}
// In ImageExtension.php
class ImageExtension extends DataExtension {
private static $belongs_many_many = array(
'Gallery' => 'Gallery'
);
function canEdit($member) {
// This part is important!
return Permission::check('ADMIN');
}
}
Image::add_extension('ImageExtension');

View File

@ -123,4 +123,26 @@ Step 3: Use sake to start and stop your process
Note that sake processes are currently a little brittle, in that the pid and log files are placed in the site root Note that sake processes are currently a little brittle, in that the pid and log files are placed in the site root
directory, rather than somewhere sensible like /var/log or /var/run. directory, rather than somewhere sensible like /var/log or /var/run.
### Running Regular Tasks With Cron
On a unix machine, you can typically run a scheduled task with a [cron job](http://en.wikipedia.org/wiki/Cron),
using one of the following command-line calls:
```
/path/to/site_root/framework/sake dev/tasks/MyTask
php /path/to/site_root/framework/cli-script.php dev/tasks/MyTask
```
If you find that your cron job appears to be retrieving the login screen, then you may need to use `php-cli`
instead. This is typical of a cPanel-based setup.
```
php-cli /path/to/site_root/framework/cli-script.php dev/tasks/MyTask
```
A good approach to setting up and testing your task to run with cron is:
1. Try running the task via the command-line on your server. `/path/to/site_root/framework/sake dev/tasks/MyTask`
2. Set up a cron job to run the task every minute. `* * * * * /path/to/site_root/framework/sake dev/tasks/MyTask`
3. Finally, set the task to run when you want it to. `0 2 * * * /path/to/site_root/framework/sake dev/tasks/MyTask` (2am)

View File

@ -112,9 +112,9 @@ class File extends DataObject {
private static $allowed_extensions = array( private static $allowed_extensions = array(
'','ace','arc','arj','asf','au','avi','bmp','bz2','cab','cda','css','csv','dmg','doc','docx', '','ace','arc','arj','asf','au','avi','bmp','bz2','cab','cda','css','csv','dmg','doc','docx',
'flv','gif','gpx','gz','hqx','htm','html','ico','jar','jpeg','jpg','js','kml', 'm4a','m4v', 'flv','gif','gpx','gz','hqx','htm','html','ico','jar','jpeg','jpg','js','kml', 'm4a','m4v',
'mid','midi','mkv','mov','mp3','mp4','mpa','mpeg','mpg','ogg','pages','pcx','pdf','pkg', 'mid','midi','mkv','mov','mp3','mp4','mpa','mpeg','mpg','ogg','ogv','pages','pcx','pdf','pkg',
'png','pps','ppt','pptx','ra','ram','rm','rtf','sit','sitx','swf','tar','tgz','tif','tiff', 'png','pps','ppt','pptx','ra','ram','rm','rtf','sit','sitx','swf','tar','tgz','tif','tiff',
'txt','wav','wma','wmv','xhtml','xls','xlsx','xml','zip','zipx', 'txt','wav','webm','wma','wmv','xhtml','xls','xlsx','xml','zip','zipx',
); );
/** /**
@ -127,7 +127,7 @@ class File extends DataObject {
"apl", "avr" ,"cda" ,"mp4" ,"ogg" "apl", "avr" ,"cda" ,"mp4" ,"ogg"
), ),
'mov' => array( 'mov' => array(
"mpeg" ,"mpg" ,"m1v" ,"mp2" ,"mpa" ,"mpe" ,"ifo" ,"vob","avi" ,"wmv" ,"asf" ,"m2v" ,"qt" "mpeg" ,"mpg" ,"m1v" ,"mp2" ,"mpa" ,"mpe" ,"ifo" ,"vob","avi" ,"wmv" ,"asf" ,"m2v" ,"qt", "ogv", "webm"
), ),
'zip' => array( 'zip' => array(
"arc" ,"rar" ,"tar" ,"gz" ,"tgz" ,"bz2" ,"dmg" ,"jar","ace" ,"arj" ,"bz" ,"cab" "arc" ,"rar" ,"tar" ,"gz" ,"tgz" ,"bz2" ,"dmg" ,"jar","ace" ,"arj" ,"bz" ,"cab"
@ -340,10 +340,10 @@ class File extends DataObject {
} }
// Upload // Upload
$uploadField = new UploadField('UploadField','Upload Field'); $uploadField = UploadField::create('UploadField','Upload Field')
$uploadField->setConfig('previewMaxWidth', 40); ->setPreviewMaxWidth(40)
$uploadField->setConfig('previewMaxHeight', 30); ->setPreviewMaxHeight(30)
$uploadField->setConfig('allowedMaxFileNumber', 1); ->setAllowedMaxFileNumber(1);
//$uploadField->setTemplate('FileEditUploadField'); //$uploadField->setTemplate('FileEditUploadField');
if ($this->ParentID) { if ($this->ParentID) {
$parent = $this->Parent(); $parent = $this->Parent();
@ -527,13 +527,13 @@ class File extends DataObject {
if(!is_a($this, 'Folder')) { if(!is_a($this, 'Folder')) {
// Only throw a fatal error if *both* before and after paths don't exist. // Only throw a fatal error if *both* before and after paths don't exist.
if(!file_exists($pathBeforeAbs)) { if(!file_exists($pathBeforeAbs)) {
throw new Exception("Cannot move $pathBefore to $pathAfter - $pathBefore doesn't exist"); throw new Exception("Cannot move $pathBeforeAbs to $pathAfterAbs - $pathBeforeAbs doesn't exist");
} }
// Check that target directory (not the file itself) exists. // Check that target directory (not the file itself) exists.
// Only check if we're dealing with a file, otherwise the folder will need to be created // Only check if we're dealing with a file, otherwise the folder will need to be created
if(!file_exists(dirname($pathAfterAbs))) { if(!file_exists(dirname($pathAfterAbs))) {
throw new Exception("Cannot move $pathBefore to $pathAfter - Directory " . dirname($pathAfter) throw new Exception("Cannot move $pathBeforeAbs to $pathAfterAbs - Directory " . dirname($pathAfter)
. " doesn't exist"); . " doesn't exist");
} }
} }

View File

@ -28,12 +28,14 @@ class Upload extends Controller {
/** /**
* A File object * A File object
*
* @var File * @var File
*/ */
protected $file; protected $file;
/** /**
* An instance of Upload_Validator * Validator for this upload field
*
* @var Upload_Validator * @var Upload_Validator
*/ */
protected $validator; protected $validator;
@ -48,7 +50,8 @@ class Upload extends Controller {
/** /**
* Replace an existing file rather than renaming the new one. * Replace an existing file rather than renaming the new one.
* @var Boolean *
* @var boolean
*/ */
protected $replaceFile; protected $replaceFile;
@ -72,13 +75,13 @@ class Upload extends Controller {
public function __construct() { public function __construct() {
parent::__construct(); parent::__construct();
$this->validator = new Upload_Validator(); $this->validator = new Upload_Validator();
$this->replaceFile = $this->config()->replaceFile; $this->replaceFile = self::config()->replaceFile;
} }
/** /**
* Get current validator * Get current validator
* *
* @return object $validator * @return Upload_Validator $validator
*/ */
public function getValidator() { public function getValidator() {
return $this->validator; return $this->validator;
@ -153,24 +156,27 @@ class Upload extends Controller {
// if filename already exists, version the filename (e.g. test.gif to test1.gif) // if filename already exists, version the filename (e.g. test.gif to test1.gif)
if(!$this->replaceFile) { if(!$this->replaceFile) {
while(file_exists("$base/$relativeFilePath")) { while(file_exists("$base/$relativeFilePath")) {
$i = isset($i) ? ($i+1) : 2; $i = isset($i) ? ($i+1) : 2;
$oldFilePath = $relativeFilePath; $oldFilePath = $relativeFilePath;
// make sure archives retain valid extensions // make sure archives retain valid extensions
if(substr($relativeFilePath, strlen($relativeFilePath) - strlen('.tar.gz')) == '.tar.gz' || if(substr($relativeFilePath, strlen($relativeFilePath) - strlen('.tar.gz')) == '.tar.gz' ||
substr($relativeFilePath, strlen($relativeFilePath) - strlen('.tar.bz2')) == '.tar.bz2') { substr($relativeFilePath, strlen($relativeFilePath) - strlen('.tar.bz2')) == '.tar.bz2') {
$relativeFilePath = preg_replace('/[0-9]*(\.tar\.[^.]+$)/', $i . '\\1', $relativeFilePath); $relativeFilePath = preg_replace('/[0-9]*(\.tar\.[^.]+$)/', $i . '\\1', $relativeFilePath);
} else if (strpos($relativeFilePath, '.') !== false) { } else if (strpos($relativeFilePath, '.') !== false) {
$relativeFilePath = preg_replace('/[0-9]*(\.[^.]+$)/', $i . '\\1', $relativeFilePath); $relativeFilePath = preg_replace('/[0-9]*(\.[^.]+$)/', $i . '\\1', $relativeFilePath);
} else if (strpos($relativeFilePath, '_') !== false) { } else if (strpos($relativeFilePath, '_') !== false) {
$relativeFilePath = preg_replace('/_([^_]+$)/', '_'.$i, $relativeFilePath); $relativeFilePath = preg_replace('/_([^_]+$)/', '_'.$i, $relativeFilePath);
} else { } else {
$relativeFilePath .= '_'.$i; $relativeFilePath .= '_'.$i;
} }
if($oldFilePath == $relativeFilePath && $i > 2) { if($oldFilePath == $relativeFilePath && $i > 2) {
user_error("Couldn't fix $relativeFilePath with $i tries", E_USER_ERROR); user_error("Couldn't fix $relativeFilePath with $i tries", E_USER_ERROR);
} }
} }
} else {
//reset the ownerID to the current member when replacing files
$this->file->OwnerID = (Member::currentUser() ? Member::currentUser()->ID : 0);
} }
if(file_exists($tmpFile['tmp_name']) && copy($tmpFile['tmp_name'], "$base/$relativeFilePath")) { if(file_exists($tmpFile['tmp_name']) && copy($tmpFile['tmp_name'], "$base/$relativeFilePath")) {
@ -178,6 +184,7 @@ class Upload extends Controller {
// This is to prevent it from trying to rename the file // This is to prevent it from trying to rename the file
$this->file->Name = basename($relativeFilePath); $this->file->Name = basename($relativeFilePath);
$this->file->write(); $this->file->write();
$this->extend('onAfterLoad', $this->file); //to allow extensions to e.g. create a version after an upload
return true; return true;
} else { } else {
$this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.'); $this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.');
@ -273,7 +280,7 @@ class Upload extends Controller {
* @return array * @return array
*/ */
public function getErrors() { public function getErrors() {
return $this->errors; return $this->errors;
} }
} }

View File

@ -170,10 +170,10 @@ class FieldList extends ArrayList {
} }
/** /**
* Remove a field from this FieldList by Name. * Remove a field or fields from this FieldList by Name.
* The field could also be inside a CompositeField. * The field could also be inside a CompositeField.
* *
* @param string $fieldName The name of the field or tab * @param string|array $fieldName The name of, or an array with the field(s) or tab(s)
* @param boolean $dataFieldOnly If this is true, then a field will only * @param boolean $dataFieldOnly If this is true, then a field will only
* be removed if it's a data field. Dataless fields, such as tabs, will * be removed if it's a data field. Dataless fields, such as tabs, will
* be left as-is. * be left as-is.
@ -182,8 +182,16 @@ class FieldList extends ArrayList {
if(!$fieldName) { if(!$fieldName) {
user_error('FieldList::removeByName() was called with a blank field name.', E_USER_WARNING); user_error('FieldList::removeByName() was called with a blank field name.', E_USER_WARNING);
} }
// Handle array syntax
if(is_array($fieldName)) {
foreach($fieldName as $field){
$this->removeByName($field, $dataFieldOnly);
}
return;
}
$this->flushFieldsCache(); $this->flushFieldsCache();
foreach($this->items as $i => $child) { foreach($this->items as $i => $child) {
if(is_object($child)){ if(is_object($child)){
$childName = $child->getName(); $childName = $child->getName();

View File

@ -132,7 +132,7 @@ class FileField extends FormField {
/** /**
* Get custom validator for this field * Get custom validator for this field
* *
* @param object $validator * @param Upload_Validator $validator
*/ */
public function getValidator() { public function getValidator() {
return $this->upload->getValidator(); return $this->upload->getValidator();
@ -141,7 +141,8 @@ class FileField extends FormField {
/** /**
* Set custom validator for this field * Set custom validator for this field
* *
* @param object $validator * @param Upload_Validator $validator
* @return FileField Self reference
*/ */
public function setValidator($validator) { public function setValidator($validator) {
$this->upload->setValidator($validator); $this->upload->setValidator($validator);
@ -149,7 +150,10 @@ class FileField extends FormField {
} }
/** /**
* Sets the upload folder name
*
* @param string $folderName * @param string $folderName
* @return FileField Self reference
*/ */
public function setFolderName($folderName) { public function setFolderName($folderName) {
$this->folderName = $folderName; $this->folderName = $folderName;
@ -157,6 +161,8 @@ class FileField extends FormField {
} }
/** /**
* Gets the upload folder name
*
* @return string * @return string
*/ */
public function getFolderName() { public function getFolderName() {
@ -181,14 +187,23 @@ class FileField extends FormField {
} }
/** /**
* Retrieves the Upload handler
*
* @return Upload * @return Upload
*/ */
public function getUpload() { public function getUpload() {
return $this->upload; return $this->upload;
} }
/**
* Sets the upload handler
*
* @param Upload $upload
* @return FileField Self reference
*/
public function setUpload(Upload $upload) { public function setUpload(Upload $upload) {
$this->upload = $upload; $this->upload = $upload;
return $this;
} }
} }

View File

@ -259,10 +259,6 @@ class Form extends RequestHandler {
} else { } else {
$vars = $request->requestVars(); $vars = $request->requestVars();
} }
if(isset($funcName)) {
Form::set_current_action($funcName);
}
// Populate the form // Populate the form
$this->loadDataFrom($vars, true); $this->loadDataFrom($vars, true);
@ -299,6 +295,7 @@ class Form extends RequestHandler {
} }
if(isset($funcName)) { if(isset($funcName)) {
Form::set_current_action($funcName);
$this->setButtonClicked($funcName); $this->setButtonClicked($funcName);
} }

View File

@ -208,6 +208,8 @@ class FormField extends RequestHandler {
/** /**
* Method to save this form field into the given data object. * Method to save this form field into the given data object.
* By default, makes use of $this->dataValue() * By default, makes use of $this->dataValue()
*
* @param DataObjectInterface $record DataObject to save data into
*/ */
public function saveInto(DataObjectInterface $record) { public function saveInto(DataObjectInterface $record) {
if($this->name) { if($this->name) {
@ -403,7 +405,9 @@ class FormField extends RequestHandler {
/** /**
* Set the field value. * Set the field value.
* Returns $this. *
* @param mixed $value
* @return FormField Self reference
*/ */
public function setValue($value) { public function setValue($value) {
$this->value = $value; $this->value = $value;

View File

@ -2,14 +2,13 @@
/** /**
* Field for uploading single or multiple files of all types, including images. * Field for uploading single or multiple files of all types, including images.
* <b>NOTE: this Field will call write() on the supplied record</b>
* *
* <b>Features (some might not be available to old browsers):</b> * <b>Features (some might not be available to old browsers):</b>
* *
* - File Drag&Drop support * - File Drag&Drop support
* - Progressbar * - Progressbar
* - Image thumbnail/file icons even before upload finished * - Image thumbnail/file icons even before upload finished
* - Saving into relations * - Saving into relations on form submit
* - Edit file * - Edit file
* - allowedExtensions is by default File::$allowed_extensions<li>maxFileSize the value of min(upload_max_filesize, * - allowedExtensions is by default File::$allowed_extensions<li>maxFileSize the value of min(upload_max_filesize,
* post_max_size) from php.ini * post_max_size) from php.ini
@ -17,9 +16,9 @@
* <>Usage</b> * <>Usage</b>
* *
* @example <code> * @example <code>
* $UploadField = new UploadField('myFiles', 'Please upload some images <span>(max. 5 files)</span>'); * $UploadField = new UploadField('AttachedImages', 'Please upload some images <span>(max. 5 files)</span>');
* $UploadField->getValidator()->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif')); * $UploadField->setAllowedFileCategories('image');
* $UploadField->setConfig('allowedMaxFileNumber', 5); * $UploadField->setAllowedMaxFileNumber(5);
* </code> * </code>
* *
* @author Zauberfisch * @author Zauberfisch
@ -36,6 +35,7 @@ class UploadField extends FileField {
'attach', 'attach',
'handleItem', 'handleItem',
'handleSelect', 'handleSelect',
'fileexists'
); );
/** /**
@ -48,103 +48,141 @@ class UploadField extends FileField {
); );
/** /**
* @var String * Template to use for the file button widget
*
* @var string
*/ */
protected $templateFileButtons = 'UploadField_FileButtons'; protected $templateFileButtons = 'UploadField_FileButtons';
/** /**
* @var String * Template to use for the edit form
*
* @var string
*/ */
protected $templateFileEdit = 'UploadField_FileEdit'; protected $templateFileEdit = 'UploadField_FileEdit';
/** /**
* Parent data record. Will be infered from parent form or controller if blank.
*
* @var DataObject * @var DataObject
*/ */
protected $record; protected $record;
/** /**
* Items loaded into this field. May be a RelationList, or any other SS_List
*
* @var SS_List * @var SS_List
*/ */
protected $items; protected $items;
/** /**
* @var array Config for this field used in both, php and javascript * Config for this field used in the front-end javascript
* (will be merged into the config of the javascript file upload plugin). * (will be merged into the config of the javascript file upload plugin).
* See framework/_config/uploadfield.yml for configuration defaults and documentation. * See framework/_config/uploadfield.yml for configuration defaults and documentation.
*
* @var array
*/ */
protected $ufConfig = array( protected $ufConfig = array(
/** /**
* Automatically upload the file once selected
*
* @var boolean * @var boolean
*/ */
'autoUpload' => true, 'autoUpload' => true,
/** /**
* php validation of allowedMaxFileNumber only works when a db relation is available, set to null to allow * Restriction on number of files that may be set for this field. Set to null to allow
* unlimited if record has a has_one and allowedMaxFileNumber is null, it will be set to 1 * unlimited. If record has a has_one and allowedMaxFileNumber is null, it will be set to 1.
* @var int * The resulting value will be set to maxNumberOfFiles
*
* @var integer
*/ */
'allowedMaxFileNumber' => null, 'allowedMaxFileNumber' => null,
/** /**
* @var boolean|string Can the user upload new files, or just select from existing files. * Can the user upload new files, or just select from existing files.
* String values are interpreted as permission codes. * String values are interpreted as permission codes.
*
* @var boolean|string
*/ */
'canUpload' => true, 'canUpload' => true,
/** /**
* @var boolean|string Can the user attach files from the assets archive on the site? * Can the user attach files from the assets archive on the site?
* String values are interpreted as permission codes. * String values are interpreted as permission codes.
*
* @var boolean|string
*/ */
'canAttachExisting' => "CMS_ACCESS_AssetAdmin", 'canAttachExisting' => "CMS_ACCESS_AssetAdmin",
/** /**
* @var boolean Shows the target folder for new uploads in the field UI. * Shows the target folder for new uploads in the field UI.
* Disable to keep the internal filesystem structure hidden from users. * Disable to keep the internal filesystem structure hidden from users.
*
* @var boolean|string
*/ */
'canPreviewFolder' => true, 'canPreviewFolder' => true,
/** /**
* @var boolean If a second file is uploaded, should it replace the existing one rather than throwing an errror? * Maximum width of the preview thumbnail
* This only applies for has_one relationships, and only replaces the association *
* rather than the actual file database record or filesystem entry. * @var integer
*/
'replaceExistingFile' => false,
/**
* @var int
*/ */
'previewMaxWidth' => 80, 'previewMaxWidth' => 80,
/** /**
* @var int * Maximum height of the preview thumbnail
*
* @var integer
*/ */
'previewMaxHeight' => 60, 'previewMaxHeight' => 60,
/** /**
* javascript template used to display uploading files * javascript template used to display uploading files
*
* @see javascript/UploadField_uploadtemplate.js * @see javascript/UploadField_uploadtemplate.js
* @var string * @var string
*/ */
'uploadTemplateName' => 'ss-uploadfield-uploadtemplate', 'uploadTemplateName' => 'ss-uploadfield-uploadtemplate',
/** /**
* javascript template used to display already uploaded files * javascript template used to display already uploaded files
*
* @see javascript/UploadField_downloadtemplate.js * @see javascript/UploadField_downloadtemplate.js
* @var string * @var string
*/ */
'downloadTemplateName' => 'ss-uploadfield-downloadtemplate', 'downloadTemplateName' => 'ss-uploadfield-downloadtemplate',
/** /**
* FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm * Show a warning when overwriting a file.
* @example 'getCMSFields' * This requires Upload->replaceFile config to be set to true, otherwise
* @var FieldList|string * files will be renamed instead of overwritten (although the warning will
* still be displayed)
*
* @see Upload
* @var boolean
*/ */
'fileEditFields' => null, 'overwriteWarning' => true
/**
* FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
* @example 'getCMSActions'
* @var FieldList|string
*/
'fileEditActions' => null,
/**
* Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm
* @example 'getCMSValidator'
* @var string
*/
'fileEditValidator' => null
); );
/** /**
* FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm
* @example 'getCMSFields'
*
* @var FieldList|string
*/
protected $fileEditFields = null;
/**
* FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
* @example 'getCMSActions'
*
* @var FieldList|string
*/
protected $fileEditActions = null;
/**
* Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm
* @example 'getCMSValidator'
*
* @var RequiredFields|string
*/
protected $fileEditValidator = null;
/**
* Construct a new UploadField instance
*
* @param string $name The internal field name, passed to forms. * @param string $name The internal field name, passed to forms.
* @param string $title The field label. * @param string $title The field label.
* @param SS_List $items If no items are defined, the field will try to auto-detect an existing relation on * @param SS_List $items If no items are defined, the field will try to auto-detect an existing relation on
@ -152,11 +190,12 @@ class UploadField extends FileField {
* @param Form $form Reference to the container form * @param Form $form Reference to the container form
*/ */
public function __construct($name, $title = null, SS_List $items = null) { public function __construct($name, $title = null, SS_List $items = null) {
// TODO thats the first thing that came to my head, feel free to change it // TODO thats the first thing that came to my head, feel free to change it
$this->addExtraClass('ss-upload'); // class, used by js $this->addExtraClass('ss-upload'); // class, used by js
$this->addExtraClass('ss-uploadfield'); // class, used by css for uploadfield only $this->addExtraClass('ss-uploadfield'); // class, used by css for uploadfield only
$this->ufConfig = array_merge($this->ufConfig, Config::inst()->get('UploadField', 'defaultConfig')); $this->ufConfig = array_merge($this->ufConfig, self::config()->defaultConfig);
parent::__construct($name, $title); parent::__construct($name, $title);
@ -166,15 +205,17 @@ class UploadField extends FileField {
$this->getValidator()->setAllowedExtensions( $this->getValidator()->setAllowedExtensions(
array_filter(Config::inst()->get('File', 'allowed_extensions')) array_filter(Config::inst()->get('File', 'allowed_extensions'))
); );
// get the lower max size // get the lower max size
$this->getValidator()->setAllowedMaxFileSize(min(File::ini2bytes(ini_get('upload_max_filesize')), $maxUpload = File::ini2bytes(ini_get('upload_max_filesize'));
File::ini2bytes(ini_get('post_max_size')))); $maxPost = File::ini2bytes(ini_get('post_max_size'));
$this->getValidator()->setAllowedMaxFileSize(min($maxUpload, $maxPost));
} }
/** /**
* Set name of template used for Buttons on each file (replace, edit, remove, delete) (without path or extension) * Set name of template used for Buttons on each file (replace, edit, remove, delete) (without path or extension)
* *
* @param String * @param string
*/ */
public function setTemplateFileButtons($template) { public function setTemplateFileButtons($template) {
$this->templateFileButtons = $template; $this->templateFileButtons = $template;
@ -182,7 +223,7 @@ class UploadField extends FileField {
} }
/** /**
* @return String * @return string
*/ */
public function getTemplateFileButtons() { public function getTemplateFileButtons() {
return $this->templateFileButtons; return $this->templateFileButtons;
@ -191,7 +232,7 @@ class UploadField extends FileField {
/** /**
* Set name of template used for the edit (inline & popup) of a file file (without path or extension) * Set name of template used for the edit (inline & popup) of a file file (without path or extension)
* *
* @param String * @param string
*/ */
public function setTemplateFileEdit($template) { public function setTemplateFileEdit($template) {
$this->templateFileEdit = $template; $this->templateFileEdit = $template;
@ -199,12 +240,60 @@ class UploadField extends FileField {
} }
/** /**
* @return String * @return string
*/ */
public function getTemplateFileEdit() { public function getTemplateFileEdit() {
return $this->templateFileEdit; return $this->templateFileEdit;
} }
/**
* Determine if the target folder for new uploads in is visible the field UI.
*
* @return boolean
*/
public function canPreviewFolder() {
if(!$this->isActive()) return false;
$can = $this->getConfig('canPreviewFolder');
return (is_bool($can)) ? $can : Permission::check($can);
}
/**
* Determine if the target folder for new uploads in is visible the field UI.
* Disable to keep the internal filesystem structure hidden from users.
*
* @param boolean|string $canPreviewFolder Either a boolean flag, or a
* required permission code
* @return UploadField Self reference
*/
public function setCanPreviewFolder($canPreviewFolder) {
return $this->setConfig('canPreviewFolder', $canPreviewFolder);
}
/**
* Determine if the field should show a warning when overwriting a file.
* This requires Upload->replaceFile config to be set to true, otherwise
* files will be renamed instead of overwritten (although the warning will
* still be displayed)
*
* @return boolean
*/
public function getOverwriteWarning() {
return $this->getConfig('overwriteWarning');
}
/**
* Determine if the field should show a warning when overwriting a file.
* This requires Upload->replaceFile config to be set to true, otherwise
* files will be renamed instead of overwritten (although the warning will
* still be displayed)
*
* @param boolean $overwriteWarning
* @return UploadField Self reference
*/
public function setOverwriteWarning($overwriteWarning) {
return $this->setConfig('overwriteWarning', $overwriteWarning);
}
/** /**
* Force a record to be used as "Parent" for uploaded Files (eg a Page with a has_one to File) * Force a record to be used as "Parent" for uploaded Files (eg a Page with a has_one to File)
* @param DataObject $record * @param DataObject $record
@ -216,73 +305,193 @@ class UploadField extends FileField {
/** /**
* Get the record to use as "Parent" for uploaded Files (eg a Page with a has_one to File) If none is set, it will * Get the record to use as "Parent" for uploaded Files (eg a Page with a has_one to File) If none is set, it will
* use Form->getRecord() or Form->Controller()->data() * use Form->getRecord() or Form->Controller()->data()
*
* @return DataObject * @return DataObject
*/ */
public function getRecord() { public function getRecord() {
if (!$this->record && $this->form) { if (!$this->record && $this->form) {
if ($this->form->getRecord() && is_a($this->form->getRecord(), 'DataObject')) { if (($record = $this->form->getRecord()) && ($record instanceof DataObject)) {
$this->record = $this->form->getRecord(); $this->record = $record;
} elseif ($this->form->Controller() && $this->form->Controller()->hasMethod('data') } elseif (($controller = $this->form->Controller())
&& $this->form->Controller()->data() && is_a($this->form->Controller()->data(), 'DataObject')) { && $controller->hasMethod('data')
$this->record = $this->form->Controller()->data(); && ($record = $controller->data())
&& ($record instanceof DataObject)
) {
$this->record = $record;
} }
} }
return $this->record; return $this->record;
} }
/**
* Loads the related record values into this field. UploadField can be uploaded
* in one of three ways:
*
* - By passing in a list of file IDs in the $value parameter (an array with a single
* key 'Files', with the value being the actual array of IDs).
* - By passing in an explicit list of File objects in the $record parameter, and
* leaving $value blank.
* - By passing in a dataobject in the $record parameter, from which file objects
* will be extracting using the field name as the relation field.
*
* Each of these methods will update both the items (list of File objects) and the
* field value (list of file ID values).
*
* @param array $value Array of submitted form data, if submitting from a form
* @param array|DataObject|SS_List $record Full source record, either as a DataObject,
* SS_List of items, or an array of submitted form data
* @return UploadField Self reference
*/
public function setValue($value, $record = null) {
// If we're not passed a value directly, we can attempt to infer the field
// value from the second parameter by inspecting its relations
$items = new ArrayList();
// Determine format of presented data
if(empty($value) && $record) {
// If a record is given as a second parameter, but no submitted values,
// then we should inspect this instead for the form values
if(($record instanceof DataObject) && $record->hasMethod($this->getName())) {
// If given a dataobject use reflection to extract details
$data = $record->{$this->getName()}();
if($data instanceof DataObject) {
// If has_one, add sole item to default list
$items->push($data);
} elseif($data instanceof SS_List) {
// For many_many and has_many relations we can use the relation list directly
$items = $data;
}
} elseif($record instanceof SS_List) {
// If directly passing a list then save the items directly
$items = $record;
}
} elseif(!empty($value['Files'])) {
// If value is given as an array (such as a posted form), extract File IDs from this
$class = $this->getRelationAutosetClass();
$items = DataObject::get($class)->byIDs($value['Files']);
}
// If javascript is disabled, direct file upload (non-html5 style) can
// trigger a single or multiple file submission. Note that this may be
// included in addition to re-submitted File IDs as above, so these
// should be added to the list instead of operated on independently.
if($uploadedFiles = $this->extractUploadedFileData($value)) {
foreach($uploadedFiles as $tempFile) {
$file = $this->saveTemporaryFile($tempFile, $error);
if($file) {
$items->add($file);
} else {
throw new ValidationException($error);
}
}
}
// Filter items by what's allowed to be viewed
$filteredItems = new ArrayList();
$fileIDs = array();
foreach($items as $file) {
if($file->exists() && $file->canView()) {
$filteredItems->push($file);
$fileIDs[] = $file->ID;
}
}
// Filter and cache updated item list
$this->items = $filteredItems;
// Same format as posted form values for this field. Also ensures that
// $this->setValue($this->getValue()); is non-destructive
$value = $fileIDs ? array('Files' => $fileIDs) : null;
// Set value using parent
return parent::setValue($value, $record);
}
/** /**
* Sets the items assigned to this field as an SS_List of File objects.
* Calling setItems will also update the value of this field, as well as
* updating the internal list of File items.
*
* @param SS_List $items * @param SS_List $items
* @return UploadField self reference
*/ */
public function setItems(SS_List $items) { public function setItems(SS_List $items) {
$this->items = $items; return $this->setValue(null, $items);
}
/**
* Retrieves the current list of files
*
* @return SS_List
*/
public function getItems() {
return $this->items ? $this->items : new ArrayList();
}
/**
* Retrieves a customised list of all File records to ensure they are
* properly viewable when rendered in the field template.
*
* @return SS_List[ViewableData_Customised]
*/
public function getCustomisedItems() {
$customised = new ArrayList();
foreach($this->getItems() as $file) {
$customised->push($this->customiseFile($file));
}
return $customised;
}
/**
* Retrieves the list of selected file IDs
*
* @return array
*/
public function getItemIDs() {
$value = $this->Value();
return empty($value['Files']) ? array() : $value['Files'];
}
public function Value() {
// Re-override FileField Value to use data value
return $this->dataValue();
}
public function saveInto(DataObjectInterface $record) {
// Check required relation details are available
$fieldname = $this->getName();
if(!$fieldname) return $this;
// Get details to save
$idList = $this->getItemIDs();
// Check type of relation
$relation = $record->hasMethod($fieldname) ? $record->$fieldname() : null;
if($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
// has_many or many_many
$relation->setByIDList($idList);
} elseif($record->has_one($fieldname)) {
// has_one
$record->{"{$fieldname}ID"} = $idList ? reset($idList) : 0;
}
return $this; return $this;
} }
/** /**
* @return SS_List * Customises a file with additional details suitable for rendering in the
*/ * UploadField.ss template
public function getItems() { *
$name = $this->getName();
if (!$this->items || !$this->items->exists()) {
$record = $this->getRecord();
$this->items = array();
// Try to auto-detect relationship
if ($record && $record->exists()) {
if ($record->has_many($name) || $record->many_many($name)) {
// Ensure relationship is cast to an array, as we can't alter the items of a DataList/RelationList
// (see below)
$this->items = $record->{$name}()->toArray();
} elseif($record->has_one($name)) {
$item = $record->{$name}();
if ($item && $item->exists())
$this->items = array($record->{$name}());
}
}
$this->items = new ArrayList($this->items);
// hack to provide $UploadFieldThumbnailURL, $hasRelation and $UploadFieldEditLink in template for each
// file
if ($this->items->exists()) {
foreach ($this->items as $i=>$file) {
$this->items[$i] = $this->customiseFile($file);
if(!$file->canView()) unset($this->items[$i]); // Respect model permissions
}
}
}
return $this->items;
}
/**
* Hack to add some Variables and a dynamic template to a File
* @param File $file * @param File $file
* @return ViewableData_Customised * @return ViewableData_Customised
*/ */
protected function customiseFile(File $file) { protected function customiseFile(File $file) {
$file = $file->customise(array( $file = $file->customise(array(
'UploadFieldHasRelation' => $this->managesRelation(),
'UploadFieldThumbnailURL' => $this->getThumbnailURLForFile($file), 'UploadFieldThumbnailURL' => $this->getThumbnailURLForFile($file),
'UploadFieldRemoveLink' => $this->getItemHandler($file->ID)->RemoveLink(),
'UploadFieldDeleteLink' => $this->getItemHandler($file->ID)->DeleteLink(), 'UploadFieldDeleteLink' => $this->getItemHandler($file->ID)->DeleteLink(),
'UploadFieldEditLink' => $this->getItemHandler($file->ID)->EditLink() 'UploadFieldEditLink' => $this->getItemHandler($file->ID)->EditLink(),
'UploadField' => $this
)); ));
// we do this in a second customise to have the access to the previous customisations // we do this in a second customise to have the access to the previous customisations
return $file->customise(array( return $file->customise(array(
@ -291,44 +500,409 @@ class UploadField extends FileField {
} }
/** /**
* Assign a front-end config variable for the upload field
*
* @param string $key * @param string $key
* @param mixed $val * @param mixed $val
* @return UploadField self reference
*/ */
public function setConfig($key, $val) { public function setConfig($key, $val) {
if(!array_key_exists($key, $this->ufConfig)) {
user_error("UploadField->setConfig called with invalid option: '$key'", E_USER_ERROR);
}
$this->ufConfig[$key] = $val; $this->ufConfig[$key] = $val;
return $this; return $this;
} }
/** /**
* Gets a front-end config variable for the upload field
*
* @param string $key * @param string $key
* @return mixed * @return mixed
*/ */
public function getConfig($key) { public function getConfig($key) {
if(!array_key_exists($key, $this->ufConfig)) {
user_error("UploadField->getConfig called with invalid option: '$key'", E_USER_ERROR);
}
return $this->ufConfig[$key]; return $this->ufConfig[$key];
} }
/** /**
* Used to get config in the template * Determine if the field should automatically upload the file.
*
* @return boolean
*/ */
public function getAutoUpload() { public function getAutoUpload() {
return $this->getConfig('autoUpload'); return $this->getConfig('autoUpload');
} }
/**
* Determine if the field should automatically upload the file
*
* @param boolean $autoUpload
* @return UploadField Self reference
*/
public function setAutoUpload($autoUpload) {
return $this->setConfig('autoUpload', $autoUpload);
}
/**
* Determine maximum number of files allowed to be attached
* Defaults to 1 for has_one and null (unlimited) for
* many_many and has_many relations.
*
* @return integer|null Maximum limit, or null for no limit
*/
public function getAllowedMaxFileNumber() {
$allowedMaxFileNumber = $this->getConfig('allowedMaxFileNumber');
// if there is a has_one relation with that name on the record and
// allowedMaxFileNumber has not been set, it's wanted to be 1
if(empty($allowedMaxFileNumber)) {
$record = $this->getRecord();
$name = $this->getName();
if($record && $record->has_one($name)) {
return 1; // Default for has_one
} else {
return null; // Default for has_many and many_many
}
} else {
return $allowedMaxFileNumber;
}
}
/**
* Limit allowed file extensions. Empty by default, allowing all extensions.
* To allow files without an extension, use an empty string.
* See {@link File::$allowed_extensions} to get a good standard set of
* extensions that are typically not harmful in a webserver context.
* See {@link setAllowedMaxFileSize()} to limit file size by extension.
*
* @param array $rules List of extensions
* @return UploadField Self reference
*/
public function setAllowedExtensions($rules) {
$this->getValidator()->setAllowedExtensions($rules);
return $this;
}
/**
* Limit allowed file extensions by specifying categories of file types.
* These may be 'image', 'audio', 'mov', 'zip', 'flash', or 'doc'
* See {@link File::$allowed_extensions} for details of allowed extensions
* for each of these categories
*
* @param string $category Category name
* @param string,... $categories Additional category names
* @return UploadField Self reference
*/
public function setAllowedFileCategories($category) {
$extensions = array();
$knownCategories = File::config()->app_categories;
// Parse arguments
$categories = func_get_args();
if(func_num_args() === 1 && is_array(reset($categories))) {
$categories = reset($categories);
}
// Merge all categories into list of extensions
foreach(array_filter($categories) as $category) {
if(isset($knownCategories[$category])) {
$extensions = array_merge($extensions, $knownCategories[$category]);
} else {
user_error("Unknown file category: $category", E_USER_ERROR);
}
}
return $this->setAllowedExtensions($extensions);
}
/**
* Returns list of extensions allowed by this field, or an empty array
* if there is no restriction
*
* @return array
*/
public function getAllowedExtensions() {
return $this->getValidator()->getAllowedExtensions();
}
/**
* Determine maximum number of files allowed to be attached.
*
* @param integer|null $allowedMaxFileNumber Maximum limit. 0 or null will be treated as unlimited
* @return UploadField Self reference
*/
public function setAllowedMaxFileNumber($allowedMaxFileNumber) {
return $this->setConfig('allowedMaxFileNumber', $allowedMaxFileNumber);
}
/**
* Determine if the user has permission to upload.
*
* @return boolean
*/
public function canUpload() {
if(!$this->isActive()) return false;
$can = $this->getConfig('canUpload');
return (is_bool($can)) ? $can : Permission::check($can);
}
/**
* Specify whether the user can upload files.
* String values will be treated as required permission codes
*
* @param boolean|string $canUpload Either a boolean flag, or a required
* permission code
* @return UploadField Self reference
*/
public function setCanUpload($canUpload) {
return $this->setConfig('canUpload', $canUpload);
}
/**
* Determine if the user has permission to attach existing files
* By default returns true if the user has the CMS_ACCESS_AssetAdmin permission
*
* @return boolean
*/
public function canAttachExisting() {
if(!$this->isActive()) return false;
$can = $this->getConfig('canAttachExisting');
return (is_bool($can)) ? $can : Permission::check($can);
}
/**
* Returns true if the field is neither readonly nor disabled
*
* @return boolean
*/
public function isActive() {
return !$this->isDisabled() && !$this->isReadonly();
}
/**
* Specify whether the user can attach existing files
* String values will be treated as required permission codes
*
* @param boolean|string $canAttachExisting Either a boolean flag, or a
* required permission code
* @return UploadField Self reference
*/
public function setCanAttachExisting($canAttachExisting) {
return $this->setConfig('canAttachExisting', $canAttachExisting);
}
/**
* Gets thumbnail width. Defaults to 80
*
* @return integer
*/
public function getPreviewMaxWidth() {
return $this->getConfig('previewMaxWidth');
}
/**
* @see UploadField::getPreviewMaxWidth()
*
* @param integer $previewMaxWidth
* @return UploadField Self reference
*/
public function setPreviewMaxWidth($previewMaxWidth) {
return $this->setConfig('previewMaxWidth', $previewMaxWidth);
}
/**
* Gets thumbnail height. Defaults to 60
*
* @return integer
*/
public function getPreviewMaxHeight() {
return $this->getConfig('previewMaxHeight');
}
/**
* @see UploadField::getPreviewMaxHeight()
*
* @param integer $previewMaxHeight
* @return UploadField Self reference
*/
public function setPreviewMaxHeight($previewMaxHeight) {
return $this->setConfig('previewMaxHeight', $previewMaxHeight);
}
/**
* javascript template used to display uploading files
* Defaults to 'ss-uploadfield-uploadtemplate'
*
* @see javascript/UploadField_uploadtemplate.js
* @var string
*/
public function getUploadTemplateName() {
return $this->getConfig('uploadTemplateName');
}
/**
* @see UploadField::getUploadTemplateName()
*
* @param string $uploadTemplateName
* @return UploadField Self reference
*/
public function setUploadTemplateName($uploadTemplateName) {
return $this->setConfig('uploadTemplateName', $uploadTemplateName);
}
/**
* javascript template used to display already uploaded files
* Defaults to 'ss-downloadfield-downloadtemplate'
*
* @see javascript/DownloadField_downloadtemplate.js
* @var string
*/
public function getDownloadTemplateName() {
return $this->getConfig('downloadTemplateName');
}
/**
* @see Uploadfield::getDownloadTemplateName()
*
* @param string $downloadTemplateName
* @return Uploadfield Self reference
*/
public function setDownloadTemplateName($downloadTemplateName) {
return $this->setConfig('downloadTemplateName', $downloadTemplateName);
}
/**
* FieldList $fields for the EditForm
* @example 'getCMSFields'
*
* @param File $file File context to generate fields for
* @return FieldList List of form fields
*/
public function getFileEditFields(File $file) {
// Empty actions, generate default
if(empty($this->fileEditFields)) {
$fields = $file->getCMSFields();
// Only display main tab, to avoid overly complex interface
if($fields->hasTabSet() && ($mainTab = $fields->findOrMakeTab('Root.Main'))) {
$fields = $mainTab->Fields();
}
return $fields;
}
// Fields instance
if ($this->fileEditFields instanceof FieldList) return $this->fileEditFields;
// Method to call on the given file
if($file->hasMethod($this->fileEditFields)) {
return $file->{$this->fileEditFields}();
}
user_error("Invalid value for UploadField::fileEditFields", E_USER_ERROR);
}
/**
* FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm
* @example 'getCMSFields'
*
* @param FieldList|string
* @return Uploadfield Self reference
*/
public function setFileEditFields($fileEditFields) {
$this->fileEditFields = $fileEditFields;
return $this;
}
/**
* FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
* @example 'getCMSActions'
*
* @param File $file File context to generate form actions for
* @return FieldList Field list containing FormAction
*/
public function getFileEditActions(File $file) {
// Empty actions, generate default
if(empty($this->fileEditActions)) {
$actions = new FieldList($saveAction = new FormAction('doEdit', _t('UploadField.DOEDIT', 'Save')));
$saveAction->addExtraClass('ss-ui-action-constructive icon-accept');
return $actions;
}
// Actions instance
if ($this->fileEditActions instanceof FieldList) return $this->fileEditActions;
// Method to call on the given file
if($file->hasMethod($this->fileEditActions)) {
return $file->{$this->fileEditActions}();
}
user_error("Invalid value for UploadField::fileEditActions", E_USER_ERROR);
}
/**
* FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
* @example 'getCMSActions'
*
* @param FieldList|string
* @return Uploadfield Self reference
*/
public function setFileEditActions($fileEditActions) {
$this->fileEditActions = $fileEditActions;
return $this;
}
/**
* Determines the validator to use for the edit form
* @example 'getCMSValidator'
*
* @param File $file File context to generate validator from
* @return Validator Validator object
*/
public function getFileEditValidator(File $file) {
// Empty validator
if(empty($this->fileEditValidator)) return null;
// Validator instance
if($this->fileEditValidator instanceof Validator) return $this->fileEditValidator;
// Method to call on the given file
if($file->hasMethod($this->fileEditValidator)) {
return $file->{$this->fileEditValidator}();
}
user_error("Invalid value for UploadField::fileEditValidator", E_USER_ERROR);
}
/**
* Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm
* @example 'getCMSValidator'
*
* @param Validator|string
* @return Uploadfield Self reference
*/
public function setFileEditValidator($fileEditValidator) {
$this->fileEditValidator = $fileEditValidator;
return $this;
}
/** /**
* @param File $file * @param File $file
* @return string * @return string
*/ */
protected function getThumbnailURLForFile(File $file) { protected function getThumbnailURLForFile(File $file) {
if ($file && $file->exists() && file_exists(Director::baseFolder() . '/' . $file->getFilename())) { if ($file->exists() && file_exists(Director::baseFolder() . '/' . $file->getFilename())) {
$width = $this->getPreviewMaxWidth();
$height = $this->getPreviewMaxHeight();
if ($file->hasMethod('getThumbnail')) { if ($file->hasMethod('getThumbnail')) {
return $file->getThumbnail($this->getConfig('previewMaxWidth'), return $file->getThumbnail($width, $height)->getURL();
$this->getConfig('previewMaxHeight'))->getURL();
} elseif ($file->hasMethod('getThumbnailURL')) { } elseif ($file->hasMethod('getThumbnailURL')) {
return $file->getThumbnailURL($this->getConfig('previewMaxWidth'), return $file->getThumbnailURL($width, $height);
$this->getConfig('previewMaxHeight'));
} elseif ($file->hasMethod('SetRatioSize')) { } elseif ($file->hasMethod('SetRatioSize')) {
return $file->SetRatioSize($this->getConfig('previewMaxWidth'), return $file->SetRatioSize($width, $height)->getURL();
$this->getConfig('previewMaxHeight'))->getURL();
} else { } else {
return $file->Icon(); return $file->Icon();
} }
@ -350,18 +924,6 @@ class UploadField extends FileField {
} }
public function Field($properties = array()) { public function Field($properties = array()) {
$record = $this->getRecord();
$name = $this->getName();
// if there is a has_one relation with that name on the record and
// allowedMaxFileNumber has not been set, it's wanted to be 1
if(
$record && $record->exists()
&& $record->has_one($name) && !$this->getConfig('allowedMaxFileNumber')
) {
$this->setConfig('allowedMaxFileNumber', 1);
}
Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
@ -369,7 +931,8 @@ class UploadField extends FileField {
Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/ssui.core.js'); Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/ssui.core.js');
Requirements::combine_files('uploadfield.js', array( Requirements::combine_files('uploadfield.js', array(
THIRDPARTY_DIR . '/javascript-templates/tmpl.js', // @todo jquery templates is a project no longer maintained and should be retired at some point.
THIRDPARTY_DIR . '/javascript-templates/tmpl.js',
THIRDPARTY_DIR . '/javascript-loadimage/load-image.js', THIRDPARTY_DIR . '/javascript-loadimage/load-image.js',
THIRDPARTY_DIR . '/jquery-fileupload/jquery.iframe-transport.js', THIRDPARTY_DIR . '/jquery-fileupload/jquery.iframe-transport.js',
THIRDPARTY_DIR . '/jquery-fileupload/cors/jquery.xdr-transport.js', THIRDPARTY_DIR . '/jquery-fileupload/cors/jquery.xdr-transport.js',
@ -382,58 +945,120 @@ class UploadField extends FileField {
Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css'); // TODO hmmm, remove it? Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css'); // TODO hmmm, remove it?
Requirements::css(FRAMEWORK_DIR . '/css/UploadField.css'); Requirements::css(FRAMEWORK_DIR . '/css/UploadField.css');
// Calculated config as per jquery.fileupload-ui.js
$allowedMaxFileNumber = $this->getAllowedMaxFileNumber();
$config = array( $config = array(
'url' => $this->Link('upload'), 'url' => $this->Link('upload'),
'urlSelectDialog' => $this->Link('select'), 'urlSelectDialog' => $this->Link('select'),
'urlAttach' => $this->Link('attach'), 'urlAttach' => $this->Link('attach'),
'urlFileExists' => $this->link('fileexists'),
'acceptFileTypes' => '.+$', 'acceptFileTypes' => '.+$',
'maxNumberOfFiles' => $this->getConfig('allowedMaxFileNumber') // Fileupload treats maxNumberOfFiles as the max number of _additional_ items allowed
'maxNumberOfFiles' => $allowedMaxFileNumber ? ($allowedMaxFileNumber - count($this->getItemIDs())) : null
); );
if (count($this->getValidator()->getAllowedExtensions())) {
$allowedExtensions = $this->getValidator()->getAllowedExtensions(); // Validation: File extensions
if ($allowedExtensions = $this->getAllowedExtensions()) {
$config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$'; $config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$';
$config['errorMessages']['acceptFileTypes'] = _t( $config['errorMessages']['acceptFileTypes'] = _t(
'File.INVALIDEXTENSIONSHORT', 'File.INVALIDEXTENSIONSHORT',
'Extension is not allowed' 'Extension is not allowed'
); );
} }
if ($this->getValidator()->getAllowedMaxFileSize()) {
$config['maxFileSize'] = $this->getValidator()->getAllowedMaxFileSize(); // Validation: File size
if ($allowedMaxFileSize = $this->getValidator()->getAllowedMaxFileSize()) {
$config['maxFileSize'] = $allowedMaxFileSize;
$config['errorMessages']['maxFileSize'] = _t( $config['errorMessages']['maxFileSize'] = _t(
'File.TOOLARGESHORT', 'File.TOOLARGESHORT',
'Filesize exceeds {size}', 'Filesize exceeds {size}',
array('size' => File::format_size($config['maxFileSize'])) array('size' => File::format_size($config['maxFileSize']))
); );
} }
if ($config['maxNumberOfFiles'] > 1) {
$config['errorMessages']['maxNumberOfFiles'] = _t( // Validation: Number of files
'UploadField.MAXNUMBEROFFILESSHORT', if ($allowedMaxFileNumber) {
'Can only upload {count} files', if($allowedMaxFileNumber > 1) {
array('count' => $config['maxNumberOfFiles']) $config['errorMessages']['maxNumberOfFiles'] = _t(
); 'UploadField.MAXNUMBEROFFILESSHORT',
'Can only upload {count} files',
array('count' => $allowedMaxFileNumber)
);
} else {
$config['errorMessages']['maxNumberOfFiles'] = _t(
'UploadField.MAXNUMBEROFFILESONE',
'Can only upload one file'
);
}
} }
$configOverwrite = array();
if (is_numeric($config['maxNumberOfFiles']) && $this->getItems()->count()) { //get all the existing files in the current folder
$configOverwrite['maxNumberOfFiles'] = $config['maxNumberOfFiles'] - $this->getItems()->count(); if ($this->getOverwriteWarning()) {
//add overwrite warning error message to the config object sent to Javascript
$config['errorMessages']['overwriteWarning'] =
_t('UploadField.OVERWRITEWARNING', 'File with the same name already exists');
} }
$config = array_merge($config, $this->ufConfig, $configOverwrite); $mergedConfig = array_merge($config, $this->ufConfig);
return $this->customise(array( return $this->customise(array(
'configString' => str_replace('"', "'", Convert::raw2json($config)), 'configString' => str_replace('"', "'", Convert::raw2json($mergedConfig)),
'config' => new ArrayData($config), 'config' => new ArrayData($mergedConfig),
'multiple' => $config['maxNumberOfFiles'] !== 1, 'multiple' => $allowedMaxFileNumber !== 1
'displayInput' => (!isset($configOverwrite['maxNumberOfFiles']) || $configOverwrite['maxNumberOfFiles'])
))->renderWith($this->getTemplates()); ))->renderWith($this->getTemplates());
} }
/** /**
* Validation method for this field, called when the entire form is validated * Validation method for this field, called when the entire form is validated
* *
* @param $validator * @param Validator $validator
* @return Boolean * @return boolean
*/ */
public function validate($validator) { public function validate($validator) {
// @todo Test compatibility with RequiredFields
$name = $this->getName();
$files = $this->getItems();
// If there are no files then quit
if($files->count() == 0) return true;
// Check max number of files
$maxFiles = $this->getAllowedMaxFileNumber();
if($maxFiles && ($files->count() > $maxFiles)) {
$validator->validationError(
$name,
_t(
'UploadField.MAXNUMBEROFFILES',
'Max number of {count} file(s) exceeded.',
array('count' => $maxFiles)
),
"validation"
);
return false;
}
// Revalidate each file against nested validator
$this->upload->clearErrors();
foreach($files as $file) {
// Generate $_FILES style file attribute array for upload validator
$tmpFile = array(
'name' => $file->Name,
'type' => null, // Not used for type validation
'size' => $file->AbsoluteSize,
'tmp_name' => null, // Should bypass is_uploaded_file check
'error' => UPLOAD_ERR_OK,
);
$this->upload->validate($tmpFile);
}
// Check all errors
if($errors = $this->upload->getErrors()) {
foreach($errors as $error) {
$validator->validationError($name, $error, "validation");
}
return false;
}
return true; return true;
} }
@ -460,12 +1085,117 @@ class UploadField extends FileField {
public function handleSelect(SS_HTTPRequest $request) { public function handleSelect(SS_HTTPRequest $request) {
return UploadField_SelectHandler::create($this, $this->getFolderName()); return UploadField_SelectHandler::create($this, $this->getFolderName());
} }
/**
* Given an array of post variables, extract all temporary file data into an array
*
* @param array $postVars Array of posted form data
* @return array List of temporary file data
*/
protected function extractUploadedFileData($postVars) {
// Note: Format of posted file parameters in php is a feature of using
// <input name='{$Name}[Uploads][]' /> for multiple file uploads
$tmpFiles = array();
if( !empty($postVars['tmp_name'])
&& is_array($postVars['tmp_name'])
&& !empty($postVars['tmp_name']['Uploads'])
) {
for($i = 0; $i < count($postVars['tmp_name']['Uploads']); $i++) {
// Skip if "empty" file
if(empty($postVars['tmp_name']['Uploads'][$i])) continue;
$tmpFile = array();
foreach(array('name', 'type', 'tmp_name', 'error', 'size') as $field) {
$tmpFile[$field] = $postVars[$field]['Uploads'][$i];
}
$tmpFiles[] = $tmpFile;
}
} elseif(!empty($postVars['tmp_name'])) {
// Fallback to allow single file uploads (method used by AssetUploadField)
$tmpFiles[] = $postVars;
}
return $tmpFiles;
}
/**
* Loads the temporary file data into a File object
*
* @param array $tmpFile Temporary file data
* @param string $error Error message
* @return File File object, or null if error
*/
protected function saveTemporaryFile($tmpFile, &$error = null) {
// Determine container object
$error = null;
$fileObject = null;
if (empty($tmpFile)) {
$error = _t('UploadField.FIELDNOTSET', 'File information not found');
return null;
}
if($tmpFile['error']) {
$error = $tmpFile['error'];
return null;
}
// Search for relations that can hold the uploaded files, but don't fallback
// to default if there is no automatic relation
if ($relationClass = $this->getRelationAutosetClass(null)) {
// Create new object explicitly. Otherwise rely on Upload::load to choose the class.
$fileObject = Object::create($relationClass);
}
// Get the uploaded file into a new file object.
try {
$this->upload->loadIntoFile($tmpFile, $fileObject, $this->getFolderName());
} catch (Exception $e) {
// we shouldn't get an error here, but just in case
$error = $e->getMessage();
return null;
}
// Check if upload field has an error
if ($this->upload->isError()) {
$error = implode(' ' . PHP_EOL, $this->upload->getErrors());
return null;
}
// return file
return $this->upload->getFile();
}
/**
* Safely encodes the File object with all standard fields required
* by the front end
*
* @param File $file
* @return array Array encoded list of file attributes
*/
protected function encodeFileAttributes(File $file) {
// Collect all output data.
$file = $this->customiseFile($file);
return array(
'id' => $file->ID,
'name' => $file->Name,
'url' => $file->URL,
'thumbnail_url' => $file->UploadFieldThumbnailURL,
'edit_url' => $file->UploadFieldEditLink,
'size' => $file->AbsoluteSize,
'type' => $file->FileType,
'buttons' => $file->UploadFieldFileButtons,
'fieldname' => $this->getName()
);
}
/** /**
* Action to handle upload of a single file * Action to handle upload of a single file
* *
* @param SS_HTTPRequest $request * @param SS_HTTPRequest $request
* @return string json * @return SS_HTTPResponse
*/ */
public function upload(SS_HTTPRequest $request) { public function upload(SS_HTTPRequest $request) {
if($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) { if($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) {
@ -475,144 +1205,75 @@ class UploadField extends FileField {
// Protect against CSRF on destructive action // Protect against CSRF on destructive action
$token = $this->getForm()->getSecurityToken(); $token = $this->getForm()->getSecurityToken();
if(!$token->checkRequest($request)) return $this->httpError(400); if(!$token->checkRequest($request)) return $this->httpError(400);
$name = $this->getName();
$tmpfile = $request->postVar($name);
$record = $this->getRecord();
// Check if the file has been uploaded into the temporary storage. // Get form details
if (!$tmpfile) { $name = $this->getName();
$return = array('error' => _t('UploadField.FIELDNOTSET', 'File information not found')); $postVars = $request->postVar($name);
// Save the temporary file into a File object
$uploadedFiles = $this->extractUploadedFileData($postVars);
$firstFile = reset($uploadedFiles);
$file = $this->saveTemporaryFile($firstFile, $error);
if(empty($file)) {
$return = array('error' => $error);
} else { } else {
$return = array( $return = $this->encodeFileAttributes($file);
'name' => $tmpfile['name'],
'size' => $tmpfile['size'],
'type' => $tmpfile['type'],
'error' => $tmpfile['error']
);
}
// Check for constraints on the record to which the file will be attached.
if (!$return['error'] && $this->relationAutoSetting && $record && $record->exists()) {
$tooManyFiles = false;
// Some relationships allow many files to be attached.
if ($this->getConfig('allowedMaxFileNumber') && ($record->has_many($name) || $record->many_many($name))) {
if(!$record->isInDB()) $record->write();
$tooManyFiles = $record->{$name}()->count() >= $this->getConfig('allowedMaxFileNumber');
// has_one only allows one file at any given time.
} elseif($record->has_one($name)) {
// If we're allowed to replace an existing file, clear out the old one
if($record->$name && $this->getConfig('replaceExistingFile')) {
$record->$name = null;
}
$tooManyFiles = $record->{$name}() && $record->{$name}()->exists();
}
// Report the constraint violation.
if ($tooManyFiles) {
if(!$this->getConfig('allowedMaxFileNumber')) $this->setConfig('allowedMaxFileNumber', 1);
$return['error'] = _t(
'UploadField.MAXNUMBEROFFILES',
'Max number of {count} file(s) exceeded.',
array('count' => $this->getConfig('allowedMaxFileNumber'))
);
}
}
// Process the uploaded file
if (!$return['error']) {
$fileObject = null;
if ($this->relationAutoSetting) {
// Search for relations that can hold the uploaded files.
if ($relationClass = $this->getRelationAutosetClass()) {
// Create new object explicitly. Otherwise rely on Upload::load to choose the class.
$fileObject = Object::create($relationClass);
}
}
// Get the uploaded file into a new file object.
try {
$this->upload->loadIntoFile($tmpfile, $fileObject, $this->getFolderName());
} catch (Exception $e) {
// we shouldn't get an error here, but just in case
$return['error'] = $e->getMessage();
}
if (!$return['error']) {
if ($this->upload->isError()) {
$return['error'] = implode(' '.PHP_EOL, $this->upload->getErrors());
} else {
$file = $this->upload->getFile();
// Attach the file to the related record.
if ($this->relationAutoSetting) {
$this->attachFile($file);
}
// Collect all output data.
$file = $this->customiseFile($file);
$return = array_merge($return, array(
'id' => $file->ID,
'name' => $file->getTitle() . '.' . $file->getExtension(),
'url' => $file->getURL(),
'thumbnail_url' => $file->UploadFieldThumbnailURL,
'edit_url' => $file->UploadFieldEditLink,
'size' => $file->getAbsoluteSize(),
'buttons' => $file->UploadFieldFileButtons
));
}
}
} }
// Format response with json
$response = new SS_HTTPResponse(Convert::raw2json(array($return))); $response = new SS_HTTPResponse(Convert::raw2json(array($return)));
$response->addHeader('Content-Type', 'text/plain'); $response->addHeader('Content-Type', 'text/plain');
return $response; return $response;
} }
/** /**
* Add existing {@link File} records to the relationship. * Retrieves details for files that this field wishes to attache to the
* client-side form
*
* @param SS_HTTPRequest $request
* @return SS_HTTPResponse
*/ */
public function attach($request) { public function attach(SS_HTTPRequest $request) {
if(!$request->isPOST()) return $this->httpError(403); if(!$request->isPOST()) return $this->httpError(403);
if(!$this->managesRelation()) return $this->httpError(403);
if(!$this->canAttachExisting()) return $this->httpError(403); if(!$this->canAttachExisting()) return $this->httpError(403);
// Retrieve file attributes required by front end
$return = array(); $return = array();
$files = File::get()->byIDs($request->postVar('ids')); $files = File::get()->byIDs($request->postVar('ids'));
foreach($files as $file) { foreach($files as $file) {
$this->attachFile($file); $return[] = $this->encodeFileAttributes($file);
$file = $this->customiseFile($file);
$return[] = array(
'id' => $file->ID,
'name' => $file->getTitle() . '.' . $file->getExtension(),
'url' => $file->getURL(),
'thumbnail_url' => $file->UploadFieldThumbnailURL,
'edit_url' => $file->UploadFieldEditLink,
'size' => $file->getAbsoluteSize(),
'buttons' => $file->UploadFieldFileButtons
);
} }
$response = new SS_HTTPResponse(Convert::raw2json($return)); $response = new SS_HTTPResponse(Convert::raw2json($return));
$response->addHeader('Content-Type', 'application/json'); $response->addHeader('Content-Type', 'application/json');
return $response; return $response;
} }
/** /**
* @param File * Determines if a specified file exists
*
* @param SS_HTTPRequest $request
*/ */
protected function attachFile($file) { public function fileexists(SS_HTTPRequest $request) {
$record = $this->getRecord();
$name = $this->getName(); // Check both original and safely filtered filename
if ($record && $record->exists()) { $originalFile = $request->requestVar('filename');
if ($record->has_many($name) || $record->many_many($name)) { $nameFilter = FileNameFilter::create();
if(!$record->isInDB()) $record->write(); $filteredFile = basename($nameFilter->filter($originalFile));
$record->{$name}()->add($file);
} elseif($record->has_one($name)) { // check if either file exists
$record->{$name . 'ID'} = $file->ID; $folder = $this->getFolderName();
$record->write(); $exists = false;
foreach(array($originalFile, $filteredFile) as $file) {
if(file_exists(ASSETS_PATH."/$folder/$file")) {
$exists = true;
break;
} }
} }
// Encode and present response
$response = new SS_HTTPResponse(Convert::raw2json(array('exists' => $exists)));
$response->addHeader('Content-Type', 'application/json');
return $response;
} }
public function performReadonlyTransformation() { public function performReadonlyTransformation() {
@ -623,55 +1284,26 @@ class UploadField extends FileField {
} }
/** /**
* Determines if the underlying record (if any) has a relationship * Gets the foreign class that needs to be created, or 'File' as default if there
* matching the field name. Important for permission control. * is no relationship, or it cannot be determined.
*
* @return boolean
*/
public function managesRelation() {
$record = $this->getRecord();
$fieldName = $this->getName();
return (
$record
&& ($record->has_one($fieldName) || $record->has_many($fieldName) || $record->many_many($fieldName))
);
}
/**
* Gets the foreign class that needs to be created.
* *
* @param $default Default value to return if no value could be calculated
* @return string Foreign class name. * @return string Foreign class name.
*/ */
public function getRelationAutosetClass() { public function getRelationAutosetClass($default = 'File') {
// Don't autodetermine relation if no relationship between parent record
if(!$this->relationAutoSetting) return $default;
// Check record and name
$name = $this->getName(); $name = $this->getName();
$record = $this->getRecord(); $record = $this->getRecord();
if(empty($name) || empty($record)) {
if (isset($name) && isset($record)) return $record->getRelationClass($name); return $default;
} } else {
$class = $record->getRelationClass($name);
public function isDisabled() { return empty($class) ? $default : $class;
return (parent::isDisabled() || !$this->isSaveable()); }
}
/**
* Determines if the field can be saved into a database record.
*
* @return boolean
*/
public function isSaveable() {
$record = $this->getRecord();
// Don't allow upload or edit of a relation when the underlying record hasn't been persisted yet
return (!$record || !$this->managesRelation() || $record->exists());
}
public function canUpload() {
$can = $this->getConfig('canUpload');
return (is_bool($can)) ? $can : Permission::check($can);
}
public function canAttachExisting() {
$can = $this->getConfig('canAttachExisting');
return (is_bool($can)) ? $can : Permission::check($can);
} }
} }
@ -726,14 +1358,6 @@ class UploadField_ItemHandler extends RequestHandler {
return Controller::join_links($this->parent->Link(), '/item/', $this->itemID, $action); return Controller::join_links($this->parent->Link(), '/item/', $this->itemID, $action);
} }
/**
* @return string
*/
public function RemoveLink() {
$token = $this->parent->getForm()->getSecurityToken();
return $token->addToUrl($this->Link('remove'));
}
/** /**
* @return string * @return string
*/ */
@ -749,42 +1373,6 @@ class UploadField_ItemHandler extends RequestHandler {
return $this->Link('edit'); return $this->Link('edit');
} }
/**
* Action to handle removing a single file from the db relation
*
* @param SS_HTTPRequest $request
* @return SS_HTTPResponse
*/
public function remove(SS_HTTPRequest $request) {
// Check form field state
if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403);
// Protect against CSRF on destructive action
$token = $this->parent->getForm()->getSecurityToken();
if(!$token->checkRequest($request)) return $this->httpError(400);
$response = new SS_HTTPResponse();
$response->setStatusCode(500);
$fieldName = $this->parent->getName();
$record = $this->parent->getRecord();
$id = $this->getItem()->ID;
if ($id && $record && $record->exists()) {
if (($record->has_many($fieldName) || $record->many_many($fieldName))
&& $file = $record->{$fieldName}()->byID($id)) {
$record->{$fieldName}()->remove($file);
$response->setStatusCode(200);
} elseif($record->has_one($fieldName) && $record->{$fieldName . 'ID'} == $id) {
$record->{$fieldName . 'ID'} = 0;
$record->write();
$response->setStatusCode(200);
}
}
if ($response->getStatusCode() != 200)
$response->setStatusDescription(_t('UploadField.REMOVEERROR', 'Error removing file'));
return $response;
}
/** /**
* Action to handle deleting of a single file * Action to handle deleting of a single file
* *
@ -804,14 +1392,9 @@ class UploadField_ItemHandler extends RequestHandler {
if(!$item) return $this->httpError(404); if(!$item) return $this->httpError(404);
if(!$item->canDelete()) return $this->httpError(403); if(!$item->canDelete()) return $this->httpError(403);
// Only allow actions on files in the managed relation (if one exists) // Delete the file from the filesystem. The file will be removed
$items = $this->parent->getItems(); // from the relation on save
if($this->parent->managesRelation() && !$items->byID($item->ID)) return $this->httpError(403); // @todo Investigate if references to deleted files (if unsaved) is dangerous
// First remove the file from the current relationship
$this->remove($request);
// Then delete the file from the filesystem
$item->delete(); $item->delete();
} }
@ -830,10 +1413,6 @@ class UploadField_ItemHandler extends RequestHandler {
if(!$item) return $this->httpError(404); if(!$item) return $this->httpError(404);
if(!$item->canEdit()) return $this->httpError(403); if(!$item->canEdit()) return $this->httpError(403);
// Only allow actions on files in the managed relation (if one exists)
$items = $this->parent->getItems();
if($this->parent->managesRelation() && !$items->byID($item->ID)) return $this->httpError(403);
Requirements::css(FRAMEWORK_DIR . '/css/UploadField.css'); Requirements::css(FRAMEWORK_DIR . '/css/UploadField.css');
return $this->customise(array( return $this->customise(array(
@ -846,30 +1425,10 @@ class UploadField_ItemHandler extends RequestHandler {
*/ */
public function EditForm() { public function EditForm() {
$file = $this->getItem(); $file = $this->getItem();
if (is_a($this->parent->getConfig('fileEditFields'), 'FieldList')) { // Get form components
$fields = $this->parent->getConfig('fileEditFields'); $fields = $this->parent->getFileEditFields($file);
} elseif ($file->hasMethod($this->parent->getConfig('fileEditFields'))) { $actions = $this->parent->getFileEditActions($file);
$fields = $file->{$this->parent->getConfig('fileEditFields')}(); $validator = $this->parent->getFileEditValidator($file);
} else {
$fields = $file->getCMSFields();
// Only display main tab, to avoid overly complex interface
if($fields->hasTabSet() && $mainTab = $fields->findOrMakeTab('Root.Main')) $fields = $mainTab->Fields();
}
if (is_a($this->parent->getConfig('fileEditActions'), 'FieldList')) {
$actions = $this->parent->getConfig('fileEditActions');
} elseif ($file->hasMethod($this->parent->getConfig('fileEditActions'))) {
$actions = $file->{$this->parent->getConfig('fileEditActions')}();
} else {
$actions = new FieldList($saveAction = new FormAction('doEdit', _t('UploadField.DOEDIT', 'Save')));
$saveAction->addExtraClass('ss-ui-action-constructive icon-accept');
}
if (is_a($this->parent->getConfig('fileEditValidator'), 'Validator')) {
$validator = $this->parent->getConfig('fileEditValidator');
} elseif ($file->hasMethod($this->parent->getConfig('fileEditValidator'))) {
$validator = $file->{$this->parent->getConfig('fileEditValidator')}();
} else {
$validator = null;
}
$form = new Form( $form = new Form(
$this, $this,
__FUNCTION__, __FUNCTION__,
@ -896,10 +1455,6 @@ class UploadField_ItemHandler extends RequestHandler {
$item = $this->getItem(); $item = $this->getItem();
if(!$item) return $this->httpError(404); if(!$item) return $this->httpError(404);
// Only allow actions on files in the managed relation (if one exists)
$items = $this->parent->getItems();
if($this->parent->managesRelation() && !$items->byID($item->ID)) return $this->httpError(403);
$form->saveInto($item); $form->saveInto($item);
$item->write(); $item->write();
@ -921,7 +1476,7 @@ class UploadField_SelectHandler extends RequestHandler {
protected $parent; protected $parent;
/** /**
* @var String * @var string
*/ */
protected $folderName; protected $folderName;
@ -961,7 +1516,7 @@ class UploadField_SelectHandler extends RequestHandler {
$folderID = $this->parent->getRequest()->requestVar('ParentID'); $folderID = $this->parent->getRequest()->requestVar('ParentID');
if (!isset($folderID)) { if (!isset($folderID)) {
$folder = Folder::find_or_make($this->folderName); $folder = Folder::find_or_make($this->folderName);
$folderID = $folder->ID; $folderID = $folder ? $folder->ID : 0;
} }
// Construct the form // Construct the form
@ -988,7 +1543,7 @@ class UploadField_SelectHandler extends RequestHandler {
// Generate the folder selection field. // Generate the folder selection field.
$folderField = new TreeDropdownField('ParentID', _t('HtmlEditorField.FOLDER', 'Folder'), 'Folder'); $folderField = new TreeDropdownField('ParentID', _t('HtmlEditorField.FOLDER', 'Folder'), 'Folder');
$folderField->setValue($folderID); $folderField->setValue($folderID);
// Generate the file list field. // Generate the file list field.
$config = GridFieldConfig::create(); $config = GridFieldConfig::create();
$config->addComponent(new GridFieldSortableHeader()); $config->addComponent(new GridFieldSortableHeader());
@ -997,20 +1552,16 @@ class UploadField_SelectHandler extends RequestHandler {
$config->addComponent(new GridFieldPaginator(10)); $config->addComponent(new GridFieldPaginator(10));
// If relation is to be autoset, we need to make sure we only list compatible objects. // If relation is to be autoset, we need to make sure we only list compatible objects.
$baseClass = null; $baseClass = $this->parent->getRelationAutosetClass();
if ($this->parent->relationAutoSetting) {
$baseClass = $this->parent->getRelationAutosetClass();
}
// By default we can attach anything that is a file, or derives from file.
if (!$baseClass) $baseClass = 'File';
// Create the data source for the list of files within the current directory. // Create the data source for the list of files within the current directory.
$files = DataList::create($baseClass)->filter('ParentID', $folderID); $files = DataList::create($baseClass)->filter('ParentID', $folderID);
$fileField = new GridField('Files', false, $files, $config); $fileField = new GridField('Files', false, $files, $config);
$fileField->setAttribute('data-selectable', true); $fileField->setAttribute('data-selectable', true);
if($this->parent->getConfig('allowedMaxFileNumber') > 1) $fileField->setAttribute('data-multiselect', true); if($this->parent->getAllowedMaxFileNumber() !== 1) {
$fileField->setAttribute('data-multiselect', true);
}
$selectComposite = new CompositeField( $selectComposite = new CompositeField(
$folderField, $folderField,
@ -1021,7 +1572,7 @@ class UploadField_SelectHandler extends RequestHandler {
} }
public function doAttach($data, $form) { public function doAttach($data, $form) {
// TODO Only implemented via JS for now // Popup-window attach does not require server side action, as it is implemented via JS
} }
} }

View File

@ -114,7 +114,7 @@ class GridFieldFilterHeader implements GridField_HTMLProvider, GridField_DataMan
$currentColumn++; $currentColumn++;
$metadata = $gridField->getColumnMetadata($columnField); $metadata = $gridField->getColumnMetadata($columnField);
$title = $metadata['title']; $title = $metadata['title'];
$fields = new FieldGroup();
if($title && $gridField->getList()->canFilterBy($columnField)) { if($title && $gridField->getList()->canFilterBy($columnField)) {
$value = ''; $value = '';
@ -128,34 +128,33 @@ class GridFieldFilterHeader implements GridField_HTMLProvider, GridField_DataMan
$field->setAttribute('placeholder', $field->setAttribute('placeholder',
_t('GridField.FilterBy', "Filter by ") . _t('GridField.'.$metadata['title'], $metadata['title'])); _t('GridField.FilterBy', "Filter by ") . _t('GridField.'.$metadata['title'], $metadata['title']));
$field = new FieldGroup( $fields->push($field);
$field, $fields->push(
GridField_FormAction::create($gridField, 'reset', false, 'reset', null) GridField_FormAction::create($gridField, 'reset', false, 'reset', null)
->addExtraClass('ss-gridfield-button-reset') ->addExtraClass('ss-gridfield-button-reset')
->setAttribute('title', _t('GridField.ResetFilter', "Reset")) ->setAttribute('title', _t('GridField.ResetFilter', "Reset"))
->setAttribute('id', 'action_reset_' . $gridField->getModelClass() . '_' . $columnField) ->setAttribute('id', 'action_reset_' . $gridField->getModelClass() . '_' . $columnField)
); );
} else { }
if($currentColumn == count($columns)){
$field = new FieldGroup( if($currentColumn == count($columns)){
GridField_FormAction::create($gridField, 'filter', false, 'filter', null) $fields->push(
->addExtraClass('ss-gridfield-button-filter') GridField_FormAction::create($gridField, 'filter', false, 'filter', null)
->setAttribute('title', _t('GridField.Filter', "Filter")) ->addExtraClass('ss-gridfield-button-filter')
->setAttribute('id', 'action_filter_' . $gridField->getModelClass() . '_' . $columnField), ->setAttribute('title', _t('GridField.Filter', "Filter"))
GridField_FormAction::create($gridField, 'reset', false, 'reset', null) ->setAttribute('id', 'action_filter_' . $gridField->getModelClass() . '_' . $columnField)
->addExtraClass('ss-gridfield-button-close') );
->setAttribute('title', _t('GridField.ResetFilter', "Reset")) $fields->push(
->setAttribute('id', 'action_reset_' . $gridField->getModelClass() . '_' . $columnField) GridField_FormAction::create($gridField, 'reset', false, 'reset', null)
); ->addExtraClass('ss-gridfield-button-close')
$field->addExtraClass('filter-buttons'); ->setAttribute('title', _t('GridField.ResetFilter', "Reset"))
$field->addExtraClass('no-change-track'); ->setAttribute('id', 'action_reset_' . $gridField->getModelClass() . '_' . $columnField)
}else{ );
$field = new LiteralField('', ''); $fields->addExtraClass('filter-buttons');
} $fields->addExtraClass('no-change-track');
} }
$forTemplate->Fields->push($field); $forTemplate->Fields->push($fields);
} }
return array( return array(

BIN
images/drive-upload-white.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

View File

@ -58,9 +58,7 @@
$('body').bind('click', _clickTestFn); $('body').bind('click', _clickTestFn);
var panel = this.getPanel(), tree = this.find('.tree-holder'); var panel = this.getPanel(), tree = this.find('.tree-holder');
var top = this.position().top + this.height();
panel.css('top', top);
panel.css('width', this.width()); panel.css('width', this.width());
panel.show(); panel.show();

View File

@ -1,12 +1,12 @@
(function($) { (function($) {
$.widget('blueimpUIX.fileupload', $.blueimpUI.fileupload, { $.widget('blueimpUIX.fileupload', $.blueimpUI.fileupload, {
_initTemplates: function() { _initTemplates: function() {
this.options.templateContainer = document.createElement( this.options.templateContainer = document.createElement(
this._files.prop('nodeName') this._files.prop('nodeName')
); );
this.options.uploadTemplate = window.tmpl(this.options.uploadTemplateName); this.options.uploadTemplate = window.tmpl(this.options.uploadTemplateName);
this.options.downloadTemplate = window.tmpl(this.options.downloadTemplateName); this.options.downloadTemplate = window.tmpl(this.options.downloadTemplateName);
}, },
_enableFileInputButton: function() { _enableFileInputButton: function() {
$.blueimpUI.fileupload.prototype._enableFileInputButton.call(this); $.blueimpUI.fileupload.prototype._enableFileInputButton.call(this);
this.element.find('.ss-uploadfield-addfile').show(); this.element.find('.ss-uploadfield-addfile').show();
@ -48,6 +48,39 @@
return result; return result;
}, },
_onSend: function (e, data) {
//check the array of existing files to see if we are trying to upload a file that already exists
var that = this;
var config = this.options;
if (config.overwriteWarning) {
$.get(
config['urlFileExists'],
{'filename': data.files[0].name},
function(response, status, xhr) {
if(response.exists) {
//display the dialogs with the question to overwrite or not
data.context.find('.ss-uploadfield-item-status')
.text(config.errorMessages.overwriteWarning)
.addClass('ui-state-warning-text');
data.context.find('.ss-uploadfield-item-progress').hide();
data.context.find('.ss-uploadfield-item-overwrite').show();
data.context.find('.ss-uploadfield-item-overwrite-warning').on('click', function(){
data.context.find('.ss-uploadfield-item-progress').show();
data.context.find('.ss-uploadfield-item-overwrite').hide();
data.context.find('.ss-uploadfield-item-status')
.removeClass('ui-state-warning-text');
//upload only if the "overwrite" button is clicked
$.blueimpUI.fileupload.prototype._onSend.call(that, e, data);
});
} else { //regular file upload
return $.blueimpUI.fileupload.prototype._onSend.call(that, e, data);
}
}
);
} else {
return $.blueimpUI.fileupload.prototype._onSend.call(that, e, data);
}
},
_onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
$.blueimpUI.fileupload.prototype._onAlways.call(this, jqXHRorResult, textStatus, jqXHRorError, options); $.blueimpUI.fileupload.prototype._onAlways.call(this, jqXHRorResult, textStatus, jqXHRorError, options);
if (this._active === 0) { if (this._active === 0) {
@ -55,7 +88,53 @@
$('.ss-uploadfield-item-edit-all').show(); $('.ss-uploadfield-item-edit-all').show();
$('.fileOverview .uploadStatus').addClass("good").removeClass("notice").removeClass("bad"); $('.fileOverview .uploadStatus').addClass("good").removeClass("notice").removeClass("bad");
} }
} },
_create: function() {
$.blueimpUI.fileupload.prototype._create.call(this);
// Ensures that the visibility of the fileupload dialog is set correctly at initialisation
this._adjustMaxNumberOfFiles(0);
},
attach: function(data) {
// Handles attachment of already uploaded files, similar to add
var self = this,
files = data.files,
replaceFileID = data.replaceFileID,
valid = true;
// If replacing an element (and it exists), adjust max number of files at this point
var replacedElement = null;
if(replaceFileID) {
replacedElement = $(".ss-uploadfield-item[data-fileid='"+replaceFileID+"']");
if(replacedElement.length === 0) {
replacedElement = null;
} else {
self._adjustMaxNumberOfFiles(1);
}
}
// Validate each file
$.each(files, function (index, file) {
self._adjustMaxNumberOfFiles(-1);
error = self._validate([file]);
valid = error && valid;
});
data.isAdjusted = true;
data.files.valid = data.isValidated = valid;
// Generate new file HTMl, and either append or replace (if replacing
// an already uploaded file).
data.context = this._renderDownload(files);
if(replacedElement) {
replacedElement.replaceWith(data.context);
} else {
data.context.appendTo(this._files);
}
data.context.data('data', data);
// Force reflow:
this._reflow = this._transition && data.context[0].offsetWidth;
data.context.addClass('in');
}
}); });
@ -69,7 +148,7 @@
if(this.is('.readonly,.disabled')) return; if(this.is('.readonly,.disabled')) return;
var fileInput = this.find('input'); var fileInput = this.find('input[type=file]');
var dropZone = this.find('.ss-uploadfield-dropzone'); var dropZone = this.find('.ss-uploadfield-dropzone');
var config = $.parseJSON(fileInput.data('config').replace(/'/g,'"')); var config = $.parseJSON(fileInput.data('config').replace(/'/g,'"'));
@ -115,7 +194,7 @@
]; ];
}, },
errorMessages: { errorMessages: {
// errorMessages for all error codes suggested from the plugin author, some will be overwritten by the config comming from php // errorMessages for all error codes suggested from the plugin author, some will be overwritten by the config coming from php
1: ss.i18n._t('UploadField.PHP_MAXFILESIZE'), 1: ss.i18n._t('UploadField.PHP_MAXFILESIZE'),
2: ss.i18n._t('UploadField.HTML_MAXFILESIZE'), 2: ss.i18n._t('UploadField.HTML_MAXFILESIZE'),
3: ss.i18n._t('UploadField.ONLYPARTIALUPLOADED'), 3: ss.i18n._t('UploadField.ONLYPARTIALUPLOADED'),
@ -169,13 +248,20 @@
onunmatch: function() { onunmatch: function() {
this._super(); this._super();
}, },
openSelectDialog: function() { openSelectDialog: function(uploadedFile) {
// Create dialog and load iframe // Create dialog and load iframe
var self = this, config = this.getConfig(), dialogId = 'ss-uploadfield-dialog-' + this.attr('id'), dialog = jQuery('#' + dialogId); var self = this, config = this.getConfig(), dialogId = 'ss-uploadfield-dialog-' + this.attr('id'), dialog = jQuery('#' + dialogId);
if(!dialog.length) dialog = jQuery('<div class="ss-uploadfield-dialog" id="' + dialogId + '" />'); if(!dialog.length) dialog = jQuery('<div class="ss-uploadfield-dialog" id="' + dialogId + '" />');
// If user selected 'Choose another file', we need the ID of the file to replace
var iframeUrl = config['urlSelectDialog'];
var uploadedFileId = null;
if (uploadedFile && uploadedFile.attr('data-fileid') > 0){
uploadedFileId = uploadedFile.attr('data-fileid');
}
// Show dialog // Show dialog
dialog.ssdialog({iframeUrl: config['urlSelectDialog'], height: 550}); dialog.ssdialog({iframeUrl: iframeUrl, height: 550});
// TODO Allow single-select // TODO Allow single-select
dialog.find('iframe').bind('load', function(e) { dialog.find('iframe').bind('load', function(e) {
@ -191,7 +277,7 @@
contents.find('input[name=action_doAttach]').unbind('click.openSelectDialog').bind('click.openSelectDialog', function() { contents.find('input[name=action_doAttach]').unbind('click.openSelectDialog').bind('click.openSelectDialog', function() {
// TODO Fix entwine method calls across iframe/document boundaries // TODO Fix entwine method calls across iframe/document boundaries
var ids = $.map(gridField.find('.ss-gridfield-item.ui-selected'), function(el) {return $(el).data('id');}); var ids = $.map(gridField.find('.ss-gridfield-item.ui-selected'), function(el) {return $(el).data('id');});
if(ids && ids.length) self.attachFiles(ids); if(ids && ids.length) self.attachFiles(ids, uploadedFileId);
dialog.ssdialog('close'); dialog.ssdialog('close');
return false; return false;
@ -199,23 +285,17 @@
}); });
dialog.ssdialog('open'); dialog.ssdialog('open');
}, },
attachFiles: function(ids) { attachFiles: function(ids, uploadedFileId) {
var self = this, config = this.getConfig(); var self = this, config = this.getConfig();
$.post( $.post(
config['urlAttach'], config['urlAttach'],
{'ids': ids}, {'ids': ids},
function(data, status, xhr) { function(data, status, xhr) {
var fn = self.fileupload('option', 'downloadTemplate'); self.fileupload('attach', {
self.find('.ss-uploadfield-files').append(fn({
files: data, files: data,
formatFileSize: function (bytes) { options: self.fileupload('option'),
if (typeof bytes !== 'number') return ''; replaceFileID: uploadedFileId
if (bytes >= 1000000000) return (bytes / 1000000000).toFixed(2) + ' GB'; });
if (bytes >= 1000000) return (bytes / 1000000).toFixed(2) + ' MB';
return (bytes / 1000).toFixed(2) + ' KB';
},
options: self.fileupload('option')
}));
} }
); );
} }
@ -255,14 +335,18 @@
var fileupload = this.closest('div.ss-upload').data('fileupload'), var fileupload = this.closest('div.ss-upload').data('fileupload'),
item = this.closest('.ss-uploadfield-item'), msg = ''; item = this.closest('.ss-uploadfield-item'), msg = '';
if(this.is('.ss-uploadfield-item-delete')) msg = ss.i18n._t('UploadField.ConfirmDelete'); if(this.is('.ss-uploadfield-item-delete')) {
if(!msg || confirm(msg)) { if(confirm(ss.i18n._t('UploadField.ConfirmDelete'))) {
fileupload._trigger('destroy', e, { fileupload._trigger('destroy', e, {
context: item, context: item,
url: this.data('href'), url: this.data('href'),
type: 'get', type: 'get',
dataType: fileupload.options.dataType dataType: fileupload.options.dataType
}); });
}
} else {
// Removed files will be applied to object on save
fileupload._trigger('destroy', e, {context: item});
} }
return false; return false;
@ -289,7 +373,7 @@
e.preventDefault(); // Avoid a form submit e.preventDefault(); // Avoid a form submit
} }
}); });
$('div.ss-upload .ss-uploadfield-item-edit, div.ss-upload .ss-uploadfield-item-name').entwine({ $( 'div.ss-upload:not(.disabled):not(.readonly) .ss-uploadfield-item-edit').entwine({
onclick: function(e) { onclick: function(e) {
var editform = this.closest('.ss-uploadfield-item').find('.ss-uploadfield-item-editform'); var editform = this.closest('.ss-uploadfield-item').find('.ss-uploadfield-item-editform');
var disabled; var disabled;
@ -403,7 +487,7 @@
$('div.ss-upload .ss-uploadfield-fromfiles').entwine({ $('div.ss-upload .ss-uploadfield-fromfiles').entwine({
onclick: function(e) { onclick: function(e) {
e.preventDefault(); e.preventDefault();
this.getUploadField().openSelectDialog(); this.getUploadField().openSelectDialog(this.closest('.ss-uploadfield-item'));
} }
}); });
}); });

View File

@ -5,8 +5,12 @@ window.tmpl.cache['ss-uploadfield-downloadtemplate'] = tmpl(
'<img src="{%=file.thumbnail_url%}" alt="" />' + '<img src="{%=file.thumbnail_url%}" alt="" />' +
'</span></div>' + '</span></div>' +
'<div class="ss-uploadfield-item-info">' + '<div class="ss-uploadfield-item-info">' +
'{% if (!file.error) { %}' +
'<input type="hidden" name="{%=file.fieldname%}[Files][]" value="{%=file.id%}" />' +
'{% } %}' +
'<label class="ss-uploadfield-item-name">' + '<label class="ss-uploadfield-item-name">' +
'<span class="name" title="{%=file.name%}">{%=file.name%}</span> ' + '<span class="name" title="{%=file.name%}">{%=file.name%}</span> ' +
'<span class="size">{%=o.formatFileSize(file.size)%}</span>' +
'{% if (!file.error) { %}' + '{% if (!file.error) { %}' +
'<div class="ss-uploadfield-item-status ui-state-success-text" title="'+ss.i18n._t('UploadField.Uploaded', 'Uploaded')+'">'+ss.i18n._t('UploadField.Uploaded', 'Uploaded')+'</div>' + '<div class="ss-uploadfield-item-status ui-state-success-text" title="'+ss.i18n._t('UploadField.Uploaded', 'Uploaded')+'">'+ss.i18n._t('UploadField.Uploaded', 'Uploaded')+'</div>' +
'{% } else { %}' + '{% } else { %}' +
@ -16,7 +20,7 @@ window.tmpl.cache['ss-uploadfield-downloadtemplate'] = tmpl(
'</label>' + '</label>' +
'{% if (file.error) { %}' + '{% if (file.error) { %}' +
'<div class="ss-uploadfield-item-actions">' + '<div class="ss-uploadfield-item-actions">' +
'<div class="ss-uploadfield-item-cancel ss-uploadfield-item-cancelfailed"><button class="icon icon-16">' + ss.i18n._t('UploadField.CANCEL', 'Cancel') + '</button></div>' + '<div class="ss-uploadfield-item-cancel ss-uploadfield-item-cancelfailed delete"><button class="icon icon-16" data-icon="delete" title="' + ss.i18n._t('UploadField.CANCELREMOVE', 'Cancel/Remove') + '">' + ss.i18n._t('UploadField.CANCELREMOVE', 'Cancel/Remove') + '</button></div>' +
'</div>' + '</div>' +
'{% } else { %}' + '{% } else { %}' +
'<div class="ss-uploadfield-item-actions">{% print(file.buttons, true); %}</div>' + '<div class="ss-uploadfield-item-actions">{% print(file.buttons, true); %}</div>' +
@ -27,4 +31,4 @@ window.tmpl.cache['ss-uploadfield-downloadtemplate'] = tmpl(
'{% } %}' + '{% } %}' +
'</li>' + '</li>' +
'{% } %}' '{% } %}'
); );

View File

@ -20,9 +20,14 @@ window.tmpl.cache['ss-uploadfield-uploadtemplate'] = tmpl(
'<div class="ss-uploadfield-item-start start"><button class="icon icon-16" data-icon="navigation">' + ss.i18n._t('UploadField.START', 'Start') + '</button></div>' + '<div class="ss-uploadfield-item-start start"><button class="icon icon-16" data-icon="navigation">' + ss.i18n._t('UploadField.START', 'Start') + '</button></div>' +
'{% } %}' + '{% } %}' +
'{% } %}' + '{% } %}' +
'<div class="ss-uploadfield-item-cancel cancel"><button data-icon="deleteLight" class="ss-uploadfield-item-cancel" title="' + ss.i18n._t('UploadField.CANCELREMOVE', 'Cancel/Remove') + '">' + ss.i18n._t('UploadField.CANCEL', 'Cancel') + '</button></div>' + '<div class="ss-uploadfield-item-cancel cancel">' +
'<button class="icon icon-16" data-icon="minus-circle" title="' + ss.i18n._t('UploadField.CANCELREMOVE', 'Cancel/Remove') + '">' + ss.i18n._t('UploadField.CANCELREMOVE', 'Cancel/Remove') + '</button>' +
'</div>' +
'<div class="ss-uploadfield-item-overwrite hide ">'+
'<button data-icon="drive-upload" class="ss-uploadfield-item-overwrite-warning" title="' + ss.i18n._t('UploadField.OVERWRITE', 'Overwrite') + '">' + ss.i18n._t('UploadField.OVERWRITE', 'Overwrite') + '</button>' +
'</div>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
'</li>' + '</li>' +
'{% } %}' '{% } %}'
); );

View File

@ -34,6 +34,7 @@ if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
'UploadField.EMPTYRESULT': 'Leere Datei erhalten', 'UploadField.EMPTYRESULT': 'Leere Datei erhalten',
'UploadField.LOADING': 'Lädt ...', 'UploadField.LOADING': 'Lädt ...',
'UploadField.Editing': 'Bearbeite ...', 'UploadField.Editing': 'Bearbeite ...',
'UploadField.Uploaded': 'Hochgeladen' 'UploadField.Uploaded': 'Hochgeladen',
'UploadField.OVERWRITEWARNING': 'Datei mit diesem Namen existiert bereits'
}); });
} }

View File

@ -35,6 +35,7 @@ if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
'UploadField.EMPTYRESULT': 'Empty file upload result', 'UploadField.EMPTYRESULT': 'Empty file upload result',
'UploadField.LOADING': 'Loading ...', 'UploadField.LOADING': 'Loading ...',
'UploadField.Editing': 'Editing ...', 'UploadField.Editing': 'Editing ...',
'UploadField.Uploaded': 'Uploaded' 'UploadField.Uploaded': 'Uploaded',
'UploadField.OVERWRITEWARNING': 'File with the same name already exists'
}); });
} }

View File

@ -567,6 +567,7 @@ en:
HOTLINKINFO: 'Info: This image will be hotlinked. Please ensure you have permissions from the original site creator to do so.' HOTLINKINFO: 'Info: This image will be hotlinked. Please ensure you have permissions from the original site creator to do so.'
MAXNUMBEROFFILES: 'Max number of {count} file(s) exceeded.' MAXNUMBEROFFILES: 'Max number of {count} file(s) exceeded.'
MAXNUMBEROFFILESSHORT: 'Can only upload {count} files' MAXNUMBEROFFILESSHORT: 'Can only upload {count} files'
MAXNUMBEROFFILESONE: 'Can only upload one file'
REMOVE: Remove REMOVE: Remove
REMOVEERROR: 'Error removing file' REMOVEERROR: 'Error removing file'
REMOVEINFO: 'Remove this file from here, but do not delete it from the file store' REMOVEINFO: 'Remove this file from here, but do not delete it from the file store'

View File

@ -566,6 +566,7 @@ en_GB:
HOTLINKINFO: 'Info: This image will be hotlinked. Please ensure you have permissions from the original site creator to do so.' HOTLINKINFO: 'Info: This image will be hotlinked. Please ensure you have permissions from the original site creator to do so.'
MAXNUMBEROFFILES: 'Max number of {count} file(s) exceeded.' MAXNUMBEROFFILES: 'Max number of {count} file(s) exceeded.'
MAXNUMBEROFFILESSHORT: 'Can only upload {count} files' MAXNUMBEROFFILESSHORT: 'Can only upload {count} files'
MAXNUMBEROFFILESONE: 'Can only upload one file'
REMOVE: Remove REMOVE: Remove
REMOVEERROR: 'Error removing file' REMOVEERROR: 'Error removing file'
REMOVEINFO: 'Remove this file from here, but do not delete it from the file store' REMOVEINFO: 'Remove this file from here, but do not delete it from the file store'

View File

@ -192,6 +192,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @var [string] - class => ClassName field definition cache for self::database_fields * @var [string] - class => ClassName field definition cache for self::database_fields
*/ */
private static $classname_spec_cache = array(); private static $classname_spec_cache = array();
/**
* Clear all cached classname specs. It's necessary to clear all cached subclassed names
* for any classes if a new class manifest is generated.
*/
public static function clear_classname_spec_cache() {
self::$classname_spec_cache = array();
}
/** /**
* Return the complete map of fields on this object, including "Created", "LastEdited" and "ClassName". * Return the complete map of fields on this object, including "Created", "LastEdited" and "ClassName".
@ -202,7 +210,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/ */
public static function database_fields($class) { public static function database_fields($class) {
if(get_parent_class($class) == 'DataObject') { if(get_parent_class($class) == 'DataObject') {
if(!isset(self::$classname_spec_cache[$class])) { if(empty(self::$classname_spec_cache[$class])) {
$classNames = ClassInfo::subclassesFor($class); $classNames = ClassInfo::subclassesFor($class);
$db = DB::getConn(); $db = DB::getConn();
@ -784,10 +792,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// no support for has_many or many_many relationships, // no support for has_many or many_many relationships,
// as the updater wouldn't know which object to write to (or create) // as the updater wouldn't know which object to write to (or create)
if($relObj->$relation() instanceof DataObject) { if($relObj->$relation() instanceof DataObject) {
$parentObj = $relObj;
$relObj = $relObj->$relation(); $relObj = $relObj->$relation();
// If the intermediate relationship objects have been created, then write them // If the intermediate relationship objects have been created, then write them
if($i<sizeof($relation)-1 && !$relObj->ID) $relObj->write(); if($i<sizeof($relation)-1 && !$relObj->ID || (!$relObj->ID && $parentObj != $this)) {
$relObj->write();
$relatedFieldName = $relation."ID";
$parentObj->$relatedFieldName = $relObj->ID;
$parentObj->write();
}
} else { } else {
user_error( user_error(
"DataObject::update(): Can't traverse relationship '$relation'," . "DataObject::update(): Can't traverse relationship '$relation'," .
@ -803,6 +816,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if($relObj) { if($relObj) {
$relObj->$fieldName = $v; $relObj->$fieldName = $v;
$relObj->write(); $relObj->write();
$relatedFieldName = $relation."ID";
$this->$relatedFieldName = $relObj->ID;
$relObj->flushCache(); $relObj->flushCache();
} else { } else {
user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING); user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING);

View File

@ -418,15 +418,23 @@ class DataQuery {
} }
} }
/**
* Set the GROUP BY clause of this query.
*
* @param String $groupby Escaped SQL statement
*/
public function groupby($groupby) {
$this->query->addGroupBy($groupby);
return $this;
}
/** /**
* Set the HAVING clause of this query. * Set the HAVING clause of this query.
* *
* @param String $having Escaped SQL statement * @param String $having Escaped SQL statement
*/ */
public function having($having) { public function having($having) {
if($having) { $this->query->addHaving($having);
$this->query->addHaving($having);
}
return $this; return $this;
} }

View File

@ -55,6 +55,7 @@ class UnsavedRelationList extends ArrayList {
$this->baseClass = $baseClass; $this->baseClass = $baseClass;
$this->relationName = $relationName; $this->relationName = $relationName;
$this->dataClass = $dataClass; $this->dataClass = $dataClass;
parent::__construct();
} }
/** /**

View File

@ -96,6 +96,13 @@ abstract class StringField extends DBField {
return parent::prepValueForDB($value); return parent::prepValueForDB($value);
} }
} }
/**
* @return string
*/
public function forTemplate() {
return nl2br($this->XML());
}
/** /**
* Limit this field's content by a number of characters. * Limit this field's content by a number of characters.

View File

@ -239,7 +239,10 @@ class Text extends StringField {
public function ContextSummary($characters = 500, $string = false, $striphtml = true, $highlight = true, public function ContextSummary($characters = 500, $string = false, $striphtml = true, $highlight = true,
$prefix = "... ", $suffix = "...") { $prefix = "... ", $suffix = "...") {
if(!$string) $string = $_REQUEST['Search']; // Use the default "Search" request variable (from SearchForm) if(!$string) {
// Use the default "Search" request variable (from SearchForm)
$string = isset($_REQUEST['Search']) ? $_REQUEST['Search'] : '';
}
// Remove HTML tags so we don't have to deal with matching tags // Remove HTML tags so we don't have to deal with matching tags
$text = $striphtml ? $this->NoHTML() : $this->value; $text = $striphtml ? $this->NoHTML() : $this->value;

View File

@ -311,9 +311,9 @@ class Oembed_Result extends ViewableData {
case 'video': case 'video':
case 'rich': case 'rich':
if($this->extraClass) { if($this->extraClass) {
return "<div class='$this->extraClass'>$this->HTML</div>"; return "<div class='media $this->extraClass'>$this->HTML</div>";
} else { } else {
return $this->HTML; return "<div class='media'>$this->HTML</div>";
} }
break; break;
case 'link': case 'link':

View File

@ -274,14 +274,14 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
.ss-uploadfield-item-info { .ss-uploadfield-item-info {
float: left; float: left;
margin: 10px 0 0; margin: 25px 0 0;
.ss-insert-media &{ .ss-insert-media &{
margin: 10px 0px 0 20px; margin: 10px 0px 0 20px;
} }
label{ label{
font-size: 18px; font-size: 18px;
line-height: 30px; line-height: 30px;
padding: 8px 16px; padding: 0px 16px;
margin-right:0px; margin-right:0px;
} }
} }
@ -290,7 +290,7 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
overflow: hidden; overflow: hidden;
display: block; display: block;
.btn-icon-drive-upload-large { .btn-icon-drive-upload-large {
background: url(../images/drive-upload-large.png) no-repeat 0px -4px; background: url(../images/drive-upload-white.png) no-repeat 0px 0px;
width:32px; width:32px;
height:32px; height:32px;
margin-top:-12px; margin-top:-12px;
@ -302,24 +302,25 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
font-size: 22px; font-size: 22px;
padding: 0 20px; padding: 0 20px;
line-height: 70px; line-height: 70px;
margin-top:4px; margin-top:10px;
display: none; display: none;
.ss-insert-media &{ .ss-insert-media &{
font-size: 18px; font-size: 18px;
margin-top: 0; margin-top: -5px;
} }
} }
.ss-uploadfield-dropzone { .ss-uploadfield-dropzone {
margin-top:9px; margin-top:9px;
padding: 8px 0;
@include border-radius(13px); @include border-radius(13px);
@include box-shadow(rgba($color-medium-separator, 0.4) 0 0 4px 0 inset, 0 1px 0 #FAFAFA); @include box-shadow(rgba($color-medium-separator, 0.4) 0 0 4px 0 inset, 0 1px 0 #FAFAFA);
border: 2px dashed $color-medium-separator; border: 2px dashed $color-medium-separator;
background: lighten($color-base,12%); background: lighten($color-base,12%);
display: none; display: none;
height: 54px; height: 54px;
width: 360px; min-width: 280px;
float: left; float: left;
text-align: left; text-align: center;
&.active{ &.active{
&.hover{ &.hover{
@ -336,7 +337,6 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: bold;
display: inline-block; display: inline-block;
margin-left: 83px;
span { span {
display: block; display: block;
font-size: 12px; font-size: 12px;
@ -346,10 +346,11 @@ body.cms.ss-uploadfield-edit-iframe, .composite.ss-assetuploadfield .details fie
} }
.ss-insert-media &{ .ss-insert-media &{
height: 54px; height: 54px;
width: 277px; //Design has these smaller than main upload area min-width: 250px; //Design has these smaller than main upload area
overflow:hidden; overflow:hidden;
padding:0;
div{ margin-top:2px;
div{
background-position:0 11px; background-position:0 11px;
padding-top:21px; padding-top:21px;
margin-left: 33px; margin-left: 33px;

View File

@ -198,31 +198,31 @@ $gf_grid_x: 16px;
@include border-radius(2px, 2px); @include border-radius(2px, 2px);
} }
span.badge.modified { span.badge.status-modified {
color: #7E7470; color: #7E7470;
border: 1px solid #C9B800; border: 1px solid #C9B800;
background-color: #FFF0BC; background-color: #FFF0BC;
} }
span.badge.addedtodraft { span.badge.status-addedtodraft {
color: #7E7470; color: #7E7470;
border: 1px solid #C9B800; border: 1px solid #C9B800;
background-color: #FFF0BC; background-color: #FFF0BC;
} }
span.badge.deletedonlive { span.badge.status-deletedonlive {
color: #636363; color: #636363;
border: 1px solid #E49393; border: 1px solid #E49393;
background-color: #F2DADB; background-color: #F2DADB;
} }
span.badge.removedfromdraft { span.badge.status-removedfromdraft {
color: #636363; color: #636363;
border: 1px solid #E49393; border: 1px solid #E49393;
background-color: #F2DADB; background-color: #F2DADB;
} }
span.badge.workflow-approval { span.badge.status-workflow-approval {
color: #56660C; color: #56660C;
border: 1px solid #7C8816; border: 1px solid #7C8816;
background-color: #DAE79A; background-color: #DAE79A;

View File

@ -3,7 +3,8 @@ div.TreeDropdownField {
background: #fff; background: #fff;
border: 1px solid #aaa; border: 1px solid #aaa;
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: visible;
position:relative;
input { input {
border: none; border: none;

View File

@ -17,12 +17,17 @@
float:left; float:left;
} }
// Stop nolabel from resetting margin on tree dropdown
&.from-CMS .nolabel.treedropdown .middleColumn{
margin-left:184px;
}
} }
.middleColumn { .middleColumn {
// TODO .middleColumn styling should probably be theme specific (eg cms ui will look different than blackcandy) // 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 // so we should move this style into the cms and black candy files
width: 526px; width: 510px;
padding: 0; padding: 0;
background: #fff; background: #fff;
border: 1px solid lighten($color-medium-separator, 20%); border: 1px solid lighten($color-medium-separator, 20%);
@ -51,39 +56,43 @@
} }
} }
.ss-uploadfield-item-info { .ss-uploadfield-item-info {
float: left; margin-left: 95px;
margin-left: 15px;
.ss-uploadfield-item-name { .ss-uploadfield-item-name {
display: block; display: block;
line-height: 13px; line-height: 13px;
height: 26px; height: 26px;
margin: 0; margin: 0;
text-align: left; text-align: left;
b {
font-weight: bold;
padding: 0 5px 0 0;
}
.name { .name {
font-size: $font-base-size - 1; max-width: 240px;
color: lighten($color-text, 25%); font-weight: bold;
width:290px; //Ensures the title doesn't interfer with the status message
@include hide-text-overflow; @include hide-text-overflow;
display:inline; display:inline;
float:left;
}
.size {
color: lighten($color-text, 25%);
padding: 0 0 0 5px;
display:inline;
float:left; float:left;
} }
.ss-uploadfield-item-status { .ss-uploadfield-item-status {
float: right; float: right;
padding: 0 0 0 5px; padding: 0 0 0 5px;
width:100px; //Allocates the status message enough room to be useful. Will wrap if it is longer text-align:right;
text-align:right; max-width: 75%;
&.ui-state-error-text { &.ui-state-error-text {
color: $color-button-destructive; color: $color-button-destructive;
font-weight: bold; font-weight: bold;
width:150px; //Allocates the status message enough room to be useful. Will wrap if it is longer
} }
&.ui-state-warning-text { &.ui-state-warning-text {
color: darken($color-warning, 10%); color: darken($color-warning, 10%);
} }
&.ui-state-success-text { &.ui-state-success-text {
color: $color-button-constructive; color: $color-button-constructive;
} }
@ -94,7 +103,7 @@
.ss-ui-button { .ss-ui-button {
display: block; display: block;
float: left; float: left;
margin: 0 10px 0 0; margin: 0 10px 6px 0;
&.ss-uploadfield-fromcomputer { &.ss-uploadfield-fromcomputer {
position: relative; position: relative;
@ -119,8 +128,9 @@
} }
} }
.ss-uploadfield-item-actions { .ss-uploadfield-item-actions {
height: 28px; min-height: 28px;
margin: 6px 0 0; overflow: hidden;
margin: 6px 0 -6px 0;
position: relative; position: relative;
} }
.ss-uploadfield-item-progress { .ss-uploadfield-item-progress {
@ -166,7 +176,6 @@
height: 16px; height: 16px;
cursor: pointer; cursor: pointer;
@include single-box-shadow(none); @include single-box-shadow(none);
background: none;
position: relative; position: relative;
// background: sprite($sprites16, cross-circle) no-repeat; // background: sprite($sprites16, cross-circle) no-repeat;

View File

@ -29,7 +29,7 @@
// TODO tmp hack until we have permissions and can disable delete // TODO tmp hack until we have permissions and can disable delete
display: none; display: none;
} }
&.ss-uploadfield-item-cancel{ &.ss-uploadfield-item-cancel, &.ss-uploadfield-item-overwrite-warning {
@include border-radius(0); @include border-radius(0);
border-left:1px solid rgba(#fff, 0.2); border-left:1px solid rgba(#fff, 0.2);
margin-top:0px; margin-top:0px;

View File

@ -251,9 +251,11 @@ class Group extends DataObject {
// Remove the default foreign key filter in prep for re-applying a filter containing all children groups. // Remove the default foreign key filter in prep for re-applying a filter containing all children groups.
// Filters are conjunctive in DataQuery by default, so this filter would otherwise overrule any less specific // Filters are conjunctive in DataQuery by default, so this filter would otherwise overrule any less specific
// ones. // ones.
$result = $result->alterDataQuery(function($query){ if(!($result instanceof UnsavedRelationList)) {
$query->removeFilterOn('Group_Members'); $result = $result->alterDataQuery(function($query){
}); $query->removeFilterOn('Group_Members');
});
}
// Now set all children groups as a new foreign key // Now set all children groups as a new foreign key
$groups = Group::get()->byIDs($this->collateFamilyIDs()); $groups = Group::get()->byIDs($this->collateFamilyIDs());
$result = $result->forForeignID($groups->column('ID'))->where($filter)->sort($sort)->limit($limit); $result = $result->forForeignID($groups->column('ID'))->where($filter)->sort($sort)->limit($limit);

View File

@ -447,7 +447,8 @@ class Member extends DataObject implements TemplateGlobalProvider {
public function logOut() { public function logOut() {
Session::clear("loggedInAs"); Session::clear("loggedInAs");
if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, null, 0); if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, null, 0);
self::session_regenerate_id();
Session::destroy();
$this->extend('memberLoggedOut'); $this->extend('memberLoggedOut');

View File

@ -6,11 +6,12 @@
</span> </span>
</button> </button>
<% end_if %> <% end_if %>
<% if UploadFieldHasRelation %> <button class="ss-uploadfield-item-remove ss-ui-button ui-corner-all" title="<% _t('UploadField.REMOVEINFO', 'Remove this file from here, but do not delete it from the file store') %>" data-icon="plug-disconnect-prohibition">
<button data-href="$UploadFieldRemoveLink" class="ss-uploadfield-item-remove ss-ui-button ui-corner-all" title="<% _t('UploadField.REMOVEINFO', 'Remove this file from here, but do not delete it from the file store') %>" data-icon="plug-disconnect-prohibition"> <% _t('UploadField.REMOVE', 'Remove') %></button>
<% _t('UploadField.REMOVE', 'Remove') %></button>
<% end_if %>
<% if canDelete %> <% if canDelete %>
<button data-href="$UploadFieldDeleteLink" class="ss-uploadfield-item-delete ss-ui-button ui-corner-all" title="<% _t('UploadField.DELETEINFO', 'Permanently delete this file from the file store') %>" data-icon="minus-circle"><% _t('UploadField.DELETE', 'Delete from files') %></button> <button data-href="$UploadFieldDeleteLink" class="ss-uploadfield-item-delete ss-ui-button ui-corner-all" title="<% _t('UploadField.DELETEINFO', 'Permanently delete this file from the file store') %>" data-icon="minus-circle"><% _t('UploadField.DELETE', 'Delete from files') %></button>
<% end_if %> <% end_if %>
<% if UploadField.canAttachExisting %>
<button class="ss-uploadfield-item-choose-another ss-uploadfield-fromfiles ss-ui-button ui-corner-all" title="<% _t('UploadField.CHOOSEANOTHERINFO', 'Replace this file with another one from the file store') %>" data-icon="network-cloud">
<% _t('UploadField.CHOOSEANOTHERFILE', 'Choose another file') %></button>
<% end_if %>

View File

@ -1,19 +1,19 @@
<ul class="ss-uploadfield-files files"> <ul class="ss-uploadfield-files files">
<% if $Items %> <% if $CustomisedItems %>
<% loop $Items %> <% loop $CustomisedItems %>
<li class="ss-uploadfield-item template-download" data-fileid="$ID"> <li class="ss-uploadfield-item template-download" data-fileid="$ID">
<div class="ss-uploadfield-item-preview preview"><span> <div class="ss-uploadfield-item-preview preview"><span>
<img alt="$hasRelation" src="$UploadFieldThumbnailURL" /> <img alt="$hasRelation" src="$UploadFieldThumbnailURL" />
</span></div> </span></div>
<div class="ss-uploadfield-item-info"> <div class="ss-uploadfield-item-info">
<input type='hidden' value='$ID' name='{$Top.Name}[Files][]' />
<label class="ss-uploadfield-item-name"> <label class="ss-uploadfield-item-name">
<b>{$Title}.{$Extension}</b> <span class="name">$Name.XML</span>
<span>$Size</span> <span class="size">$Size</span>
<div class="clear"><!-- --></div> <div class="clear"><!-- --></div>
</label> </label>
<div class="ss-uploadfield-item-actions"> <div class="ss-uploadfield-item-actions">
<% if Top.isDisabled || Top.isReadonly %> <% if Top.isActive %>
<% else %>
$UploadFieldFileButtons $UploadFieldFileButtons
<% end_if %> <% end_if %>
</div> </div>
@ -25,15 +25,8 @@
<% end_loop %> <% end_loop %>
<% end_if %> <% end_if %>
</ul> </ul>
<% if isDisabled || isReadonly %> <% if canUpload || canAttachExisting %>
<% if isSaveable %> <div class="ss-uploadfield-item ss-uploadfield-addfile<% if $CustomisedItems %> borderTop<% end_if %>">
<% else %>
<div class="ss-uploadfield-item">
<em><% _t('FileIFrameField.ATTACHONCESAVED2', 'Files can be attached once you have saved the record for the first time.') %></em>
</div>
<% end_if %>
<% else %>
<div class="ss-uploadfield-item ss-uploadfield-addfile<% if $Items && $displayInput %> borderTop<% end_if %>" <% if not $displayInput %>style="display: none;"<% end_if %>>
<% if canUpload %> <% if canUpload %>
<div class="ss-uploadfield-item-preview ss-uploadfield-dropzone ui-corner-all"> <div class="ss-uploadfield-item-preview ss-uploadfield-dropzone ui-corner-all">
<% if $multiple %> <% if $multiple %>
@ -50,24 +43,24 @@
<% else %> <% else %>
<b><% _t('UploadField.ATTACHFILE', 'Attach a file') %></b> <b><% _t('UploadField.ATTACHFILE', 'Attach a file') %></b>
<% end_if %> <% end_if %>
<% if getConfig('canPreviewFolder') %> <% if canPreviewFolder %>
<small>(<%t UploadField.UPLOADSINTO 'saves into /{path}' path=$FolderName %>)</small> <small>(<%t UploadField.UPLOADSINTO 'saves into /{path}' path=$FolderName %>)</small>
<% end_if %> <% end_if %>
</label> </label>
<% if canUpload %> <% if canUpload %>
<label class="ss-uploadfield-fromcomputer ss-ui-button ui-corner-all" title="<% _t('UploadField.FROMCOMPUTERINFO', 'Upload from your computer') %>" data-icon="drive-upload"> <label class="ss-uploadfield-fromcomputer ss-ui-button ui-corner-all" title="<% _t('UploadField.FROMCOMPUTERINFO', 'Upload from your computer') %>" data-icon="drive-upload">
<% _t('UploadField.FROMCOMPUTER', 'From your computer') %> <% _t('UploadField.FROMCOMPUTER', 'From your computer') %>
<input id="$id" name="$getName" class="$extraClass ss-uploadfield-fromcomputer-fileinput" data-config="$configString" type="file"<% if $multiple %> multiple="multiple"<% end_if %> /> <input id="$id" name="{$Name}[Uploads][]" class="$extraClass ss-uploadfield-fromcomputer-fileinput" data-config="$configString" type="file"<% if $multiple %> multiple="multiple"<% end_if %> />
</label> </label>
<% else %>
<input style="display: none" id="$id" name="$getName" class="$extraClass ss-uploadfield-fromcomputer-fileinput" data-config="$configString" type="file"<% if $multiple %> multiple="multiple"<% end_if %> />
<% end_if %> <% end_if %>
<% if canAttachExisting %> <% if canAttachExisting %>
<button class="ss-uploadfield-fromfiles ss-ui-button ui-corner-all" title="<% _t('UploadField.FROMCOMPUTERINFO', 'Select from files') %>" data-icon="network-cloud"><% _t('UploadField.FROMFILES', 'From files') %></button> <button class="ss-uploadfield-fromfiles ss-ui-button ui-corner-all" title="<% _t('UploadField.FROMCOMPUTERINFO', 'Select from files') %>" data-icon="network-cloud"><% _t('UploadField.FROMFILES', 'From files') %></button>
<% end_if %> <% end_if %>
<% if not $autoUpload %> <% if canUpload %>
<button class="ss-uploadfield-startall ss-ui-button ui-corner-all" data-icon="navigation"><% _t('UploadField.STARTALL', 'Start all') %></button> <% if not $autoUpload %>
<button class="ss-uploadfield-startall ss-ui-button ui-corner-all" data-icon="navigation"><% _t('UploadField.STARTALL', 'Start all') %></button>
<% end_if %>
<% end_if %> <% end_if %>
<div class="clear"><!-- --></div> <div class="clear"><!-- --></div>
</div> </div>

View File

@ -41,7 +41,8 @@ class SessionTest extends SapphireTest {
Session::set('Test-2', 'Test-2'); Session::set('Test-2', 'Test-2');
$session = Session::get_all(); $session = Session::get_all();
unset($session['HTTP_USER_AGENT']);
$this->assertEquals($session, array('Test' => 'Test', 'Test-2' => 'Test-2')); $this->assertEquals($session, array('Test' => 'Test', 'Test-2' => 'Test-2'));
} }
@ -49,7 +50,9 @@ class SessionTest extends SapphireTest {
$s = new Session(array('something' => array('does' => 'exist'))); $s = new Session(array('something' => array('does' => 'exist')));
$s->inst_set('something.does', 'exist'); $s->inst_set('something.does', 'exist');
$this->assertEquals(array(), $s->inst_changedData()); $result = $s->inst_changedData();
unset($result['HTTP_USER_AGENT']);
$this->assertEquals(array(), $result);
} }
/** /**
@ -59,11 +62,15 @@ class SessionTest extends SapphireTest {
$s = new Session(array('something' => array('does' => 'exist'))); $s = new Session(array('something' => array('does' => 'exist')));
$s->inst_clear('something.doesnt.exist'); $s->inst_clear('something.doesnt.exist');
$this->assertEquals(array(), $s->inst_changedData()); $result = $s->inst_changedData();
unset($result['HTTP_USER_AGENT']);
$this->assertEquals(array(), $result);
$s->inst_set('something-else', 'val'); $s->inst_set('something-else', 'val');
$s->inst_clear('something-new'); $s->inst_clear('something-new');
$this->assertEquals(array('something-else' => 'val'), $s->inst_changedData()); $result = $s->inst_changedData();
unset($result['HTTP_USER_AGENT']);
$this->assertEquals(array('something-else' => 'val'), $result);
} }
/** /**
@ -73,7 +80,9 @@ class SessionTest extends SapphireTest {
$s = new Session(array('something' => array('does' => 'exist'))); $s = new Session(array('something' => array('does' => 'exist')));
$s->inst_clear('something.does'); $s->inst_clear('something.does');
$this->assertEquals(array('something' => array('does' => null)), $s->inst_changedData()); $result = $s->inst_changedData();
unset($result['HTTP_USER_AGENT']);
$this->assertEquals(array('something' => array('does' => null)), $result);
} }
public function testNonStandardPath(){ public function testNonStandardPath(){
@ -82,4 +91,20 @@ class SessionTest extends SapphireTest {
$this->assertEquals(Config::inst()->get('Session', 'store_path'), ''); $this->assertEquals(Config::inst()->get('Session', 'store_path'), '');
} }
public function testUserAgentLockout() {
// Set a user agent
$_SERVER['HTTP_USER_AGENT'] = 'Test Agent';
// Generate our session
$s = new Session(array());
$s->inst_set('val', 123);
// Change our UA
$_SERVER['HTTP_USER_AGENT'] = 'Fake Agent';
// Verify the new session reset our values
$s2 = new Session($s);
$this->assertNotEquals($s2->inst_get('val'), 123);
}
} }

View File

@ -146,6 +146,26 @@ class FieldListTest extends SapphireTest {
$this->assertEquals(0, $fields->Count()); $this->assertEquals(0, $fields->Count());
} }
/**
* Test removing multiple fields from a set by their names in an array.
*/
public function testRemoveFieldsByName() {
$fields = new FieldList();
/* First of all, we add some fields into our FieldList object */
$fields->push(new TextField('Name', 'Your name'));
$fields->push(new TextField('Email', 'Your email'));
/* We have 2 fields in our set now */
$this->assertEquals(2, $fields->Count());
/* Then, we call up removeByName() to take it out again */
$fields->removeByName(array('Name', 'Email'));
/* We have 0 fields in our set now, as we've just removed the one we added */
$this->assertEquals(0, $fields->Count());
}
/** /**
* Test replacing a field with another one. * Test replacing a field with another one.
*/ */

View File

@ -14,23 +14,25 @@ class UploadFieldTest extends FunctionalTest {
'File' => array('UploadFieldTest_FileExtension') 'File' => array('UploadFieldTest_FileExtension')
); );
/**
* Test that files can be uploaded against an object with no relation
*/
public function testUploadNoRelation() { public function testUploadNoRelation() {
$this->loginWithPermission('ADMIN'); $this->loginWithPermission('ADMIN');
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); $record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$tmpFileName = 'testUploadBasic.txt'; $tmpFileName = 'testUploadBasic.txt';
$_FILES = array('NoRelationField' => $this->getUploadFile($tmpFileName)); $response = $this->mockFileUpload('NoRelationField', $tmpFileName);
$response = $this->post(
'UploadFieldTest_Controller/Form/field/NoRelationField/upload',
array('NoRelationField' => $this->getUploadFile($tmpFileName))
);
$this->assertFalse($response->isError()); $this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName"); $this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName)); $uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName));
$this->assertTrue(is_object($uploadedFile), 'The file object is created'); $this->assertTrue(is_object($uploadedFile), 'The file object is created');
} }
/**
* Test that an object can be uploaded against an object with a has_one relation
*/
public function testUploadHasOneRelation() { public function testUploadHasOneRelation() {
$this->loginWithPermission('ADMIN'); $this->loginWithPermission('ADMIN');
@ -39,22 +41,29 @@ class UploadFieldTest extends FunctionalTest {
$record->HasOneFileID = null; $record->HasOneFileID = null;
$record->write(); $record->write();
// Firstly, ensure the file can be uploaded
$tmpFileName = 'testUploadHasOneRelation.txt'; $tmpFileName = 'testUploadHasOneRelation.txt';
$_FILES = array('HasOneFile' => $this->getUploadFile($tmpFileName)); $response = $this->mockFileUpload('HasOneFile', $tmpFileName);
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasOneFile/upload',
array('HasOneFile' => $this->getUploadFile($tmpFileName))
);
$this->assertFalse($response->isError()); $this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName"); $this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName)); $uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName));
$this->assertTrue(is_object($uploadedFile), 'The file object is created'); $this->assertTrue(is_object($uploadedFile), 'The file object is created');
// Secondly, ensure that simply uploading an object does not save the file against the relation
$record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertFalse($record->HasOneFile()->exists());
// Thirdly, test submitting the form with the encoded data
$response = $this->mockUploadFileIDs('HasOneFile', array($uploadedFile->ID));
$this->assertEmpty($response['errors']);
$record = DataObject::get_by_id($record->class, $record->ID, false); $record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertTrue($record->HasOneFile()->exists()); $this->assertTrue($record->HasOneFile()->exists());
$this->assertEquals($record->HasOneFile()->Name, $tmpFileName); $this->assertEquals($record->HasOneFile()->Name, $tmpFileName);
} }
/**
* Tests that has_one relations work with subclasses of File
*/
public function testUploadHasOneRelationWithExtendedFile() { public function testUploadHasOneRelationWithExtendedFile() {
$this->loginWithPermission('ADMIN'); $this->loginWithPermission('ADMIN');
@ -63,201 +72,252 @@ class UploadFieldTest extends FunctionalTest {
$record->HasOneExtendedFileID = null; $record->HasOneExtendedFileID = null;
$record->write(); $record->write();
// Test that the file can be safely uploaded
$tmpFileName = 'testUploadHasOneRelationWithExtendedFile.txt'; $tmpFileName = 'testUploadHasOneRelationWithExtendedFile.txt';
$_FILES = array('HasOneExtendedFile' => $this->getUploadFile($tmpFileName)); $response = $this->mockFileUpload('HasOneExtendedFile', $tmpFileName);
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasOneExtendedFile/upload',
array('HasOneExtendedFile' => $this->getUploadFile($tmpFileName))
);
$this->assertFalse($response->isError()); $this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName"); $this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('UploadFieldTest_ExtendedFile', sprintf('"Name" = \'%s\'', $tmpFileName)); $uploadedFile = DataObject::get_one('UploadFieldTest_ExtendedFile', sprintf('"Name" = \'%s\'', $tmpFileName));
$this->assertTrue(is_object($uploadedFile), 'The file object is created'); $this->assertTrue(is_object($uploadedFile), 'The file object is created');
// Test that the record isn't written to automatically
$record = DataObject::get_by_id($record->class, $record->ID, false); $record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertTrue($record->HasOneExtendedFile()->exists(), 'The extended file is attached to the class'); $this->assertFalse($record->HasOneExtendedFile()->exists());
$this->assertEquals($record->HasOneExtendedFile()->Name, $tmpFileName, 'Proper file has been attached');
// Test that saving the form writes the record
$response = $this->mockUploadFileIDs('HasOneExtendedFile', array($uploadedFile->ID));
$this->assertEmpty($response['errors']);
$record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertTrue($record->HasOneExtendedFile()->exists());
$this->assertEquals($record->HasOneExtendedFile()->Name, $tmpFileName);
} }
/**
* Test that has_many relations work with files
*/
public function testUploadHasManyRelation() { public function testUploadHasManyRelation() {
$this->loginWithPermission('ADMIN'); $this->loginWithPermission('ADMIN');
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); $record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
// Test that uploaded files can be posted to a has_many relation
$tmpFileName = 'testUploadHasManyRelation.txt'; $tmpFileName = 'testUploadHasManyRelation.txt';
$_FILES = array('HasManyFiles' => $this->getUploadFile($tmpFileName)); $response = $this->mockFileUpload('HasManyFiles', $tmpFileName);
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasManyFiles/upload',
array('HasManyFiles' => $this->getUploadFile($tmpFileName))
);
$this->assertFalse($response->isError()); $this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName"); $this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName)); $uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName));
$this->assertTrue(is_object($uploadedFile), 'The file object is created'); $this->assertTrue(is_object($uploadedFile), 'The file object is created');
// Test that the record isn't written to automatically
$record = DataObject::get_by_id($record->class, $record->ID, false); $record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertEquals(3, $record->HasManyFiles()->Count()); $this->assertEquals(2, $record->HasManyFiles()->Count()); // Existing two files should be retained
$this->assertEquals($record->HasManyFiles()->Last()->Name, $tmpFileName);
// Test that saving the form writes the record
$ids = array_merge($record->HasManyFiles()->getIDList(), array($uploadedFile->ID));
$response = $this->mockUploadFileIDs('HasManyFiles', $ids);
$this->assertEmpty($response['errors']);
$record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertEquals(3, $record->HasManyFiles()->Count()); // New record should appear here now
} }
/**
* Test that many_many relationships work with files
*/
public function testUploadManyManyRelation() { public function testUploadManyManyRelation() {
$this->loginWithPermission('ADMIN'); $this->loginWithPermission('ADMIN');
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); $record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$relationCount = $record->ManyManyFiles()->Count(); $relationCount = $record->ManyManyFiles()->Count();
// Test that uploaded files can be posted to a many_many relation
$tmpFileName = 'testUploadManyManyRelation.txt'; $tmpFileName = 'testUploadManyManyRelation.txt';
$_FILES = array('ManyManyFiles' => $this->getUploadFile($tmpFileName)); $response = $this->mockFileUpload('ManyManyFiles', $tmpFileName);
$response = $this->post(
'UploadFieldTest_Controller/Form/field/ManyManyFiles/upload',
array('ManyManyFiles' => $this->getUploadFile($tmpFileName))
);
$this->assertFalse($response->isError()); $this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName"); $this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName)); $uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName));
$this->assertTrue(is_object($uploadedFile), 'The file object is created'); $this->assertTrue(is_object($uploadedFile), 'The file object is created');
// Test that the record isn't written to automatically
$record = DataObject::get_by_id($record->class, $record->ID, false); $record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertEquals($relationCount+1, $record->ManyManyFiles()->Count()); // Existing file count should be retained
$this->assertEquals($record->ManyManyFiles()->Last()->Name, $tmpFileName); $this->assertEquals($relationCount, $record->ManyManyFiles()->Count());
// Test that saving the form writes the record
$ids = array_merge($record->ManyManyFiles()->getIDList(), array($uploadedFile->ID));
$response = $this->mockUploadFileIDs('ManyManyFiles', $ids);
$this->assertEmpty($response['errors']);
$record = DataObject::get_by_id($record->class, $record->ID, false);
$record->flushCache();
// New record should appear here now
$this->assertEquals($relationCount + 1, $record->ManyManyFiles()->Count());
} }
/**
* Test that has_one relations do not support multiple files
*/
public function testAllowedMaxFileNumberWithHasOne() { public function testAllowedMaxFileNumberWithHasOne() {
$this->loginWithPermission('ADMIN'); $this->loginWithPermission('ADMIN');
// Get references for each file to upload
$file1 = $this->objFromFixture('File', 'file1');
$file2 = $this->objFromFixture('File', 'file2');
$fileIDs = array($file1->ID, $file2->ID);
// Test each of the three cases - has one with no max filel limit, has one with a limit of // Test each of the three cases - has one with no max filel limit, has one with a limit of
// one, has one with a limit of more than one (makes no sense, but should test it anyway). // one, has one with a limit of more than one (makes no sense, but should test it anyway).
// Each of them should public function in the same way - attaching the first file should work, the // Each of them should public function in the same way - attaching the first file should work, the
// second should cause an error. // second should cause an error.
foreach (array('HasOneFile', 'HasOneFileMaxOne', 'HasOneFileMaxTwo') as $recordName) { foreach (array('HasOneFile', 'HasOneFileMaxOne', 'HasOneFileMaxTwo') as $recordName) {
// Unset existing has_one relation before re-uploading // Unset existing has_one relation before re-uploading
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); $record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$record->{$recordName . 'ID'} = null; $record->{"{$recordName}ID"} = null;
$record->write(); $record->write();
$tmpFileName = 'testUploadHasOneRelation.txt';
$_FILES = array($recordName => $this->getUploadFile($tmpFileName));
$response = $this->post(
"UploadFieldTest_Controller/Form/field/$recordName/upload",
array($recordName => $this->getUploadFile($tmpFileName))
);
$body = json_decode($response->getBody());
$this->assertEquals(0, $body[0]->error);
// Write to it again, should result in an error. // Post form with two files for this field, should result in an error
$response = $this->post( $response = $this->mockUploadFileIDs($recordName, $fileIDs);
"UploadFieldTest_Controller/Form/field/$recordName/upload", $isError = !empty($response['errors']);
array($recordName => $this->getUploadFile($tmpFileName))
); // Strictly, a has_one should not allow two files, but this is overridden
$body = json_decode($response->getBody()); // by the setAllowedMaxFileNumber(2) call
$this->assertNotEquals(0, $body[0]->error); $maxFiles = ($recordName === 'HasOneFileMaxTwo') ? 2 : 1;
// Assert that the form fails if the maximum number of files is exceeded
$this->assertTrue((count($fileIDs) > $maxFiles) == $isError);
} }
} }
/**
* Test that max number of items on has_many is validated
*/
public function testAllowedMaxFileNumberWithHasMany() { public function testAllowedMaxFileNumberWithHasMany() {
$this->loginWithPermission('ADMIN'); $this->loginWithPermission('ADMIN');
// The 'HasManyFilesMaxTwo' field has a maximum of two files able to be attached to it. // The 'HasManyFilesMaxTwo' field has a maximum of two files able to be attached to it.
// We want to add files to it until we attempt to add the third. We expect that the first // We want to add files to it until we attempt to add the third. We expect that the first
// two should work and the third will fail. // two should work and the third will fail.
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); $record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$record->HasManyFilesMaxTwo()->removeAll(); $record->HasManyFilesMaxTwo()->removeAll();
$tmpFileName = 'testUploadHasManyRelation.txt'; // Get references for each file to upload
$_FILES = array('HasManyFilesMaxTwo' => $this->getUploadFile($tmpFileName)); $file1 = $this->objFromFixture('File', 'file1');
$file2 = $this->objFromFixture('File', 'file2');
$file3 = $this->objFromFixture('File', 'file3');
// Write the first element, should be okay. // Write the first element, should be okay.
$response = $this->post( $response = $this->mockUploadFileIDs('HasManyFilesMaxTwo', array($file1->ID));
'UploadFieldTest_Controller/Form/field/HasManyFilesMaxTwo/upload', $this->assertEmpty($response['errors']);
array('HasManyFilesMaxTwo' => $this->getUploadFile($tmpFileName))
);
$body = json_decode($response->getBody());
$this->assertEquals(0, $body[0]->error);
// Write the second element, should be okay. // Write the second element, should be okay.
$response = $this->post( $response = $this->mockUploadFileIDs('HasManyFilesMaxTwo', array($file1->ID, $file2->ID));
'UploadFieldTest_Controller/Form/field/HasManyFilesMaxTwo/upload', $this->assertEmpty($response['errors']);
array('HasManyFilesMaxTwo' => $this->getUploadFile($tmpFileName))
);
$body = json_decode($response->getBody());
$this->assertEquals(0, $body[0]->error);
// Write the third element, should result in error. // Write the third element, should result in error.
$response = $this->post( $response = $this->mockUploadFileIDs('HasManyFilesMaxTwo', array($file1->ID, $file2->ID, $file3->ID));
'UploadFieldTest_Controller/Form/field/HasManyFilesMaxTwo/upload', $this->assertNotEmpty($response['errors']);
array('HasManyFilesMaxTwo' => $this->getUploadFile($tmpFileName))
);
$body = json_decode($response->getBody());
$this->assertNotEquals(0, $body[0]->error);
} }
/**
* Test that files can be removed from has_one relations
*/
public function testRemoveFromHasOne() { public function testRemoveFromHasOne() {
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); $record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$file1 = $this->objFromFixture('File', 'file1'); $file1 = $this->objFromFixture('File', 'file1');
// Check record exists
$this->assertTrue($record->HasOneFile()->exists()); $this->assertTrue($record->HasOneFile()->exists());
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasOneFile/item/' . $file1->ID . '/remove', // Remove from record
array() $response = $this->mockUploadFileIDs('HasOneFile', array());
); $this->assertEmpty($response['errors']);
$this->assertFalse($response->isError());
// Check file is removed
$record = DataObject::get_by_id($record->class, $record->ID, false); $record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertFalse($record->HasOneFile()->exists()); $this->assertFalse($record->HasOneFile()->exists());
// Check file object itself exists
$this->assertFileExists($file1->FullPath, 'File is only detached, not deleted from filesystem'); $this->assertFileExists($file1->FullPath, 'File is only detached, not deleted from filesystem');
} }
/**
* Test that items can be removed from has_many
*/
public function testRemoveFromHasMany() { public function testRemoveFromHasMany() {
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); $record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$file2 = $this->objFromFixture('File', 'file2'); $file2 = $this->objFromFixture('File', 'file2');
$file3 = $this->objFromFixture('File', 'file3'); $file3 = $this->objFromFixture('File', 'file3');
// Check record has two files attached
$this->assertEquals(array('File2', 'File3'), $record->HasManyFiles()->column('Title')); $this->assertEquals(array('File2', 'File3'), $record->HasManyFiles()->column('Title'));
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasManyFiles/item/' . $file2->ID . '/remove', // Remove file 2
array() $response = $this->mockUploadFileIDs('HasManyFiles', array($file3->ID));
); $this->assertEmpty($response['errors']);
$this->assertFalse($response->isError());
// check only file 3 is left
$record = DataObject::get_by_id($record->class, $record->ID, false); $record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertEquals(array('File3'), $record->HasManyFiles()->column('Title')); $this->assertEquals(array('File3'), $record->HasManyFiles()->column('Title'));
// Check file 2 object itself exists
$this->assertFileExists($file3->FullPath, 'File is only detached, not deleted from filesystem'); $this->assertFileExists($file3->FullPath, 'File is only detached, not deleted from filesystem');
} }
/**
* Test that items can be removed from many_many
*/
public function testRemoveFromManyMany() { public function testRemoveFromManyMany() {
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); $record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$file4 = $this->objFromFixture('File', 'file4'); $file4 = $this->objFromFixture('File', 'file4');
$file5 = $this->objFromFixture('File', 'file5'); $file5 = $this->objFromFixture('File', 'file5');
// Check that both files are currently set
$this->assertContains('File4', $record->ManyManyFiles()->column('Title')); $this->assertContains('File4', $record->ManyManyFiles()->column('Title'));
$this->assertContains('File5', $record->ManyManyFiles()->column('Title')); $this->assertContains('File5', $record->ManyManyFiles()->column('Title'));
$response = $this->post(
'UploadFieldTest_Controller/Form/field/ManyManyFiles/item/' . $file4->ID . '/remove', // Remove file 4
array() $response = $this->mockUploadFileIDs('ManyManyFiles', array($file5->ID));
); $this->assertEmpty($response['errors']);
$this->assertFalse($response->isError());
// check only file 5 is left
$record = DataObject::get_by_id($record->class, $record->ID, false); $record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertNotContains('File4', $record->ManyManyFiles()->column('Title')); $this->assertNotContains('File4', $record->ManyManyFiles()->column('Title'));
$this->assertContains('File5', $record->ManyManyFiles()->column('Title')); $this->assertContains('File5', $record->ManyManyFiles()->column('Title'));
// check file 4 object exists
$this->assertFileExists($file4->FullPath, 'File is only detached, not deleted from filesystem'); $this->assertFileExists($file4->FullPath, 'File is only detached, not deleted from filesystem');
} }
/**
* Test that files can be deleted from has_one and the filesystem
*/
public function testDeleteFromHasOne() { public function testDeleteFromHasOne() {
$this->loginWithPermission('ADMIN'); $this->loginWithPermission('ADMIN');
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); $record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$file1 = $this->objFromFixture('File', 'file1'); $file1 = $this->objFromFixture('File', 'file1');
// Check that file initially exists
$this->assertTrue($record->HasOneFile()->exists()); $this->assertTrue($record->HasOneFile()->exists());
$response = $this->post( $this->assertFileExists($file1->FullPath);
'UploadFieldTest_Controller/Form/field/HasOneFile/item/' . $file1->ID . '/delete',
array() // Delete physical file and update record
); $response = $this->mockFileDelete('HasOneFile', $file1->ID);
$this->assertFalse($response->isError()); $this->assertFalse($response->isError());
$response = $this->mockUploadFileIDs('HasOneFile', array());
$this->assertEmpty($response['errors']);
// Check that file is not set against record
$record = DataObject::get_by_id($record->class, $record->ID, false); $record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertFalse($record->HasOneFile()->exists()); $this->assertFalse($record->HasOneFile()->exists());
// Check that the physical file is deleted
$this->assertFileNotExists($file1->FullPath, 'File is also removed from filesystem'); $this->assertFileNotExists($file1->FullPath, 'File is also removed from filesystem');
} }
/**
* Test that files can be deleted from has_many and the filesystem
*/
public function testDeleteFromHasMany() { public function testDeleteFromHasMany() {
$this->loginWithPermission('ADMIN'); $this->loginWithPermission('ADMIN');
@ -265,25 +325,28 @@ class UploadFieldTest extends FunctionalTest {
$file2 = $this->objFromFixture('File', 'file2'); $file2 = $this->objFromFixture('File', 'file2');
$file3 = $this->objFromFixture('File', 'file3'); $file3 = $this->objFromFixture('File', 'file3');
// Check that files initially exists
$this->assertEquals(array('File2', 'File3'), $record->HasManyFiles()->column('Title')); $this->assertEquals(array('File2', 'File3'), $record->HasManyFiles()->column('Title'));
$response = $this->post( $this->assertFileExists($file2->FullPath);
'UploadFieldTest_Controller/Form/field/HasManyFiles/item/' . $file2->ID . '/delete', $this->assertFileExists($file3->FullPath);
array()
); // Delete physical file and update record without file 2
$response = $this->mockFileDelete('HasManyFiles', $file2->ID);
$this->assertFalse($response->isError()); $this->assertFalse($response->isError());
$response = $this->mockUploadFileIDs('HasManyFiles', array($file3->ID));
$this->assertEmpty($response['errors']);
// Test that file is removed from record
$record = DataObject::get_by_id($record->class, $record->ID, false); $record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertEquals(array('File3'), $record->HasManyFiles()->column('Title')); $this->assertEquals(array('File3'), $record->HasManyFiles()->column('Title'));
// Test that physical file is removed
$this->assertFileNotExists($file2->FullPath, 'File is also removed from filesystem'); $this->assertFileNotExists($file2->FullPath, 'File is also removed from filesystem');
$fileNotOnRelationship = $this->objFromFixture('File', 'file1');
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasManyFiles/item/' . $fileNotOnRelationship->ID . '/delete',
array()
);
$this->assertEquals(403, $response->getStatusCode(),
"Denies deleting files if they're not on the current relationship");
} }
/**
* Test that files can be deleted from many_many and the filesystem
*/
public function testDeleteFromManyMany() { public function testDeleteFromManyMany() {
$this->loginWithPermission('ADMIN'); $this->loginWithPermission('ADMIN');
@ -292,26 +355,35 @@ class UploadFieldTest extends FunctionalTest {
$file5 = $this->objFromFixture('File', 'file5'); $file5 = $this->objFromFixture('File', 'file5');
$fileNoDelete = $this->objFromFixture('File', 'file-nodelete'); $fileNoDelete = $this->objFromFixture('File', 'file-nodelete');
$this->assertContains('File4', $record->ManyManyFiles()->column('Title')); // Test that files initially exist
$this->assertContains('File5', $record->ManyManyFiles()->column('Title')); $setFiles = $record->ManyManyFiles()->column('Title');
$response = $this->post( $this->assertContains('File4', $setFiles);
'UploadFieldTest_Controller/Form/field/ManyManyFiles/item/' . $file4->ID . '/delete', $this->assertContains('File5', $setFiles);
array() $this->assertContains('nodelete.txt', $setFiles);
); $this->assertFileExists($file4->FullPath);
$this->assertFileExists($file5->FullPath);
$this->assertFileExists($fileNoDelete->FullPath);
// Delete physical file and update record without file 4
$response = $this->mockFileDelete('ManyManyFiles', $file4->ID);
$this->assertFalse($response->isError()); $this->assertFalse($response->isError());
// Check file is removed from record
$record = DataObject::get_by_id($record->class, $record->ID, false); $record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertNotContains('File4', $record->ManyManyFiles()->column('Title')); $this->assertNotContains('File4', $record->ManyManyFiles()->column('Title'));
$this->assertContains('File5', $record->ManyManyFiles()->column('Title')); $this->assertContains('File5', $record->ManyManyFiles()->column('Title'));
// Check physical file is removed from filesystem
$this->assertFileNotExists($file4->FullPath, 'File is also removed from filesystem'); $this->assertFileNotExists($file4->FullPath, 'File is also removed from filesystem');
// Test record-based permissions // Test record-based permissions
$response = $this->post( $response = $this->mockFileDelete('ManyManyFiles/', $fileNoDelete->ID);
'UploadFieldTest_Controller/Form/field/ManyManyFiles/item/' . $fileNoDelete->ID . '/delete',
array()
);
$this->assertEquals(403, $response->getStatusCode()); $this->assertEquals(403, $response->getStatusCode());
} }
/**
* Test control output html
*/
public function testView() { public function testView() {
$this->loginWithPermission('ADMIN'); $this->loginWithPermission('ADMIN');
@ -326,7 +398,7 @@ class UploadFieldTest extends FunctionalTest {
$this->assertFalse($response->isError()); $this->assertFalse($response->isError());
$parser = new CSSContentParser($response->getBody()); $parser = new CSSContentParser($response->getBody());
$items = $parser->getBySelector('#ManyManyFiles .ss-uploadfield-files .ss-uploadfield-item'); $items = $parser->getBySelector('#HasManyNoViewFiles .ss-uploadfield-files .ss-uploadfield-item');
$ids = array(); $ids = array();
foreach($items as $item) $ids[] = (int)$item['data-fileid']; foreach($items as $item) $ids[] = (int)$item['data-fileid'];
@ -368,16 +440,16 @@ class UploadFieldTest extends FunctionalTest {
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); $record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$form = $this->getMockForm(); $form = $this->getMockForm();
$field = new UploadField('MyField'); $field = UploadField::create('MyField');
$field->setForm($form); $field->setForm($form);
$this->assertNull($field->getRecord(), 'Returns no record by default'); $this->assertNull($field->getRecord(), 'Returns no record by default');
$field = new UploadField('MyField'); $field = UploadField::create('MyField');
$field->setForm($form); $field->setForm($form);
$form->loadDataFrom($record); $form->loadDataFrom($record);
$this->assertEquals($record, $field->getRecord(), 'Returns record from form if available'); $this->assertEquals($record, $field->getRecord(), 'Returns record from form if available');
$field = new UploadField('MyField'); $field = UploadField::create('MyField');
$field->setForm($form); $field->setForm($form);
$field->setRecord($record); $field->setRecord($record);
$this->assertEquals($record, $field->getRecord(), 'Returns record when set explicitly'); $this->assertEquals($record, $field->getRecord(), 'Returns record when set explicitly');
@ -385,22 +457,24 @@ class UploadFieldTest extends FunctionalTest {
public function testSetItems() { public function testSetItems() {
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); $record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$form = $this->getMockForm();
$items = new ArrayList(array( $items = new ArrayList(array(
$this->objFromFixture('File', 'file1'), $this->objFromFixture('File', 'file1'),
$this->objFromFixture('File', 'file2') $this->objFromFixture('File', 'file2')
)); ));
// Field with no record attached
$field = UploadField::create('DummyField');
$field->setItems($items);
$this->assertEquals(array('File1', 'File2'), $field->getItems()->column('Title'));
// Anonymous field // Anonymous field
$field = new UploadField('MyField'); $field = UploadField::create('MyField');
$field->setForm($form);
$field->setRecord($record); $field->setRecord($record);
$field->setItems($items); $field->setItems($items);
$this->assertEquals(array('File1', 'File2'), $field->getItems()->column('Title')); $this->assertEquals(array('File1', 'File2'), $field->getItems()->column('Title'));
// Field with has_one auto-detected // Field with has_one auto-detected
$field = new UploadField('HasOneFile'); $field = UploadField::create('HasOneFile');
$field->setForm($form);
$field->setRecord($record); $field->setRecord($record);
$field->setItems($items); $field->setItems($items);
$this->assertEquals(array('File1', 'File2'), $field->getItems()->column('Title'), $this->assertEquals(array('File1', 'File2'), $field->getItems()->column('Title'),
@ -410,30 +484,25 @@ class UploadFieldTest extends FunctionalTest {
public function testGetItems() { public function testGetItems() {
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1'); $record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$form = $this->getMockForm();
// Anonymous field // Anonymous field
$field = new UploadField('MyField'); $field = UploadField::create('MyField');
$field->setForm($form); $field->setValue(null, $record);
$field->setRecord($record);
$this->assertEquals(array(), $field->getItems()->column('Title')); $this->assertEquals(array(), $field->getItems()->column('Title'));
// Field with has_one auto-detected // Field with has_one auto-detected
$field = new UploadField('HasOneFile'); $field = UploadField::create('HasOneFile');
$field->setForm($form); $field->setValue(null, $record);
$field->setRecord($record);
$this->assertEquals(array('File1'), $field->getItems()->column('Title')); $this->assertEquals(array('File1'), $field->getItems()->column('Title'));
// Field with has_many auto-detected // Field with has_many auto-detected
$field = new UploadField('HasManyFiles'); $field = UploadField::create('HasManyFiles');
$field->setForm($form); $field->setValue(null, $record);
$field->setRecord($record);
$this->assertEquals(array('File2', 'File3'), $field->getItems()->column('Title')); $this->assertEquals(array('File2', 'File3'), $field->getItems()->column('Title'));
// Field with many_many auto-detected // Field with many_many auto-detected
$field = new UploadField('ManyManyFiles'); $field = UploadField::create('ManyManyFiles');
$field->setForm($form); $field->setValue(null, $record);
$field->setRecord($record);
$this->assertNotContains('File1',$field->getItems()->column('Title')); $this->assertNotContains('File1',$field->getItems()->column('Title'));
$this->assertNotContains('File2',$field->getItems()->column('Title')); $this->assertNotContains('File2',$field->getItems()->column('Title'));
$this->assertNotContains('File3',$field->getItems()->column('Title')); $this->assertNotContains('File3',$field->getItems()->column('Title'));
@ -448,14 +517,17 @@ class UploadFieldTest extends FunctionalTest {
$this->assertFalse($response->isError()); $this->assertFalse($response->isError());
$parser = new CSSContentParser($response->getBody()); $parser = new CSSContentParser($response->getBody());
$this->assertFalse((bool)$parser->getBySelector(
'#ReadonlyField .ss-uploadfield-files .ss-uploadfield-item .ss-ui-button'), $this->assertFalse(
(bool)$parser->getBySelector('#ReadonlyField .ss-uploadfield-files .ss-uploadfield-item .ss-ui-button'),
'Removes all buttons on items'); 'Removes all buttons on items');
$this->assertFalse((bool)$parser->getBySelector('#ReadonlyField .ss-uploadfield-dropzone'), $this->assertFalse(
'Removes dropzone'); (bool)$parser->getBySelector('#ReadonlyField .ss-uploadfield-dropzone'),
$this->assertFalse((bool)$parser->getBySelector( 'Removes dropzone'
'#ReadonlyField .ss-uploadfield-addfile .ss-ui-button'), );
'Removes all buttons from "add" area'); $this->assertFalse((bool)$parser->getBySelector('#ReadonlyField .ss-uploadfield-addfile'),
'Entire "add" area'
);
} }
public function testDisabled() { public function testDisabled() {
@ -471,9 +543,9 @@ class UploadFieldTest extends FunctionalTest {
$this->assertFalse((bool)$parser->getBySelector('#DisabledField .ss-uploadfield-dropzone'), $this->assertFalse((bool)$parser->getBySelector('#DisabledField .ss-uploadfield-dropzone'),
'Removes dropzone'); 'Removes dropzone');
$this->assertFalse( $this->assertFalse(
(bool)$parser->getBySelector('#DisabledField .ss-uploadfield-addfile .ss-ui-button'), (bool)$parser->getBySelector('#DisabledField .ss-uploadfield-addfile'),
'Removes all buttons from "add" area'); 'Entire "add" area'
);
} }
public function testCanUpload() { public function testCanUpload() {
@ -491,20 +563,20 @@ class UploadFieldTest extends FunctionalTest {
} }
public function testCanUploadWithPermissionCode() { public function testCanUploadWithPermissionCode() {
$field = new UploadField('MyField'); $field = UploadField::create('MyField');
$field->setConfig('canUpload', true); $field->setCanUpload(true);
$this->assertTrue($field->canUpload()); $this->assertTrue($field->canUpload());
$field->setConfig('canUpload', false); $field->setCanUpload(false);
$this->assertFalse($field->canUpload()); $this->assertFalse($field->canUpload());
$this->loginWithPermission('ADMIN'); $this->loginWithPermission('ADMIN');
$field->setConfig('canUpload', false); $field->setCanUpload(false);
$this->assertFalse($field->canUpload()); $this->assertFalse($field->canUpload());
$field->setConfig('canUpload', 'ADMIN'); $field->setCanUpload('ADMIN');
$this->assertTrue($field->canUpload()); $this->assertTrue($field->canUpload());
} }
@ -522,28 +594,6 @@ class UploadFieldTest extends FunctionalTest {
(bool)$parser->getBySelector('#CanAttachExistingFalseField .ss-uploadfield-fromfiles'), (bool)$parser->getBySelector('#CanAttachExistingFalseField .ss-uploadfield-fromfiles'),
'Removes "From files" button' 'Removes "From files" button'
); );
}
public function testIsSaveable() {
$form = $this->getMockForm();
$field = new UploadField('MyField');
$this->assertTrue($field->isSaveable(), 'Field without relation is always marked as saveable');
$field = new UploadField('HasOneFile');
$this->assertTrue($field->isSaveable(), 'Field with has_one relation is saveable without record on form');
$field = new UploadField('HasOneFile');
$newRecord = new UploadFieldTest_Record();
$form->loadDataFrom($newRecord);
$field->setForm($form);
$this->assertFalse($field->isSaveable(), 'Field with has_one relation not saveable with new record on form');
$field = new UploadField('HasOneFile');
$existingRecord = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$form->loadDataFrom($existingRecord);
$field->setForm($form);
$this->assertTrue($field->isSaveable(), 'Field with has_one relation saveable with saved record on form');
} }
public function testSelect() { public function testSelect() {
@ -566,93 +616,6 @@ class UploadFieldTest extends FunctionalTest {
$this->assertNotContains($fileSubfolder->ID, $itemIDs, 'Does not contain file in subfolder'); $this->assertNotContains($fileSubfolder->ID, $itemIDs, 'Does not contain file in subfolder');
} }
public function testAttachHasOne() {
$this->loginWithPermission('ADMIN');
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$file1 = $this->objFromFixture('File', 'file1');
$file2 = $this->objFromFixture('File', 'file2');
$file3AlreadyAttached = $this->objFromFixture('File', 'file3');
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasOneFile/attach',
array('ids' => array($file1->ID/* first file should be ignored */, $file2->ID))
);
$this->assertFalse($response->isError());
$record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertEquals($file2->ID, $record->HasOneFileID, 'Attaches new relations');
}
public function testAttachHasMany() {
$this->loginWithPermission('ADMIN');
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$file1 = $this->objFromFixture('File', 'file1');
$file2 = $this->objFromFixture('File', 'file2');
$file3AlreadyAttached = $this->objFromFixture('File', 'file3');
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasManyFiles/attach',
array('ids' => array($file1->ID, $file2->ID))
);
$this->assertFalse($response->isError());
$record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertContains($file1->ID, $record->HasManyFiles()->column('ID'),
'Attaches new relations');
$this->assertContains($file2->ID, $record->HasManyFiles()->column('ID'),
'Attaches new relations');
$this->assertContains($file3AlreadyAttached->ID, $record->HasManyFiles()->column('ID'),
'Does not detach existing relations');
}
public function testAttachManyMany() {
$this->loginWithPermission('ADMIN');
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$file1 = $this->objFromFixture('File', 'file1');
$file2 = $this->objFromFixture('File', 'file2');
$file5AlreadyAttached = $this->objFromFixture('File', 'file5');
$response = $this->post(
'UploadFieldTest_Controller/Form/field/ManyManyFiles/attach',
array('ids' => array($file1->ID, $file2->ID))
);
$this->assertFalse($response->isError());
$record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertContains($file1->ID, $record->ManyManyFiles()->column('ID'),
'Attaches new relations');
$this->assertContains($file2->ID, $record->ManyManyFiles()->column('ID'),
'Attaches new relations');
$this->assertContains($file5AlreadyAttached->ID, $record->ManyManyFiles()->column('ID'),
'Does not detach existing relations');
}
public function testManagesRelation() {
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$field = new UploadField('ManyManyFiles');
$this->assertFalse($field->managesRelation(), 'False if no record is set');
$field = new UploadField('NoRelationField');
$field->setRecord($record);
$this->assertFalse($field->managesRelation(), 'False if no relation found by name');
$field = new UploadField('HasOneFile');
$field->setRecord($record);
$this->assertTrue($field->managesRelation(), 'True for has_one');
$field = new UploadField('HasManyFiles');
$field->setRecord($record);
$this->assertTrue($field->managesRelation(), 'True for has_many');
$field = new UploadField('ManyManyFiles');
$field->setRecord($record);
$this->assertTrue($field->managesRelation(), 'True for many_many');
}
protected function getMockForm() { protected function getMockForm() {
return new Form(new Controller(), 'Form', new FieldList(), new FieldList()); return new Form(new Controller(), 'Form', new FieldList(), new FieldList());
} }
@ -668,12 +631,76 @@ class UploadFieldTest extends FunctionalTest {
// emulates the $_FILES array // emulates the $_FILES array
return array( return array(
'name' => $tmpFileName, 'name' => array('Uploads' => array($tmpFileName)),
'type' => 'text/plaintext', 'type' => array('Uploads' => array('text/plaintext')),
'size' => filesize($tmpFilePath), 'size' => array('Uploads' => array(filesize($tmpFilePath))),
'tmp_name' => $tmpFilePath, 'tmp_name' => array('Uploads' => array($tmpFilePath)),
'extension' => 'txt', 'error' => array('Uploads' => array(UPLOAD_ERR_OK)),
'error' => UPLOAD_ERR_OK, );
}
/**
* Simulates a form post to the test controller with the specified file IDs
*
* @param string $fileField Name of field to assign ids to
* @param array $ids list of file IDs
* @return boolean Array with key 'errors'
*/
protected function mockUploadFileIDs($fileField, $ids) {
// collate file ids
$files = array();
foreach($ids as $id) {
$files[$id] = $id;
}
$data = array(
'action_submit' => 1
);
if($files) {
// Normal post requests can't submit empty array values for fields
$data[$fileField] = array('Files' => $files);
}
$form = new UploadFieldTestForm();
$form->loadDataFrom($data, true);
if($form->validate()) {
$record = $form->getRecord();
$form->saveInto($record);
$record->write();
return array('errors' => null);
} else {
return array('errors' => $form->getValidator()->getErrors());
}
}
/**
* Simulates a file upload
*
* @param string $fileField Name of the field to mock upload for
* @param array $tmpFileName Name of temporary file to upload
* @return SS_HTTPResponse form response
*/
protected function mockFileUpload($fileField, $tmpFileName) {
$upload = $this->getUploadFile($tmpFileName);
$_FILES = array($fileField => $upload);
return $this->post(
"UploadFieldTest_Controller/Form/field/{$fileField}/upload",
array($fileField => $upload)
);
}
/**
* Simulates a physical file deletion
*
* @param string $fileField Name of the field
* @param integer $fileID ID of the file to delete
* @return SS_HTTPResponse form response
*/
protected function mockFileDelete($fileField, $fileID) {
return $this->post(
"UploadFieldTest_Controller/Form/field/HasOneFile/item/{$fileID}/delete",
array()
); );
} }
@ -738,12 +765,14 @@ class UploadFieldTest_Record extends DataObject implements TestOnly {
); );
private static $has_many = array( private static $has_many = array(
'HasManyFiles' => 'File', 'HasManyFiles' => 'File.HasManyRecord',
'HasManyFilesMaxTwo' => 'File', 'HasManyFilesMaxTwo' => 'File.HasManyMaxTwoRecord',
'HasManyNoViewFiles' => 'File.HasManyNoViewRecord',
'ReadonlyField' => 'File.ReadonlyRecord'
); );
private static $many_many = array( private static $many_many = array(
'ManyManyFiles' => 'File', 'ManyManyFiles' => 'File'
); );
} }
@ -751,7 +780,20 @@ class UploadFieldTest_Record extends DataObject implements TestOnly {
class UploadFieldTest_FileExtension extends DataExtension implements TestOnly { class UploadFieldTest_FileExtension extends DataExtension implements TestOnly {
private static $has_one = array( private static $has_one = array(
'Record' => 'UploadFieldTest_Record' 'HasManyRecord' => 'UploadFieldTest_Record',
'HasManyMaxTwoRecord' => 'UploadFieldTest_Record',
'HasManyNoViewRecord' => 'UploadFieldTest_Record',
'ReadonlyRecord' => 'UploadFieldTest_Record'
);
private static $has_many = array(
'HasOneRecords' => 'UploadFieldTest_Record.HasOneFile',
'HasOneMaxOneRecords' => 'UploadFieldTest_Record.HasOneFileMaxOne',
'HasOneMaxTwoRecords' => 'UploadFieldTest_Record.HasOneFileMaxTwo',
);
private static $belongs_many_many = array(
'ManyManyRecords' => 'UploadFieldTest_Record'
); );
public function canDelete($member = null) { public function canDelete($member = null) {
@ -767,119 +809,119 @@ class UploadFieldTest_FileExtension extends DataExtension implements TestOnly {
} }
} }
class UploadFieldTest_Controller extends Controller implements TestOnly {
protected $template = 'BlankPage';
public function Form() {
$record = DataObject::get_one('UploadFieldTest_Record', '"Title" = \'Record 1\'');
$fieldNoRelation = new UploadField('NoRelationField');
$fieldNoRelation->setFolderName('UploadFieldTest');
$fieldNoRelation->setRecord($record);
$fieldHasOne = new UploadField('HasOneFile');
$fieldHasOne->setFolderName('UploadFieldTest');
$fieldHasOne->setRecord($record);
$fieldHasOneExtendedFile = new UploadField('HasOneExtendedFile');
$fieldHasOneExtendedFile->setFolderName('UploadFieldTest');
$fieldHasOneExtendedFile->setRecord($record);
$fieldHasOneMaxOne = new UploadField('HasOneFileMaxOne');
$fieldHasOneMaxOne->setFolderName('UploadFieldTest');
$fieldHasOneMaxOne->setConfig('allowedMaxFileNumber', 1);
$fieldHasOneMaxOne->setRecord($record);
$fieldHasOneMaxTwo = new UploadField('HasOneFileMaxTwo');
$fieldHasOneMaxTwo->setFolderName('UploadFieldTest');
$fieldHasOneMaxTwo->setConfig('allowedMaxFileNumber', 2);
$fieldHasOneMaxTwo->setRecord($record);
$fieldHasMany = new UploadField('HasManyFiles');
$fieldHasMany->setFolderName('UploadFieldTest');
$fieldHasMany->setRecord($record);
$fieldHasManyMaxTwo = new UploadField('HasManyFilesMaxTwo');
$fieldHasManyMaxTwo->setFolderName('UploadFieldTest');
$fieldHasManyMaxTwo->setConfig('allowedMaxFileNumber', 2);
$fieldHasManyMaxTwo->setRecord($record);
$fieldManyMany = new UploadField('ManyManyFiles');
$fieldManyMany->setFolderName('UploadFieldTest');
$fieldManyMany->setRecord($record);
$fieldReadonly = new UploadField('ReadonlyField');
$fieldReadonly->setFolderName('UploadFieldTest');
$fieldReadonly->setRecord($record);
$fieldReadonly = $fieldReadonly->performReadonlyTransformation();
$fieldDisabled = new UploadField('DisabledField');
$fieldDisabled->setFolderName('UploadFieldTest');
$fieldDisabled->setRecord($record);
$fieldDisabled = $fieldDisabled->performDisabledTransformation();
$fieldSubfolder = new UploadField('SubfolderField');
$fieldSubfolder->setFolderName('UploadFieldTest/subfolder1');
$fieldSubfolder->setRecord($record);
$fieldCanUploadFalse = new UploadField('CanUploadFalseField');
$fieldCanUploadFalse->setConfig('canUpload', false);
$fieldCanUploadFalse->setRecord($record);
$fieldCanAttachExisting = new UploadField('CanAttachExistingFalseField');
$fieldCanAttachExisting->setConfig('canAttachExisting', false);
$fieldCanAttachExisting->setRecord($record);
$form = new Form(
$this,
'Form',
new FieldList(
$fieldNoRelation,
$fieldHasOne,
$fieldHasOneMaxOne,
$fieldHasOneMaxTwo,
$fieldHasOneExtendedFile,
$fieldHasMany,
$fieldHasManyMaxTwo,
$fieldManyMany,
$fieldReadonly,
$fieldDisabled,
$fieldSubfolder,
$fieldCanUploadFalse,
$fieldCanAttachExisting
),
new FieldList(
new FormAction('submit')
),
new RequiredFields(
'NoRelationField',
'HasOneFile',
'HasOneFileMaxOne',
'HasOneFileMaxTwo',
'HasOneExtendedFile',
'HasManyFiles',
'HasManyFilesMaxTwo',
'ManyManyFiles',
'ReadonlyField',
'DisabledField',
'SubfolderField',
'CanUploadFalseField',
'CanAttachExistingField'
)
);
return $form;
}
public function submit($data, $form) {
}
}
/** /**
* Used for testing the create-on-upload * Used for testing the create-on-upload
*/ */
class UploadFieldTest_ExtendedFile extends File implements TestOnly { class UploadFieldTest_ExtendedFile extends File implements TestOnly {
private static $has_many = array(
'HasOneExtendedRecords' => 'UploadFieldTest_Record.HasOneExtendedFile'
);
} }
class UploadFieldTestForm extends Form implements TestOnly {
public function getRecord() {
if(empty($this->record)) {
$this->record = DataObject::get_one('UploadFieldTest_Record', '"Title" = \'Record 1\'');
}
return $this->record;
}
function __construct($controller = null, $name = 'Form') {
if(empty($controller)) {
$controller = new UploadFieldTest_Controller();
}
$fieldNoRelation = UploadField::create('NoRelationField')
->setFolderName('UploadFieldTest');
$fieldHasOne = UploadField::create('HasOneFile')
->setFolderName('UploadFieldTest');
$fieldHasOneExtendedFile = UploadField::create('HasOneExtendedFile')
->setFolderName('UploadFieldTest');
$fieldHasOneMaxOne = UploadField::create('HasOneFileMaxOne')
->setFolderName('UploadFieldTest')
->setAllowedMaxFileNumber(1);
$fieldHasOneMaxTwo = UploadField::create('HasOneFileMaxTwo')
->setFolderName('UploadFieldTest')
->setAllowedMaxFileNumber(2);
$fieldHasMany = UploadField::create('HasManyFiles')
->setFolderName('UploadFieldTest');
$fieldHasManyMaxTwo = UploadField::create('HasManyFilesMaxTwo')
->setFolderName('UploadFieldTest')
->setAllowedMaxFileNumber(2);
$fieldManyMany = UploadField::create('ManyManyFiles')
->setFolderName('UploadFieldTest');
$fieldHasManyNoView = UploadField::create('HasManyNoViewFiles')
->setFolderName('UploadFieldTest');
$fieldReadonly = UploadField::create('ReadonlyField')
->setFolderName('UploadFieldTest')
->performReadonlyTransformation();
$fieldDisabled = UploadField::create('DisabledField')
->setFolderName('UploadFieldTest')
->performDisabledTransformation();
$fieldSubfolder = UploadField::create('SubfolderField')
->setFolderName('UploadFieldTest/subfolder1');
$fieldCanUploadFalse = UploadField::create('CanUploadFalseField')
->setCanUpload(false);
$fieldCanAttachExisting = UploadField::create('CanAttachExistingFalseField')
->setCanAttachExisting(false);
$fields = new FieldList(
$fieldNoRelation,
$fieldHasOne,
$fieldHasOneMaxOne,
$fieldHasOneMaxTwo,
$fieldHasOneExtendedFile,
$fieldHasMany,
$fieldHasManyMaxTwo,
$fieldManyMany,
$fieldHasManyNoView,
$fieldReadonly,
$fieldDisabled,
$fieldSubfolder,
$fieldCanUploadFalse,
$fieldCanAttachExisting
);
$actions = new FieldList(
new FormAction('submit')
);
$validator = new RequiredFields();
parent::__construct($controller, $name, $fields, $actions, $validator);
$this->loadDataFrom($this->getRecord());
}
public function submit($data, Form $form) {
$record = $this->getRecord();
$form->saveInto($record);
$record->write();
return json_encode($record->toMap());
}
}
class UploadFieldTest_Controller extends Controller implements TestOnly {
protected $template = 'BlankPage';
private static $allowed_actions = array('Form', 'index', 'submit');
public function Form() {
return new UploadFieldTestForm($this, 'Form');
}
}

View File

@ -1,53 +1,55 @@
Folder: Folder:
folder1: folder1:
Name: UploadFieldTest Name: UploadFieldTest
folder1-subfolder1: folder1-subfolder1:
Name: subfolder1 Name: subfolder1
ParentID: =>Folder.folder1 ParentID: =>Folder.folder1
File: File:
file1: file1:
Title: File1 Title: File1
Filename: assets/UploadFieldTest/file1.txt Filename: assets/UploadFieldTest/file1.txt
ParentID: =>Folder.folder1 ParentID: =>Folder.folder1
file2: file2:
Title: File2 Title: File2
Filename: assets/UploadFieldTest/file2.txt Filename: assets/UploadFieldTest/file2.txt
ParentID: =>Folder.folder1 ParentID: =>Folder.folder1
file3: file3:
Title: File3 Title: File3
Filename: assets/UploadFieldTest/file3.txt Filename: assets/UploadFieldTest/file3.txt
ParentID: =>Folder.folder1 ParentID: =>Folder.folder1
file4: file4:
Title: File4 Title: File4
Filename: assets/UploadFieldTest/file4.txt Filename: assets/UploadFieldTest/file4.txt
ParentID: =>Folder.folder1 ParentID: =>Folder.folder1
file5: file5:
Title: File5 Title: File5
Filename: assets/UploadFieldTest/file5.txt Filename: assets/UploadFieldTest/file5.txt
ParentID: =>Folder.folder1 ParentID: =>Folder.folder1
file-noview: file-noview:
Title: noview.txt Title: noview.txt
Name: noview.txt Name: noview.txt
Filename: assets/UploadFieldTest/noview.txt Filename: assets/UploadFieldTest/noview.txt
ParentID: =>Folder.folder1 ParentID: =>Folder.folder1
file-noedit: file-noedit:
Title: noedit.txt Title: noedit.txt
Name: noedit.txt Name: noedit.txt
Filename: assets/UploadFieldTest/noedit.txt Filename: assets/UploadFieldTest/noedit.txt
ParentID: =>Folder.folder1 ParentID: =>Folder.folder1
file-nodelete: file-nodelete:
Title: nodelete.txt Title: nodelete.txt
Name: nodelete.txt Name: nodelete.txt
Filename: assets/UploadFieldTest/nodelete.txt Filename: assets/UploadFieldTest/nodelete.txt
ParentID: =>Folder.folder1 ParentID: =>Folder.folder1
file-subfolder: file-subfolder:
Title: file-subfolder.txt Title: file-subfolder.txt
Name: file-subfolder.txt Name: file-subfolder.txt
Filename: assets/UploadFieldTest/subfolder1/file-subfolder.txt Filename: assets/UploadFieldTest/subfolder1/file-subfolder.txt
ParentID: =>Folder.folder1-subfolder1 ParentID: =>Folder.folder1-subfolder1
UploadFieldTest_Record: UploadFieldTest_Record:
record1: record1:
Title: Record 1 Title: Record 1
HasOneFileID: =>File.file1 HasOneFileID: =>File.file1
HasManyFiles: =>File.file2,=>File.file3 HasManyFiles: =>File.file2,=>File.file3
ManyManyFiles: =>File.file4,=>File.file5,=>File.file-noview,=>File.file-noedit,=>File.file-nodelete ManyManyFiles: =>File.file4,=>File.file5,=>File.file-noedit,=>File.file-nodelete
HasManyNoViewFiles: =>File.file4,=>File.file5,=>File.file-noedit,=>File.file-nodelete,=>File.file-noview
ReadonlyField: =>File.file4

View File

@ -619,14 +619,14 @@ class DataObjectTest extends SapphireTest {
* objects */ * objects */
$team1 = $this->objFromFixture('DataObjectTest_Team', 'team1'); $team1 = $this->objFromFixture('DataObjectTest_Team', 'team1');
$team1->CaptainID = $this->idFromFixture('DataObjectTest_Player', 'captain1'); $team1->CaptainID = $this->idFromFixture('DataObjectTest_Player', 'captain1');
$team1->update(array( $team1->update(array(
'DatabaseField' => 'Something', 'DatabaseField' => 'Something',
'Captain.FirstName' => 'Jim', 'Captain.FirstName' => 'Jim',
'Captain.Email' => 'jim@example.com', 'Captain.Email' => 'jim@example.com',
'Captain.FavouriteTeam.Title' => 'New and improved team 1', 'Captain.FavouriteTeam.Title' => 'New and improved team 1',
)); ));
/* Test the simple case of updating fields on the object itself */ /* Test the simple case of updating fields on the object itself */
$this->assertEquals('Something', $team1->DatabaseField); $this->assertEquals('Something', $team1->DatabaseField);
@ -642,6 +642,29 @@ class DataObjectTest extends SapphireTest {
$this->assertEquals('New and improved team 1', $reloadedTeam1->Title); $this->assertEquals('New and improved team 1', $reloadedTeam1->Title);
} }
public function testDataObjectUpdateNew() {
/* update() calls can use the dot syntax to reference has_one relations and other methods that return
* objects */
$team1 = $this->objFromFixture('DataObjectTest_Team', 'team1');
$team1->CaptainID = 0;
$team1->update(array(
'Captain.FirstName' => 'Jim',
'Captain.FavouriteTeam.Title' => 'New and improved team 1',
));
/* Test that the captain ID has been updated */
$this->assertGreaterThan(0, $team1->CaptainID);
/* Fetch the newly created captain */
$captain1 = DataObjectTest_Player::get()->byID($team1->CaptainID);
$this->assertEquals('Jim', $captain1->FirstName);
/* Grab the favourite team and make sure it has the correct values */
$reloadedTeam1 = $captain1->FavouriteTeam();
$this->assertEquals($reloadedTeam1->ID, $captain1->FavouriteTeamID);
$this->assertEquals('New and improved team 1', $reloadedTeam1->Title);
}
public function testWritingInvalidDataObjectThrowsException() { public function testWritingInvalidDataObjectThrowsException() {
$validatedObject = new DataObjectTest_ValidatedObject(); $validatedObject = new DataObjectTest_ValidatedObject();

View File

@ -5,6 +5,16 @@
*/ */
class StringFieldTest extends SapphireTest { class StringFieldTest extends SapphireTest {
/**
* @covers StringField->forTemplate()
*/
public function testForTemplate() {
$this->assertEquals(
"this is<br />\na test!",
DBField::create_field('StringFieldTest_MyStringField', "this is\na test!")->forTemplate()
);
}
/** /**
* @covers StringField->LowerCase() * @covers StringField->LowerCase()

View File

@ -62,8 +62,8 @@
var content = jQuery(o.content); var content = jQuery(o.content);
content.find('.ss-htmleditorfield-file.embed').each(function() { content.find('.ss-htmleditorfield-file.embed').each(function() {
var el = jQuery(this); var el = jQuery(this);
var shortCode = '[embed width="' + el.data('width') + '"' var shortCode = '[embed width="' + el.attr('width') + '"'
+ ' height="' + el.data('height') + '"' + ' height="' + el.attr('height') + '"'
+ ' class="' + el.data('cssclass') + '"' + ' class="' + el.data('cssclass') + '"'
+ ' thumbnail="' + el.data('thumbnail') + '"' + ' thumbnail="' + el.data('thumbnail') + '"'
+ ']' + el.data('url') + ']' + el.data('url')

View File

@ -644,7 +644,7 @@ class SSViewer {
foreach (scandir($path) as $item) { foreach (scandir($path) as $item) {
if ($item[0] != '.' && is_dir("$path/$item")) { if ($item[0] != '.' && is_dir("$path/$item")) {
if ($subthemes || !strpos($item, '_')) { if ($subthemes || strpos($item, '_') === false) {
$themes[$item] = $item; $themes[$item] = $item;
} }
} }