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:
canUpload: true
canAttachExisting: 'CMS_ACCESS_AssetAdmin'
replaceExistingFile: false
canPreviewFolder: true
previewMaxWidth: 80
previewMaxHeight: 60
uploadTemplateName: 'ss-uploadfield-uploadtemplate'
downloadTemplateName: 'ss-uploadfield-downloadtemplate'
fileEditFields:
fileEditActions:
fileEditValidator:
overwriteWarning: true # Warning before overwriting existing file (only relevant when Upload: replaceFile is true)

View File

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

View File

@ -152,6 +152,15 @@ class LeftAndMain extends Controller implements PermissionProvider {
*/
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
*/
@ -327,28 +336,30 @@ class LeftAndMain extends Controller implements PermissionProvider {
HTMLEditorField::include_js();
Requirements::combine_files(
'leftandmain.js',
array_unique(array_merge(
array(
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Layout.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.ActionTabSet.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Panel.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Tree.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Ping.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Content.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.EditForm.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Menu.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Preview.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.BatchActions.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.FieldHelp.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.TreeDropdownField.js',
),
Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang', true, true),
Requirements::add_i18n_javascript(FRAMEWORK_ADMIN_DIR . '/javascript/lang', true, true)
))
);
$leftAndMainIncludes = array_unique(array_merge(
array(
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Layout.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.ActionTabSet.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Panel.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Tree.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Content.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.EditForm.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Menu.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Preview.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.BatchActions.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.FieldHelp.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.TreeDropdownField.js',
),
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()
if (Director::isDev()) {
@ -381,7 +392,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
if($extraCss) foreach($extraCss as $file => $config) {
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) {
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")
*/
public static function require_themed_css($name, $media = null) {
Deprecation::notice('3.2', 'Use "LeftAndMain.extra_requirements_css" config setting instead');
Config::inst()->update('LeftAndMain', 'extra_requirements_css', array($name => array('media' => $media)));
Deprecation::notice('3.2', 'Use "LeftAndMain.extra_requirements_themedCss" config setting instead');
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();
if($this->isCurrent) $classes .= " current";
$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;
}

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 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 .ui-icon-closethick { background: url('../images/sprites-32x32-sf6890c994e.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 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-s972e408b05.png') 0 -292px no-repeat; width: 30px; height: 30px; }
.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 .middleColumn { margin-left: 0; }
.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 .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.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; }
@ -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 .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 .CompositeField .text select { margin: 5px 0 0 0; }
.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-label > * { display: inline-block; vertical-align: top; }
.step-label .flyout { height: 18px; font-size: 14px; font-weight: bold; -moz-border-radius-topleft: 3px; -webkit-border-top-left-radius: 3px; border-top-left-radius: 3px; -moz-border-radius-bottomleft: 3px; -webkit-border-bottom-left-radius: 3px; border-bottom-left-radius: 3px; background-color: #667980; padding: 4px 3px 4px 6px; text-align: center; text-shadow: none; color: #fff; }
.step-label .arrow { height: 26px; width: 10px; background: url('../images/sprites-32x32-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; }
/** -------------------------------------------- Item Edit Form -------------------------------------------- */
@ -710,10 +712,10 @@ form.import-form label.left { width: 250px; }
/** -------------------------------------------- Buttons for FileUpload -------------------------------------------- */
.ss-uploadfield-item-edit-all .ui-button-text { padding-right: 0; }
.toggle-details-icon { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -433px 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; }
.toggle-details-icon.opened { background: url('../images/sprites-32x32-sf6890c994e.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; }
.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-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-s972e408b05.png') 0 -1121px 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 -------------------------------------------- */
.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 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, .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 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; }
@ -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 .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-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-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; }
@ -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.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.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.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; }
@ -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 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.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.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.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-modified, .tree-holder.jstree-apple span.badge.status-addedtodraft, .cms-tree.jstree-apple span.badge.status-modified, .cms-tree.jstree-apple span.badge.status-addedtodraft { color: #7E7470; border: 1px solid #C9B800; background-color: #FFF0BC; }
.tree-holder.jstree-apple span.badge.status-deletedonlive, .tree-holder.jstree-apple span.badge.status-removedfromdraft, .cms-tree.jstree-apple span.badge.status-deletedonlive, .cms-tree.jstree-apple span.badge.status-removedfromdraft { color: #636363; border: 1px solid #E49393; background-color: #F2DADB; }
.tree-holder.jstree-apple span.badge.status-workflow-approval, .cms-tree.jstree-apple span.badge.status-workflow-approval { color: #56660C; border: 1px solid #7C8816; background-color: #DAE79A; }
.tree-holder.jstree-apple span.comment-count, .cms-tree.jstree-apple span.comment-count { clear: both; position: relative; text-transform: uppercase; display: inline-block; overflow: visible; padding: 0px 3px; font-size: 0.75em; line-height: 1em; margin-left: 3px; margin-right: 6px; -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: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; }
@ -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-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 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 .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 .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.opened .toggle-children-icon { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -359px no-repeat; }
.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-s972e408b05.png') 0 -333px no-repeat; }
.cms-menu-list li ul li a { border-top: 1px solid #b6c3cb; }
.cms-menu-list li.current a { color: white; text-shadow: #1e5270 0 -1px 0; border-top: 1px solid #55a4d2; border-bottom: 1px solid #236184; background-color: #338dc1; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+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.opened .toggle-children-icon { background: url('../images/sprites-32x32-sf6890c994e.png') 0 -1121px 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-s972e408b05.png') 0 -1121px no-repeat; }
.cms-menu-list li.current ul { border-top: none; display: block; }
.cms-menu-list li.current li { background-color: #287099; }
.cms-menu-list li.current li a { font-size: 11px; padding: 0 10px 0 40px; height: 32px; line-height: 32px; color: #e2f0f7; background: none; border-top: 1px solid #2f81b1; border-bottom: 1px solid #1e5270; }
@ -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 .icon-view, .cms-content-controls .preview-selector.dropdown a.chzn-single { white-space: nowrap; }
.cms-content-controls .icon-view:before, .cms-content-controls .preview-selector.dropdown a.chzn-single:before { display: inline-block; float: left; content: ''; width: 23px; height: 17px; overflow: hidden; }
.cms-content-controls .icon-auto:before { background: url('../images/sprites-32x32-sf6890c994e.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-tablet:before { background: url('../images/sprites-32x32-sf6890c994e.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-split:before { background: url('../images/sprites-32x32-sf6890c994e.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-preview:before { background: url('../images/sprites-32x32-sf6890c994e.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-auto:before { background: url('../images/sprites-32x32-s972e408b05.png') 0 -108px 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-s972e408b05.png') 0 -162px 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-s972e408b05.png') 0 -135px 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-s972e408b05.png') 0 0 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 .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; }
@ -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: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: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: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.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: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 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-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-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-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 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; }

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
var firstTabWithErrors = this.find('.message.validation:first').closest('.tab');
$('.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();

View File

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

View File

@ -291,6 +291,10 @@
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
var origChildren = node.children('ul').detach();
node.addClass(origClasses).html(newNode.html()).append(origChildren);

View File

@ -821,15 +821,18 @@ jQuery.noConflict();
$('.cms-content .Actions').entwine({
onmatch: function() {
this.find('.ss-ui-button').click(function() {
var form = this.form;
// forms don't natively store the button they've been triggered with
if(form) {
form.clickedButton = this;
// Reset the clicked button shortly after the onsubmit handlers
// have fired on the form
setTimeout(function() {form.clickedButton = null;}, 10);
}
});
var form = this.form;
// forms don't natively store the button they've been triggered with
if(form) {
form.clickedButton = this;
// Reset the clicked button shortly after the onsubmit handlers
// have fired on the form
setTimeout(function() {
form.clickedButton = null;
}, 10);
}
});
this.redraw();
this._super();
@ -942,34 +945,46 @@ jQuery.noConflict();
* Generic search form in the CMS, often hooked up to a GridField results display.
*/
$('.cms-search-form').entwine({
onsubmit: function() {
onsubmit: function(e) {
// 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(),
// as it handles checkbox values more consistently
var vals = $.grep($(this).fieldValue(), function(val) { return (val);});
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');
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;
},
/**
* 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
* 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;
background: sprite($sprites32, logout) no-repeat;
text-indent: -9999em;
opacity:0.9;
&:hover, &:focus{
opacity:1;
}
}
}

View File

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

View File

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

View File

@ -68,10 +68,43 @@ global $databaseConfig;
// We don't have a session in cli-script, but this prevents errors
$_SESSION = null;
// Connect to database
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);
// Get the request URL from the querystring arguments
$url = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : null;
if(!$url) {

View File

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

View File

@ -137,6 +137,24 @@ class Session {
if($data instanceof Session) $data = $data->inst_getAll();
$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!
if($sid) session_id($sid);
@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) {
if(session_id()) {
if($removeCookie) {
$path = Config::inst()->get('cookie_path');
$path = Config::inst()->get('Session', 'cookie_path');
if(!$path) $path = Director::baseURL();
$domain = Config::inst()->get('cookie_domain');
$secure = Config::inst()->get('cookie_secure');
$domain = Config::inst()->get('Session', 'cookie_domain');
$secure = Config::inst()->get('Session', 'cookie_secure');
if($domain) {
setcookie(session_name(), '', null, $path, $domain, $secure, true);
setcookie(session_name(), '', null, $path, $domain, $secure, true);
}
else {
setcookie(session_name(), '', null, $path, null, $secure, true);
else {
setcookie(session_name(), '', null, $path, null, $secure, true);
}
unset($_COOKIE[session_name()]);
}
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($_POST) stripslashes_recursively($_POST);
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 {
// Please do not change the exception code number below.
throw new Exception("Object->__call(): the method '$method' does not exist on '$this->class'", 2175);
$class = get_class($this);
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 .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-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: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, .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, .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, .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.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; }
@ -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-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-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-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 .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-item-uploador { float: left; font-weight: bold; font-size: 22px; padding: 0 20px; line-height: 70px; margin-top: 4px; display: none; }
.ss-insert-media .ss-assetuploadfield .ss-uploadfield-addfile .ss-uploadfield-item-uploador { font-size: 18px; margin-top: 0; }
.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-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: 10px; display: none; }
.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; 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 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-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 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-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.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.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.workflow-approval { color: #56660C; border: 1px solid #7C8816; background-color: #DAE79A; }
.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.status-addedtodraft { color: #7E7470; border: 1px solid #C9B800; background-color: #FFF0BC; }
.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.status-removedfromdraft { color: #636363; border: 1px solid #E49393; background-color: #F2DADB; }
.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.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; }

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 .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); }

View File

@ -13,30 +13,31 @@ Used in side panels and action tabs
.ss-uploadfield .clear { clear: both; }
.ss-insert-media .ss-uploadfield { margin-top: 20px; }
.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 .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-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 b { font-weight: bold; padding: 0 5px 0 0; }
.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 .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.ui-state-error-text { color: red; font-weight: bold; }
.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 .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; 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; 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-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-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: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 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-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 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.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; }

View File

@ -860,6 +860,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
}
$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(
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() {

View File

@ -31,6 +31,8 @@ if (function_exists('session_start')) {
/**
* Include _ss_environment.php file
*/
$usingEnv = false;
$envFileExists = false;
//define the name of the environment file
$envFile = '_ss_environment.php';
//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 the file exists, then we include it, set relevant vars and break out
if (file_exists($dir . $envFile)) {
define('SS_ENVIRONMENT_FILE', $dir . $envFile);
include_once(SS_ENVIRONMENT_FILE);
include_once($dir . $envFile);
$envFileExists = true;
//legacy variable assignment
$usingEnv = true;
//break out of BOTH loops because we found the $envFile
break(2);
}
@ -418,6 +422,7 @@ class InstallRequirements {
$this->requireModule(FRAMEWORK_NAME, array("File permissions", FRAMEWORK_NAME . "/ directory exists?"));
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));
} elseif($isIIS) {
$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->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('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->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('asp_tags', array(false), array('PHP Configuration', 'asp_tags option', 'This should be turned off as it can cause issues with SilverStripe'));
$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), 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
$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) {
$this->testing($testDetails);
@ -673,6 +688,17 @@ class InstallRequirements {
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) {
$this->testing($testDetails);
@ -1350,16 +1376,15 @@ HTML;
ErrorDocument 404 /assets/error-404.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>
SetEnv HTTP_MOD_REWRITE On
RewriteEngine On
$baseClause
RewriteRule ^vendor(/|$) - [F,L,NC]
RewriteRule silverstripe-cache(/|$) - [F,L,NC]
RewriteRule composer\.(json|lock) - [F,L,NC]
RewriteCond %{REQUEST_URI} ^(.*)$
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !\.php$

View File

@ -389,33 +389,44 @@ you can enable those warnings and future-proof your code already.
### 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
* `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
* `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
* `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
* Deprecated `Profiler` class, use third-party solutions like [xhprof](https://github.com/facebook/xhprof/)
* Removed defunct or unnecessary debug GET parameters:
`debug_profile`, `debug_memory`, `profile_trace`, `debug_javascript`, `debug_behaviour`
* Removed `Member_ProfileForm`, use `CMSProfileController` instead
* `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`.
* Moved email bounce handling to new ["emailbouncehandler" module](https://github.com/silverstripe-labs/silverstripe-emailbouncehandler),
* 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`.
* Moved email bounce handling to new
["emailbouncehandler" module](https://github.com/silverstripe-labs/silverstripe-emailbouncehandler),
including `Email_BounceHandler` and `Email_BounceRecord` classes,
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 support for keyed arrays in `SelectionGroup`, use new `SelectionGroup_Item` object
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->rootFieldSet()`: Use rootFieldList()
* 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 `SQLMap::map()`: Use DataList::("Member")->map()
* Removed `SQLMap::mapInGroups()`: Use Member::map_in_groups()
* 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#sort`
- `ArrayList#filter`
@ -430,12 +441,15 @@ you can enable those warnings and future-proof your code already.
- `DataList#find`
- `DataList#byIDs`
- `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
`YearlyTask` are deprecated, please extend from `BuildTask` or `CliController`,
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.
Before: `array('de_DE' => array('German', 'Deutsch'))`, after: `array('de_DE' => array('name' => 'German', 'native' => 'Deutsch'))`.
* `i18n::$common_locales` and `i18n::$common_languages` are now accessed via the Config API, and contain
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.
Please use it to toggle theme behaviour rather than relying on the custom theme being set in the
(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.
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)`.
* 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 %>`
@ -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.
`DataObject` also has `beforeUpdateCMSFields` to insert fields between automatic scaffolding and extension
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
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)
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.
## 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:
* The main installation directory (for creating .htaccess file and assets directory)
* The mysite folder (to create _config.php)
* 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 original instructions on this page have been removed, as they are obsolete and misleading. Please use Composer
instead.
The original instructions on this page have been removed, as they were obsolete and misleading. Please use Composer
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
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.
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.
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
@ -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".
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/))
* 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:
* Apache 1.3+ with mod_rewrite and "AllowOverride All" set
* Apache 2.0+ with mod_rewrite and "AllowOverride All" set
* IIS 7+
* 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.
@ -36,7 +36,7 @@ Our web-based [PHP installer](/installation) can check if you meet the requireme
## 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.

View File

@ -1,13 +1,13 @@
# 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.
See our [upgrade notes and changelogs](/changelogs) for release-specific information.
## 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 webroot files
* 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/)
* [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
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.
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"**
**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**

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
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

View File

@ -10,8 +10,19 @@ like for instance in creating and managing a simple gallery.
## Usage
The field can be used in two ways: To upload a single file into a `has_one` relationship,
or allow multiple files into a fixed folder (or 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 `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
@ -42,13 +53,13 @@ based on a has_one relation:
The UploadField will autodetect the relation based on it's `name` property, and
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.
### Multiple fileupload
Enable multiple fileuploads by using a many_many relation. Again, the
UploadField will detect the relation based on its $name property value:
Enable multiple fileuploads by using a many_many (or has_many) relation. Again,
the `UploadField` will detect the relation based on its $name property value:
:::php
class GalleryPage extends Page {
@ -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)'
)
);
$uploadField->setConfig('allowedMaxFileNumber', 10);
$uploadField->setAllowedMaxFileNumber(10);
return $fields;
}
}
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
### Overview
The field can either be configured on an instance level through `setConfig(<key>, <value>)`,
or globally by overriding the YAML defaults. See the [Configuration Reference](uploadfield#configuration-reference) section for possible values.
The field can either be configured on an instance level with the various
getProperty and setProperty functions, or globally by overriding the YAML defaults.
See the [Configuration Reference](uploadfield#configuration-reference) section for possible values.
Example: mysite/_config/uploadfield.yml
@ -93,7 +115,6 @@ Example: mysite/_config/uploadfield.yml
defaultConfig:
canUpload: false
### Set a custom folder
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');
## Limit the allowed filetypes
### Limit the allowed filetypes
`AllowedExtensions` defaults to the `File.allowed_extensions` configuration setting,
but can be overwritten for each UploadField:
:::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
@ -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.
:::php
$uploadField->setConfig('previewMaxWidth', 100);
$uploadField->setConfig('previewMaxHeight', 100);
$uploadField->setPreviewMaxWidth(100);
$uploadField->setPreviewMaxHeight(100);
### Disable attachment of existing files
This can force the user to upload a new file, rather than link to the already
existing file 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
@ -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:
:::php
$uploadField->setConfig('autoUpload', false);
$uploadField->setAutoUpload(false);
### Build a simple gallery
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
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
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:
:::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).
### Configuration Reference
- `autoUpload`: (boolean)
- `allowedMaxFileNumber`: (int) php validation of allowedMaxFileNumber
- `setAllowedMaxFileNumber`: (int) php validation of allowedMaxFileNumber
only works when a db relation is available, set to null to allow
unlimited if record has a has_one and allowedMaxFileNumber is null, it will be set to 1
- `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.
- `previewMaxWidth`: (int)
- `previewMaxHeight`: (int)
- `uploadTemplateName`: (string) javascript template used to display uploading
files, see javascript/UploadField_uploadtemplate.js
- `downloadTemplateName`: (string) javascript template used to display already
- `setCanPreviewFolder`: (boolean|string) Can the user preview the folder files will be saved into?
String values are interpreted as permission codes.
- `setCanUpload`: (boolean|string) Can the user upload new files, or just select from existing files.
String values are interpreted as permission codes.
- `setDownloadTemplateName`: (string) javascript template used to display already
uploaded files, see javascript/UploadField_downloadtemplate.js
- `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')
- `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')
- `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')
- `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.
:::yaml
Upload:
# Globally disables automatic renaming of files
# Globally disables automatic renaming of files and displays a warning before overwriting an existing file
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
form.*
The UploadField can be used in a frontend form, given that sufficient attention is given
to the permissions granted to non-authorised users.
By default Image::canDelete and Image::canEdit do not require admin privileges, so
make sure you override the methods in your Image extension class.
For instance, to generate an upload form suitable for saving images into a user-defined
gallery the below code could be used:
:::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
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(
'','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',
'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',
'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"
),
'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(
"arc" ,"rar" ,"tar" ,"gz" ,"tgz" ,"bz2" ,"dmg" ,"jar","ace" ,"arj" ,"bz" ,"cab"
@ -340,10 +340,10 @@ class File extends DataObject {
}
// Upload
$uploadField = new UploadField('UploadField','Upload Field');
$uploadField->setConfig('previewMaxWidth', 40);
$uploadField->setConfig('previewMaxHeight', 30);
$uploadField->setConfig('allowedMaxFileNumber', 1);
$uploadField = UploadField::create('UploadField','Upload Field')
->setPreviewMaxWidth(40)
->setPreviewMaxHeight(30)
->setAllowedMaxFileNumber(1);
//$uploadField->setTemplate('FileEditUploadField');
if ($this->ParentID) {
$parent = $this->Parent();
@ -527,13 +527,13 @@ class File extends DataObject {
if(!is_a($this, 'Folder')) {
// Only throw a fatal error if *both* before and after paths don't exist.
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.
// Only check if we're dealing with a file, otherwise the folder will need to be created
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");
}
}

View File

@ -28,12 +28,14 @@ class Upload extends Controller {
/**
* A File object
*
* @var File
*/
protected $file;
/**
* An instance of Upload_Validator
* Validator for this upload field
*
* @var Upload_Validator
*/
protected $validator;
@ -48,7 +50,8 @@ class Upload extends Controller {
/**
* Replace an existing file rather than renaming the new one.
* @var Boolean
*
* @var boolean
*/
protected $replaceFile;
@ -72,13 +75,13 @@ class Upload extends Controller {
public function __construct() {
parent::__construct();
$this->validator = new Upload_Validator();
$this->replaceFile = $this->config()->replaceFile;
$this->replaceFile = self::config()->replaceFile;
}
/**
* Get current validator
*
* @return object $validator
* @return Upload_Validator $validator
*/
public function getValidator() {
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(!$this->replaceFile) {
while(file_exists("$base/$relativeFilePath")) {
$i = isset($i) ? ($i+1) : 2;
$oldFilePath = $relativeFilePath;
// make sure archives retain valid extensions
if(substr($relativeFilePath, strlen($relativeFilePath) - strlen('.tar.gz')) == '.tar.gz' ||
substr($relativeFilePath, strlen($relativeFilePath) - strlen('.tar.bz2')) == '.tar.bz2') {
$relativeFilePath = preg_replace('/[0-9]*(\.tar\.[^.]+$)/', $i . '\\1', $relativeFilePath);
} else if (strpos($relativeFilePath, '.') !== false) {
$relativeFilePath = preg_replace('/[0-9]*(\.[^.]+$)/', $i . '\\1', $relativeFilePath);
} else if (strpos($relativeFilePath, '_') !== false) {
$relativeFilePath = preg_replace('/_([^_]+$)/', '_'.$i, $relativeFilePath);
} else {
$relativeFilePath .= '_'.$i;
}
if($oldFilePath == $relativeFilePath && $i > 2) {
user_error("Couldn't fix $relativeFilePath with $i tries", E_USER_ERROR);
}
}
while(file_exists("$base/$relativeFilePath")) {
$i = isset($i) ? ($i+1) : 2;
$oldFilePath = $relativeFilePath;
// make sure archives retain valid extensions
if(substr($relativeFilePath, strlen($relativeFilePath) - strlen('.tar.gz')) == '.tar.gz' ||
substr($relativeFilePath, strlen($relativeFilePath) - strlen('.tar.bz2')) == '.tar.bz2') {
$relativeFilePath = preg_replace('/[0-9]*(\.tar\.[^.]+$)/', $i . '\\1', $relativeFilePath);
} else if (strpos($relativeFilePath, '.') !== false) {
$relativeFilePath = preg_replace('/[0-9]*(\.[^.]+$)/', $i . '\\1', $relativeFilePath);
} else if (strpos($relativeFilePath, '_') !== false) {
$relativeFilePath = preg_replace('/_([^_]+$)/', '_'.$i, $relativeFilePath);
} else {
$relativeFilePath .= '_'.$i;
}
if($oldFilePath == $relativeFilePath && $i > 2) {
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")) {
@ -178,6 +184,7 @@ class Upload extends Controller {
// This is to prevent it from trying to rename the file
$this->file->Name = basename($relativeFilePath);
$this->file->write();
$this->extend('onAfterLoad', $this->file); //to allow extensions to e.g. create a version after an upload
return true;
} else {
$this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.');
@ -273,7 +280,7 @@ class Upload extends Controller {
* @return array
*/
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.
*
* @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
* be removed if it's a data field. Dataless fields, such as tabs, will
* be left as-is.
@ -182,8 +182,16 @@ class FieldList extends ArrayList {
if(!$fieldName) {
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();
foreach($this->items as $i => $child) {
if(is_object($child)){
$childName = $child->getName();

View File

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

View File

@ -259,10 +259,6 @@ class Form extends RequestHandler {
} else {
$vars = $request->requestVars();
}
if(isset($funcName)) {
Form::set_current_action($funcName);
}
// Populate the form
$this->loadDataFrom($vars, true);
@ -299,6 +295,7 @@ class Form extends RequestHandler {
}
if(isset($funcName)) {
Form::set_current_action($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.
* By default, makes use of $this->dataValue()
*
* @param DataObjectInterface $record DataObject to save data into
*/
public function saveInto(DataObjectInterface $record) {
if($this->name) {
@ -403,7 +405,9 @@ class FormField extends RequestHandler {
/**
* Set the field value.
* Returns $this.
*
* @param mixed $value
* @return FormField Self reference
*/
public function setValue($value) {
$this->value = $value;

View File

@ -2,14 +2,13 @@
/**
* 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>
*
* - File Drag&Drop support
* - Progressbar
* - Image thumbnail/file icons even before upload finished
* - Saving into relations
* - Saving into relations on form submit
* - Edit file
* - allowedExtensions is by default File::$allowed_extensions<li>maxFileSize the value of min(upload_max_filesize,
* post_max_size) from php.ini
@ -17,9 +16,9 @@
* <>Usage</b>
*
* @example <code>
* $UploadField = new UploadField('myFiles', 'Please upload some images <span>(max. 5 files)</span>');
* $UploadField->getValidator()->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif'));
* $UploadField->setConfig('allowedMaxFileNumber', 5);
* $UploadField = new UploadField('AttachedImages', 'Please upload some images <span>(max. 5 files)</span>');
* $UploadField->setAllowedFileCategories('image');
* $UploadField->setAllowedMaxFileNumber(5);
* </code>
*
* @author Zauberfisch
@ -36,6 +35,7 @@ class UploadField extends FileField {
'attach',
'handleItem',
'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';
/**
* @var String
* Template to use for the edit form
*
* @var string
*/
protected $templateFileEdit = 'UploadField_FileEdit';
/**
* Parent data record. Will be infered from parent form or controller if blank.
*
* @var DataObject
*/
protected $record;
/**
* Items loaded into this field. May be a RelationList, or any other SS_List
*
* @var SS_List
*/
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).
* See framework/_config/uploadfield.yml for configuration defaults and documentation.
*
* @var array
*/
protected $ufConfig = array(
/**
* Automatically upload the file once selected
*
* @var boolean
*/
'autoUpload' => true,
/**
* php validation of allowedMaxFileNumber only works when a db relation is available, set to null to allow
* unlimited if record has a has_one and allowedMaxFileNumber is null, it will be set to 1
* @var int
* 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.
* The resulting value will be set to maxNumberOfFiles
*
* @var integer
*/
'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.
*
* @var boolean|string
*/
'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.
*
* @var boolean|string
*/
'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.
*
* @var boolean|string
*/
'canPreviewFolder' => true,
/**
* @var boolean If a second file is uploaded, should it replace the existing one rather than throwing an errror?
* This only applies for has_one relationships, and only replaces the association
* rather than the actual file database record or filesystem entry.
*/
'replaceExistingFile' => false,
/**
* @var int
* Maximum width of the preview thumbnail
*
* @var integer
*/
'previewMaxWidth' => 80,
/**
* @var int
* Maximum height of the preview thumbnail
*
* @var integer
*/
'previewMaxHeight' => 60,
/**
* javascript template used to display uploading files
*
* @see javascript/UploadField_uploadtemplate.js
* @var string
*/
'uploadTemplateName' => 'ss-uploadfield-uploadtemplate',
/**
* javascript template used to display already uploaded files
*
* @see javascript/UploadField_downloadtemplate.js
* @var string
*/
'downloadTemplateName' => 'ss-uploadfield-downloadtemplate',
/**
* FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm
* @example 'getCMSFields'
* @var FieldList|string
* 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)
*
* @see Upload
* @var boolean
*/
'fileEditFields' => null,
/**
* 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
'overwriteWarning' => true
);
/**
* 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 $title The field label.
* @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
*/
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
$this->addExtraClass('ss-upload'); // class, used by js
$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);
@ -166,15 +205,17 @@ class UploadField extends FileField {
$this->getValidator()->setAllowedExtensions(
array_filter(Config::inst()->get('File', 'allowed_extensions'))
);
// get the lower max size
$this->getValidator()->setAllowedMaxFileSize(min(File::ini2bytes(ini_get('upload_max_filesize')),
File::ini2bytes(ini_get('post_max_size'))));
$maxUpload = File::ini2bytes(ini_get('upload_max_filesize'));
$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)
*
* @param String
* @param string
*/
public function setTemplateFileButtons($template) {
$this->templateFileButtons = $template;
@ -182,7 +223,7 @@ class UploadField extends FileField {
}
/**
* @return String
* @return string
*/
public function getTemplateFileButtons() {
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)
*
* @param String
* @param string
*/
public function setTemplateFileEdit($template) {
$this->templateFileEdit = $template;
@ -199,12 +240,60 @@ class UploadField extends FileField {
}
/**
* @return String
* @return string
*/
public function getTemplateFileEdit() {
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)
* @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
* use Form->getRecord() or Form->Controller()->data()
*
* @return DataObject
*/
public function getRecord() {
if (!$this->record && $this->form) {
if ($this->form->getRecord() && is_a($this->form->getRecord(), 'DataObject')) {
$this->record = $this->form->getRecord();
} elseif ($this->form->Controller() && $this->form->Controller()->hasMethod('data')
&& $this->form->Controller()->data() && is_a($this->form->Controller()->data(), 'DataObject')) {
$this->record = $this->form->Controller()->data();
if (($record = $this->form->getRecord()) && ($record instanceof DataObject)) {
$this->record = $record;
} elseif (($controller = $this->form->Controller())
&& $controller->hasMethod('data')
&& ($record = $controller->data())
&& ($record instanceof DataObject)
) {
$this->record = $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
* @return UploadField self reference
*/
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 SS_List
*/
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
* Customises a file with additional details suitable for rendering in the
* UploadField.ss template
*
* @param File $file
* @return ViewableData_Customised
*/
protected function customiseFile(File $file) {
$file = $file->customise(array(
'UploadFieldHasRelation' => $this->managesRelation(),
'UploadFieldThumbnailURL' => $this->getThumbnailURLForFile($file),
'UploadFieldRemoveLink' => $this->getItemHandler($file->ID)->RemoveLink(),
'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
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 mixed $val
* @return UploadField self reference
*/
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;
return $this;
}
/**
* Gets a front-end config variable for the upload field
*
* @param string $key
* @return mixed
*/
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];
}
/**
* Used to get config in the template
* Determine if the field should automatically upload the file.
*
* @return boolean
*/
public function getAutoUpload() {
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
* @return string
*/
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')) {
return $file->getThumbnail($this->getConfig('previewMaxWidth'),
$this->getConfig('previewMaxHeight'))->getURL();
return $file->getThumbnail($width, $height)->getURL();
} elseif ($file->hasMethod('getThumbnailURL')) {
return $file->getThumbnailURL($this->getConfig('previewMaxWidth'),
$this->getConfig('previewMaxHeight'));
return $file->getThumbnailURL($width, $height);
} elseif ($file->hasMethod('SetRatioSize')) {
return $file->SetRatioSize($this->getConfig('previewMaxWidth'),
$this->getConfig('previewMaxHeight'))->getURL();
return $file->SetRatioSize($width, $height)->getURL();
} else {
return $file->Icon();
}
@ -350,18 +924,6 @@ class UploadField extends FileField {
}
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-ui/jquery-ui.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::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 . '/jquery-fileupload/jquery.iframe-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(FRAMEWORK_DIR . '/css/UploadField.css');
// Calculated config as per jquery.fileupload-ui.js
$allowedMaxFileNumber = $this->getAllowedMaxFileNumber();
$config = array(
'url' => $this->Link('upload'),
'urlSelectDialog' => $this->Link('select'),
'urlAttach' => $this->Link('attach'),
'urlFileExists' => $this->link('fileexists'),
'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['errorMessages']['acceptFileTypes'] = _t(
'File.INVALIDEXTENSIONSHORT',
'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(
'File.TOOLARGESHORT',
'Filesize exceeds {size}',
array('size' => File::format_size($config['maxFileSize']))
);
}
if ($config['maxNumberOfFiles'] > 1) {
$config['errorMessages']['maxNumberOfFiles'] = _t(
'UploadField.MAXNUMBEROFFILESSHORT',
'Can only upload {count} files',
array('count' => $config['maxNumberOfFiles'])
);
// Validation: Number of files
if ($allowedMaxFileNumber) {
if($allowedMaxFileNumber > 1) {
$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()) {
$configOverwrite['maxNumberOfFiles'] = $config['maxNumberOfFiles'] - $this->getItems()->count();
//get all the existing files in the current folder
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(
'configString' => str_replace('"', "'", Convert::raw2json($config)),
'config' => new ArrayData($config),
'multiple' => $config['maxNumberOfFiles'] !== 1,
'displayInput' => (!isset($configOverwrite['maxNumberOfFiles']) || $configOverwrite['maxNumberOfFiles'])
'configString' => str_replace('"', "'", Convert::raw2json($mergedConfig)),
'config' => new ArrayData($mergedConfig),
'multiple' => $allowedMaxFileNumber !== 1
))->renderWith($this->getTemplates());
}
/**
* Validation method for this field, called when the entire form is validated
*
* @param $validator
* @return Boolean
* @param Validator $validator
* @return boolean
*/
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;
}
@ -460,12 +1085,117 @@ class UploadField extends FileField {
public function handleSelect(SS_HTTPRequest $request) {
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
*
* @param SS_HTTPRequest $request
* @return string json
* @return SS_HTTPResponse
*/
public function upload(SS_HTTPRequest $request) {
if($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) {
@ -475,144 +1205,75 @@ class UploadField extends FileField {
// Protect against CSRF on destructive action
$token = $this->getForm()->getSecurityToken();
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.
if (!$tmpfile) {
$return = array('error' => _t('UploadField.FIELDNOTSET', 'File information not found'));
// Get form details
$name = $this->getName();
$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 {
$return = array(
'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
));
}
}
$return = $this->encodeFileAttributes($file);
}
// Format response with json
$response = new SS_HTTPResponse(Convert::raw2json(array($return)));
$response->addHeader('Content-Type', 'text/plain');
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(!$this->managesRelation()) return $this->httpError(403);
if(!$this->canAttachExisting()) return $this->httpError(403);
// Retrieve file attributes required by front end
$return = array();
$files = File::get()->byIDs($request->postVar('ids'));
foreach($files as $file) {
$this->attachFile($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
);
$return[] = $this->encodeFileAttributes($file);
}
$response = new SS_HTTPResponse(Convert::raw2json($return));
$response->addHeader('Content-Type', 'application/json');
return $response;
}
/**
* @param File
* Determines if a specified file exists
*
* @param SS_HTTPRequest $request
*/
protected function attachFile($file) {
$record = $this->getRecord();
$name = $this->getName();
if ($record && $record->exists()) {
if ($record->has_many($name) || $record->many_many($name)) {
if(!$record->isInDB()) $record->write();
$record->{$name}()->add($file);
} elseif($record->has_one($name)) {
$record->{$name . 'ID'} = $file->ID;
$record->write();
public function fileexists(SS_HTTPRequest $request) {
// Check both original and safely filtered filename
$originalFile = $request->requestVar('filename');
$nameFilter = FileNameFilter::create();
$filteredFile = basename($nameFilter->filter($originalFile));
// check if either file exists
$folder = $this->getFolderName();
$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() {
@ -623,55 +1284,26 @@ class UploadField extends FileField {
}
/**
* Determines if the underlying record (if any) has a relationship
* matching the field name. Important for permission control.
*
* @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.
* Gets the foreign class that needs to be created, or 'File' as default if there
* is no relationship, or it cannot be determined.
*
* @param $default Default value to return if no value could be calculated
* @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();
$record = $this->getRecord();
if (isset($name) && isset($record)) return $record->getRelationClass($name);
}
public function isDisabled() {
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);
if(empty($name) || empty($record)) {
return $default;
} else {
$class = $record->getRelationClass($name);
return empty($class) ? $default : $class;
}
}
}
@ -726,14 +1358,6 @@ class UploadField_ItemHandler extends RequestHandler {
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
*/
@ -749,42 +1373,6 @@ class UploadField_ItemHandler extends RequestHandler {
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
*
@ -804,14 +1392,9 @@ class UploadField_ItemHandler extends RequestHandler {
if(!$item) return $this->httpError(404);
if(!$item->canDelete()) 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);
// First remove the file from the current relationship
$this->remove($request);
// Then delete the file from the filesystem
// Delete the file from the filesystem. The file will be removed
// from the relation on save
// @todo Investigate if references to deleted files (if unsaved) is dangerous
$item->delete();
}
@ -830,10 +1413,6 @@ class UploadField_ItemHandler extends RequestHandler {
if(!$item) return $this->httpError(404);
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');
return $this->customise(array(
@ -846,30 +1425,10 @@ class UploadField_ItemHandler extends RequestHandler {
*/
public function EditForm() {
$file = $this->getItem();
if (is_a($this->parent->getConfig('fileEditFields'), 'FieldList')) {
$fields = $this->parent->getConfig('fileEditFields');
} elseif ($file->hasMethod($this->parent->getConfig('fileEditFields'))) {
$fields = $file->{$this->parent->getConfig('fileEditFields')}();
} 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;
}
// Get form components
$fields = $this->parent->getFileEditFields($file);
$actions = $this->parent->getFileEditActions($file);
$validator = $this->parent->getFileEditValidator($file);
$form = new Form(
$this,
__FUNCTION__,
@ -896,10 +1455,6 @@ class UploadField_ItemHandler extends RequestHandler {
$item = $this->getItem();
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);
$item->write();
@ -921,7 +1476,7 @@ class UploadField_SelectHandler extends RequestHandler {
protected $parent;
/**
* @var String
* @var string
*/
protected $folderName;
@ -961,7 +1516,7 @@ class UploadField_SelectHandler extends RequestHandler {
$folderID = $this->parent->getRequest()->requestVar('ParentID');
if (!isset($folderID)) {
$folder = Folder::find_or_make($this->folderName);
$folderID = $folder->ID;
$folderID = $folder ? $folder->ID : 0;
}
// Construct the form
@ -988,7 +1543,7 @@ class UploadField_SelectHandler extends RequestHandler {
// Generate the folder selection field.
$folderField = new TreeDropdownField('ParentID', _t('HtmlEditorField.FOLDER', 'Folder'), 'Folder');
$folderField->setValue($folderID);
// Generate the file list field.
$config = GridFieldConfig::create();
$config->addComponent(new GridFieldSortableHeader());
@ -997,20 +1552,16 @@ class UploadField_SelectHandler extends RequestHandler {
$config->addComponent(new GridFieldPaginator(10));
// If relation is to be autoset, we need to make sure we only list compatible objects.
$baseClass = null;
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';
$baseClass = $this->parent->getRelationAutosetClass();
// Create the data source for the list of files within the current directory.
$files = DataList::create($baseClass)->filter('ParentID', $folderID);
$fileField = new GridField('Files', false, $files, $config);
$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(
$folderField,
@ -1021,7 +1572,7 @@ class UploadField_SelectHandler extends RequestHandler {
}
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++;
$metadata = $gridField->getColumnMetadata($columnField);
$title = $metadata['title'];
$fields = new FieldGroup();
if($title && $gridField->getList()->canFilterBy($columnField)) {
$value = '';
@ -128,34 +128,33 @@ class GridFieldFilterHeader implements GridField_HTMLProvider, GridField_DataMan
$field->setAttribute('placeholder',
_t('GridField.FilterBy', "Filter by ") . _t('GridField.'.$metadata['title'], $metadata['title']));
$field = new FieldGroup(
$field,
$fields->push($field);
$fields->push(
GridField_FormAction::create($gridField, 'reset', false, 'reset', null)
->addExtraClass('ss-gridfield-button-reset')
->setAttribute('title', _t('GridField.ResetFilter', "Reset"))
->setAttribute('id', 'action_reset_' . $gridField->getModelClass() . '_' . $columnField)
);
} else {
if($currentColumn == count($columns)){
$field = new FieldGroup(
GridField_FormAction::create($gridField, 'filter', false, 'filter', null)
->addExtraClass('ss-gridfield-button-filter')
->setAttribute('title', _t('GridField.Filter', "Filter"))
->setAttribute('id', 'action_filter_' . $gridField->getModelClass() . '_' . $columnField),
GridField_FormAction::create($gridField, 'reset', false, 'reset', null)
->addExtraClass('ss-gridfield-button-close')
->setAttribute('title', _t('GridField.ResetFilter', "Reset"))
->setAttribute('id', 'action_reset_' . $gridField->getModelClass() . '_' . $columnField)
);
$field->addExtraClass('filter-buttons');
$field->addExtraClass('no-change-track');
}else{
$field = new LiteralField('', '');
}
}
if($currentColumn == count($columns)){
$fields->push(
GridField_FormAction::create($gridField, 'filter', false, 'filter', null)
->addExtraClass('ss-gridfield-button-filter')
->setAttribute('title', _t('GridField.Filter', "Filter"))
->setAttribute('id', 'action_filter_' . $gridField->getModelClass() . '_' . $columnField)
);
$fields->push(
GridField_FormAction::create($gridField, 'reset', false, 'reset', null)
->addExtraClass('ss-gridfield-button-close')
->setAttribute('title', _t('GridField.ResetFilter', "Reset"))
->setAttribute('id', 'action_reset_' . $gridField->getModelClass() . '_' . $columnField)
);
$fields->addExtraClass('filter-buttons');
$fields->addExtraClass('no-change-track');
}
$forTemplate->Fields->push($field);
$forTemplate->Fields->push($fields);
}
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);
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.show();

View File

@ -1,12 +1,12 @@
(function($) {
$.widget('blueimpUIX.fileupload', $.blueimpUI.fileupload, {
_initTemplates: function() {
this.options.templateContainer = document.createElement(
this._files.prop('nodeName')
);
this.options.uploadTemplate = window.tmpl(this.options.uploadTemplateName);
this.options.downloadTemplate = window.tmpl(this.options.downloadTemplateName);
},
this.options.templateContainer = document.createElement(
this._files.prop('nodeName')
);
this.options.uploadTemplate = window.tmpl(this.options.uploadTemplateName);
this.options.downloadTemplate = window.tmpl(this.options.downloadTemplateName);
},
_enableFileInputButton: function() {
$.blueimpUI.fileupload.prototype._enableFileInputButton.call(this);
this.element.find('.ss-uploadfield-addfile').show();
@ -48,6 +48,39 @@
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) {
$.blueimpUI.fileupload.prototype._onAlways.call(this, jqXHRorResult, textStatus, jqXHRorError, options);
if (this._active === 0) {
@ -55,7 +88,53 @@
$('.ss-uploadfield-item-edit-all').show();
$('.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;
var fileInput = this.find('input');
var fileInput = this.find('input[type=file]');
var dropZone = this.find('.ss-uploadfield-dropzone');
var config = $.parseJSON(fileInput.data('config').replace(/'/g,'"'));
@ -115,7 +194,7 @@
];
},
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'),
2: ss.i18n._t('UploadField.HTML_MAXFILESIZE'),
3: ss.i18n._t('UploadField.ONLYPARTIALUPLOADED'),
@ -169,13 +248,20 @@
onunmatch: function() {
this._super();
},
openSelectDialog: function() {
openSelectDialog: function(uploadedFile) {
// Create dialog and load iframe
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 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
dialog.ssdialog({iframeUrl: config['urlSelectDialog'], height: 550});
dialog.ssdialog({iframeUrl: iframeUrl, height: 550});
// TODO Allow single-select
dialog.find('iframe').bind('load', function(e) {
@ -191,7 +277,7 @@
contents.find('input[name=action_doAttach]').unbind('click.openSelectDialog').bind('click.openSelectDialog', function() {
// 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');});
if(ids && ids.length) self.attachFiles(ids);
if(ids && ids.length) self.attachFiles(ids, uploadedFileId);
dialog.ssdialog('close');
return false;
@ -199,23 +285,17 @@
});
dialog.ssdialog('open');
},
attachFiles: function(ids) {
attachFiles: function(ids, uploadedFileId) {
var self = this, config = this.getConfig();
$.post(
config['urlAttach'],
{'ids': ids},
function(data, status, xhr) {
var fn = self.fileupload('option', 'downloadTemplate');
self.find('.ss-uploadfield-files').append(fn({
self.fileupload('attach', {
files: data,
formatFileSize: function (bytes) {
if (typeof bytes !== 'number') return '';
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')
}));
options: self.fileupload('option'),
replaceFileID: uploadedFileId
});
}
);
}
@ -255,14 +335,18 @@
var fileupload = this.closest('div.ss-upload').data('fileupload'),
item = this.closest('.ss-uploadfield-item'), msg = '';
if(this.is('.ss-uploadfield-item-delete')) msg = ss.i18n._t('UploadField.ConfirmDelete');
if(!msg || confirm(msg)) {
fileupload._trigger('destroy', e, {
context: item,
url: this.data('href'),
type: 'get',
dataType: fileupload.options.dataType
});
if(this.is('.ss-uploadfield-item-delete')) {
if(confirm(ss.i18n._t('UploadField.ConfirmDelete'))) {
fileupload._trigger('destroy', e, {
context: item,
url: this.data('href'),
type: 'get',
dataType: fileupload.options.dataType
});
}
} else {
// Removed files will be applied to object on save
fileupload._trigger('destroy', e, {context: item});
}
return false;
@ -289,7 +373,7 @@
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) {
var editform = this.closest('.ss-uploadfield-item').find('.ss-uploadfield-item-editform');
var disabled;
@ -403,7 +487,7 @@
$('div.ss-upload .ss-uploadfield-fromfiles').entwine({
onclick: function(e) {
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="" />' +
'</span></div>' +
'<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">' +
'<span class="name" title="{%=file.name%}">{%=file.name%}</span> ' +
'<span class="size">{%=o.formatFileSize(file.size)%}</span>' +
'{% 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>' +
'{% } else { %}' +
@ -16,7 +20,7 @@ window.tmpl.cache['ss-uploadfield-downloadtemplate'] = tmpl(
'</label>' +
'{% if (file.error) { %}' +
'<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>' +
'{% } else { %}' +
'<div class="ss-uploadfield-item-actions">{% print(file.buttons, true); %}</div>' +
@ -27,4 +31,4 @@ window.tmpl.cache['ss-uploadfield-downloadtemplate'] = tmpl(
'{% } %}' +
'</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-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>' +
'</li>' +
'{% } %}'
);
);

View File

@ -34,6 +34,7 @@ if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
'UploadField.EMPTYRESULT': 'Leere Datei erhalten',
'UploadField.LOADING': 'Lädt ...',
'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.LOADING': 'Loading ...',
'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.'
MAXNUMBEROFFILES: 'Max number of {count} file(s) exceeded.'
MAXNUMBEROFFILESSHORT: 'Can only upload {count} files'
MAXNUMBEROFFILESONE: 'Can only upload one file'
REMOVE: Remove
REMOVEERROR: 'Error removing file'
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.'
MAXNUMBEROFFILES: 'Max number of {count} file(s) exceeded.'
MAXNUMBEROFFILESSHORT: 'Can only upload {count} files'
MAXNUMBEROFFILESONE: 'Can only upload one file'
REMOVE: Remove
REMOVEERROR: 'Error removing file'
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
*/
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".
@ -202,7 +210,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public static function database_fields($class) {
if(get_parent_class($class) == 'DataObject') {
if(!isset(self::$classname_spec_cache[$class])) {
if(empty(self::$classname_spec_cache[$class])) {
$classNames = ClassInfo::subclassesFor($class);
$db = DB::getConn();
@ -784,10 +792,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// no support for has_many or many_many relationships,
// as the updater wouldn't know which object to write to (or create)
if($relObj->$relation() instanceof DataObject) {
$parentObj = $relObj;
$relObj = $relObj->$relation();
// 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 {
user_error(
"DataObject::update(): Can't traverse relationship '$relation'," .
@ -803,6 +816,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if($relObj) {
$relObj->$fieldName = $v;
$relObj->write();
$relatedFieldName = $relation."ID";
$this->$relatedFieldName = $relObj->ID;
$relObj->flushCache();
} else {
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.
*
* @param String $having Escaped SQL statement
*/
public function having($having) {
if($having) {
$this->query->addHaving($having);
}
$this->query->addHaving($having);
return $this;
}

View File

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

View File

@ -96,6 +96,13 @@ abstract class StringField extends DBField {
return parent::prepValueForDB($value);
}
}
/**
* @return string
*/
public function forTemplate() {
return nl2br($this->XML());
}
/**
* 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,
$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
$text = $striphtml ? $this->NoHTML() : $this->value;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@
// TODO tmp hack until we have permissions and can disable delete
display: none;
}
&.ss-uploadfield-item-cancel{
&.ss-uploadfield-item-cancel, &.ss-uploadfield-item-overwrite-warning {
@include border-radius(0);
border-left:1px solid rgba(#fff, 0.2);
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.
// Filters are conjunctive in DataQuery by default, so this filter would otherwise overrule any less specific
// ones.
$result = $result->alterDataQuery(function($query){
$query->removeFilterOn('Group_Members');
});
if(!($result instanceof UnsavedRelationList)) {
$result = $result->alterDataQuery(function($query){
$query->removeFilterOn('Group_Members');
});
}
// Now set all children groups as a new foreign key
$groups = Group::get()->byIDs($this->collateFamilyIDs());
$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() {
Session::clear("loggedInAs");
if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, null, 0);
self::session_regenerate_id();
Session::destroy();
$this->extend('memberLoggedOut');

View File

@ -6,11 +6,12 @@
</span>
</button>
<% end_if %>
<% if UploadFieldHasRelation %>
<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>
<% end_if %>
<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">
<% _t('UploadField.REMOVE', 'Remove') %></button>
<% 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>
<% 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">
<% if $Items %>
<% loop $Items %>
<% if $CustomisedItems %>
<% loop $CustomisedItems %>
<li class="ss-uploadfield-item template-download" data-fileid="$ID">
<div class="ss-uploadfield-item-preview preview"><span>
<img alt="$hasRelation" src="$UploadFieldThumbnailURL" />
</span></div>
<div class="ss-uploadfield-item-info">
<input type='hidden' value='$ID' name='{$Top.Name}[Files][]' />
<label class="ss-uploadfield-item-name">
<b>{$Title}.{$Extension}</b>
<span>$Size</span>
<span class="name">$Name.XML</span>
<span class="size">$Size</span>
<div class="clear"><!-- --></div>
</label>
<div class="ss-uploadfield-item-actions">
<% if Top.isDisabled || Top.isReadonly %>
<% else %>
<% if Top.isActive %>
$UploadFieldFileButtons
<% end_if %>
</div>
@ -25,15 +25,8 @@
<% end_loop %>
<% end_if %>
</ul>
<% if isDisabled || isReadonly %>
<% if isSaveable %>
<% 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 || canAttachExisting %>
<div class="ss-uploadfield-item ss-uploadfield-addfile<% if $CustomisedItems %> borderTop<% end_if %>">
<% if canUpload %>
<div class="ss-uploadfield-item-preview ss-uploadfield-dropzone ui-corner-all">
<% if $multiple %>
@ -50,24 +43,24 @@
<% else %>
<b><% _t('UploadField.ATTACHFILE', 'Attach a file') %></b>
<% end_if %>
<% if getConfig('canPreviewFolder') %>
<% if canPreviewFolder %>
<small>(<%t UploadField.UPLOADSINTO 'saves into /{path}' path=$FolderName %>)</small>
<% end_if %>
</label>
<% 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">
<% _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 %> />
</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 %> />
<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') %>
<input id="$id" name="{$Name}[Uploads][]" class="$extraClass ss-uploadfield-fromcomputer-fileinput" data-config="$configString" type="file"<% if $multiple %> multiple="multiple"<% end_if %> />
</label>
<% end_if %>
<% 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 %>
<% if not $autoUpload %>
<button class="ss-uploadfield-startall ss-ui-button ui-corner-all" data-icon="navigation"><% _t('UploadField.STARTALL', 'Start all') %></button>
<% if canUpload %>
<% 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 %>
<div class="clear"><!-- --></div>
</div>

View File

@ -41,7 +41,8 @@ class SessionTest extends SapphireTest {
Session::set('Test-2', 'Test-2');
$session = Session::get_all();
unset($session['HTTP_USER_AGENT']);
$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->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->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_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->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(){
@ -82,4 +91,20 @@ class SessionTest extends SapphireTest {
$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());
}
/**
* 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.
*/

View File

@ -14,23 +14,25 @@ class UploadFieldTest extends FunctionalTest {
'File' => array('UploadFieldTest_FileExtension')
);
/**
* Test that files can be uploaded against an object with no relation
*/
public function testUploadNoRelation() {
$this->loginWithPermission('ADMIN');
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$tmpFileName = 'testUploadBasic.txt';
$_FILES = array('NoRelationField' => $this->getUploadFile($tmpFileName));
$response = $this->post(
'UploadFieldTest_Controller/Form/field/NoRelationField/upload',
array('NoRelationField' => $this->getUploadFile($tmpFileName))
);
$response = $this->mockFileUpload('NoRelationField', $tmpFileName);
$this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName));
$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() {
$this->loginWithPermission('ADMIN');
@ -39,22 +41,29 @@ class UploadFieldTest extends FunctionalTest {
$record->HasOneFileID = null;
$record->write();
// Firstly, ensure the file can be uploaded
$tmpFileName = 'testUploadHasOneRelation.txt';
$_FILES = array('HasOneFile' => $this->getUploadFile($tmpFileName));
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasOneFile/upload',
array('HasOneFile' => $this->getUploadFile($tmpFileName))
);
$response = $this->mockFileUpload('HasOneFile', $tmpFileName);
$this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName));
$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);
$this->assertTrue($record->HasOneFile()->exists());
$this->assertEquals($record->HasOneFile()->Name, $tmpFileName);
}
/**
* Tests that has_one relations work with subclasses of File
*/
public function testUploadHasOneRelationWithExtendedFile() {
$this->loginWithPermission('ADMIN');
@ -63,201 +72,252 @@ class UploadFieldTest extends FunctionalTest {
$record->HasOneExtendedFileID = null;
$record->write();
// Test that the file can be safely uploaded
$tmpFileName = 'testUploadHasOneRelationWithExtendedFile.txt';
$_FILES = array('HasOneExtendedFile' => $this->getUploadFile($tmpFileName));
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasOneExtendedFile/upload',
array('HasOneExtendedFile' => $this->getUploadFile($tmpFileName))
);
$response = $this->mockFileUpload('HasOneExtendedFile', $tmpFileName);
$this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('UploadFieldTest_ExtendedFile', sprintf('"Name" = \'%s\'', $tmpFileName));
$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);
$this->assertTrue($record->HasOneExtendedFile()->exists(), 'The extended file is attached to the class');
$this->assertEquals($record->HasOneExtendedFile()->Name, $tmpFileName, 'Proper file has been attached');
$this->assertFalse($record->HasOneExtendedFile()->exists());
// 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() {
$this->loginWithPermission('ADMIN');
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
// Test that uploaded files can be posted to a has_many relation
$tmpFileName = 'testUploadHasManyRelation.txt';
$_FILES = array('HasManyFiles' => $this->getUploadFile($tmpFileName));
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasManyFiles/upload',
array('HasManyFiles' => $this->getUploadFile($tmpFileName))
);
$response = $this->mockFileUpload('HasManyFiles', $tmpFileName);
$this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName));
$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);
$this->assertEquals(3, $record->HasManyFiles()->Count());
$this->assertEquals($record->HasManyFiles()->Last()->Name, $tmpFileName);
$this->assertEquals(2, $record->HasManyFiles()->Count()); // Existing two files should be retained
// 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() {
$this->loginWithPermission('ADMIN');
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$relationCount = $record->ManyManyFiles()->Count();
// Test that uploaded files can be posted to a many_many relation
$tmpFileName = 'testUploadManyManyRelation.txt';
$_FILES = array('ManyManyFiles' => $this->getUploadFile($tmpFileName));
$response = $this->post(
'UploadFieldTest_Controller/Form/field/ManyManyFiles/upload',
array('ManyManyFiles' => $this->getUploadFile($tmpFileName))
);
$response = $this->mockFileUpload('ManyManyFiles', $tmpFileName);
$this->assertFalse($response->isError());
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
$uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName));
$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);
$this->assertEquals($relationCount+1, $record->ManyManyFiles()->Count());
$this->assertEquals($record->ManyManyFiles()->Last()->Name, $tmpFileName);
// Existing file count should be retained
$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() {
$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
// 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
// second should cause an error.
foreach (array('HasOneFile', 'HasOneFileMaxOne', 'HasOneFileMaxTwo') as $recordName) {
// Unset existing has_one relation before re-uploading
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$record->{$recordName . 'ID'} = null;
$record->{"{$recordName}ID"} = null;
$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.
$response = $this->post(
"UploadFieldTest_Controller/Form/field/$recordName/upload",
array($recordName => $this->getUploadFile($tmpFileName))
);
$body = json_decode($response->getBody());
$this->assertNotEquals(0, $body[0]->error);
// Post form with two files for this field, should result in an error
$response = $this->mockUploadFileIDs($recordName, $fileIDs);
$isError = !empty($response['errors']);
// Strictly, a has_one should not allow two files, but this is overridden
// by the setAllowedMaxFileNumber(2) call
$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() {
$this->loginWithPermission('ADMIN');
// 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
// two should work and the third will fail.
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$record->HasManyFilesMaxTwo()->removeAll();
$tmpFileName = 'testUploadHasManyRelation.txt';
$_FILES = array('HasManyFilesMaxTwo' => $this->getUploadFile($tmpFileName));
// Get references for each file to upload
$file1 = $this->objFromFixture('File', 'file1');
$file2 = $this->objFromFixture('File', 'file2');
$file3 = $this->objFromFixture('File', 'file3');
// Write the first element, should be okay.
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasManyFilesMaxTwo/upload',
array('HasManyFilesMaxTwo' => $this->getUploadFile($tmpFileName))
);
$body = json_decode($response->getBody());
$this->assertEquals(0, $body[0]->error);
$response = $this->mockUploadFileIDs('HasManyFilesMaxTwo', array($file1->ID));
$this->assertEmpty($response['errors']);
// Write the second element, should be okay.
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasManyFilesMaxTwo/upload',
array('HasManyFilesMaxTwo' => $this->getUploadFile($tmpFileName))
);
$body = json_decode($response->getBody());
$this->assertEquals(0, $body[0]->error);
$response = $this->mockUploadFileIDs('HasManyFilesMaxTwo', array($file1->ID, $file2->ID));
$this->assertEmpty($response['errors']);
// Write the third element, should result in error.
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasManyFilesMaxTwo/upload',
array('HasManyFilesMaxTwo' => $this->getUploadFile($tmpFileName))
);
$body = json_decode($response->getBody());
$this->assertNotEquals(0, $body[0]->error);
$response = $this->mockUploadFileIDs('HasManyFilesMaxTwo', array($file1->ID, $file2->ID, $file3->ID));
$this->assertNotEmpty($response['errors']);
}
/**
* Test that files can be removed from has_one relations
*/
public function testRemoveFromHasOne() {
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$file1 = $this->objFromFixture('File', 'file1');
// Check record exists
$this->assertTrue($record->HasOneFile()->exists());
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasOneFile/item/' . $file1->ID . '/remove',
array()
);
$this->assertFalse($response->isError());
// Remove from record
$response = $this->mockUploadFileIDs('HasOneFile', array());
$this->assertEmpty($response['errors']);
// Check file is removed
$record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertFalse($record->HasOneFile()->exists());
// Check file object itself exists
$this->assertFileExists($file1->FullPath, 'File is only detached, not deleted from filesystem');
}
/**
* Test that items can be removed from has_many
*/
public function testRemoveFromHasMany() {
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$file2 = $this->objFromFixture('File', 'file2');
$file3 = $this->objFromFixture('File', 'file3');
// Check record has two files attached
$this->assertEquals(array('File2', 'File3'), $record->HasManyFiles()->column('Title'));
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasManyFiles/item/' . $file2->ID . '/remove',
array()
);
$this->assertFalse($response->isError());
// Remove file 2
$response = $this->mockUploadFileIDs('HasManyFiles', array($file3->ID));
$this->assertEmpty($response['errors']);
// check only file 3 is left
$record = DataObject::get_by_id($record->class, $record->ID, false);
$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');
}
/**
* Test that items can be removed from many_many
*/
public function testRemoveFromManyMany() {
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$file4 = $this->objFromFixture('File', 'file4');
$file5 = $this->objFromFixture('File', 'file5');
// Check that both files are currently set
$this->assertContains('File4', $record->ManyManyFiles()->column('Title'));
$this->assertContains('File5', $record->ManyManyFiles()->column('Title'));
$response = $this->post(
'UploadFieldTest_Controller/Form/field/ManyManyFiles/item/' . $file4->ID . '/remove',
array()
);
$this->assertFalse($response->isError());
// Remove file 4
$response = $this->mockUploadFileIDs('ManyManyFiles', array($file5->ID));
$this->assertEmpty($response['errors']);
// check only file 5 is left
$record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertNotContains('File4', $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');
}
/**
* Test that files can be deleted from has_one and the filesystem
*/
public function testDeleteFromHasOne() {
$this->loginWithPermission('ADMIN');
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$file1 = $this->objFromFixture('File', 'file1');
// Check that file initially exists
$this->assertTrue($record->HasOneFile()->exists());
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasOneFile/item/' . $file1->ID . '/delete',
array()
);
$this->assertFileExists($file1->FullPath);
// Delete physical file and update record
$response = $this->mockFileDelete('HasOneFile', $file1->ID);
$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);
$this->assertFalse($record->HasOneFile()->exists());
// Check that the physical file is deleted
$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() {
$this->loginWithPermission('ADMIN');
@ -265,25 +325,28 @@ class UploadFieldTest extends FunctionalTest {
$file2 = $this->objFromFixture('File', 'file2');
$file3 = $this->objFromFixture('File', 'file3');
// Check that files initially exists
$this->assertEquals(array('File2', 'File3'), $record->HasManyFiles()->column('Title'));
$response = $this->post(
'UploadFieldTest_Controller/Form/field/HasManyFiles/item/' . $file2->ID . '/delete',
array()
);
$this->assertFileExists($file2->FullPath);
$this->assertFileExists($file3->FullPath);
// Delete physical file and update record without file 2
$response = $this->mockFileDelete('HasManyFiles', $file2->ID);
$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);
$this->assertEquals(array('File3'), $record->HasManyFiles()->column('Title'));
// Test that physical file is removed
$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() {
$this->loginWithPermission('ADMIN');
@ -292,26 +355,35 @@ class UploadFieldTest extends FunctionalTest {
$file5 = $this->objFromFixture('File', 'file5');
$fileNoDelete = $this->objFromFixture('File', 'file-nodelete');
$this->assertContains('File4', $record->ManyManyFiles()->column('Title'));
$this->assertContains('File5', $record->ManyManyFiles()->column('Title'));
$response = $this->post(
'UploadFieldTest_Controller/Form/field/ManyManyFiles/item/' . $file4->ID . '/delete',
array()
);
// Test that files initially exist
$setFiles = $record->ManyManyFiles()->column('Title');
$this->assertContains('File4', $setFiles);
$this->assertContains('File5', $setFiles);
$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());
// Check file is removed from record
$record = DataObject::get_by_id($record->class, $record->ID, false);
$this->assertNotContains('File4', $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');
// Test record-based permissions
$response = $this->post(
'UploadFieldTest_Controller/Form/field/ManyManyFiles/item/' . $fileNoDelete->ID . '/delete',
array()
);
$response = $this->mockFileDelete('ManyManyFiles/', $fileNoDelete->ID);
$this->assertEquals(403, $response->getStatusCode());
}
/**
* Test control output html
*/
public function testView() {
$this->loginWithPermission('ADMIN');
@ -326,7 +398,7 @@ class UploadFieldTest extends FunctionalTest {
$this->assertFalse($response->isError());
$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();
foreach($items as $item) $ids[] = (int)$item['data-fileid'];
@ -368,16 +440,16 @@ class UploadFieldTest extends FunctionalTest {
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$form = $this->getMockForm();
$field = new UploadField('MyField');
$field = UploadField::create('MyField');
$field->setForm($form);
$this->assertNull($field->getRecord(), 'Returns no record by default');
$field = new UploadField('MyField');
$field = UploadField::create('MyField');
$field->setForm($form);
$form->loadDataFrom($record);
$this->assertEquals($record, $field->getRecord(), 'Returns record from form if available');
$field = new UploadField('MyField');
$field = UploadField::create('MyField');
$field->setForm($form);
$field->setRecord($record);
$this->assertEquals($record, $field->getRecord(), 'Returns record when set explicitly');
@ -385,22 +457,24 @@ class UploadFieldTest extends FunctionalTest {
public function testSetItems() {
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$form = $this->getMockForm();
$items = new ArrayList(array(
$this->objFromFixture('File', 'file1'),
$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
$field = new UploadField('MyField');
$field->setForm($form);
$field = UploadField::create('MyField');
$field->setRecord($record);
$field->setItems($items);
$this->assertEquals(array('File1', 'File2'), $field->getItems()->column('Title'));
// Field with has_one auto-detected
$field = new UploadField('HasOneFile');
$field->setForm($form);
$field = UploadField::create('HasOneFile');
$field->setRecord($record);
$field->setItems($items);
$this->assertEquals(array('File1', 'File2'), $field->getItems()->column('Title'),
@ -410,30 +484,25 @@ class UploadFieldTest extends FunctionalTest {
public function testGetItems() {
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$form = $this->getMockForm();
// Anonymous field
$field = new UploadField('MyField');
$field->setForm($form);
$field->setRecord($record);
$field = UploadField::create('MyField');
$field->setValue(null, $record);
$this->assertEquals(array(), $field->getItems()->column('Title'));
// Field with has_one auto-detected
$field = new UploadField('HasOneFile');
$field->setForm($form);
$field->setRecord($record);
$field = UploadField::create('HasOneFile');
$field->setValue(null, $record);
$this->assertEquals(array('File1'), $field->getItems()->column('Title'));
// Field with has_many auto-detected
$field = new UploadField('HasManyFiles');
$field->setForm($form);
$field->setRecord($record);
$field = UploadField::create('HasManyFiles');
$field->setValue(null, $record);
$this->assertEquals(array('File2', 'File3'), $field->getItems()->column('Title'));
// Field with many_many auto-detected
$field = new UploadField('ManyManyFiles');
$field->setForm($form);
$field->setRecord($record);
$field = UploadField::create('ManyManyFiles');
$field->setValue(null, $record);
$this->assertNotContains('File1',$field->getItems()->column('Title'));
$this->assertNotContains('File2',$field->getItems()->column('Title'));
$this->assertNotContains('File3',$field->getItems()->column('Title'));
@ -448,14 +517,17 @@ class UploadFieldTest extends FunctionalTest {
$this->assertFalse($response->isError());
$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');
$this->assertFalse((bool)$parser->getBySelector('#ReadonlyField .ss-uploadfield-dropzone'),
'Removes dropzone');
$this->assertFalse((bool)$parser->getBySelector(
'#ReadonlyField .ss-uploadfield-addfile .ss-ui-button'),
'Removes all buttons from "add" area');
$this->assertFalse(
(bool)$parser->getBySelector('#ReadonlyField .ss-uploadfield-dropzone'),
'Removes dropzone'
);
$this->assertFalse((bool)$parser->getBySelector('#ReadonlyField .ss-uploadfield-addfile'),
'Entire "add" area'
);
}
public function testDisabled() {
@ -471,9 +543,9 @@ class UploadFieldTest extends FunctionalTest {
$this->assertFalse((bool)$parser->getBySelector('#DisabledField .ss-uploadfield-dropzone'),
'Removes dropzone');
$this->assertFalse(
(bool)$parser->getBySelector('#DisabledField .ss-uploadfield-addfile .ss-ui-button'),
'Removes all buttons from "add" area');
(bool)$parser->getBySelector('#DisabledField .ss-uploadfield-addfile'),
'Entire "add" area'
);
}
public function testCanUpload() {
@ -491,20 +563,20 @@ class UploadFieldTest extends FunctionalTest {
}
public function testCanUploadWithPermissionCode() {
$field = new UploadField('MyField');
$field = UploadField::create('MyField');
$field->setConfig('canUpload', true);
$field->setCanUpload(true);
$this->assertTrue($field->canUpload());
$field->setConfig('canUpload', false);
$field->setCanUpload(false);
$this->assertFalse($field->canUpload());
$this->loginWithPermission('ADMIN');
$field->setConfig('canUpload', false);
$field->setCanUpload(false);
$this->assertFalse($field->canUpload());
$field->setConfig('canUpload', 'ADMIN');
$field->setCanUpload('ADMIN');
$this->assertTrue($field->canUpload());
}
@ -522,28 +594,6 @@ class UploadFieldTest extends FunctionalTest {
(bool)$parser->getBySelector('#CanAttachExistingFalseField .ss-uploadfield-fromfiles'),
'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() {
@ -566,93 +616,6 @@ class UploadFieldTest extends FunctionalTest {
$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() {
return new Form(new Controller(), 'Form', new FieldList(), new FieldList());
}
@ -668,12 +631,76 @@ class UploadFieldTest extends FunctionalTest {
// emulates the $_FILES array
return array(
'name' => $tmpFileName,
'type' => 'text/plaintext',
'size' => filesize($tmpFilePath),
'tmp_name' => $tmpFilePath,
'extension' => 'txt',
'error' => UPLOAD_ERR_OK,
'name' => array('Uploads' => array($tmpFileName)),
'type' => array('Uploads' => array('text/plaintext')),
'size' => array('Uploads' => array(filesize($tmpFilePath))),
'tmp_name' => array('Uploads' => array($tmpFilePath)),
'error' => array('Uploads' => array(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(
'HasManyFiles' => 'File',
'HasManyFilesMaxTwo' => 'File',
'HasManyFiles' => 'File.HasManyRecord',
'HasManyFilesMaxTwo' => 'File.HasManyMaxTwoRecord',
'HasManyNoViewFiles' => 'File.HasManyNoViewRecord',
'ReadonlyField' => 'File.ReadonlyRecord'
);
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 {
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) {
@ -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
*/
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:
folder1:
Name: UploadFieldTest
folder1-subfolder1:
Name: subfolder1
ParentID: =>Folder.folder1
folder1:
Name: UploadFieldTest
folder1-subfolder1:
Name: subfolder1
ParentID: =>Folder.folder1
File:
file1:
Title: File1
Filename: assets/UploadFieldTest/file1.txt
ParentID: =>Folder.folder1
file2:
Title: File2
Filename: assets/UploadFieldTest/file2.txt
ParentID: =>Folder.folder1
file3:
Title: File3
Filename: assets/UploadFieldTest/file3.txt
ParentID: =>Folder.folder1
file4:
Title: File4
Filename: assets/UploadFieldTest/file4.txt
ParentID: =>Folder.folder1
file5:
Title: File5
Filename: assets/UploadFieldTest/file5.txt
ParentID: =>Folder.folder1
file-noview:
Title: noview.txt
Name: noview.txt
Filename: assets/UploadFieldTest/noview.txt
ParentID: =>Folder.folder1
file-noedit:
Title: noedit.txt
Name: noedit.txt
Filename: assets/UploadFieldTest/noedit.txt
ParentID: =>Folder.folder1
file-nodelete:
Title: nodelete.txt
Name: nodelete.txt
Filename: assets/UploadFieldTest/nodelete.txt
ParentID: =>Folder.folder1
file-subfolder:
Title: file-subfolder.txt
Name: file-subfolder.txt
Filename: assets/UploadFieldTest/subfolder1/file-subfolder.txt
ParentID: =>Folder.folder1-subfolder1
file1:
Title: File1
Filename: assets/UploadFieldTest/file1.txt
ParentID: =>Folder.folder1
file2:
Title: File2
Filename: assets/UploadFieldTest/file2.txt
ParentID: =>Folder.folder1
file3:
Title: File3
Filename: assets/UploadFieldTest/file3.txt
ParentID: =>Folder.folder1
file4:
Title: File4
Filename: assets/UploadFieldTest/file4.txt
ParentID: =>Folder.folder1
file5:
Title: File5
Filename: assets/UploadFieldTest/file5.txt
ParentID: =>Folder.folder1
file-noview:
Title: noview.txt
Name: noview.txt
Filename: assets/UploadFieldTest/noview.txt
ParentID: =>Folder.folder1
file-noedit:
Title: noedit.txt
Name: noedit.txt
Filename: assets/UploadFieldTest/noedit.txt
ParentID: =>Folder.folder1
file-nodelete:
Title: nodelete.txt
Name: nodelete.txt
Filename: assets/UploadFieldTest/nodelete.txt
ParentID: =>Folder.folder1
file-subfolder:
Title: file-subfolder.txt
Name: file-subfolder.txt
Filename: assets/UploadFieldTest/subfolder1/file-subfolder.txt
ParentID: =>Folder.folder1-subfolder1
UploadFieldTest_Record:
record1:
Title: Record 1
HasOneFileID: =>File.file1
HasManyFiles: =>File.file2,=>File.file3
ManyManyFiles: =>File.file4,=>File.file5,=>File.file-noview,=>File.file-noedit,=>File.file-nodelete
record1:
Title: Record 1
HasOneFileID: =>File.file1
HasManyFiles: =>File.file2,=>File.file3
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 */
$team1 = $this->objFromFixture('DataObjectTest_Team', 'team1');
$team1->CaptainID = $this->idFromFixture('DataObjectTest_Player', 'captain1');
$team1->update(array(
'DatabaseField' => 'Something',
'Captain.FirstName' => 'Jim',
'Captain.Email' => 'jim@example.com',
'Captain.FavouriteTeam.Title' => 'New and improved team 1',
));
/* Test the simple case of updating fields on the object itself */
$this->assertEquals('Something', $team1->DatabaseField);
@ -642,6 +642,29 @@ class DataObjectTest extends SapphireTest {
$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() {
$validatedObject = new DataObjectTest_ValidatedObject();

View File

@ -5,6 +5,16 @@
*/
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()

View File

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

View File

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