Merge remote-tracking branch 'origin/3'

Conflicts:
	forms/Form.php
	model/ManyManyList.php
This commit is contained in:
Damian Mooyman 2015-06-17 15:41:13 +12:00
commit 0103b076c3
74 changed files with 2108 additions and 1207 deletions

View File

@ -43,7 +43,7 @@ matrix:
- sudo apt-get install -y tidy - sudo apt-get install -y tidy
before_script: before_script:
- composer self-update - composer self-update || true
- phpenv rehash - phpenv rehash
- git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support - git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support
- "if [ \"$BEHAT_TEST\" = \"\" ]; then php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss; fi" - "if [ \"$BEHAT_TEST\" = \"\" ]; then php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss; fi"

View File

@ -5,4 +5,9 @@ Upload:
# Replace an existing file rather than renaming the new one. # Replace an existing file rather than renaming the new one.
replaceFile: false replaceFile: false
MySQLDatabase: MySQLDatabase:
connection_charset: utf8 connection_charset: utf8
HTTP:
cache_control:
max-age: 0
must-revalidate: "true"
no-transform: "true"

View File

@ -339,8 +339,6 @@ class LeftAndMain extends Controller implements PermissionProvider {
if (Director::isDev()) Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/leaktools.js'); if (Director::isDev()) Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/leaktools.js');
HTMLEditorField::include_js();
$leftAndMainIncludes = array_unique(array_merge( $leftAndMainIncludes = array_unique(array_merge(
array( array(
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Layout.js', FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Layout.js',
@ -832,16 +830,19 @@ class LeftAndMain extends Controller implements PermissionProvider {
$record = ($rootID) ? $this->getRecord($rootID) : null; $record = ($rootID) ? $this->getRecord($rootID) : null;
$obj = $record ? $record : singleton($className); $obj = $record ? $record : singleton($className);
// Get the current page
// NOTE: This *must* be fetched before markPartialTree() is called, as this
// causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord)
// which means that deleted pages stored in the marked tree would be removed
$currentPage = $this->currentPage();
// Mark the nodes of the tree to return // Mark the nodes of the tree to return
if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction); if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction);
$obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod); $obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
// Ensure current page is exposed // Ensure current page is exposed
// This call flushes the Hierarchy::$marked cache when the current node is deleted if($currentPage) $obj->markToExpose($currentPage);
// @see CMSMain::getRecord()
// This will make it impossible to show children under a deleted parent page
// if($p = $this->currentPage()) $obj->markToExpose($p);
// NOTE: SiteTree/CMSMain coupling :-( // NOTE: SiteTree/CMSMain coupling :-(
if(class_exists('SiteTree')) { if(class_exists('SiteTree')) {

View File

@ -140,7 +140,7 @@ body, html { font-size: 12px; line-height: 16px; font-family: Arial, sans-serif;
.ui-accordion .ui-accordion-header { border-color: #c0c0c2; margin-bottom: 0; } .ui-accordion .ui-accordion-header { border-color: #c0c0c2; margin-bottom: 0; }
.ui-accordion .ui-accordion-content { border: 1px solid #c0c0c2; border-top: none; } .ui-accordion .ui-accordion-content { border: 1px solid #c0c0c2; border-top: none; }
.ui-autocomplete { max-height: 240px; overflow-x: hidden; overflow-y: auto; } .ui-autocomplete { max-height: 240px; overflow-x: hidden; overflow-y: auto; /** sorry about the !important but the specificity of other selectors mandates it over writing out very specific selectors **/ }
.ui-autocomplete-loading { background-image: url(../images/throbber.gif) !important; background-position: 97% center !important; background-repeat: no-repeat !important; background-size: auto !important; } .ui-autocomplete-loading { background-image: url(../images/throbber.gif) !important; background-position: 97% center !important; background-repeat: no-repeat !important; background-size: auto !important; }
/** This file defines common styles for form elements used throughout the CMS interface. It is an addition to the base styles defined in framework/css/Form.css. @package framework @subpackage admin */ /** This file defines common styles for form elements used throughout the CMS interface. It is an addition to the base styles defined in framework/css/Form.css. @package framework @subpackage admin */
@ -233,7 +233,7 @@ form.small .field input.text, form.small .field textarea, form.small .field sele
.cms .ss-ui-button.ui-state-hover, .cms .ss-ui-button:hover { text-decoration: none; background-color: white; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2U2ZTZlNiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: -moz-linear-gradient(#ffffff, #e6e6e6); background: -webkit-linear-gradient(#ffffff, #e6e6e6); background: linear-gradient(#ffffff, #e6e6e6); -moz-box-shadow: 0 0 5px #b3b3b3; -webkit-box-shadow: 0 0 5px #b3b3b3; box-shadow: 0 0 5px #b3b3b3; } .cms .ss-ui-button.ui-state-hover, .cms .ss-ui-button:hover { text-decoration: none; background-color: white; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2U2ZTZlNiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: -moz-linear-gradient(#ffffff, #e6e6e6); background: -webkit-linear-gradient(#ffffff, #e6e6e6); background: linear-gradient(#ffffff, #e6e6e6); -moz-box-shadow: 0 0 5px #b3b3b3; -webkit-box-shadow: 0 0 5px #b3b3b3; box-shadow: 0 0 5px #b3b3b3; }
.cms .ss-ui-button:active, .cms .ss-ui-button:focus, .cms .ss-ui-button.ui-state-active, .cms .ss-ui-button.ui-state-focus { border: 1px solid #b3b3b3; background-color: white; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2U2ZTZlNiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: -moz-linear-gradient(#ffffff, #e6e6e6); background: -webkit-linear-gradient(#ffffff, #e6e6e6); background: linear-gradient(#ffffff, #e6e6e6); -moz-box-shadow: 0 0 5px #b3b3b3 inset; -webkit-box-shadow: 0 0 5px #b3b3b3 inset; box-shadow: 0 0 5px #b3b3b3 inset; } .cms .ss-ui-button:active, .cms .ss-ui-button:focus, .cms .ss-ui-button.ui-state-active, .cms .ss-ui-button.ui-state-focus { border: 1px solid #b3b3b3; background-color: white; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2U2ZTZlNiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e6e6e6)); background: -moz-linear-gradient(#ffffff, #e6e6e6); background: -webkit-linear-gradient(#ffffff, #e6e6e6); background: linear-gradient(#ffffff, #e6e6e6); -moz-box-shadow: 0 0 5px #b3b3b3 inset; -webkit-box-shadow: 0 0 5px #b3b3b3 inset; box-shadow: 0 0 5px #b3b3b3 inset; }
.cms .ss-ui-button.ss-ui-action-minor span { padding-left: 0; padding-right: 0; } .cms .ss-ui-button.ss-ui-action-minor span { padding-left: 0; padding-right: 0; }
.cms .ss-ui-button.ss-ui-action-constructive { text-shadow: none; font-weight: bold; color: white; border-color: #1F9433; border-bottom-color: #166a24; background-color: #1F9433; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzkzYmU0MiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzFmOTQzMyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #93be42), color-stop(100%, #1f9433)); background: -moz-linear-gradient(#93be42, #1f9433); background: -webkit-linear-gradient(#93be42, #1f9433); background: linear-gradient(#93be42, #1f9433); text-shadow: #1c872f 0 -1px -1px; } .cms .ss-ui-button.ss-ui-action-constructive { text-shadow: none; font-weight: bold; color: white; border-color: #1F9433; border-bottom-color: #166a24; background-color: #1F9433; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzk0YmU0MiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzFmOTQzMyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #94be42), color-stop(100%, #1f9433)); background: -moz-linear-gradient(#94be42, #1f9433); background: -webkit-linear-gradient(#94be42, #1f9433); background: linear-gradient(#94be42, #1f9433); text-shadow: #1c872f 0 -1px -1px; }
.cms .ss-ui-button.ss-ui-action-constructive.ui-state-hover, .cms .ss-ui-button.ss-ui-action-constructive:hover { border-color: #166a24; background-color: #1F9433; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2E0Y2EzYSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzIzYTkzYSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #a4ca3a), color-stop(100%, #23a93a)); background: -moz-linear-gradient(#a4ca3a, #23a93a); background: -webkit-linear-gradient(#a4ca3a, #23a93a); background: linear-gradient(#a4ca3a, #23a93a); } .cms .ss-ui-button.ss-ui-action-constructive.ui-state-hover, .cms .ss-ui-button.ss-ui-action-constructive:hover { border-color: #166a24; background-color: #1F9433; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2E0Y2EzYSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzIzYTkzYSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #a4ca3a), color-stop(100%, #23a93a)); background: -moz-linear-gradient(#a4ca3a, #23a93a); background: -webkit-linear-gradient(#a4ca3a, #23a93a); background: linear-gradient(#a4ca3a, #23a93a); }
.cms .ss-ui-button.ss-ui-action-constructive:active, .cms .ss-ui-button.ss-ui-action-constructive:focus, .cms .ss-ui-button.ss-ui-action-constructive.ui-state-active, .cms .ss-ui-button.ss-ui-action-constructive.ui-state-focus { background-color: #1d8c30; -moz-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); -webkit-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); } .cms .ss-ui-button.ss-ui-action-constructive:active, .cms .ss-ui-button.ss-ui-action-constructive:focus, .cms .ss-ui-button.ss-ui-action-constructive.ui-state-active, .cms .ss-ui-button.ss-ui-action-constructive.ui-state-focus { background-color: #1d8c30; -moz-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); -webkit-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); }
.cms .ss-ui-button.ss-ui-action-destructive { color: #f00; background-color: #e6e6e6; } .cms .ss-ui-button.ss-ui-action-destructive { color: #f00; background-color: #e6e6e6; }
@ -265,6 +265,7 @@ form.small .field input.text, form.small .field textarea, form.small .field sele
.ss-toggle .ui-accordion-content .field:last-child { margin-bottom: 0; } .ss-toggle .ui-accordion-content .field:last-child { margin-bottom: 0; }
.ss-toggle .ui-accordion-content .field .middleColumn { margin-left: 0; } .ss-toggle .ui-accordion-content .field .middleColumn { margin-left: 0; }
.ss-toggle .ui-accordion-content .field label { float: none; margin-left: 0; } .ss-toggle .ui-accordion-content .field label { float: none; margin-left: 0; }
.ss-toggle .ui-accordion-content .field label.ss-ui-button { float: left; }
.ss-toggle .ui-accordion-content .field .description { margin-left: 0; } .ss-toggle .ui-accordion-content .field .description { margin-left: 0; }
/** ---------------------------------------------------- Checkbox Field ---------------------------------------------------- */ /** ---------------------------------------------------- Checkbox Field ---------------------------------------------------- */
@ -687,6 +688,7 @@ body.cms-dialog { overflow: auto; background: url("../images/textures/bg_cms_mai
.htmleditorfield-dialog .CompositeField .text select { margin: 5px 0 0 0; } .htmleditorfield-dialog .CompositeField .text select { margin: 5px 0 0 0; }
.htmleditorfield-linkform .step2 { margin-bottom: 16px; } .htmleditorfield-linkform .step2 { margin-bottom: 16px; }
.htmleditorfield-linkform .ss-uploadfield .middleColumn { width: auto; }
.htmleditorfield-mediaform .ss-gridfield .gridfield-button-delete { display: none; } .htmleditorfield-mediaform .ss-gridfield .gridfield-button-delete { display: none; }
.htmleditorfield-mediaform .ss-gridfield table.ss-gridfield-table tbody td:first-child { padding: 0; text-align: center; } .htmleditorfield-mediaform .ss-gridfield table.ss-gridfield-table tbody td:first-child { padding: 0; text-align: center; }
@ -846,15 +848,15 @@ form.import-form label.left { width: 250px; }
.tree-holder.jstree span.comment-count:before, .cms-tree.jstree span.comment-count:before { content: ""; position: absolute; border-style: solid; display: block; width: 0; bottom: -4px; /* value = - border-top-width - border-bottom-width */ left: 3px; /* controls horizontal position */ border-width: 4px 4px 0; border-color: #C9B800 transparent; } .tree-holder.jstree span.comment-count:before, .cms-tree.jstree span.comment-count:before { content: ""; position: absolute; border-style: solid; display: block; width: 0; bottom: -4px; /* value = - border-top-width - border-bottom-width */ left: 3px; /* controls horizontal position */ border-width: 4px 4px 0; border-color: #C9B800 transparent; }
.tree-holder.jstree span.comment-count:after, .cms-tree.jstree span.comment-count:after { content: ""; position: absolute; border-style: solid; /* reduce the damage in FF3.0 */ display: block; width: 0; bottom: -3px; /* value = - border-top-width - border-bottom-width */ left: 4px; /* value = (:before left) + (:before border-left) - (:after border-left) */ border-width: 3px 3px 0; border-color: #FFF0BC transparent; } .tree-holder.jstree span.comment-count:after, .cms-tree.jstree span.comment-count:after { content: ""; position: absolute; border-style: solid; /* reduce the damage in FF3.0 */ display: block; width: 0; bottom: -3px; /* value = - border-top-width - border-bottom-width */ left: 4px; /* value = (:before left) + (:before border-left) - (:after border-left) */ border-width: 3px 3px 0; border-color: #FFF0BC transparent; }
.tree-holder.jstree .jstree-hovered, .cms-tree.jstree .jstree-hovered { text-shadow: none; text-decoration: none; } .tree-holder.jstree .jstree-hovered, .cms-tree.jstree .jstree-hovered { text-shadow: none; text-decoration: none; }
.tree-holder.jstree .jstree-closed > ins, .cms-tree.jstree .jstree-closed > ins { background-position: 0 0; } .tree-holder.jstree .jstree-closed > ins, .cms-tree.jstree .jstree-closed > ins { background-position: 2px -1px; }
.tree-holder.jstree .jstree-open > ins, .cms-tree.jstree .jstree-open > ins { background-position: -20px 0; } .tree-holder.jstree .jstree-open > ins, .cms-tree.jstree .jstree-open > ins { background-position: -18px -1px; }
.tree-holder.filtered-list li:not(.filtered-item) > a, .cms-tree.filtered-list li:not(.filtered-item) > a { color: #aaa; } .tree-holder.filtered-list li:not(.filtered-item) > a, .cms-tree.filtered-list li:not(.filtered-item) > a { color: #aaa; }
.cms-content-fields .cms-tree.jstree .jstree-no-checkboxes li a { padding-left: 15px; } .cms-tree.jstree .jstree-no-checkboxes li a { padding-left: 12px; }
.cms-content-fields .cms-tree.jstree .jstree-no-checkboxes li .jstree-hovered, .cms-content-fields .cms-tree.jstree .jstree-no-checkboxes li .jstree-clicked, .cms-content-fields .cms-tree.jstree .jstree-no-checkboxes li a:focus { padding-left: 3px; } .cms-tree.jstree .jstree-no-checkboxes li .jstree-hovered, .cms-tree.jstree .jstree-no-checkboxes li .jstree-clicked, .cms-tree.jstree .jstree-no-checkboxes li a:focus { padding-left: 0; }
.cms-content-fields .cms-tree.jstree .jstree-no-checkboxes li .jstree-hovered .jstree-icon, .cms-content-fields .cms-tree.jstree .jstree-no-checkboxes li .jstree-clicked .jstree-icon, .cms-content-fields .cms-tree.jstree .jstree-no-checkboxes li a:focus .jstree-icon { display: block; } .cms-tree.jstree .jstree-no-checkboxes li .jstree-hovered .jstree-icon, .cms-tree.jstree .jstree-no-checkboxes li .jstree-clicked .jstree-icon, .cms-tree.jstree .jstree-no-checkboxes li a:focus .jstree-icon { display: block; }
.jstree-default a .jstree-icon, .jstree-default-rtl a .jstree-icon, .jstree-classic a .jstree-icon, .jstree-apple a .jstree-icon { background-position: -62px -19px; } .jstree-default a .jstree-icon, .jstree-default-rtl a .jstree-icon, .jstree-classic a .jstree-icon, .jstree-apple a .jstree-icon { background-position: -60px -19px; }
/* ensure status is visible in sidebar */ /* ensure status is visible in sidebar */
.cms-content-tools .cms-tree.jstree li { min-width: 159px; } .cms-content-tools .cms-tree.jstree li { min-width: 159px; }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -681,6 +681,10 @@ form.small .field, .field.small {
label { label {
float: none; float: none;
margin-left: 0; margin-left: 0;
&.ss-ui-button {
float: left;
}
} }
.description { .description {
margin-left: 0; margin-left: 0;

View File

@ -1520,6 +1520,11 @@ body.cms-dialog {
.step2 { .step2 {
margin-bottom: $grid-x*2; margin-bottom: $grid-x*2;
} }
.ss-uploadfield {
.middleColumn {
width: auto;
}
}
} }
.htmleditorfield-mediaform { .htmleditorfield-mediaform {

View File

@ -542,10 +542,10 @@
text-decoration: none; text-decoration: none;
} }
.jstree-closed > ins { .jstree-closed > ins {
background-position:0 0; background-position:2px -1px;
} }
.jstree-open > ins { .jstree-open > ins {
background-position:-20px 0; background-position:-18px -1px;
} }
} }
@ -559,27 +559,25 @@
// For drag and drop icons to not appear whilst in multi-selection // For drag and drop icons to not appear whilst in multi-selection
.cms-content-fields { .cms-tree {
.cms-tree { &.jstree {
&.jstree { .jstree-no-checkboxes {
.jstree-no-checkboxes { li {
li { a {
a { padding-left: 12px;
padding-left: 15px; }
}
.jstree-hovered, .jstree-hovered,
.jstree-clicked, .jstree-clicked,
a:focus { a:focus {
padding-left: 3px; padding-left: 0;
} }
.jstree-hovered, .jstree-hovered,
.jstree-clicked, .jstree-clicked,
a:focus { a:focus {
.jstree-icon { .jstree-icon {
display: block; display: block;
}
} }
} }
} }
@ -592,7 +590,7 @@
.jstree-default-rtl a .jstree-icon, .jstree-default-rtl a .jstree-icon,
.jstree-classic a .jstree-icon, .jstree-classic a .jstree-icon,
.jstree-apple a .jstree-icon { .jstree-apple a .jstree-icon {
background-position:-62px -19px; background-position:-60px -19px;
} }
/* ensure status is visible in sidebar */ /* ensure status is visible in sidebar */

View File

@ -3,7 +3,7 @@
/** /**
* A default backend for the setting and getting of cookies * A default backend for the setting and getting of cookies
* *
* This backend allows one to better test Cookie setting and seperate cookie * This backend allows one to better test Cookie setting and separate cookie
* handling from the core * handling from the core
* *
* @todo Create a config array for defaults (eg: httpOnly, secure, path, domain, expiry) * @todo Create a config array for defaults (eg: httpOnly, secure, path, domain, expiry)
@ -14,7 +14,7 @@
class CookieJar implements Cookie_Backend { class CookieJar implements Cookie_Backend {
/** /**
* Hold the cookies that were existing at time of instatiation (ie: The ones * Hold the cookies that were existing at time of instantiation (ie: The ones
* sent to PHP by the browser) * sent to PHP by the browser)
* *
* @var array Existing cookies sent by the browser * @var array Existing cookies sent by the browser
@ -30,7 +30,7 @@ class CookieJar implements Cookie_Backend {
protected $current = array(); protected $current = array();
/** /**
* Hold any NEW cookies that were set by the appliation and will be sent * Hold any NEW cookies that were set by the application and will be sent
* in the next response * in the next response
* *
* @var array New cookies set by the application * @var array New cookies set by the application
@ -39,7 +39,7 @@ class CookieJar implements Cookie_Backend {
/** /**
* When creating the backend we want to store the existing cookies in our * When creating the backend we want to store the existing cookies in our
* "existing" array. This allows us to distinguish between cookies we recieved * "existing" array. This allows us to distinguish between cookies we received
* or we set ourselves (and didn't get from the browser) * or we set ourselves (and didn't get from the browser)
* *
* @param array $cookies The existing cookies to load into the cookie jar. * @param array $cookies The existing cookies to load into the cookie jar.
@ -92,6 +92,8 @@ class CookieJar implements Cookie_Backend {
/** /**
* Get the cookie value by name * Get the cookie value by name
* *
* Cookie names are normalised to work around PHP's behaviour of replacing incoming variable name . with _
*
* @param string $name The name of the cookie to get * @param string $name The name of the cookie to get
* @param boolean $includeUnsent Include cookies we've yet to send when fetching values * @param boolean $includeUnsent Include cookies we've yet to send when fetching values
* *
@ -102,6 +104,12 @@ class CookieJar implements Cookie_Backend {
if (isset($cookies[$name])) { if (isset($cookies[$name])) {
return $cookies[$name]; return $cookies[$name];
} }
//Normalise cookie names by replacing '.' with '_'
$safeName = str_replace('.', '_', $name);
if (isset($cookies[$safeName])) {
return $cookies[$safeName];
}
} }
/** /**

View File

@ -330,10 +330,15 @@ class HTTP {
// Popuplate $responseHeaders with all the headers that we want to build // Popuplate $responseHeaders with all the headers that we want to build
$responseHeaders = array(); $responseHeaders = array();
$config = Config::inst(); $config = Config::inst();
$cacheControlHeaders = Config::inst()->get('HTTP', 'cache_control');
// currently using a config setting to cancel this, seems to be so taht the CMS caches ajax requests // currently using a config setting to cancel this, seems to be so taht the CMS caches ajax requests
if(function_exists('apache_request_headers') && $config->get(get_called_class(), 'cache_ajax_requests')) { if(function_exists('apache_request_headers') && $config->get(get_called_class(), 'cache_ajax_requests')) {
$requestHeaders = apache_request_headers(); $requestHeaders = apache_request_headers();
if(isset($requestHeaders['X-Requested-With']) && $requestHeaders['X-Requested-With']=='XMLHttpRequest') { if(isset($requestHeaders['X-Requested-With']) && $requestHeaders['X-Requested-With']=='XMLHttpRequest') {
$cacheAge = 0; $cacheAge = 0;
} }
@ -344,7 +349,7 @@ class HTTP {
} }
if($cacheAge > 0) { if($cacheAge > 0) {
$responseHeaders["Cache-Control"] = "max-age={$cacheAge}, must-revalidate, no-transform"; $cacheControlHeaders['max-age'] = self::$cache_age;
$responseHeaders["Pragma"] = ""; $responseHeaders["Pragma"] = "";
// To do: User-Agent should only be added in situations where you *are* actually // To do: User-Agent should only be added in situations where you *are* actually
@ -368,10 +373,20 @@ class HTTP {
// IE6-IE8 have problems saving files when https and no-cache are used // IE6-IE8 have problems saving files when https and no-cache are used
// (http://support.microsoft.com/kb/323308) // (http://support.microsoft.com/kb/323308)
// Note: this is also fixable by ticking "Do not save encrypted pages to disk" in advanced options. // Note: this is also fixable by ticking "Do not save encrypted pages to disk" in advanced options.
$responseHeaders["Cache-Control"] = "max-age=3, must-revalidate, no-transform"; $cacheControlHeaders['max-age'] = 3;
$responseHeaders["Pragma"] = ""; $responseHeaders["Pragma"] = "";
} else { } else {
$responseHeaders["Cache-Control"] = "no-cache, max-age=0, must-revalidate, no-transform"; $cacheControlHeaders['no-cache'] = "true";
}
}
foreach($cacheControlHeaders as $header => $value) {
if(is_null($value)) {
unset($cacheControlHeaders[$header]);
} elseif(is_bool($value) || $value === "true") {
$cacheControlHeaders[$header] = $header;
} else {
$cacheControlHeaders[$header] = $header."=".$value;
} }
} }

View File

@ -105,21 +105,21 @@ This is my `_ss_environment.php` file. I have it placed in `/var`, as each of th
| Name | Description | | Name | Description |
| ---- | ----------- | | ---- | ----------- |
| `TEMP_FOLDER` | Absolute file path to store temporary files such as cached templates or the class manifest. Needs to be writeable by the webserver user. Defaults to *silverstripe-cache* in the webroot, and falls back to *sys_get_temp_dir()*. See *getTempFolder()* in *framework/core/TempPath.php* | | `TEMP_FOLDER` | Absolute file path to store temporary files such as cached templates or the class manifest. Needs to be writeable by the webserver user. Defaults to *silverstripe-cache* in the webroot, and falls back to *sys_get_temp_dir()*. See *getTempFolder()* in *framework/core/TempPath.php*.|
| `SS_DATABASE_CLASS` | The database class to use, MySQLDatabase, MSSQLDatabase, etc. defaults to MySQLDatabase| | `SS_DATABASE_CLASS` | The database class to use, MySQLDatabase, MSSQLDatabase, etc. defaults to MySQLDatabase.|
| `SS_DATABASE_SERVER`| The database server to use, defaulting to localhost| | `SS_DATABASE_SERVER`| The database server to use, defaulting to localhost.|
| `SS_DATABASE_USERNAME`| The database username (mandatory)| | `SS_DATABASE_USERNAME`| The database username (mandatory).|
| `SS_DATABASE_PASSWORD`| The database password (mandatory)| | `SS_DATABASE_PASSWORD`| The database password (mandatory).|
| `SS_DATABASE_PORT`| The database port| | `SS_DATABASE_PORT`| The database port.|
| `SS_DATABASE_SUFFIX`| A suffix to add to the database name.| | `SS_DATABASE_SUFFIX`| A suffix to add to the database name.|
| `SS_DATABASE_PREFIX`| A prefix to add to the database name.| | `SS_DATABASE_PREFIX`| A prefix to add to the database name.|
| `SS_DATABASE_TIMEZONE`| Set the database timezone to something other than the system timezone. | `SS_DATABASE_TIMEZONE`| Set the database timezone to something other than the system timezone.
| `SS_DATABASE_NAME` | Set the database name. Assumes the `$database` global variable in your config is missing or empty. | | `SS_DATABASE_NAME` | Set the database name. Assumes the `$database` global variable in your config is missing or empty. |
| `SS_DATABASE_CHOOSE_NAME`| Boolean/Int. If set, then the system will choose a default database name for you if one isn't give in the $database variable. The database name will be "SS_" followed by the name of the folder into which you have installed SilverStripe. If this is enabled, it means that the phpinstaller will work out of the box without the installer needing to alter any files. This helps prevent accidental changes to the environment. If `SS_DATABASE_CHOOSE_NAME` is an integer greater than one, then an ancestor folder will be used for the database name. This is handy for a site that's hosted from /sites/examplesite/www or /buildbot/allmodules-2.3/build. If it's 2, the parent folder will be chosen; if it's 3 the grandparent, and so on.| | `SS_DATABASE_CHOOSE_NAME`| Boolean/Int. If defined, then the system will choose a default database name for you if one isn't give in the $database variable. The database name will be "SS_" followed by the name of the folder into which you have installed SilverStripe. If this is enabled, it means that the phpinstaller will work out of the box without the installer needing to alter any files. This helps prevent accidental changes to the environment. If `SS_DATABASE_CHOOSE_NAME` is an integer greater than one, then an ancestor folder will be used for the database name. This is handy for a site that's hosted from /sites/examplesite/www or /buildbot/allmodules-2.3/build. If it's 2, the parent folder will be chosen; if it's 3 the grandparent, and so on.|
| `SS_ENVIRONMENT_TYPE`| The environment type: dev, test or live.| | `SS_ENVIRONMENT_TYPE`| The environment type: dev, test or live.|
| `SS_DEFAULT_ADMIN_USERNAME`| The username of the default admin. This is a user with administrative privileges.| | `SS_DEFAULT_ADMIN_USERNAME`| The username of the default admin. This is a user with administrative privileges.|
| `SS_DEFAULT_ADMIN_PASSWORD`| The password of the default admin. This will not be stored in the database.| | `SS_DEFAULT_ADMIN_PASSWORD`| The password of the default admin. This will not be stored in the database.|
| `SS_USE_BASIC_AUTH`| Protect the site with basic auth (good for test sites).<br/>When using CGI/FastCGI with Apache, you will have to add the `RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]` rewrite rule to your `.htaccess` file| | `SS_USE_BASIC_AUTH`| Protect the site with basic auth (good for test sites).<br/>When using CGI/FastCGI with Apache, you will have to add the `RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]` rewrite rule to your `.htaccess` file|
| `SS_SEND_ALL_EMAILS_TO`| If you set this define, all emails will be redirected to this address.| | `SS_SEND_ALL_EMAILS_TO`| If you define this constant, all emails will be redirected to this address.|
| `SS_SEND_ALL_EMAILS_FROM`| If you set this define, all emails will be send from this address.| | `SS_SEND_ALL_EMAILS_FROM`| If you define this constant, all emails will be sent from this address.|
| `SS_ERROR_LOG` | Relative path to the log file | | `SS_ERROR_LOG` | Relative path to the log file. |

View File

@ -512,7 +512,7 @@ The staff section templates aren't too difficult to create, thanks to the utilit
<% loop $Children %> <% loop $Children %>
<article> <article>
<h2><a href="$Link" title="Read more on &quot;{$Title}&quot;">$Title</a></h2> <h2><a href="$Link" title="Read more on &quot;{$Title}&quot;">$Title</a></h2>
$Photo.SetWidth(150) $Photo.ScaleWidth(150)
<p>$Content.FirstParagraph</p> <p>$Content.FirstParagraph</p>
<a href="$Link" title="Read more on &quot;{$Title}&quot;">Read more &gt;&gt;</a> <a href="$Link" title="Read more on &quot;{$Title}&quot;">Read more &gt;&gt;</a>
</article> </article>
@ -521,7 +521,7 @@ The staff section templates aren't too difficult to create, thanks to the utilit
</div> </div>
This template is very similar to the *ArticleHolder* template. The *SetWidth* method of the `[api:Image]` class This template is very similar to the *ArticleHolder* template. The *ScaleWidth* method of the `[api:Image]` class
will resize the image before sending it to the browser. The resized image is cached, so the server doesn't have to will resize the image before sending it to the browser. The resized image is cached, so the server doesn't have to
resize the image every time the page is viewed. resize the image every time the page is viewed.
@ -537,13 +537,13 @@ The *StaffPage* template is also very straight forward.
<article> <article>
<h1>$Title</h1> <h1>$Title</h1>
<div class="content"> <div class="content">
$Photo.SetWidth(433) $Photo.ScaleWidth(433)
$Content</div> $Content</div>
</article> </article>
$Form $Form
</div> </div>
Here we use the *SetWidth* method to get a different sized image from the same source image. You should now have Here we use the *ScaleWidth* method to get a different sized image from the same source image. You should now have
a complete staff section. a complete staff section.
![](../_images/tutorial2_einstein.jpg) ![](../_images/tutorial2_einstein.jpg)

View File

@ -11,7 +11,7 @@ These include video screencasts, written tutorials and code examples to get you
* [How to set up a local development environment in SilverStripe](https://vimeo.com/108861537) * [How to set up a local development environment in SilverStripe](https://vimeo.com/108861537)
* [Lesson 1: Creating your first theme](http://www.silverstripe.org/learn/lessons/creating-your-first-theme) * [Lesson 1: Creating your first theme](http://www.silverstripe.org/learn/lessons/creating-your-first-theme)
* [Lesson 2: Migrating static templates into your theme]http://www.silverstripe.org/learn/lessons/migrating-static-templates-into-your-theme) * [Lesson 2: Migrating static templates into your theme](http://www.silverstripe.org/learn/lessons/migrating-static-templates-into-your-theme)
* [Lesson 3: Adding dynamic content](http://www.silverstripe.org/learn/lessons/adding-dynamic-content) * [Lesson 3: Adding dynamic content](http://www.silverstripe.org/learn/lessons/adding-dynamic-content)
* [Lesson 4: Working with multiple templates](http://www.silverstripe.org/learn/lessons/working-with-multiple-templates) * [Lesson 4: Working with multiple templates](http://www.silverstripe.org/learn/lessons/working-with-multiple-templates)
* [Lesson 5: The holder/page pattern](http://www.silverstripe.org/learn/lessons/the-holderpage-pattern) * [Lesson 5: The holder/page pattern](http://www.silverstripe.org/learn/lessons/the-holderpage-pattern)
@ -20,7 +20,13 @@ These include video screencasts, written tutorials and code examples to get you
* [Lesson 8: Introduction to the ORM](http://www.silverstripe.org/learn/lessons/introduction-to-the-orm) * [Lesson 8: Introduction to the ORM](http://www.silverstripe.org/learn/lessons/introduction-to-the-orm)
* [Lesson 9: Data Relationships - $has_many](http://www.silverstripe.org/learn/lessons/working-with-data-relationships-has-many) * [Lesson 9: Data Relationships - $has_many](http://www.silverstripe.org/learn/lessons/working-with-data-relationships-has-many)
* [Lesson 10: Introduction to the ORM](http://www.silverstripe.org/learn/lessons/working-with-data-relationships-many-many) * [Lesson 10: Introduction to the ORM](http://www.silverstripe.org/learn/lessons/working-with-data-relationships-many-many)
* [Lesson 11: Introduction to frontend forms](http://www.silverstripe.org/learn/lessons/introduction-to-frontend-forms)
* [Lesson 12: Data Extensions and SiteConfig](http://www.silverstripe.org/learn/lessons/data-extensions-and-siteconfig)
* [Lesson 13: Introduction to ModelAdmin](http://www.silverstripe.org/learn/lessons/introduction-to-modeladmin)
* [Lesson 14: Controller Actions/DataObjects as Pages](http://www.silverstripe.org/learn/lessons/controller-actions-dataobjects-as-pages)
* [Lesson 15: Building a Search Form](http://www.silverstripe.org/learn/lessons/building-a-search-form)
* [Lesson 16: Lists and Pagination](http://www.silverstripe.org/learn/lessons/lists-and-pagination)
* [Lesson 17: Ajax Behaviour and Viewable Data](http://www.silverstripe.org/learn/lessons/ajax-behaviour-and-viewabledata)
## Help: If you get stuck ## Help: If you get stuck

View File

@ -31,6 +31,34 @@ functionality. It is usually added through the `[api:DataObject->getCMSFields()]
} }
} }
### Specify which configuration to use
By default, a config named 'cms' is used in any new `[api:HTMLEditorField]`.
If you have created your own `[api:HtmlEditorConfig]` and would like to use it,
you can call `HtmlEditorConfig::set_active('myConfig')` and all subsequently created `[api:HTMLEditorField]`
will use the configuration with the name 'myConfig'.
You can also specify which `[api:HtmlEditorConfig]` to use on a per field basis via the construct argument.
This is particularly useful if you need different configurations for multiple `[api:HTMLEditorField]` on the same page or form.
:::php
class MyObject extends DataObject {
private static $db = array(
'Content' => 'HTMLText',
'OtherContent' => 'HTMLText'
);
public function getCMSFields() {
return new FieldList(array(
new HTMLEditorField('Content'),
new HTMLEditorField('OtherContent', 'Other content', $this->OtherContent, 'myConfig')
));
}
}
In the above example, the 'Content' field will use the default 'cms' config while 'OtherContent' will be using 'myConfig'.
## Configuration ## Configuration
To keep the JavaScript editor configuration manageable and extensible, we've wrapped it in a PHP class called To keep the JavaScript editor configuration manageable and extensible, we've wrapped it in a PHP class called
@ -41,7 +69,6 @@ There can be multiple configs, which should always be created / accessed using `
then set the currently active config using `set_active()`. then set the currently active config using `set_active()`.
<div class="info" markdown="1"> <div class="info" markdown="1">
By default, a config named 'cms' is used in any field created throughout the CMS interface.
</div> </div>
<div class="notice" markdown='1'> <div class="notice" markdown='1'>

View File

@ -37,7 +37,7 @@ explicitly logging in or by invoking the "remember me" functionality.
DB::query(sprintf( DB::query(sprintf(
'UPDATE "Member" SET "LastVisited" = %s, "NumVisit" = "NumVisit" + 1 WHERE "ID" = %d', 'UPDATE "Member" SET "LastVisited" = %s, "NumVisit" = "NumVisit" + 1 WHERE "ID" = %d',
DB::getConn()->now(), DB::get_conn()->now(),
$this->owner->ID $this->owner->ID
)); ));
} }

View File

@ -15,7 +15,7 @@ how you can load default records into the test database.
/** /**
* Defines the fixture file to use for this test class * Defines the fixture file to use for this test class
* *
/ */
protected static $fixture_file = 'SiteTreeTest.yml'; protected static $fixture_file = 'SiteTreeTest.yml';
/** /**
@ -79,4 +79,4 @@ For more information on PHPUnit's assertions see the [PHPUnit manual](http://www
## API Documentation ## API Documentation
* [api:SapphireTest] * [api:SapphireTest]
* [api:FunctionalTest] * [api:FunctionalTest]

View File

@ -27,7 +27,7 @@ come from user input.
Example: Example:
:::php :::php
$records = DB::preparedQuery('SELECT * FROM "MyClass" WHERE "ID" = ?', array(3)); $records = DB::prepared_query('SELECT * FROM "MyClass" WHERE "ID" = ?', array(3));
$records = MyClass::get()->where(array('"ID" = ?' => 3)); $records = MyClass::get()->where(array('"ID" = ?' => 3));
$records = MyClass::get()->where(array('"ID"' => 3)); $records = MyClass::get()->where(array('"ID"' => 3));
$records = DataObject::get_by_id('MyClass', 3); $records = DataObject::get_by_id('MyClass', 3);
@ -48,7 +48,7 @@ Parameterised updates and inserts are also supported, but the syntax is a little
)) ))
->assignSQL('"Created"', 'NOW()') ->assignSQL('"Created"', 'NOW()')
->execute(); ->execute();
DB::preparedQuery( DB::prepared_query(
'INSERT INTO "MyClass" ("Name", "Position", "Age", "Created") VALUES(?, ?, GREATEST(0,?,?), NOW())' 'INSERT INTO "MyClass" ("Name", "Position", "Age", "Created") VALUES(?, ?, GREATEST(0,?,?), NOW())'
array('Daniel', 'Accountant', 24, 28) array('Daniel', 'Accountant', 24, 28)
); );
@ -100,7 +100,7 @@ and [datamodel](/developer_guides/model) for ways to parameterise, cast, and con
* `SQLQuery` * `SQLQuery`
* `DB::query()` * `DB::query()`
* `DB::preparedQuery()` * `DB::prepared_query()`
* `Director::urlParams()` * `Director::urlParams()`
* `Controller->requestParams`, `Controller->urlParams` * `Controller->requestParams`, `Controller->urlParams`
* `SS_HTTPRequest` data * `SS_HTTPRequest` data

View File

@ -0,0 +1,40 @@
summary: Learn how to work with File and Image records
# File Management
## Files, Images and Folders as database records
All files, images and folders in the 'assets' directory are stored in the database. Each record has the following database fields:
| Field name | Description |
| ---------- | ----------- |
| `ClassName` | The class name of the file (e.g. File, Image or Folder). |
| `Name` | The 'basename' of the file, or the folder name. For example 'my-image.jpg', or 'images' for a folder. |
| `Title` | The optional, human-readable title of the file for display only (doesn't apply to folders). |
| `Filename` | The path to the file/folder, relative to the webroot. For example 'assets/images/my-image.jpg', or 'assets/images/' for a folder. |
| `Content` | Typically unused, but handy for a textual representation of files. For example for fulltext indexing of PDF documents. |
| `ShowInSearch` | Whether the file should be shown in search results, defaults to '1'. See ["Tutorial 4 - Site Search"](/tutorials/site_search) for enabling search. |
| `ParentID` | The ID of the parent Folder that this File/Folder is in. A ParentID of '0' indicates that the File/Folder is in the 'assets' directory. |
| `OwnerID` | The ID of the Member that 'owns' the File/Folder (not related to filesystem permissions). |
## Management through the "Files" section of the CMS
If you have the CMS module installed, you can manage files, folders and images in the "Files" section of the CMS. Inside this section, you will see a list of files and folders like below:
![](../../_images/assets.png)
You can click on any file to edit it, or click on any folder to open it. To delete a file or a folder, simply click the red 'X' symbol next to it. If you click to open a folder, you can go back up one level by clicking the 'up' arrow above the folder name (highlighted below):
![](../../_images/assets_up.png)
Once you click to edit a file, you will see a form similar to the one below, in which you can edit the file's title, filename, owner, or even change which folder the file is located in:
![](../../_images/assets_editform.png)
You may also notice the 'Sync files' button (highlighted below). This button allows CMS users to 'synchronise' the database (remember, all files/folders are stored as database records) with the filesystem. This is particularly useful if someone has uploaded or removed files/folders via FTP, for example.
![](../../_images/assets_sync.png)
## Upload
Files can be managed through a `FileField` or an `UploadField`. The `[api:FileField]` class provides a simple HTML input with a type of "file", whereas an `[api:UploadField]` provides a much more feature-rich field (including AJAX-based uploads, previews, relationship management and file data management). See [`Reference - UploadField`](/developer_guides/forms/field_types/uploadfield) for more information about how to use the `UploadField` class.

View File

@ -1,135 +0,0 @@
# Image
## Introduction
Represents an image object through the `[api:Image]` class, inheriting all base functionality from the `[api:File]` class with extra functionality including resizing.
## Usage
### Managing images through form fields
Images can be uploaded like any other file, through `[api:FileField]`.
More advanced usage is possible through `[api:UploadField]`,
which provides thumbnails, a detail view of the image properties,
and management of relationships to other DataObject instances.
Allows upload of images through limiting file extensions with `setAllowedExtensions()`.
### Inserting images into the WYSIWYG editor
See [Topics: Rich Text Editing](/topics/rich-text-editing).
### Resizing Images in PHP
The following are methods defined on the GD class which you can call on Image Objects. Note to get the following to work
you need to have GD2 support in your PHP installation and because these generate files you must have write access to
your tmp folder.
:::php
// manipulation functions
$image->resize(width,height); // Basic resize, just skews the image
$image->resizeRatio(width,height) // Resizes an image with max width and height
$image->paddedResize(width,height) // Adds padding after resizing to width or height.
$image->croppedImage(width,height) // Crops the image from the centre, to given values.
$image->resizeByHeight(height) // Maximum height the image resizes to, keeps proportion
$image->resizeByWidth(width) // Maximum width the image resizes to, keeps proportion
$image->greyscale(r,g,b) // alters image channels ===
// values
$image->getHeight() // Returns the height of the image.
$image->getWidth() // Returns the width of the image
$image->getOrienation() // Returns a class constant: ORIENTATION_SQUARE or ORIENTATION_PORTRAIT or ORIENTATION_LANDSCAPE
You can also create your own functions by extending the image class, for example
:::php
class MyImage extends Image {
public function generateRotateClockwise(GD $gd) {
return $gd->rotate(90);
}
public function generateRotateCounterClockwise(GD $gd) {
return $gd->rotate(270);
}
public function clearResampledImages() {
$files = glob(Director::baseFolder().'/'.$this->Parent()->Filename."_resampled/*-$this->Name");
foreach($files as $file) {unlink($file);}
}
public function Landscape() {
return $this->getWidth() > $this->getHeight();
}
public function Portrait() {
return $this->getWidth() < $this->getHeight();
}
public function generatePaddedImageByWidth(GD $gd,$width=600,$color="fff"){
return $gd->paddedResize($width, round($gd->getHeight()/($gd->getWidth()/$width),0),$color);
}
public function Exif(){
//http://www.v-nessa.net/2010/08/02/using-php-to-extract-image-exif-data
$image = $this->AbsoluteURL;
$d=new ArrayList();
$exif = exif_read_data($image, 0, true);
foreach ($exif as $key => $section) {
$a=new ArrayList();
foreach ($section as $name => $val)
$a->push(new ArrayData(array("Title"=>$name,"Content"=>$val)));
$d->push(new ArrayData(array("Title"=>strtolower($key),"Content"=>$a)));
}
return $d;
}
}
### Resizing in Templates
You can call certain resize functions directly from the template, to use the inbuilt GD functions as the template parser
supports these, for example SetWidth() or SetHeight().
For output of an image tag with the image automatically resized to 80px width, you can use:
:::php
$Image.SetWidth(80) // returns a image 80px wide, ratio kept the same
$Image.SetHeight(80) // returns a image 80px tall, ratio kept the same
$Image.SetSize(80,80) // returns a 80x80px padded image
$Image.SetRatioSize(80,80) // Returns an image scaled proportional, with its greatest diameter scaled to 80px
$Image.CroppedImage(80,80) // Returns an 80x80 image cropped from the center.
$Image.PaddedImage(80, 80, FFFFFF) // Returns an 80x80 image. Unused space is padded white. No crop. No stretching
$Image.Width // returns width of image
$Image.Height // returns height of image
$Image.Orientation // returns Orientation
$Image.Filename // returns filename
$Image.URL // returns filename
### Form Upload
For usage on a website form, see `[api:FileField]`.
If you want to upload images within the CMS, see `[api:UploadField]`.
### Image Quality
To adjust the quality of the generated images when they are resized add the following to your mysite/config/config.yml file:
:::yml
GDBackend:
default_quality: 90
The default value is 75.
### Clearing Thumbnail Cache
Images are (like all other Files) synchronized with the SilverStripe database.
This syncing happens whenever you load the "Files & Images" interface,
and whenever you upload or modify an Image through SilverStripe.
If you encounter problems with images not appearing, or have mysteriously disappeared, you can try manually flushing the
image cache.
http://localhost/dev/tasks/FlushGeneratedImagesTask
## API Documentation
`[api:Image]`

View File

@ -0,0 +1,163 @@
summary: Learn how to crop and resize images in templates and PHP code
# Image
Represents an image object through the `[api:Image]` class, inheriting all base functionality from the `[api:File]` class with extra functionality including resizing.
## Usage
### Managing images through form fields
Images can be uploaded like any other file, through `[api:FileField]`.
More advanced usage is possible through `[api:UploadField]`,
which provides thumbnails, a detail view of the image properties,
and management of relationships to other DataObject instances.
Allows upload of images through limiting file extensions with `setAllowedExtensions()`.
### Inserting images into the WYSIWYG editor
See [Topics: Rich Text Editing](/topics/rich-text-editing).
### Manipulating images in Templates
You can manipulate images directly from templates to create images that are
resized and cropped to suit your needs. This doesn't affect the original
image or clutter the CMS with any additional files, and any images you create
in this way are cached for later use. In most cases the pixel aspect ratios of
images are preserved (meaning images are not stretched).
![](../../_images/image-methods.jpg)
Here are some examples, assuming the `$Image` object has dimensions of 200x100px:
:::ss
// Scaling functions
$Image.ScaleWidth(150) // Returns a 150x75px image
$Image.ScaleMaxWidth(100) // Returns a 100x50px image (like ScaleWidth but prevents up-sampling)
$Image.ScaleHeight(150) // Returns a 300x150px image (up-sampled. Try to avoid doing this)
$Image.ScaleMaxHeight(150) // Returns a 200x100px image (like ScaleHeight but prevents up-sampling)
$Image.Fit(300,300) // Returns an image that fits within a 300x300px boundary, resulting in a 300x150px image (up-sampled)
$Image.FitMax(300,300) // Returns a 200x100px image (like Fit but prevents up-sampling)
// Cropping functions
$Image.Fill(150,150) // Returns a 150x150px image resized and cropped to fill specified dimensions (up-sampled)
$Image.FillMax(150,150) // Returns a 100x100px image (like Fill but prevents up-sampling)
$Image.CropWidth(150) // Returns a 150x100px image (trims excess pixels off the x axis from the center)
$Image.CropHeight(50) // Returns a 200x50px image (trims excess pixels off the y axis from the center)
// Padding functions (add space around an image)
$Image.Pad(100,100) // Returns a 100x100px padded image, with white bars added at the top and bottom
$Image.Pad(100, 100, CCCCCC) // Same as above but with a grey background
// Metadata
$Image.Width // Returns width of image
$Image.Height // Returns height of image
$Image.Orientation // Returns Orientation
$Image.Title // Returns the friendly file name
$Image.Name // Returns the actual file name
$Image.FileName // Returns the actual file name including directory path from web root
$Image.Link // Returns relative URL path to image
$Image.AbsoluteLink // Returns absolute URL path to image
Image methods are chainable. Example:
:::ss
<body style="background-image:url($Image.ScaleWidth(800).CropHeight(800).Link)">
### Manipulating images in PHP
The image manipulation functions can be used in your code with the same names, example: `$image->Fill(150,150)`.
Some of the MetaData functions need to be prefixed with 'get', example `getHeight()`, `getOrientation()` etc.
Please refer to the `[api:Image]` API documentation for specific functions.
### Creating custom image functions
You can also create your own functions by extending the image class, for example
:::php
class MyImage extends DataExtension {
public function Landscape() {
return $this->owner->getWidth() > $this->owner->getHeight();
}
public function Portrait() {
return $this->owner->getWidth() < $this->owner->getHeight();
}
public function PerfectSquare() {
return $this->owner->getFormattedImage('PerfectSquare');
}
public function generatePerfectSquare(Image_Backend $backend) {
return $backend->croppedResize(100,100);
}
public function Exif(){
//http://www.v-nessa.net/2010/08/02/using-php-to-extract-image-exif-data
$image = $this->owner->AbsoluteLink();
$d=new ArrayList();
$exif = exif_read_data($image, 0, true);
foreach ($exif as $key => $section) {
$a=new ArrayList();
foreach ($section as $name => $val)
$a->push(new ArrayData(array("Title"=>$name,"Content"=>$val)));
$d->push(new ArrayData(array("Title"=>strtolower($key),"Content"=>$a)));
}
return $d;
}
}
:::yml
Image:
extensions:
- MyImage
### Form Upload
For usage on a website form, see `[api:FileField]`.
If you want to upload images within the CMS, see `[api:UploadField]`.
### Image Quality
To adjust the quality of the generated images when they are resized add the
following to your mysite/config/config.yml file:
:::yml
GDBackend:
default_quality: 90
# or
ImagickBackend:
default_quality: 90
The default value is 75.
By default SilverStripe image functions will not resample an image if no
cropping or resizing is taking place. You can tell SilverStripe to always to
always produce resampled output by adding this to your
mysite/config/config.yml file:
:::yml
Image:
force_resample: true
If you are intending to resample images with SilverStripe it is good practice
to upload high quality (minimal compression) images as these will produce
better results when resampled. Very high resolution images may cause GD to
crash so a good size for website images is around 2000px on the longest edge.
### Clearing Thumbnail Cache
Images are (like all other Files) synchronized with the SilverStripe database.
This syncing happens whenever you load the "Files & Images" interface,
and whenever you upload or modify an Image through SilverStripe.
If you encounter problems with images not appearing, or have mysteriously
disappeared, you can try manually flushing the image cache.
http://localhost/dev/tasks/FlushGeneratedImagesTask
## API Documentation
`[api:Image]`

View File

@ -1,40 +1,11 @@
summary: Learn how to deal with File and Image records title: Files
summary: Upload, manage and manipulate files and images.
introduction: Upload, manage and manipulate files and images.
# Files, Images and Folders [CHILDREN]
## Files, Images and Folders as database records ## API Documentation
All files, images and folders in the 'assets' directory are stored in the database. Each record has the following database fields: * [api:File]
* [api:Image]
| Field name | Description | * [api:Folder]
| ---------- | ----------- |
| `ClassName` | The class name of the file (e.g. File, Image or Folder). |
| `Name` | The 'basename' of the file, or the folder name. For example 'my-image.jpg', or 'images' for a folder. |
| `Title` | The optional, human-readable title of the file for display only (doesn't apply to folders). |
| `Filename` | The path to the file/folder, relative to the webroot. For example 'assets/images/my-image.jpg', or 'assets/images/' for a folder. |
| `Content` | Typically unused, but handy for a textual representation of files. For example for fulltext indexing of PDF documents. |
| `ShowInSearch` | Whether the file should be shown in search results, defaults to '1'. See ["Tutorial 4 - Site Search"](/tutorials/site_search) for enabling search. |
| `ParentID` | The ID of the parent Folder that this File/Folder is in. A ParentID of '0' indicates that the File/Folder is in the 'assets' directory. |
| `OwnerID` | The ID of the Member that 'owns' the File/Folder (not related to filesystem permissions). |
## Management through the "Files" section of the CMS
If you have the CMS module installed, you can manage files, folders and images in the "Files" section of the CMS. Inside this section, you will see a list of files and folders like below:
![](../../_images/assets.png)
You can click on any file to edit it, or click on any folder to open it. To delete a file or a folder, simply click the red 'X' symbol next to it. If you click to open a folder, you can go back up one level by clicking the 'up' arrow above the folder name (highlighted below):
![](../../_images/assets_up.png)
Once you click to edit a file, you will see a form similar to the one below, in which you can edit the file's title, filename, owner, or even change which folder the file is located in:
![](../../_images/assets_editform.png)
You may also notice the 'Sync files' button (highlighted below). This button allows CMS users to 'synchronise' the database (remember, all files/folders are stored as database records) with the filesystem. This is particularly useful if someone has uploaded or removed files/folders via FTP, for example.
![](../../_images/assets_sync.png)
## Upload
Files can be managed through a `FileField` or an `UploadField`. The `[api:FileField]` class provides a simple HTML input with a type of "file", whereas an `[api:UploadField]` provides a much more feature-rich field (including AJAX-based uploads, previews, relationship management and file data management). See [`Reference - UploadField`](/developer_guides/forms/field_types/uploadfield) for more information about how to use the `UploadField` class.

View File

@ -11,7 +11,7 @@ SilverStripe needs to boot its core and run through several stages of processing
The first step in most environments is a rewrite of a request path into parameters passed to a PHP script. The first step in most environments is a rewrite of a request path into parameters passed to a PHP script.
This allows writing friendly URLs instead of linking directly to PHP files. This allows writing friendly URLs instead of linking directly to PHP files.
The implementation depends on your web server, we'll show you the most common one here: The implementation depends on your web server; we'll show you the most common one here:
Apache with [mod_rewrite](http://httpd.apache.org/docs/2.0/mod/mod_rewrite.html). Apache with [mod_rewrite](http://httpd.apache.org/docs/2.0/mod/mod_rewrite.html).
Check our [installation guides](/getting_started/installation) on how other web servers like IIS or nginx handle rewriting. Check our [installation guides](/getting_started/installation) on how other web servers like IIS or nginx handle rewriting.
@ -123,7 +123,7 @@ further filtering before content is sent to the end user
The framework provides the ability to hook into the request both before and The framework provides the ability to hook into the request both before and
after it is handled to allow binding custom logic. This can be used after it is handled to allow binding custom logic. This can be used
to transform or filter request data, instanciate helpers, execute global logic, to transform or filter request data, instantiate helpers, execute global logic,
or even short-circuit execution (e.g. to enforce custom authentication schemes). or even short-circuit execution (e.g. to enforce custom authentication schemes).
The ["Request Filters" documentation](../controllers/requestfilters) shows you how. The ["Request Filters" documentation](../controllers/requestfilters) shows you how.

View File

@ -1,80 +1,212 @@
# 3.2.0 (unreleased) # 3.2.0
## Overview ## Contents
### Framework * [Major Changes](#major-changes)
* [Removed API](#deprecated-classesmethods-removed)
* [New API](#new-and-changed-api)
* [Bugfixes](#bugfixes)
* [Upgrading Notes](#upgrading-notes)
* Minimum PHP version raised to 5.3.3 ## Major changes
* `DataObject::validate()` method visibility changed to public
* `NumericField` now uses HTML5 "number" type instead of "text"
* `UploadField` "Select from files" shows files in all folders by default
* `UploadField` won't display an overwrite warning unless `Upload::replaceFile` is true
* `HtmlEditorField` no longer substitutes `<blockquote />` for indented text
* `ClassInfo::dataClassesFor` now returns classes which should have tables, regardless of whether those
tables actually exist.
* `SS_Filterable`, `SS_Limitable` and `SS_Sortable` now explicitly extend `SS_List`
* `Convert::html2raw` no longer wraps text by default and can decode single quotes.
* `Mailer` no longer calls `xml2raw` on all email subject line, and now must be passed in via plain text.
* `ErrorControlChain` now supports reload on exceptions
* `FormField::validate` now requires an instance of `Validator`
* Implementation of new "Archive" concept for page removal, which supercedes "delete". Where deletion removed
pages only from draft, archiving removes from both draft and live simultaneously.
#### Deprecated classes/methods removed * Minimum PHP version raised to 5.3.3
* Introduction of new parameterised ORM
* Default support for PDO
* Moved SS_Report and ReportAdmin out to a separate module. If you're using
composer or downloading a release, this module should be included for you.
Otherwise, you'll need to include the module yourself
(https://github.com/silverstripe-labs/silverstripe-reports)
* Moved SiteConfig also out to its own module. This will be included by
default if you include the CMS module.
(https://github.com/silverstripe/silverstripe-siteconfig)
* Implementation of new "Archive" concept for page removal, which supercedes
"delete from draft". Where deletion removed pages only from draft, archiving
removes from both draft and live simultaneously.
* Most of the `Image` manipulation methods have been renamed
* `ToggleField` was deprecated in 3.1, and has been removed. Use custom Javascript with `ReadonlyField` instead. ## Deprecated classes/methods removed
* `ExactMatchMultiFilter` was deprecated in 3.1, and has been removed. Use `ExactMatchFilter` instead.
* `NegationFilter` was deprecated in 3.1, and has been removed. Use `ExactMatchFilter:not` instead. * `ToggleField` was deprecated in 3.1, and has been removed. Use custom Javascript with `ReadonlyField` instead.
* `StartsWithMultiFilter` was deprecated in 3.1, and has been removed. Use `StartsWithFilter` instead. * `ExactMatchMultiFilter` was deprecated in 3.1, and has been removed. Use `ExactMatchFilter` instead.
* `ScheduledTask` and subclasses like `DailyTask` were deprecated in 3.1, and have been removed. * `NegationFilter` was deprecated in 3.1, and has been removed. Use `ExactMatchFilter:not` instead.
* `StartsWithMultiFilter` was deprecated in 3.1, and has been removed. Use `StartsWithFilter` instead.
* `ScheduledTask` and subclasses like `DailyTask` were deprecated in 3.1, and have been removed.
Use custom code instead, or a module like silverstripe-crontask: https://github.com/silverstripe-labs/silverstripe-crontask Use custom code instead, or a module like silverstripe-crontask: https://github.com/silverstripe-labs/silverstripe-crontask
* `Cookie::forceExpiry()` was removed. Use `Cookie::force_expiry()` instead * `Cookie::forceExpiry()` was removed. Use `Cookie::force_expiry()` instead
* `Object` statics removal: `get_static()`, `set_static()`, `uninherited_static()`, `combined_static()`, * `Object` statics removal: `get_static()`, `set_static()`, `uninherited_static()`, `combined_static()`,
`addStaticVars()` and `add_static_var()` removed. Use the Config methods instead. `addStaticVars()` and `add_static_var()` removed. Use the Config methods instead.
* `GD` methods removed: `setGD()`, `getGD()`, `hasGD()`. Use `setImageResource()`, `getImageResource()`, and `hasImageResource()` instead * `GD` methods removed: `setGD()`, `getGD()`, `hasGD()`. Use `setImageResource()`, `getImageResource()`, and `hasImageResource()` instead
* `DataExtension::get_extra_config()` removed, no longer supports `extraStatics` or `extraDBFields`. Define your * `DataExtension::get_extra_config()` removed, no longer supports `extraStatics` or `extraDBFields`. Define your
statics on the class directly. statics on the class directly.
* `DataList::getRange()` removed. Use `limit()` instead. * `DataList::getRange()` removed. Use `limit()` instead.
* `SQLMap` removed. Call `map()` on a `DataList` or use `SS_Map` directly instead. * `SQLMap` removed. Call `map()` on a `DataList` or use `SS_Map` directly instead.
* `Profiler` removed. Use xhprof or xdebug for profiling instead. * `Profiler` removed. Use xhprof or xdebug for profiling instead.
* `Aggregate` removed. Call aggregate methods on a `DataList` instead e.g. `Member::get()->max('LastEdited')` * `Aggregate` removed. Call aggregate methods on a `DataList` instead e.g. `Member::get()->max('LastEdited')`
* `MySQLDatabase::set_connection_charset()` removed. Use `MySQLDatabase.connection_charset` config setting instead * `MySQLDatabase::set_connection_charset()` removed. Use `MySQLDatabase.connection_charset` config setting instead
* `SQLConditionalExpression/SQLQuery` `select()`, `limit()`, `orderby()`, `groupby()`, `having()`, `from()`, `leftjoin()`, `innerjoin()`, `where()` and `whereAny()` removed. * `SQLConditionalExpression/SQLQuery` `select()`, `limit()`, `orderby()`, `groupby()`, `having()`, `from()`, `leftjoin()`, `innerjoin()`, `where()` and `whereAny()` removed.
Use `set*()` and `add*()` methods instead. Use `set*()` and `add*()` methods instead.
* Template `<% control $MyList %>` syntax removed. Use `<% loop $MyList %>` instead. * Template `<% control $MyList %>` syntax removed. Use `<% loop $MyList %>` instead.
* Object::singleton() method for better type-friendly singleton generation * Removed `Member.LastVisited` and `Member.NumVisits` properties, see
[Howto: Track Member Logins](/extending/how_tos/track_member_logins) to restore functionality as custom code
### CMS ## New and changed API
* `SearchForm::getSearchQuery` no longer pre-escapes search keywords and must be cast in your template * Implementation of a parameterised query framework eliminating the need to manually escape variables for
use in SQL queries. This has been integrated into nearly every level of the database ORM.
* Refactor of database connectivity classes into separate components linked together through dependency injection
* Refactor of `SQLQuery` into separate objects for each query type: `SQLQuery`, `SQLDelete`, `SQLUpdate` and `SQLInsert`
* PDO is now a standard connector, and is available for all database interfaces
* `DataObject::doValidate()` method visibility added to access `DataObject::validate` externally
* `NumericField` now uses HTML5 "number" type instead of "text"
* `UploadField` "Select from files" shows files in all folders by default
* `UploadField` won't display an overwrite warning unless `Upload::replaceFile` is true
* `HtmlEditorField` no longer substitutes `<blockquote />` for indented text
* `ClassInfo::dataClassesFor` now returns classes which should have tables, regardless of whether those
tables actually exist.
* `SS_Filterable`, `SS_Limitable` and `SS_Sortable` now explicitly extend `SS_List`
* `Convert::html2raw` no longer wraps text by default and can decode single quotes.
* `Mailer` no longer calls `xml2raw` on all email subject line, and now must be passed in via plain text.
* `ErrorControlChain` now supports reload on exceptions
* `FormField::validate` now requires an instance of `Validator`
* API: Removed URL routing by controller name
* Security: The multiple authenticator login page should now be styled manually - i.e. without the default jQuery
UI layout. A new template, Security_MultiAuthenticatorLogin.ss is available.
* Security: This controller's templates can be customised by overriding the `getTemplatesFor` function.
* API: Form and FormField ID attributes rewritten.
* `SearchForm::getSearchQuery` no longer pre-escapes search keywords and must
be cast in your template
* Helper function `DB::placeholders` can be used to generate a comma separated list of placeholders
useful for creating "WHERE ... IN (?,...)" SQL fragments
* Implemented Convert::symbol2sql to safely encode database and table names and identifiers.
E.g. `Convert::symbol2sql('table.column') => '"table"."column"';`
* `Convert::raw2sql` may now quote the escaped value, as well as safely escape it, according to the current
database adaptor's preference.
* `DB` class has been updated and many static methods have been renamed to conform to coding convention.
* Renamed API:
* `affectedRows` -> `affected_rows`
* `checkAndRepairTable` -> `check_and_repair_table`
* `createDatabase` -> `create_database`
* `createField` -> `create_field`
* `createTable` -> `create_table`
* `dontRequireField` -> `dont_require_field`
* `dontRequireTable` -> `dont_require_table`
* `fieldList` -> `field_list`
* `getConn` -> `get_conn`
* `getGeneratedID` -> `get_generated_id`
* `isActive` -> `is_active`
* `requireField` -> `require_field`
* `requireIndex` -> `require_index`
* `requireTable` -> `require_table`
* `setConn` -> `set_conn`
* `tableList` -> `table_list`
* Deprecated API:
* `getConnect` (Was placeholder for PDO connection string building code, but is made
redundant after the PDOConnector being fully abstracted)
* New API:
* `build_sql` - Hook into new SQL generation code
* `get_connector` (Nothing to do with getConnect)
* `get_schema`
* `placeholders`
* `prepared_query`
* `SS_Database` class has been updated and many functions have been deprecated, or refactored into
the various other database classes. Most of the database management classes remain in the database
controller, due to individual databases (changing, creating of, etc) varying quite a lot from
API to API, but schema updates within a database itself is managed by an attached DBSchemaManager
* Refactored into DBSchemaManager:
* `createTable`
* `alterTable`
* `renameTable`
* `createField`
* `renameField`
* `fieldList`
* `tableList`
* `hasTable`
* `enumValuesForField`
* `beginSchemaUpdate` and `endSchemaUpdate` -> Use `schemaUpdate` with a callback
* `cancelSchemaUpdate`
* `isSchemaUpdating`
* `doesSchemaNeedUpdating`
* `transCreateTable`
* `transAlterTable`
* `transCreateField`
* `transCreateField`
* `transCreateIndex`
* `transAlterField`
* `transAlterIndex`
* `requireTable`
* `dontRequireTable`
* `requireIndex`
* `hasField`
* `requireField`
* `dontRequireField`
* Refactored into DBQueryBuilder
* `sqlQueryToString`
* Deprecated:
* `getConnect` - Was intended for use with PDO, but was never implemented, and is now
redundant, now that there is a stand-alone `PDOConnector`
* `prepStringForDB` - Use `quoteString` instead
* `dropDatabase` - Use `dropSelectedDatabase`
* `createDatabase` - Use `selectDatabase` with the second parameter set to true instead
* `allDatabaseNames` - Use `databaseList` instead
* `currentDatabase` - Use `getSelectedDatabase` instead
* `addslashes` - Use `escapeString` instead
* `LogErrorEmailFormatter` now better displays SQL queries in errors by respecting line breaks
* Installer has been majorly upgraded to handle the new database configuration options
and additional PDO functionality.
* Created `SS_DatabaseException` to emit database errors. Query information such as SQL
and any relevant parameters may be used by error handling user code that catches
this exception.
* The `SQLConditionGroup` interface has been created to represent dynamically
evaluated SQL conditions. This may be used to wrap a class that generates
a custom SQL clause(s) to be evaluated at the time of execution.
* `DataObject` constants CHANGE_NONE, CHANGE_STRICT, and CHANGE_VALUE have been created
to provide more verbosity to field modification detection. This replaces the use of
various magic numbers with the same meaning.
* create_table_options now uses constants as API specific filters rather than strings.
This is in order to promote better referencing of elements across the codebase.
See `FulltextSearchable->enable` for example.
* `$FromEnd` iterator variable now available in templates.
* Support for multiple HtmlEditorConfigs on the same page.
* Object::singleton() method for better type-friendly singleton generation
* New `Image` methods `CropWidth` and `CropHeight` added
* 'Max' versions of `Image` methods introduced to prevent up-sampling
* Update Image method names in PHP code and templates
* `SetRatioSize` -> `Fit`
* `CroppedImage` -> `Fill`
* `PaddedImage` -> `Pad`
* `SetSize` -> `Pad`
* `SetWidth` -> `ScaleWidth`
* `SetHeight` -> `ScaleHeight`
## Changelog ## Bugfixes
### CMS * Reduced database regeneration chances on subsequent rebuilds after the initial dev/build
* Elimination of various SQL injection vulnerability points
* `DataObject::writeComponents()` now called correctly during `DataObject::write()`
* Fixed missing theme declaration in installer
* Fixed incorrect use of non-existing exception classes (e.g. `HTTPResponse_exception`)
* `GridState` fixed to distinguish between check for missing values, and creation of
nested state values, in order to prevent non-empty values being returned for
missing keys. This was breaking `DataObject::get_by_id` by passing in an object
for the ID.
* Fixed order of `File` fulltext searchable fields to use same order as actual fields.
This is required to prevent unnecessary rebuild of MS SQL databases when fulltext
searching is enabled.
* In the past E_RECOVERABLE_ERROR would be ignored, and now correctly appear as warnings.
### DataObject::validate() method visibility changed to public ## Upgrading Notes
The visibility of `DataObject::validate()` has been changed from `protected` to `public`.
Any existing classes that currently set this as `protected` should be changed like in
this example:
::php
class MyDataClass extends DataObject {
...
public function validate() {
...
}
...
}
### UploadField "Select from files" shows files in all folders by default ### UploadField "Select from files" shows files in all folders by default
In order to list files in a single folder by default (previous default behaviour), In order to list files in a single folder by default (previous default behaviour),
use `setDisplayFolderName()` with a folder path relative to `assets/`: use `setDisplayFolderName()` with a folder path relative to `assets/`:
:::php
UploadField::create('MyField')->setDisplayFolderName('Uploads'); UploadField::create('MyField')->setDisplayFolderName('Uploads');
### UploadField won't display an overwrite warning unless Upload:replaceFile is true ### UploadField won't display an overwrite warning unless Upload:replaceFile is true
The configuration setting `UploadField:overwriteWarning` is dependent on `Upload:replaceFile` The configuration setting `UploadField:overwriteWarning` is dependent on `Upload:replaceFile`
@ -84,6 +216,7 @@ To display a warning before overwriting a file:
Via config: Via config:
::yaml ::yaml
Upload: Upload:
# Replace an existing file rather than renaming the new one. # Replace an existing file rather than renaming the new one.
@ -92,12 +225,15 @@ Via config:
# Warning before overwriting existing file (only relevant when Upload: replaceFile is true) # Warning before overwriting existing file (only relevant when Upload: replaceFile is true)
overwriteWarning: true overwriteWarning: true
Or per instance: Or per instance:
::php ::php
$uploadField->getUpload()->setReplaceFile(true); $uploadField->getUpload()->setReplaceFile(true);
$uploadField->setOverwriteWarning(true); $uploadField->setOverwriteWarning(true);
### File.allowed_extensions restrictions ### File.allowed_extensions restrictions
Certain file types such as swf, html, htm, xhtml and xml have been removed from the list Certain file types such as swf, html, htm, xhtml and xml have been removed from the list
@ -126,35 +262,160 @@ languages like JavaScript won't be able to read them.
To set it back to be non-HTTP only, you need to set the `$httpOnly` argument to false when calling To set it back to be non-HTTP only, you need to set the `$httpOnly` argument to false when calling
`Cookie::set()`. `Cookie::set()`.
### Bugfixes ### API: Removed URL routing by controller name
* Migration of code to use new parameterised framework
### Framework The auto-routing of controller class names to URL endpoints
has been removed (rule: `'$Controller//$Action/$ID/$OtherID': '*'`).
This increases clarity in routing since it makes URL entpoints explicit,
and thereby simplifies system and security reviews.
* Implementation of a parameterised query framework eliminating the need to manually escape variables for Please access any custom controllers exclusively through self-defined
use in SQL queries. This has been integrated into nearly every level of the database ORM. [routes](/reference/director). For controllers extending `Page_Controller`,
* Refactor of database connectivity classes into separate components linked together through dependency injection simply use the provided page URLs.
* Refactor of `SQLQuery` into separate objects for each query type: `SQLQuery`, `SQLDelete`, `SQLUpdate` and `SQLInsert`
* Rename of API methods to conform to coding conventions
* PDO is now a standard connector, and is available for all database interfaces
* Additional database and query generation tools
## Bugfixes
* Reduced database regeneration chances on subsequent rebuilds after the initial dev/build :::php
* Elimination of various SQL injection vulnerability points class MyController extends Controller {
* `DataObject::writeComponents()` now called correctly during `DataObject::write()` static $allowed_actions = array('myaction');
* Fixed missing theme declaration in installer public function myaction($request) {
* Fixed incorrect use of non-existing exception classes (e.g. `HTTPResponse_exception`) // ...
* `GridState` fixed to distinguish between check for missing values, and creation of }
nested state values, in order to prevent non-empty values being returned for }
missing keys. This was breaking `DataObject::get_by_id` by passing in an object
for the ID.
* Fixed order of `File` fulltext searchable fields to use same order as actual fields. Create a new file `mysite/_config/routes.yml`
This is required to prevent unnecessary rebuild of MS SQL databases when fulltext (read more about the [config format](/topics/configuration)).
searching is enabled. Your controller is now available on `http://yourdomain.com/my-controller-endpoint`,
after refreshing the configuration cache through `?flush=all`.
:::yaml
---
Name: my-routes
After: framework/routes#coreroutes
---
Director:
rules:
'my-controller-endpoint//$Action' : 'MyController'
The auto-routing is still in place for unit tests,
since its a frequently used feature there. Although we advise against it,
you can reinstate the old behaviour through a director rule:
:::yaml
---
Name: my-routes
After: framework/routes#coreroutes
---
Director:
rules:
'$Controller//$Action/$ID/$OtherID': '*'
### API: Default Form and FormField ID attributes rewritten.
Previously the automatic generation of ID attributes throughout the Form API
could generate invalid ID values such as Password[ConfirmedPassword] as well
as duplicate ID values between forms on the same page. For example, if you
created a field called `Email` on more than one form on the page, the resulting
HTML would have multiple instances of `#Email`. ID should be a unique
identifier for a single element within the document.
This rewrite has several angles, each of which is described below. If you rely
on ID values in your CSS files, Javascript code or application unit tests *you
will need to update your code*.
#### Conversion of invalid form ID values
ID attributes on Form and Form Fields will now follow the
[HTML specification](http://www.w3.org/TR/REC-html40/types.html#type-cdata).
Generating ID attributes is now handled by the new `FormTemplateHelper` class.
Please test each of your existing site forms to ensure that they work
correctly in particular, javascript and css styles which rely on specific ID
values.
#### Invalid ID attributes stripped
ID attributes will now be run through `Convert::raw2htmlid`. Invalid characters
are replaced with a single underscore character. Duplicate, leading and trailing
underscores are removed. Custom ID attributes (set through `setHTMLID`) will not
be altered.
Before:
:::html
<form id="MyForm[Form]"
<div id="MyForm[Form][ID]">
Now:
:::html
<form id="MyForm_Form">
<div id="MyForm_Form_ID">
#### Namespaced FormField ID's
Form Field ID values will now be namespaced with the parent form ID.
Before:
:::html
<div id="Email">
Now:
:::html
<div id="MyForm_Email">
#### FormField wrapper containers suffixed with `_Holder`
Previously both the container div and FormField tag shared the same ID in
certain cases. Now, the wrapper div in the default `FormField` template will be
suffixed with `_Holder`.
Before:
:::html
<div id="Email">
<input id="Email" />
After:
:::html
<div id="MyForm_Email_Holder"
<input id="MyForm_Email" />
#### Reverting to the old specification
If upgrading existing forms is not feasible, developers can opt out of the new
specifications by using the `FormTemplateHelper_Pre32` class rules instead of
the default ones.
`mysite/config/_config.yml`:
:::yaml
Injector:
FormTemplateHelper:
class: FormTemplateHelper_Pre32
## Upgrading
### Update code that uses SQLQuery ### Update code that uses SQLQuery
@ -173,6 +434,7 @@ although now a new SQLDelete object should be created from the original SQLQuery
Before: Before:
:::php :::php
<?php <?php
$query = new SQLQuery('*'); $query = new SQLQuery('*');
@ -181,8 +443,10 @@ Before:
$query->setDelete(true); $query->setDelete(true);
$query->execute(); $query->execute();
After: After:
:::php :::php
<?php <?php
$query = SQLDelete::create() $query = SQLDelete::create()
@ -190,8 +454,10 @@ After:
->setWhere(array('"SiteTree"."ShowInMenus"' => 0)); ->setWhere(array('"SiteTree"."ShowInMenus"' => 0));
$query->execute(); $query->execute();
Alternatively: Alternatively:
:::php :::php
<?php <?php
$query = SQLQuery::create() $query = SQLQuery::create()
@ -200,6 +466,7 @@ Alternatively:
->toDelete(); ->toDelete();
$query->execute(); $query->execute();
### Update code that interacts with SQL strings to use parameters ### Update code that interacts with SQL strings to use parameters
The Silverstripe ORM (object relation model) has moved from using escaped SQL strings The Silverstripe ORM (object relation model) has moved from using escaped SQL strings
@ -221,121 +488,214 @@ As a result of this upgrade there are now very few cases where `Convert::raw2sql
Examples of areas where queries should be upgraded are below: Examples of areas where queries should be upgraded are below:
1. #### Querying the database directly through DB, including non-SELECT queries #### 1. Querying the database directly through DB, including non-SELECT queries
Before: Before:
:::php
<?php
// Note: No deprecation notices will be caused here :::php
DB::query("UPDATE \"SiteTree\" SET \"Title\" LIKE '%" . Convert::raw2sql($myTitle) . "%' WHERE \"ID\" = 1"); <?php
$myPages = DB::query(sprintf('SELECT "ID" FROM "MyObject" WHERE "Title" = \'%s\'', Convert::raw2sql($parentTitle)));
After: // Note: No deprecation notices will be caused here
DB::query("UPDATE \"SiteTree\" SET \"Title\" LIKE '%" . Convert::raw2sql($myTitle) . "%' WHERE \"ID\" = 1");
$myPages = DB::query(sprintf('SELECT "ID" FROM "MyObject" WHERE "Title" = \'%s\'', Convert::raw2sql($parentTitle)));
:::php
<?php
DB::prepared_query( After:
'UPDATE "SiteTree" SET "Title" LIKE ? WHERE "ID" = ?',
array("%{$myTitle}%", 1)
);
$myPages = DB::prepared_query(
'SELECT "ID" FROM "MyObject" WHERE "Title" = ?',
array($parentTitle)
);
2. #### Querying the database through `DataList`, `DataQuery`, `SQLQuery`, and `DataObject`
Before: :::php
<?php
:::php DB::prepared_query(
<?php 'UPDATE "SiteTree" SET "Title" LIKE ? WHERE "ID" = ?',
array("%{$myTitle}%", 1)
);
$myPages = DB::prepared_query(
'SELECT "ID" FROM "MyObject" WHERE "Title" = ?',
array($parentTitle)
);
$items = DataObject::get_one('MyObject', '"Details" = \''.Convert::raw2sql($details).'\'');
$things = MyObject::get()->where('"Name" = \''.Convert::raw2sql($name).'\'');
$list = DataList::create('Banner')->where(array(
'"ParentID" IS NOT NULL',
'"Title" = \'' . Convert::raw2sql($title) . '\''
);
After: #### 2. Querying the database through `DataList`, `DataQuery`, `SQLQuery`, and `DataObject`
:::php Before:
<?php
$items = DataObject::get_one('MyObject', array('"MyObject"."Details"' => $details));
$things = MyObject::get()->where(array('"MyObject"."Name" = ?' => $name));
$list = DataList::create('Banner')->where(array(
'"ParentID" IS NOT NULL',
'"Title" = ?', $title
);
3. #### Interaction with `DataList::sql()`, `DataQuery::sql()`, `SQLQuery::sql()`, or `SQLQuery::getJoins()` methods :::php
<?php
The place where legacy code would almost certainly fail is any code that calls $items = DataObject::get_one('MyObject', '"Details" = \''.Convert::raw2sql($details).'\'');
DataList::sql`, `DataQuery::sql`, `SQLQuery::sql` or `SQLQuery::getJoins()`, as the api requires that user $things = MyObject::get()->where('"Name" = \''.Convert::raw2sql($name).'\'');
code passes in an argument here to retrieve SQL parameters by value. $list = DataList::create('Banner')->where(array(
'"ParentID" IS NOT NULL',
'"Title" = \'' . Convert::raw2sql($title) . '\''
);
User code that assumes parameterless queries will likely fail, and need to be
updated to handle this case properly.
Before: After:
:::php
<?php
// Generate query :::php
$argument = 'whatever'; <?php
$query = SQLQuery::create()
->setFrom('"SiteTree"')
->setWhere(array("\"SiteTree\".\"Title\" LIKE '" . Convert::raw2sql($argument) . "'"));
// Inspect elements of the query $items = DataObject::get_one('MyObject', array('"MyObject"."Details"' => $details));
$sql = $query->sql(); $things = MyObject::get()->where(array('"MyObject"."Name" = ?' => $name));
$sql = preg_replace('/LIKE \'(.+)\'/', 'LIKE \'%${1}%\'', $sql); // Adds %% around the argument $list = DataList::create('Banner')->where(array(
'"ParentID" IS NOT NULL',
'"Title" = ?', $title
);
// Pass new query to database connector
DB::query($sql);
After: #### 3. Interaction with `DataList::sql()`, `DataQuery::sql()`, `SQLQuery::sql()`, or `SQLQuery::getJoins()` methods
:::php The place where legacy code would almost certainly fail is any code that calls
<?php DataList::sql`, `DataQuery::sql`, `SQLQuery::sql` or `SQLQuery::getJoins()`, as the api requires that user
code passes in an argument here to retrieve SQL parameters by value.
// Generate query User code that assumes parameterless queries will likely fail, and need to be
$argument = 'whatever'; updated to handle this case properly.
$query = SQLQuery::create()
->setFrom('"SiteTree"')
->setWhere(array('"SiteTree"."Title" LIKE ?' => $argument));
// Inspect elements of the query Before:
$sql = $query->sql($parameters);
foreach($parameters as $key => $value) {
// Adds %% around arguments :::php
$parameters[$key] = "%{$value}%"; <?php
// Generate query
$argument = 'whatever';
$query = SQLQuery::create()
->setFrom('"SiteTree"')
->setWhere(array("\"SiteTree\".\"Title\" LIKE '" . Convert::raw2sql($argument) . "'"));
// Inspect elements of the query
$sql = $query->sql();
$sql = preg_replace('/LIKE \'(.+)\'/', 'LIKE \'%${1}%\'', $sql); // Adds %% around the argument
// Pass new query to database connector
DB::query($sql);
After:
:::php
<?php
// Generate query
$argument = 'whatever';
$query = SQLQuery::create()
->setFrom('"SiteTree"')
->setWhere(array('"SiteTree"."Title" LIKE ?' => $argument));
// Inspect elements of the query
$sql = $query->sql($parameters);
foreach($parameters as $key => $value) {
// Adds %% around arguments
$parameters[$key] = "%{$value}%";
}
// Pass new query to database connector
// Note that DB::query($sql) would fail, as it would contain ? with missing parameters
DB::prepared_query($sql, $parameters);
Also note that the parameters may not be a single level array, as certain values
may be forced to be cast as a certain type (where supported by the current API).
E.g.
:::php
<?php
$parameters = array(
10,
array('value' => 0, 'type' => 'boolean') // May also contain other database API specific options
)
DB::prepared_query('DELETE FROM "MyObject" WHERE ParentID = ? OR IsValid = ?', $parameters);
#### 4. Interaction with `SQLQuery::getWhere()` method
As all where conditions are now parameterised, the format of the results returned by `SQLQuery::getWhere()`
will not always equate with that in FrameWork 3.1. Once this would be a list of strings, what will
now be returned is a list of conditions, each of which is an associative array mapping the
condition string to a list of parameters provided.
Before:
:::php
<?php
// Increment value of a single condition
$conditions = $query->getWhere();
$new = array();
foreach($conditions as $condition) {
if(preg_match('/\"Count\" = (?<count>\d+)/', $condition, $matches)) {
$condition = '"Count" = '.($matches['count'] + 1);
} }
$new[] = $condition;
}
$query->setWhere($new);
// Pass new query to database connector
// Note that DB::query($sql) would fail, as it would contain ? with missing parameters
DB::prepared_query($sql, $parameters);
Also note that the parameters may not be a single level array, as certain values After:
may be forced to be cast as a certain type (where supported by the current API).
E.g.
:::php :::php
<?php // Increment value of a single condition
$conditions = $query->getWhere();
$new = array();
foreach($conditions as $condition) {
// $condition will be a single length array
foreach($condition as $predicate => $parameters) {
if('"Count" = ?' === $predicate) {
$parameters[0] = $parameters[0] + 1;
}
$new[] = array($predicate => $parameters);
}
}
$query->setWhere($new);
$parameters = array(
10,
array('value' => 0, 'type' => 'boolean') // May also contain other database API specific options
)
DB::prepared_query('DELETE FROM "MyObject" WHERE ParentID = ? OR IsValid = ?', $parameters);
4. ### Update code that interacts with the DB schema In cases where user code will manipulate the results of this value, it may be useful to
replace this method call with the new `getWhereParameterised($parameters)` method, where
applicable.
This method returns a manipulated form of the where conditions stored by the query, so
that it matches the list of strings consistent with the old 3.1 `SQLQuery::getWhere()` behaviour.
Additionally, the list of parameters is safely extracted, flattened, and can be passed out
through the `$parameters` argument which is passed by reference.
Before:
:::php
public function filtersOnColumn($query, $column) {
$regexp = '/^(.*\.)?("|`)?' . $column . ' ("|`)?\s?=/';
foreach($this->getWhere() as $predicate) {
if(preg_match($regexp, $predicate)) return true;
}
return false;
}
After:
:::php
public function filtersOnColumn($query, $column) {
$regexp = '/^(.*\.)?("|`)?' . $column . ' ("|`)?\s?=/';
foreach($this->getWhereParameterised($parameters) as $predicate) {
if(preg_match($regexp, $predicate)) return true;
}
return false;
}
#### 5. Update code that interacts with the DB schema
Updating database schema is now done by `updateSchema` with a callback, rather than relying Updating database schema is now done by `updateSchema` with a callback, rather than relying
on user code to call `beginSchemaUpdate` and `endSchemaUpdate` around the call. on user code to call `beginSchemaUpdate` and `endSchemaUpdate` around the call.
@ -346,6 +706,7 @@ interact with it via `DB::get_schema` instead of `DB::get_conn` (previously name
Before: Before:
:::php :::php
<?php <?php
$conn = DB::getConn(); $conn = DB::getConn();
@ -355,8 +716,10 @@ Before:
} }
$conn->endSchemaUpdate(); $conn->endSchemaUpdate();
After: After:
:::php :::php
<?php <?php
$schema = DB::get_schema(); $schema = DB::get_schema();
@ -366,6 +729,7 @@ After:
} }
}); });
Also should be noted is that many functions have been renamed to conform better with Also should be noted is that many functions have been renamed to conform better with
coding conventions. E.g. `DB::requireTable` is now `DB::require_table` coding conventions. E.g. `DB::requireTable` is now `DB::require_table`
@ -377,6 +741,7 @@ feature which performs both unpublish and deletion simultaneously.
To restore "delete from live" add the following config to your site's config.yml. To restore "delete from live" add the following config to your site's config.yml.
:::yml :::yml
CMSMain: CMSMain:
enabled_legacy_actions: enabled_legacy_actions:
@ -391,97 +756,3 @@ In order to remove the new "archive" action and restore the old "delete" action
enabled_legacy_actions: enabled_legacy_actions:
- CMSBatchAction_Delete - CMSBatchAction_Delete
### Other
* Helper function `DB::placeholders` can be used to generate a comma separated list of placeholders
useful for creating "WHERE ... IN (?,...)" SQL fragments
* Implemented Convert::symbol2sql to safely encode database and table names and identifiers.
E.g. `Convert::symbol2sql('table.column') => '"table"."column"';`
* `Convert::raw2sql` may now quote the escaped value, as well as safely escape it, according to the current
database adaptor's preference.
* `DB` class has been updated and many static methods have been renamed to conform to coding convention.
* Renamed API:
- `affectedRows` -> `affected_rows`
- `checkAndRepairTable` -> `check_and_repair_table`
- `createDatabase` -> `create_database`
- `createField` -> `createField`
- `createTable` -> `createTable`
- `dontRequireField` -> `dont_require_field`
- `dontRequireTable` -> `dont_require_table`
- `fieldList` -> `field_list`
- `getConn` -> `get_conn`
- `getGeneratedID` -> `get_generated_id`
- `isActive` -> `is_active`
- `requireField` -> `require_field`
- `requireIndex` -> `require_index`
- `requireTable` -> `require_table`
- `setConn` -> `set_conn`
- `tableList` -> `table_list`
* Deprecated API:
- `getConnect` (Was placeholder for PDO connection string building code, but is made
redundant after the PDOConnector being fully abstracted)
* New API:
- `build_sql` - Hook into new SQL generation code
- `get_connector` (Nothing to do with getConnect)
- `get_schema`
- `placeholders`
- `prepared_query`
* `SS_Database` class has been updated and many functions have been deprecated, or refactored into
the various other database classes. Most of the database management classes remain in the database
controller, due to individual databases (changing, creating of, etc) varying quite a lot from
API to API, but schema updates within a database itself is managed by an attached DBSchemaManager
* Refactored into DBSchemaManager:
- `createTable`
- `alterTable`
- `renameTable`
- `createField`
- `renameField`
- `fieldList`
- `tableList`
- `hasTable`
- `enumValuesForField`
- `beginSchemaUpdate` and `endSchemaUpdate` -> Use `schemaUpdate` with a callback
- `cancelSchemaUpdate`
- `isSchemaUpdating`
- `doesSchemaNeedUpdating`
- `transCreateTable`
- `transAlterTable`
- `transCreateField`
- `transCreateField`
- `transCreateIndex`
- `transAlterField`
- `transAlterIndex`
- `requireTable`
- `dontRequireTable`
- `requireIndex`
- `hasField`
- `requireField`
- `dontRequireField`
* Refactored into DBQueryBuilder
- `sqlQueryToString`
* Deprecated:
- `getConnect` - Was intended for use with PDO, but was never implemented, and is now
redundant, now that there is a stand-alone `PDOConnector`
- `prepStringForDB` - Use `quoteString` instead
- `dropDatabase` - Use `dropSelectedDatabase`
- `createDatabase` - Use `selectDatabase` with the second parameter set to true instead
- `allDatabaseNames` - Use `databaseList` instead
- `currentDatabase` - Use `getSelectedDatabase` instead
- `addslashes` - Use `escapeString` instead
* LogErrorEmailFormatter now better displays SQL queries in errors by respecting line breaks
* Installer has been majorly upgraded to handle the new database configuration options
and additional PDO functionality.
* Created SS_DatabaseException to emit database errors. Query information such as SQL
and any relevant parameters may be used by error handling user code that catches
this exception.
* The SQLConditionGroup interface has been created to represent dynamically
evaluated SQL conditions. This may be used to wrap a class that generates
a custom SQL clause(s) to be evaluated at the time of execution.
* DataObject constants CHANGE_NONE, CHANGE_STRICT, and CHANGE_VALUE have been created
to provide more verbosity to field modification detection. This replaces the use of
various magic numbers with the same meaning.
* create_table_options now uses constants as API specific filters rather than strings.
This is in order to promote better referencing of elements across the codebase.
See `FulltextSearchable->enable` for example.
* `$FromEnd` iterator variable now available in templates.

View File

@ -1,146 +0,0 @@
# 3.2.0 (unreleased)
## Overview
### CMS
* Moved SS_Report and ReportAdmin out to a separate module. If you're using
composer or downloading a release, this module should be included for you.
Otherwise, you'll need to include the module yourself
(https://github.com/silverstripe-labs/silverstripe-reports)
### Framework
* API: Removed URL routing by controller name
* Security: The multiple authenticator login page should now be styled manually - i.e. without the default jQuery
UI layout. A new template, Security_MultiAuthenticatorLogin.ss is available.
* Security: This controller's templates can be customised by overriding the `getTemplatesFor` function.
* API: Form and FormField ID attributes rewritten.
## Details
### API: Removed URL routing by controller name
The auto-routing of controller class names to URL endpoints
has been removed (rule: `'$Controller//$Action/$ID/$OtherID': '*'`).
This increases clarity in routing since it makes URL entpoints explicit,
and thereby simplifies system and security reviews.
Please access any custom controllers exclusively through self-defined
[routes](/reference/director). For controllers extending `Page_Controller`,
simply use the provided page URLs.
:::php
class MyController extends Controller {
static $allowed_actions = array('myaction');
public function myaction($request) {
// ...
}
}
Create a new file `mysite/_config/routes.yml`
(read more about the [config format](/topics/configuration)).
Your controller is now available on `http://yourdomain.com/my-controller-endpoint`,
after refreshing the configuration cache through `?flush=all`.
:::yaml
---
Name: my-routes
After: framework/routes#coreroutes
---
Director:
rules:
'my-controller-endpoint//$Action' : 'MyController'
The auto-routing is still in place for unit tests,
since its a frequently used feature there. Although we advise against it,
you can reinstate the old behaviour through a director rule:
:::yaml
---
Name: my-routes
After: framework/routes#coreroutes
---
Director:
rules:
'$Controller//$Action/$ID/$OtherID': '*'
### API: Default Form and FormField ID attributes rewritten.
Previously the automatic generation of ID attributes throughout the Form API
could generate invalid ID values such as Password[ConfirmedPassword] as well
as duplicate ID values between forms on the same page. For example, if you
created a field called `Email` on more than one form on the page, the resulting
HTML would have multiple instances of `#Email`. ID should be a unique
identifier for a single element within the document.
This rewrite has several angles, each of which is described below. If you rely
on ID values in your CSS files, Javascript code or application unit tests *you
will need to update your code*.
#### Conversion of invalid form ID values
ID attributes on Form and Form Fields will now follow the
[HTML specification](http://www.w3.org/TR/REC-html40/types.html#type-cdata).
Generating ID attributes is now handled by the new `FormTemplateHelper` class.
Please test each of your existing site forms to ensure that they work
correctly in particular, javascript and css styles which rely on specific ID
values.
#### Invalid ID attributes stripped
ID attributes will now be run through `Convert::raw2htmlid`. Invalid characters
are replaced with a single underscore character. Duplicate, leading and trailing
underscores are removed. Custom ID attributes (set through `setHTMLID`) will not
be altered.
Before:
<form id="MyForm[Form]"
<div id="MyForm[Form][ID]">
Now:
<form id="MyForm_Form">
<div id="MyForm_Form_ID">
#### Namespaced FormField ID's
Form Field ID values will now be namespaced with the parent form ID.
Before:
<div id="Email">
Now:
<div id="MyForm_Email">
#### FormField wrapper containers suffixed with `_Holder`
Previously both the container div and FormField tag shared the same ID in
certain cases. Now, the wrapper div in the default `FormField` template will be
suffixed with `_Holder`.
Before:
<div id="Email">
<input id="Email" />
After:
<div id="MyForm_Email_Holder"
<input id="MyForm_Email" />
#### Reverting to the old specification
If upgrading existing forms is not feasible, developers can opt out of the new
specifications by using the `FormTemplateHelper_Pre32` class rules instead of
the default ones.
:::yaml
# mysite/config/_config.yml
Injector:
FormTemplateHelper:
class: FormTemplateHelper_Pre32
### Further Changes
* Removed `Member.LastVisited` and `Member.NumVisits` properties, see [Howto: Track Member Logins](doc.silverstripe.org/framework/en/trunk/howto/track-member-logins) to restore functionality as custom code

View File

@ -3,45 +3,36 @@ summary: Writing guide for contributing to SilverStripe developer and CMS user h
# Contributing documentation # Contributing documentation
Documentation for a software project is a continued and collaborative effort, we encourage everybody to contribute, from Documentation for a software project is a continued and collaborative effort. We encourage everybody to contribute in any way they can, from simply fixing spelling mistakes, to writing recipes, to reviewing existing documentation and translating it to another language.
simply fixing spelling mistakes, to writing recipes, reviewing existing documentation, and translating the whole thing.
Modifying documentation requires basic [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) and Modifying documentation requires basic [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) and
[Markdown](http://daringfireball.net/projects/markdown/) knowledge, and a GitHub user account. [Markdown](http://daringfireball.net/projects/markdown/) knowledge, and a GitHub user account.
## Editing online ## Editing online
The easiest way of making a change to the documentation is by clicking the "Edit this page" link at the bottom of the The easiest way of editing any documentation is by clicking the "Edit this page" link at the bottom of the
page you want to edit. Alternatively, you can find the appropriate .md file in the page you want to edit. Alternatively, locate the appropriate .md file in the
[github.com/silverstripe/silverstripe-framework](https://github.com/silverstripe/silverstripe-framework/tree/master/docs/) [github.com/silverstripe/silverstripe-framework](https://github.com/silverstripe/silverstripe-framework/tree/master/docs/) repository and press the "edit" button. **You will need a free GitHub account to do this**.
repository and press the "edit" button. **You will need a free GitHub account to do this**.
* After you have made your change, describe it in the "commit summary" and "extended description" fields below, and * After editing the documentation, describe your changes in the "commit summary" and "extended description" fields below then press "Commit Changes".
press "Commit Changes". * After that you will see a form to submit a Pull Request: "[pull requests](http://help.github.com/pull-requests/)". You should be able to adjust the version your
* After that you will see form to submit a Pull Request. You should be able to adjust the version your document ion change is for and then submit the form. Your changes * documentation changes are for and then submit the form. Your changes will be sent to the core committers for approval.
will be sent to the core committers for approval.
<div class="warning" markdown='1'> <div class="warning" markdown='1'>
You should make the changes in the lowest branch they apply to. For instance, if you fix a spelling issue that you You should make your changes in the lowest branch they apply to. For instance, if you fix a spelling issue that you found in the 3.1 documentation, submit your fix to that branch in Github and it'll be copied to the master (3.2) version of the documentation automatically. *Don't submit multiple pull requests*.
found in the 3.1 documentation, submit your fix to that branch in Github and it'll be copied to the master (3.2)
version of the documentation automatically. *Don't submit multiple pull requests*.
</div> </div>
## Editing on your computer ## Editing on your computer
If you prefer to edit the content on your local machine, you can "[fork](http://help.github.com/forking/)" the If you prefer to edit content on your local machine, you can "[fork](http://help.github.com/forking/)" the
[github.com/silverstripe/silverstripe-framework](http://github.com/silverstripe/silverstripe-framework) and [github.com/silverstripe/silverstripe-framework](http://github.com/silverstripe/silverstripe-framework) and
[github.com/silverstripe/silverstripe-cms](http://github.com/silverstripe/silverstripe-cms) repositories and send us [github.com/silverstripe/silverstripe-cms](http://github.com/silverstripe/silverstripe-cms) repositories, and send us "[pull requests](http://help.github.com/pull-requests/)" to incorporate your changes. If you have previously downloaded SilverStripe or a module, chances are that you already have these repositories on your machine.
"[pull requests](http://help.github.com/pull-requests/)". If you have downloaded SilverStripe or a module, chances are
that you already have these repositories on your machine.
The documentation is kept alongside the source code in the `docs/` subfolder of any SilverStripe module, framework or The documentation is kept alongside the source code in the `docs/` subfolder of any SilverStripe module, framework or CMS folder.
CMS folder.
<div class="warning" markdown='1'> <div class="warning" markdown='1'>
If you submit a new feature or an API change, we strongly recommend that your patch includes updates to the necessary If you submit a new feature or an API change, we strongly recommend that your patch includes updates to the necessary documentation. This helps prevent our documentation from getting out of date.
documentation. This helps prevent our documentation from getting out of date.
</div> </div>
## Repositories ## Repositories
@ -52,44 +43,38 @@ documentation. This helps prevent our documentation from getting out of date.
## Source control ## Source control
In order to balance editorial control with effective collaboration, we keep documentation alongside the module source In order to balance editorial control with effective collaboration, we keep documentation alongside the module source code, e.g. in `framework/docs/`, or as code comments within PHP code. Contributing documentation is the same process as providing any other patch (see [Contributing code](code)).
code, e.g. in `framework/docs/`, or as code comments within PHP code. Contributing documentation is the same process as
providing any other patch (see [Contributing code](code)).
## What to write ## What to write
See [what to write (jacobian.org)](http://jacobian.org/writing/great-documentation/what-to-write/) for an excellent See [what to write (jacobian.org)](http://jacobian.org/writing/great-documentation/what-to-write/) for an excellent introduction to the different types of documentation. Also see [producing OSS: "documentation"](http://producingoss.com/en/getting-started.html#documentation) for good rules of thumb
introduction to the different types of documentation, and
[producing OSS: "documentation"](http://producingoss.com/en/getting-started.html#documentation) for good rules of thumb
for documenting open source software. for documenting open source software.
## Structure ## Structure
* Keep documentation lines to 120 characters. * Keep documentation lines to 120 characters.
* Don't duplicate: Search for existing places to put your documentation. Do you really require a new page, or just a new paragraph * Don't duplicate: Search for existing places to put your documentation. Do you really require a new page, or just a new paragraph of text somewhere?
of text somewhere?
* Use PHPDoc in source code: Leave low level technical documentation to code comments within PHP, in [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) format. * Use PHPDoc in source code: Leave low level technical documentation to code comments within PHP, in [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) format.
* API and developer guides complement each other: Both forms of documenting source code (API and Developer Guides) are valuable resources. * API and developer guides are two forms of source code documentation that complement each other.
* Provide context: Give API documentation the "bigger picture" by referring to developer guides inside your PHPDoc. * API documentation should provide context, ie, the "bigger picture", by referring to developer guides inside your PHPDoc.
* Make your documentation findable: Documentation lives by interlinking content, so please make sure your contribution doesn't become an * Make your documentation easy to find: Documentation lives by interlinking content so please make sure your contribution doesn't become an inaccessible island. At the very least, put a link to your should on the index page in the same folder. A link to your page can also appear
inaccessible island. Your page should at least be linked on the index page in the same folder. It can also appear
as "related content" on other resource (e.g. `/tutorials/site_search` might link to `/developer_guides/forms/introduction`). as "related content" on other resource (e.g. `/tutorials/site_search` might link to `/developer_guides/forms/introduction`).
## Writing style ## Writing style
* Write in second plural form: Use "we" instead of "I". It gives the text an instructive and collaborative style. * Write in second person plural form: Use "we" instead of "I". It gives the text an instructive and collaborative style.
* It's okay to address the reader: For example "First you'll install a webserver" is good style. * It's okay to address the reader: For example "First you'll install a webserver" is good style.
* Write in an active and direct voice. * Write in an active and direct voice.
* Mark up correctly: Use preformatted text, emphasis and bold to make technical writing more "scannable". * Mark up correctly: Use preformatted text. Emphasis and bold make technical writing more easily "scannable".
* Avoid FAQs: FAQs are not a replacement of a coherent, well explained documentation. If you've done a good job * Avoid FAQs: FAQs are not a replacement for coherent, well explained documentation. If you've done a good job
documenting, there shouldn't be any "frequently asked questions" left. documenting, there shouldn't be any "frequently asked questions" left.
* "SilverStripe" should always appear without a space, use two capital S. * "SilverStripe" should always appear without a space with both "S"s capitalised.
* Use simple language and words. Avoid uncommon jargon and overly long words. * Use simple language and words. Avoid uncommon jargon and overly long words.
* Use UK English and not US English. SilverStripe is proudly a New Zealand open source project we use the UK spelling and forms of English. The most common of these differences are -ize vs -ise, or -or vs our (eg color vs colour). * Use UK English and not US English. SilverStripe is proudly a New Zealand open source project we use the UK spelling and forms of English. The most common of these differences are -ize vs -ise, or -or vs our (eg color vs colour).
* We use sentence case for titles so only capitalise the first letter of the first word of a title. Only exceptions to this are when using branded (e.g. SilverStripe), acronyms (e.g. PHP) and class names (e.g. ModelAdmin). * We use sentence case for titles so only capitalise the first letter of the first word of a title. The only exceptions to this are when using brand names (e.g. SilverStripe), acronyms (e.g. PHP) and class names (e.g. ModelAdmin).
* Use gender neutral language throughout the document, unless referencing a specific person. Use them, they, their, instead of he and she, his or her. * Use gender neutral language throughout the document, unless referencing a specific person. Use them, they, their, instead of he and she, his or her.
* URLs: is the end of your sentence is a URL, you don't need to use a full stop. * URLs: if the end of your sentence is a URL then you don't need to use a full stop.
* Bullet points: Sentence case your bullet points, if it is a full sentence then end with a full stop. If it is a short point or list full stops are not required. * Bullet points: Sentence case your bullet points. If a bullet point is a full sentence then end with a full stop. If it is a short point or a list, full stops are not required.
## Highlighted blocks ## Highlighted blocks
@ -97,8 +82,7 @@ There are several built-in block styles for highlighting a paragraph of text. Pl
sparingly. sparingly.
<div class="hint" markdown='1'> <div class="hint" markdown='1'>
"Tip box": Adds, deepens or accents information in the main text. Can be used for background knowledge, or "see also" "Tip box": A tip box is great for adding, deepening or accenting information in the main text. They can be used for background knowledge, or to provide links to further information (ie, a "see also" link).
links.
</div> </div>
Code: Code:
@ -108,8 +92,7 @@ Code:
</div> </div>
<div class="notice" markdown='1'> <div class="notice" markdown='1'>
"Notification box": Technical notifications relating to the main text. For example, notifying users about a deprecated "Notification box": A notification box is good for technical notifications relating to the main text. For example, notifying users about a deprecated feature.
feature.
</div> </div>
Code: Code:
@ -119,8 +102,7 @@ Code:
</div> </div>
<div class="warning" markdown='1'> <div class="warning" markdown='1'>
"Warning box": Highlight a severe bug or technical issue requiring a users attention. For example, a code block with "Warning box": A warning box is useful for highlighting a severe bug or a technical issue requiring a user's attention. For example, suppose a rare edge case sometimes leads to a variable being overwritten incorrectly. A warning box can be used to alert the user to this case so they can write their own code to handle it.
destructive functionality might not have its URL actions secured to keep the code shorter.
</div> </div>
Code: Code:
@ -129,14 +111,12 @@ Code:
... ...
</div> </div>
See [markdown extra documentation](http://michelf.com/projects/php-markdown/extra/#html) for more restriction See [markdown extra documentation](http://michelf.com/projects/php-markdown/extra/#html) for more restrictions
on placing HTML blocks inside Markdown. on placing HTML blocks inside Markdown.
## Translating documentation ## Translating documentation
Documentation is kept alongside the source code, typically in a module subdirectory like `framework/docs/en/`. Each Documentation is kept alongside the source code, typically in a module subdirectory like `framework/docs/en/`. Each language has its own subfolder, which can duplicate parts of or the entire body of documentation. German documentation would, for example, live in `framework/docs/de/`. The
language has its own subfolder, which can duplicate parts or the whole body of documentation. German documentation
would for example live in `framework/docs/de/`. The
[docsviewer](https://github.com/silverstripe/silverstripe-docsviewer) module that drives [docsviewer](https://github.com/silverstripe/silverstripe-docsviewer) module that drives
[doc.silverstripe.org](http://doc.silverstripe.org) automatically resolves these subfolders into a language dropdown. [doc.silverstripe.org](http://doc.silverstripe.org) automatically resolves these subfolders into a language dropdown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -368,7 +368,7 @@ class File extends DataObject {
// Preview // Preview
if($this instanceof Image) { if($this instanceof Image) {
$formattedImage = $this->getFormattedImage( $formattedImage = $this->getFormattedImage(
'SetWidth', 'ScaleWidth',
Config::inst()->get('Image', 'asset_preview_width') Config::inst()->get('Image', 'asset_preview_width')
); );
$thumbnail = $formattedImage ? $formattedImage->URL : ''; $thumbnail = $formattedImage ? $formattedImage->URL : '';
@ -917,7 +917,7 @@ class File extends DataObject {
return $labels; return $labels;
} }
public function validate() { protected function validate() {
if($this->config()->apply_restrictions_to_admin || !Permission::check('ADMIN')) { if($this->config()->apply_restrictions_to_admin || !Permission::check('ADMIN')) {
// Extension validation // Extension validation
// TODO Merge this with Upload_Validator // TODO Merge this with Upload_Validator

View File

@ -326,7 +326,7 @@ class Folder extends File {
} }
} }
public function validate() { protected function validate() {
return new ValidationResult(true); return new ValidationResult(true);
} }

View File

@ -285,9 +285,11 @@ class Upload extends Controller {
/** /**
* Clear out all errors (mostly set by {loadUploaded()}) * Clear out all errors (mostly set by {loadUploaded()})
* including the validator's errors
*/ */
public function clearErrors() { public function clearErrors() {
$this->errors = array(); $this->errors = array();
$this->validator->clearErrors();
} }
/** /**
@ -367,6 +369,13 @@ class Upload_Validator {
return $this->errors; return $this->errors;
} }
/**
* Clear out all errors
*/
public function clearErrors() {
$this->errors = array();
}
/** /**
* Set information about temporary file produced by PHP. * Set information about temporary file produced by PHP.
* @param array $tmpFile * @param array $tmpFile

View File

@ -1,54 +1,59 @@
<?php <?php
/** /**
* Text input field with validation for correct email format * Text input field with validation for correct email format according to RFC 2822.
* according to RFC 2822.
* *
* @package forms * @package forms
* @subpackage fields-formattedinput * @subpackage fields-formattedinput
*/ */
class EmailField extends TextField { class EmailField extends TextField {
/**
* {@inheritdoc}
*/
public function Type() { public function Type() {
return 'email text'; return 'email text';
} }
/**
* {@inheritdoc}
*/
public function getAttributes() { public function getAttributes() {
return array_merge( return array_merge(
parent::getAttributes(), parent::getAttributes(),
array( array(
'type' => 'email' 'type' => 'email',
) )
); );
} }
/** /**
* Validates for RFC 2822 compliant email adresses. * Validates for RFC 2822 compliant email addresses.
* *
* @see http://www.regular-expressions.info/email.html * @see http://www.regular-expressions.info/email.html
* @see http://www.ietf.org/rfc/rfc2822.txt * @see http://www.ietf.org/rfc/rfc2822.txt
* *
* @param Validator $validator * @param Validator $validator
* @return String *
* @return string
*/ */
public function validate($validator) { public function validate($validator) {
$this->value = trim($this->value); $this->value = trim($this->value);
$pcrePattern = '^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*' $pattern = '^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$';
. '@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$';
// PHP uses forward slash (/) to delimit start/end of pattern, so it must be escaped // Escape delimiter characters.
$pregSafePattern = str_replace('/', '\\/', $pcrePattern); $safePattern = str_replace('/', '\\/', $pattern);
if($this->value && !preg_match('/' . $pregSafePattern . '/i', $this->value)){ if($this->value && !preg_match('/' . $safePattern . '/i', $this->value)) {
$validator->validationError( $validator->validationError(
$this->name, $this->name,
_t('EmailField.VALIDATION', "Please enter an email address"), _t('EmailField.VALIDATION', 'Please enter an email address'),
"validation" 'validation'
); );
return false;
} else{
return true;
}
}
return false;
}
return true;
}
} }

View File

@ -35,7 +35,7 @@
* "admin/EditForm". This URL will render the form without its surrounding * "admin/EditForm". This URL will render the form without its surrounding
* template when called through GET instead of POST. * template when called through GET instead of POST.
* *
* By appending to this URL, you can render invidual form elements * By appending to this URL, you can render individual form elements
* through the {@link FormField->FieldHolder()} method. * through the {@link FormField->FieldHolder()} method.
* For example, the "URLSegment" field in a standard CMS form would be * For example, the "URLSegment" field in a standard CMS form would be
* accessible through "admin/EditForm/field/URLSegment/FieldHolder". * accessible through "admin/EditForm/field/URLSegment/FieldHolder".
@ -54,19 +54,34 @@ class Form extends RequestHandler {
*/ */
public $IncludeFormTag = true; public $IncludeFormTag = true;
/**
* @var FieldList|null
*/
protected $fields; protected $fields;
/**
* @var FieldList|null
*/
protected $actions; protected $actions;
/** /**
* @var Controller * @var Controller|null
*/ */
protected $controller; protected $controller;
/**
* @var string|null
*/
protected $name; protected $name;
/**
* @var Validator|null
*/
protected $validator; protected $validator;
/**
* @var string
*/
protected $formMethod = "POST"; protected $formMethod = "POST";
/** /**
@ -74,16 +89,21 @@ class Form extends RequestHandler {
*/ */
protected $strictFormMethodCheck = false; protected $strictFormMethodCheck = false;
/**
* @var string|null
*/
protected static $current_action; protected static $current_action;
/** /**
* @var Dataobject $record Populated by {@link loadDataFrom()}. * @var DataObject|null $record Populated by {@link loadDataFrom()}.
*/ */
protected $record; protected $record;
/** /**
* Keeps track of whether this form has a default action or not. * Keeps track of whether this form has a default action or not.
* Set to false by $this->disableDefaultAction(); * Set to false by $this->disableDefaultAction();
*
* @var boolean
*/ */
protected $hasDefaultAction = true; protected $hasDefaultAction = true;
@ -92,7 +112,7 @@ class Form extends RequestHandler {
* Useful to open a new window upon * Useful to open a new window upon
* form submission. * form submission.
* *
* @var string * @var string|null
*/ */
protected $target; protected $target;
@ -101,7 +121,7 @@ class Form extends RequestHandler {
* <legend> element before the <fieldset> * <legend> element before the <fieldset>
* in Form.ss template. * in Form.ss template.
* *
* @var string * @var string|null
*/ */
protected $legend; protected $legend;
@ -111,14 +131,23 @@ class Form extends RequestHandler {
* another template for customisation. * another template for customisation.
* *
* @see Form->setTemplate() * @see Form->setTemplate()
* @var string * @var string|null
*/ */
protected $template; protected $template;
/**
* @var callable|null
*/
protected $buttonClickedFunc; protected $buttonClickedFunc;
/**
* @var string|null
*/
protected $message; protected $message;
/**
* @var string|null
*/
protected $messageType; protected $messageType;
/** /**
@ -129,10 +158,13 @@ class Form extends RequestHandler {
*/ */
protected $redirectToFormOnValidationError = false; protected $redirectToFormOnValidationError = false;
/**
* @var bool
*/
protected $security = true; protected $security = true;
/** /**
* @var SecurityToken * @var SecurityToken|null
*/ */
protected $securityToken = null; protected $securityToken = null;
@ -148,7 +180,7 @@ class Form extends RequestHandler {
private static $default_classes = array(); private static $default_classes = array();
/** /**
* @var string * @var string|null
*/ */
protected $encType; protected $encType;
@ -184,11 +216,16 @@ class Form extends RequestHandler {
*/ */
private $formActionPath = false; private $formActionPath = false;
/**
* @var bool
*/
protected $securityTokenAdded = false;
/** /**
* Create a new form, with the given fields an action buttons. * Create a new form, with the given fields an action buttons.
* *
* @param Controller $controller The parent controller, necessary to create the appropriate form action tag. * @param Controller $controller The parent controller, necessary to create the appropriate form action tag.
* @param String $name The method on the controller that will return this form object. * @param string $name The method on the controller that will return this form object.
* @param FieldList $fields All of the fields in the form - a {@link FieldList} of {@link FormField} objects. * @param FieldList $fields All of the fields in the form - a {@link FieldList} of {@link FormField} objects.
* @param FieldList $actions All of the action buttons in the form - a {@link FieldLis} of * @param FieldList $actions All of the action buttons in the form - a {@link FieldLis} of
* {@link FormAction} objects * {@link FormAction} objects
@ -239,6 +276,9 @@ class Form extends RequestHandler {
$this->setupDefaultClasses(); $this->setupDefaultClasses();
} }
/**
* @var array
*/
private static $url_handlers = array( private static $url_handlers = array(
'field/$FieldName!' => 'handleField', 'field/$FieldName!' => 'handleField',
'POST ' => 'httpSubmission', 'POST ' => 'httpSubmission',
@ -249,6 +289,8 @@ class Form extends RequestHandler {
/** /**
* Set up current form errors in session to * Set up current form errors in session to
* the current form if appropriate. * the current form if appropriate.
*
* @return $this
*/ */
public function setupFormErrors() { public function setupFormErrors() {
$errorInfo = Session::get("FormInfo.{$this->FormName()}"); $errorInfo = Session::get("FormInfo.{$this->FormName()}");
@ -294,6 +336,9 @@ class Form extends RequestHandler {
* Populates the form with {@link loadDataFrom()}, calls {@link validate()}, * Populates the form with {@link loadDataFrom()}, calls {@link validate()},
* and only triggers the requested form action/method * and only triggers the requested form action/method
* if the form is valid. * if the form is valid.
*
* @param SS_HTTPRequest $request
* @throws SS_HTTPResponse_Exception
*/ */
public function httpSubmission($request) { public function httpSubmission($request) {
// Strict method check // Strict method check
@ -356,7 +401,7 @@ class Form extends RequestHandler {
} }
} }
// If the action wasnt' set, choose the default on the form. // If the action wasn't set, choose the default on the form.
if(!isset($funcName) && $defaultAction = $this->defaultAction()){ if(!isset($funcName) && $defaultAction = $this->defaultAction()){
$funcName = $defaultAction->actionName(); $funcName = $defaultAction->actionName();
} }
@ -427,6 +472,10 @@ class Form extends RequestHandler {
return $this->httpError(404); return $this->httpError(404);
} }
/**
* @param string $action
* @return bool
*/
public function checkAccessAction($action) { public function checkAccessAction($action) {
return ( return (
parent::checkAccessAction($action) parent::checkAccessAction($action)
@ -444,7 +493,7 @@ class Form extends RequestHandler {
* Returns the appropriate response up the controller chain * Returns the appropriate response up the controller chain
* if {@link validate()} fails (which is checked prior to executing any form actions). * if {@link validate()} fails (which is checked prior to executing any form actions).
* By default, returns different views for ajax/non-ajax request, and * By default, returns different views for ajax/non-ajax request, and
* handles 'appliction/json' requests with a JSON object containing the error messages. * handles 'application/json' requests with a JSON object containing the error messages.
* Behaviour can be influenced by setting {@link $redirectToFormOnValidationError}. * Behaviour can be influenced by setting {@link $redirectToFormOnValidationError}.
* *
* @return SS_HTTPResponse|string * @return SS_HTTPResponse|string
@ -485,6 +534,8 @@ class Form extends RequestHandler {
/** /**
* Fields can have action to, let's check if anyone of the responds to $funcname them * Fields can have action to, let's check if anyone of the responds to $funcname them
* *
* @param SS_List|array $fields
* @param callable $funcName
* @return FormField * @return FormField
*/ */
protected function checkFieldsForAction($fields, $funcName) { protected function checkFieldsForAction($fields, $funcName) {
@ -533,7 +584,8 @@ class Form extends RequestHandler {
* form on the page upon validation errors in the form or if * form on the page upon validation errors in the form or if
* they just need to redirect back to the page * they just need to redirect back to the page
* *
* @param bool Redirect to the form * @param bool $bool Redirect to form on error?
* @return $this
*/ */
public function setRedirectToFormOnValidationError($bool) { public function setRedirectToFormOnValidationError($bool) {
$this->redirectToFormOnValidationError = $bool; $this->redirectToFormOnValidationError = $bool;
@ -553,6 +605,10 @@ class Form extends RequestHandler {
/** /**
* Add a plain text error message to a field on this form. It will be saved into the session * Add a plain text error message to a field on this form. It will be saved into the session
* and used the next time this form is displayed. * and used the next time this form is displayed.
* @param string $fieldName
* @param string $message
* @param string $messageType
* @param bool $escapeHtml
*/ */
public function addErrorMessage($fieldName, $message, $messageType, $escapeHtml = true) { public function addErrorMessage($fieldName, $message, $messageType, $escapeHtml = true) {
Session::add_to_array("FormInfo.{$this->FormName()}.errors", array( Session::add_to_array("FormInfo.{$this->FormName()}.errors", array(
@ -562,6 +618,9 @@ class Form extends RequestHandler {
)); ));
} }
/**
* @param FormTransformation $trans
*/
public function transform(FormTransformation $trans) { public function transform(FormTransformation $trans) {
$newFields = new FieldList(); $newFields = new FieldList();
foreach($this->fields as $field) { foreach($this->fields as $field) {
@ -591,8 +650,10 @@ class Form extends RequestHandler {
/** /**
* Set the {@link Validator} on this form. * Set the {@link Validator} on this form.
* @param Validator $validator
* @return $this
*/ */
public function setValidator( Validator $validator ) { public function setValidator(Validator $validator ) {
if($validator) { if($validator) {
$this->validator = $validator; $this->validator = $validator;
$this->validator->setForm($this); $this->validator->setForm($this);
@ -629,6 +690,7 @@ class Form extends RequestHandler {
/** /**
* Convert this form to another format. * Convert this form to another format.
* @param FormTransformation $format
*/ */
public function transformTo(FormTransformation $format) { public function transformTo(FormTransformation $format) {
$newFields = new FieldList(); $newFields = new FieldList();
@ -702,6 +764,7 @@ class Form extends RequestHandler {
* Setter for the form fields. * Setter for the form fields.
* *
* @param FieldList $fields * @param FieldList $fields
* @return $this
*/ */
public function setFields($fields) { public function setFields($fields) {
$this->fields = $fields; $this->fields = $fields;
@ -721,6 +784,7 @@ class Form extends RequestHandler {
* Setter for the form actions. * Setter for the form actions.
* *
* @param FieldList $actions * @param FieldList $actions
* @return $this
*/ */
public function setActions($actions) { public function setActions($actions) {
$this->actions = $actions; $this->actions = $actions;
@ -736,8 +800,9 @@ class Form extends RequestHandler {
} }
/** /**
* @param String * @param string $name
* @param String * @param string $value
* @return $this
*/ */
public function setAttribute($name, $value) { public function setAttribute($name, $value) {
$this->attributes[$name] = $value; $this->attributes[$name] = $value;
@ -745,12 +810,15 @@ class Form extends RequestHandler {
} }
/** /**
* @return String * @return string $name
*/ */
public function getAttribute($name) { public function getAttribute($name) {
if(isset($this->attributes[$name])) return $this->attributes[$name]; if(isset($this->attributes[$name])) return $this->attributes[$name];
} }
/**
* @return array
*/
public function getAttributes() { public function getAttributes() {
$attrs = array( $attrs = array(
'id' => $this->FormName(), 'id' => $this->FormName(),
@ -774,9 +842,10 @@ class Form extends RequestHandler {
/** /**
* Return the attributes of the form tag - used by the templates. * Return the attributes of the form tag - used by the templates.
* *
* @param Array Custom attributes to process. Falls back to {@link getAttributes()}. * @param array Custom attributes to process. Falls back to {@link getAttributes()}.
* If at least one argument is passed as a string, all arguments act as excludes by name. * If at least one argument is passed as a string, all arguments act as excludes by name.
* @return String HTML attributes, ready for insertion into an HTML tag *
* @return string HTML attributes, ready for insertion into an HTML tag
*/ */
public function getAttributesHTML($attrs = null) { public function getAttributesHTML($attrs = null) {
$exclude = (is_string($attrs)) ? func_get_args() : null; $exclude = (is_string($attrs)) ? func_get_args() : null;
@ -824,10 +893,11 @@ class Form extends RequestHandler {
} }
/** /**
* Set the {@link FormTemplateHelper} * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing
* * another frame
*
* @param string|FormTemplateHelper * @param string|FormTemplateHelper
*/ */
public function setTemplateHelper($helper) { public function setTemplateHelper($helper) {
$this->templateHelper = $helper; $this->templateHelper = $helper;
} }
@ -855,8 +925,7 @@ class Form extends RequestHandler {
* contents in a new window or refreshing another frame. * contents in a new window or refreshing another frame.
* *
* @param target $target The value of the target * @param target $target The value of the target
* * @return $this
* @return FormField
*/ */
public function setTarget($target) { public function setTarget($target) {
$this->target = $target; $this->target = $target;
@ -867,6 +936,8 @@ class Form extends RequestHandler {
/** /**
* Set the legend value to be inserted into * Set the legend value to be inserted into
* the <legend> element in the Form.ss template. * the <legend> element in the Form.ss template.
* @param string $legend
* @return $this
*/ */
public function setLegend($legend) { public function setLegend($legend) {
$this->legend = $legend; $this->legend = $legend;
@ -878,6 +949,7 @@ class Form extends RequestHandler {
* to render with. The default is "Form". * to render with. The default is "Form".
* *
* @param string $template The name of the template (without the .ss extension) * @param string $template The name of the template (without the .ss extension)
* @return $this
*/ */
public function setTemplate($template) { public function setTemplate($template) {
$this->template = $template; $this->template = $template;
@ -921,7 +993,8 @@ class Form extends RequestHandler {
* Sets the form encoding type. The most common encoding types are defined * Sets the form encoding type. The most common encoding types are defined
* in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}. * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
* *
* @param string $enctype * @param string $encType
* @return $this
*/ */
public function setEncType($encType) { public function setEncType($encType) {
$this->encType = $encType; $this->encType = $encType;
@ -961,8 +1034,9 @@ class Form extends RequestHandler {
/** /**
* Set the form method: GET, POST, PUT, DELETE. * Set the form method: GET, POST, PUT, DELETE.
* *
* @param $method string * @param string $method
* @param $strict If non-null, pass value to {@link setStrictFormMethodCheck()}. * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
* @return $this
*/ */
public function setFormMethod($method, $strict = null) { public function setFormMethod($method, $strict = null) {
$this->formMethod = strtoupper($method); $this->formMethod = strtoupper($method);
@ -981,6 +1055,7 @@ class Form extends RequestHandler {
* form. * form.
* *
* @param $bool boolean * @param $bool boolean
* @return $this
*/ */
public function setStrictFormMethodCheck($bool) { public function setStrictFormMethodCheck($bool) {
$this->strictFormMethodCheck = (bool)$bool; $this->strictFormMethodCheck = (bool)$bool;
@ -1017,9 +1092,8 @@ class Form extends RequestHandler {
* recommended only for situations where you have two relatively distinct * recommended only for situations where you have two relatively distinct
* parts of the system trying to communicate via a form post. * parts of the system trying to communicate via a form post.
* *
* @param string * @param string $path
* * @return $this
* @return Form
*/ */
public function setFormAction($path) { public function setFormAction($path) {
$this->formActionPath = $path; $this->formActionPath = $path;
@ -1040,8 +1114,7 @@ class Form extends RequestHandler {
* Set the HTML ID attribute of the form. * Set the HTML ID attribute of the form.
* *
* @param string $id * @param string $id
* * @return $this
* @return FormField
*/ */
public function setHTMLID($id) { public function setHTMLID($id) {
$this->htmlID = $id; $this->htmlID = $id;
@ -1123,8 +1196,9 @@ class Form extends RequestHandler {
} }
/** /**
* The next functions store and modify the forms message attributes. * The next functions store and modify the forms
* messages are stored in session under $_SESSION[formname][message]; * message attributes. messages are stored in session under
* $_SESSION[formname][message];
* *
* @return string * @return string
*/ */
@ -1159,12 +1233,13 @@ class Form extends RequestHandler {
/** /**
* Set a status message for the form. * Set a status message for the form.
* *
* @param string $message the text of the message * @param string $message the text of the message
* @param string $type Should be set to good, bad, or warning. * @param string $type Should be set to good, bad, or warning.
* @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML. * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
* In that case, you might want to use {@link Convert::raw2xml()} to escape any * In that case, you might want to use {@link Convert::raw2xml()} to escape any
* user supplied data in the message. * user supplied data in the message.
* @return $this
*/ */
public function setMessage($message, $type, $escapeHtml = true) { public function setMessage($message, $type, $escapeHtml = true) {
$this->message = ($escapeHtml) ? Convert::raw2xml($message) : $message; $this->message = ($escapeHtml) ? Convert::raw2xml($message) : $message;
@ -1174,7 +1249,7 @@ class Form extends RequestHandler {
/** /**
* Set a message to the session, for display next time this form is shown. * Set a message to the session, for display next time this form is shown.
* *
* @param string $message the text of the message * @param string $message the text of the message
* @param string $type Should be set to good, bad, or warning. * @param string $type Should be set to good, bad, or warning.
* @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML. * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
@ -1183,7 +1258,7 @@ class Form extends RequestHandler {
*/ */
public function sessionMessage($message, $type, $escapeHtml = true) { public function sessionMessage($message, $type, $escapeHtml = true) {
Session::set( Session::set(
"FormInfo.{$this->FormName()}.formError.message", "FormInfo.{$this->FormName()}.formError.message",
$escapeHtml ? Convert::raw2xml($message) : $message $escapeHtml ? Convert::raw2xml($message) : $message
); );
Session::set("FormInfo.{$this->FormName()}.formError.type", $type); Session::set("FormInfo.{$this->FormName()}.formError.type", $type);
@ -1191,7 +1266,7 @@ class Form extends RequestHandler {
public static function messageForForm($formName, $message, $type, $escapeHtml = true) { public static function messageForForm($formName, $message, $type, $escapeHtml = true) {
Session::set( Session::set(
"FormInfo.{$formName}.formError.message", "FormInfo.{$formName}.formError.message",
$escapeHtml ? Convert::raw2xml($message) : $message $escapeHtml ? Convert::raw2xml($message) : $message
); );
Session::set("FormInfo.{$formName}.formError.type", $type); Session::set("FormInfo.{$formName}.formError.type", $type);
@ -1282,8 +1357,8 @@ class Form extends RequestHandler {
* its value will not be saved to the field, retaining * its value will not be saved to the field, retaining
* potential existing values. * potential existing values.
* *
* Passed data should not be escaped, and is saved to the FormField * Passed data should not be escaped, and is saved to the FormField instances unescaped.
* instances unescaped. * Escaping happens automatically on saving the data through {@link saveInto()}.
* *
* Escaping happens automatically on saving the data through * Escaping happens automatically on saving the data through
* {@link saveInto()}. * {@link saveInto()}.
@ -1293,8 +1368,7 @@ class Form extends RequestHandler {
* *
* @param array|DataObject $data * @param array|DataObject $data
* @param int $mergeStrategy * @param int $mergeStrategy
* For every field, {@link $data} is interogated whether it contains a * For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
* relevant property/key, and
* what that property/key's value is. * what that property/key's value is.
* *
* By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s * By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
@ -1312,7 +1386,7 @@ class Form extends RequestHandler {
* For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing * For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
* CLEAR_MISSING * CLEAR_MISSING
* *
* @param $fieldList An optional list of fields to process. This can be useful when you have a * @param FieldList $fieldList An optional list of fields to process. This can be useful when you have a
* form that has some fields that save to one object, and some that save to another. * form that has some fields that save to one object, and some that save to another.
* @return Form * @return Form
*/ */
@ -1338,7 +1412,7 @@ class Form extends RequestHandler {
if($dataFields) foreach($dataFields as $field) { if($dataFields) foreach($dataFields as $field) {
$name = $field->getName(); $name = $field->getName();
// Skip fields that have been exlcuded // Skip fields that have been excluded
if($fieldList && !in_array($name, $fieldList)) continue; if($fieldList && !in_array($name, $fieldList)) continue;
// First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value // First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
@ -1398,8 +1472,8 @@ class Form extends RequestHandler {
* Save the contents of this form into the given data object. * Save the contents of this form into the given data object.
* It will make use of setCastedField() to do this. * It will make use of setCastedField() to do this.
* *
* @param $dataObject The object to save data into * @param DataObjectInterface $dataObject The object to save data into
* @param $fieldList An optional list of fields to process. This can be useful when you have a * @param FieldList $fieldList An optional list of fields to process. This can be useful when you have a
* form that has some fields that save to one object, and some that save to another. * form that has some fields that save to one object, and some that save to another.
*/ */
public function saveInto(DataObjectInterface $dataObject, $fieldList = null) { public function saveInto(DataObjectInterface $dataObject, $fieldList = null) {
@ -1425,11 +1499,11 @@ class Form extends RequestHandler {
/** /**
* Get the submitted data from this form through * Get the submitted data from this form through
* {@link FieldList->dataFields()}, which filters out any form-specific data * {@link FieldList->dataFields()}, which filters out
* like form-actions. * any form-specific data like form-actions.
* * Calls {@link FormField->dataValue()} on each field,
* Calls {@link FormField->dataValue()} on each field, which returns a value * which returns a value suitable for insertion into a DataObject
* suitable for insertion into a DataObject property. * property.
* *
* @return array * @return array
*/ */
@ -1451,11 +1525,8 @@ class Form extends RequestHandler {
/** /**
* Call the given method on the given field. * Call the given method on the given field.
* *
* This is used by Ajax-savvy form fields. By putting '&action=callfieldmethod' * @param array $data
* to the end of the form action, they can access server-side data. * @return mixed
*
* @param fieldName The name of the field. Can be overridden by $_REQUEST[fieldName]
* @param methodName The name of the field. Can be overridden by $_REQUEST[methodName]
*/ */
public function callfieldmethod($data) { public function callfieldmethod($data) {
$fieldName = $data['fieldName']; $fieldName = $data['fieldName'];
@ -1478,7 +1549,6 @@ class Form extends RequestHandler {
} else { } else {
user_error("Form::callfieldmethod() Field '$fieldName' not found", E_USER_ERROR); user_error("Form::callfieldmethod() Field '$fieldName' not found", E_USER_ERROR);
} }
} }
/** /**
@ -1547,14 +1617,10 @@ class Form extends RequestHandler {
} }
/** /**
* Render this form using the given template, and return the result as a * Render this form using the given template, and return the result as a string
* string. * You can pass either an SSViewer or a template name
* * @param string|array $template
* You can pass either an SSViewer or a template name. * @return HTMLText
*
* @param SSViewer|string $template
*
* @return HTML
*/ */
public function renderWithoutActionButton($template) { public function renderWithoutActionButton($template) {
$custom = $this->customise(array( $custom = $this->customise(array(
@ -1570,12 +1636,10 @@ class Form extends RequestHandler {
/** /**
* Sets the button that was clicked. This should only be called by the * Sets the button that was clicked. This should only be called by the Controller.
* {@link Controller}
* *
* @param string $funcName The name of the action method that will be called * @param callable $funcName The name of the action method that will be called.
* * @return $this
* @return Form
*/ */
public function setButtonClicked($funcName) { public function setButtonClicked($funcName) {
$this->buttonClickedFunc = $funcName; $this->buttonClickedFunc = $funcName;
@ -1713,9 +1777,8 @@ class Form extends RequestHandler {
* be added by delimiting a string with spaces. * be added by delimiting a string with spaces.
* *
* @param string $class A string containing a classname or several class * @param string $class A string containing a classname or several class
* names delimited by a single space. * names delimited by a single space.
* * @return $this
* @return Form
*/ */
public function addExtraClass($class) { public function addExtraClass($class) {
//split at white space //split at white space
@ -1732,6 +1795,7 @@ class Form extends RequestHandler {
* be passed through as a space delimited string * be passed through as a space delimited string
* *
* @param string $class * @param string $class
* @return $this
*/ */
public function removeExtraClass($class) { public function removeExtraClass($class) {
//split at white space //split at white space
@ -1763,8 +1827,11 @@ class Form extends RequestHandler {
/** /**
* Test a submission of this form. * Test a submission of this form.
* @param string $action
* @param array $data
* @return SS_HTTPResponse the response object that the handling controller produces. You can interrogate this in * @return SS_HTTPResponse the response object that the handling controller produces. You can interrogate this in
* your unit test. * your unit test.
* @throws SS_HTTPResponse_Exception
*/ */
public function testSubmission($action, $data) { public function testSubmission($action, $data) {
$data['action_' . $action] = true; $data['action_' . $action] = true;
@ -1774,6 +1841,9 @@ class Form extends RequestHandler {
/** /**
* Test an ajax submission of this form. * Test an ajax submission of this form.
*
* @param string $action
* @param array $data
* @return SS_HTTPResponse the response object that the handling controller produces. You can interrogate this in * @return SS_HTTPResponse the response object that the handling controller produces. You can interrogate this in
* your unit test. * your unit test.
*/ */
@ -1797,10 +1867,9 @@ class Form_FieldMap extends ViewableData {
} }
/** /**
* Ensure that all potential method calls get passed to __call(), therefore * Ensure that all potential method calls get passed to __call(), therefore to dataFieldByName
* to dataFieldByName. * @param string $method
* * @return bool
* @param string
*/ */
public function hasMethod($method) { public function hasMethod($method) {
return true; return true;

View File

@ -919,7 +919,7 @@ class FormField extends RequestHandler {
* Validation method each {@link FormField} subclass should implement, * Validation method each {@link FormField} subclass should implement,
* determining whether the field is valid or not based on the value. * determining whether the field is valid or not based on the value.
* *
* @param Validator * @param Validator $validator
* @return boolean * @return boolean
*/ */
public function validate($validator) { public function validate($validator) {

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Field that generates a heading tag. * Field that generates a heading tag.
* *
@ -10,50 +11,75 @@
class HeaderField extends DatalessField { class HeaderField extends DatalessField {
/** /**
* @var int $headingLevel The level of the <h1> to <h6> HTML tag. Default: 2 * The level of the <h1> to <h6> HTML tag.
*
* @var int
*/ */
protected $headingLevel = 2; protected $headingLevel = 2;
/**
* @param string $name
* @param null|string $title
* @param int $headingLevel
*/
public function __construct($name, $title = null, $headingLevel = 2) { public function __construct($name, $title = null, $headingLevel = 2) {
// legacy handling for old parameters: $title, $heading, ... // legacy handling:
// instead of new handling: $name, $title, $heading, ... // $title, $headingLevel...
$args = func_get_args(); $args = func_get_args();
if(!isset($args[1]) || is_numeric($args[1])) { if(!isset($args[1]) || is_numeric($args[1])) {
$title = (isset($args[0])) ? $args[0] : null; if(isset($args[0])) {
// Use "HeaderField(title)" as the default field name for a HeaderField; if it's just set to title then we $title = $args[0];
// risk causing accidental duplicate-field creation. }
// this means i18nized fields won't be easily accessible through fieldByName()
// Prefix name to avoid collisions.
$name = 'HeaderField' . $title; $name = 'HeaderField' . $title;
$headingLevel = (isset($args[1])) ? $args[1] : null;
$form = (isset($args[3])) ? $args[3] : null; if(isset($args[1])) {
$headingLevel = $args[1];
}
} }
if($headingLevel) $this->headingLevel = $headingLevel; $this->setHeadingLevel($headingLevel);
parent::__construct($name, $title); parent::__construct($name, $title);
} }
/**
* @return int
*/
public function getHeadingLevel() { public function getHeadingLevel() {
return $this->headingLevel; return $this->headingLevel;
} }
public function setHeadingLevel($level) { /**
$this->headingLevel = $level; * @param int $headingLevel
*
* @return $this
*/
public function setHeadingLevel($headingLevel) {
$this->headingLevel = $headingLevel;
return $this; return $this;
} }
/**
* {@inheritdoc}
*/
public function getAttributes() { public function getAttributes() {
return array_merge( return array_merge(
parent::getAttributes(),
array( array(
'id' => $this->ID(), 'id' => $this->ID(),
'class' => $this->extraClass() 'class' => $this->extraClass(),
), )
$this->attributes
); );
} }
/**
* @return null
*/
public function Type() { public function Type() {
return null; return null;
} }
} }

View File

@ -39,12 +39,20 @@ class HtmlEditorConfig {
self::$current = $identifier; self::$current = $identifier;
} }
/**
* Get the currently active configuration identifier
* @return String - the active configuration identifier
*/
public static function get_active_identifier() {
$identifier = self::$current ? self::$current : 'default';
return $identifier;
}
/** /**
* Get the currently active configuration object * Get the currently active configuration object
* @return HtmlEditorConfig - the active configuration object * @return HtmlEditorConfig - the active configuration object
*/ */
public static function get_active() { public static function get_active() {
$identifier = self::$current ? self::$current : 'default'; $identifier = self::get_active_identifier();
return self::get($identifier); return self::get($identifier);
} }
@ -291,38 +299,81 @@ class HtmlEditorConfig {
} }
/** /**
* Generate the javascript that will set tinyMCE's configuration to that of the current settings of this object * Generate the JavaScript that will set TinyMCE's configuration:
* @return string - the javascript * - Parse all configurations into JSON objects to be used in JavaScript
* - Includes TinyMCE and configurations using the {@link Requirements} system
*/ */
public function generateJS() { public static function require_js() {
$config = $this->settings; require_once 'tinymce/tiny_mce_gzip.php';
$useGzip = Config::inst()->get('HtmlEditorField', 'use_gzip');
// plugins $configs = array();
$externalPlugins = array();
$internalPlugins = array(); $internalPlugins = array();
$externalPluginsJS = ''; $languages = array();
foreach($this->plugins as $plugin => $path) {
if(!$path) { foreach (self::$configs as $configID => $config) {
$internalPlugins[] = $plugin; $settings = $config->settings;
} else { // parse plugins
$internalPlugins[] = '-' . $plugin; $configPlugins = array();
$externalPluginsJS .= sprintf( foreach($config->plugins as $plugin => $path) {
'tinymce.PluginManager.load("%s", "%s");' . "\n", if(!$path) {
$plugin, $configPlugins[] = $plugin;
$path $internalPlugins[] = $plugin;
); } else {
$configPlugins[] = '-' . $plugin;
if ( !array_key_exists($plugin, $externalPlugins) )
{
$externalPlugins[$plugin] = sprintf(
'tinymce.PluginManager.load("%s", "%s");',
$plugin,
$path
);
}
}
} }
}
$config['plugins'] = implode(',', $internalPlugins);
foreach ($this->buttons as $i=>$buttons) { // save config plugins settings
$config['theme_advanced_buttons'.$i] = implode(',', $buttons); $settings['plugins'] = implode(',', $configPlugins);
// buttons
foreach ($config->buttons as $i=>$buttons) {
$settings['theme_advanced_buttons'.$i] = implode(',', $buttons);
}
// languages
$languages[] = $config->getOption('language');
// save this config settings
$configs[$configID] = $settings;
} }
return " // tinyMCE JS requirement
if ( $useGzip )
{
$tag = TinyMCE_Compressor::renderTag(array(
'url' => THIRDPARTY_DIR . '/tinymce/tiny_mce_gzip.php',
'plugins' => implode(',', $internalPlugins),
'themes' => 'advanced',
'languages' => implode(",", array_filter($languages))
), true);
preg_match('/src="([^"]*)"/', $tag, $matches);
Requirements::javascript(html_entity_decode($matches[1]));
}
else{
Requirements::javascript(MCE_ROOT . 'tiny_mce_src.js');
}
// prepare external plugins js string
$externalPlugins = array_values($externalPlugins);
$externalPlugins = implode("\n ", $externalPlugins);
// tinyMCE config object and external plugings
$configsJS = "
if((typeof tinyMCE != 'undefined')) { if((typeof tinyMCE != 'undefined')) {
$externalPluginsJS $externalPlugins
var ssTinyMceConfig = " . Convert::raw2json($config) . "; var ssTinyMceConfig = " . Convert::raw2json($configs) . ";
} }";
"; Requirements::customScript($configsJS, 'htmlEditorConfig');
} }
} }

View File

@ -27,41 +27,31 @@ class HtmlEditorField extends TextareaField {
private static $sanitise_server_side = false; private static $sanitise_server_side = false;
protected $rows = 30; protected $rows = 30;
/** /**
* Includes the JavaScript neccesary for this field to work using the {@link Requirements} system. * @deprecated since version 3.2
*/ */
public static function include_js() { public static function include_js() {
require_once 'tinymce/tiny_mce_gzip.php'; Deprecation::notice('4.0', 'Use HtmlEditorConfig::require_js() instead');
HtmlEditorConfig::require_js();
$configObj = HtmlEditorConfig::get_active();
if(Config::inst()->get('HtmlEditorField', 'use_gzip')) {
$internalPlugins = array();
foreach($configObj->getPlugins() as $plugin => $path) if(!$path) $internalPlugins[] = $plugin;
$tag = TinyMCE_Compressor::renderTag(array(
'url' => THIRDPARTY_DIR . '/tinymce/tiny_mce_gzip.php',
'plugins' => implode(',', $internalPlugins),
'themes' => 'advanced',
'languages' => $configObj->getOption('language')
), true);
preg_match('/src="([^"]*)"/', $tag, $matches);
Requirements::javascript(html_entity_decode($matches[1]));
} else {
Requirements::javascript(MCE_ROOT . 'tiny_mce_src.js');
}
Requirements::customScript($configObj->generateJS(), 'htmlEditorConfig');
} }
protected $editorConfig = null;
/** /**
* Creates a new HTMLEditorField.
* @see TextareaField::__construct() * @see TextareaField::__construct()
*/ *
public function __construct($name, $title = null, $value = '') { * @param string $name The internal field name, passed to forms.
* @param string $title The human-readable field label.
* @param mixed $value The value of the field.
* @param string $config HTMLEditorConfig identifier to be used. Default to the active one.
*/
public function __construct($name, $title = null, $value = '', $config = null) {
parent::__construct($name, $title, $value); parent::__construct($name, $title, $value);
self::include_js(); $this->editorConfig = $config ? $config : HtmlEditorConfig::get_active_identifier();
} }
public function getAttributes() { public function getAttributes() {
@ -71,6 +61,7 @@ class HtmlEditorField extends TextareaField {
'tinymce' => 'true', 'tinymce' => 'true',
'style' => 'width: 97%; height: ' . ($this->rows * 16) . 'px', // prevents horizontal scrollbars 'style' => 'width: 97%; height: ' . ($this->rows * 16) . 'px', // prevents horizontal scrollbars
'value' => null, 'value' => null,
'data-config' => $this->editorConfig
) )
); );
} }
@ -185,6 +176,8 @@ class HtmlEditorField_Toolbar extends RequestHandler {
Requirements::javascript(THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/ssui.core.js'); Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/ssui.core.js');
HtmlEditorConfig::require_js();
Requirements::javascript(FRAMEWORK_DIR ."/javascript/HtmlEditorField.js"); Requirements::javascript(FRAMEWORK_DIR ."/javascript/HtmlEditorField.js");
Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css'); Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
@ -258,7 +251,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
$siteTree, $siteTree,
TextField::create('external', _t('HtmlEditorField.URL', 'URL'), 'http://'), TextField::create('external', _t('HtmlEditorField.URL', 'URL'), 'http://'),
EmailField::create('email', _t('HtmlEditorField.EMAIL', 'Email address')), EmailField::create('email', _t('HtmlEditorField.EMAIL', 'Email address')),
TreeDropdownField::create('file', _t('HtmlEditorField.FILE', 'File'), 'File', 'ID', 'Title', true), $fileField = UploadField::create('file', _t('HtmlEditorField.FILE', 'File')),
TextField::create('Anchor', _t('HtmlEditorField.ANCHORVALUE', 'Anchor')), TextField::create('Anchor', _t('HtmlEditorField.ANCHORVALUE', 'Anchor')),
TextField::create('Subject', _t('HtmlEditorField.SUBJECT', 'Email subject')), TextField::create('Subject', _t('HtmlEditorField.SUBJECT', 'Email subject')),
TextField::create('Description', _t('HtmlEditorField.LINKDESCR', 'Link description')), TextField::create('Description', _t('HtmlEditorField.LINKDESCR', 'Link description')),
@ -281,6 +274,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
$headerWrap->addExtraClass('CompositeField composite cms-content-header nolabel '); $headerWrap->addExtraClass('CompositeField composite cms-content-header nolabel ');
$contentComposite->addExtraClass('ss-insert-link content'); $contentComposite->addExtraClass('ss-insert-link content');
$fileField->setAllowedMaxFileNumber(1);
$form->unsetValidator(); $form->unsetValidator();
$form->loadDataFrom($this); $form->loadDataFrom($this);
@ -679,7 +673,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
*/ */
protected function getFieldsForImage($url, $file) { protected function getFieldsForImage($url, $file) {
if($file->File instanceof Image) { if($file->File instanceof Image) {
$formattedImage = $file->File->generateFormattedImage('SetWidth', $formattedImage = $file->File->generateFormattedImage('ScaleWidth',
Config::inst()->get('Image', 'asset_preview_width')); Config::inst()->get('Image', 'asset_preview_width'));
$thumbnailURL = Convert::raw2att($formattedImage ? $formattedImage->URL : $url); $thumbnailURL = Convert::raw2att($formattedImage ? $formattedImage->URL : $url);
} else { } else {

View File

@ -1,9 +1,8 @@
<?php <?php
/** /**
* This field lets you put an arbitrary piece of HTML into your forms. * This field lets you put an arbitrary piece of HTML into your forms.
* *
* <b>Usage</b>
*
* <code> * <code>
* new LiteralField ( * new LiteralField (
* $name = "literalfield", * $name = "literalfield",
@ -15,40 +14,59 @@
* @subpackage fields-dataless * @subpackage fields-dataless
*/ */
class LiteralField extends DatalessField { class LiteralField extends DatalessField {
/** /**
* @var string $content * @var string|FormField
*/ */
protected $content; protected $content;
/**
* @param string $name
* @param string|FormField $content
*/
public function __construct($name, $content) { public function __construct($name, $content) {
$this->content = $content; $this->setContent($content);
parent::__construct($name); parent::__construct($name);
} }
/**
* @param array $properties
*
* @return string
*/
public function FieldHolder($properties = array()) { public function FieldHolder($properties = array()) {
if(is_object($this->content)) { if($this->content instanceof ViewableData) {
$obj = $this->content; $context = $this->content;
if($properties)
$obj = $obj->customise($properties); if($properties) {
return $obj->forTemplate(); $context = $context->customise($properties);
} else { }
return $this->content;
return $context->forTemplate();
} }
return $this->content;
} }
/**
* @param array $properties
*
* @return string
*/
public function Field($properties = array()) { public function Field($properties = array()) {
return $this->FieldHolder($properties); return $this->FieldHolder($properties);
} }
/** /**
* Sets the content of this field to a new value * Sets the content of this field to a new value.
* *
* @param string $content * @param string|FormField $content
*
* @return $this
*/ */
public function setContent($content) { public function setContent($content) {
$this->content = $content; $this->content = $content;
return $this; return $this;
} }
@ -61,16 +79,25 @@ class LiteralField extends DatalessField {
/** /**
* Synonym of {@link setContent()} so that LiteralField is more compatible with other field types. * Synonym of {@link setContent()} so that LiteralField is more compatible with other field types.
*
* @param string|FormField $content
*
* @return $this
*/ */
public function setValue($value) { public function setValue($content) {
$this->setContent($value); $this->setContent($content);
return $this; return $this;
} }
/**
* @return static
*/
public function performReadonlyTransformation() { public function performReadonlyTransformation() {
$clone = clone $this; $clone = clone $this;
$clone->setReadonly(true); $clone->setReadonly(true);
return $clone; return $clone;
} }
} }

View File

@ -1,11 +1,12 @@
<?php <?php
/** /**
* Password input field. * Password input field.
*
* @package forms * @package forms
* @subpackage fields-formattedinput * @subpackage fields-formattedinput
*/ */
class PasswordField extends TextField { class PasswordField extends TextField {
/** /**
* Controls the autocomplete attribute on the field. * Controls the autocomplete attribute on the field.
* *
@ -14,32 +15,63 @@ class PasswordField extends TextField {
*/ */
private static $autocomplete; private static $autocomplete;
public function getAttributes() { /**
$attributes = array_merge( * Returns an input field.
parent::getAttributes(), *
array('type' => 'password') * @param string $name
); * @param null|string $title
* @param string $value
$autocomplete = Config::inst()->get('PasswordField', 'autocomplete'); */
if (isset($autocomplete)) { public function __construct($name, $title = null, $value = '') {
$attributes['autocomplete'] = $autocomplete ? 'on' : 'off'; if(count(func_get_args()) > 3) {
Deprecation::notice(
'3.0', 'Use setMaxLength() instead of constructor arguments',
Deprecation::SCOPE_GLOBAL
);
} }
return $attributes; parent::__construct($name, $title, $value);
} }
/** /**
* Makes a pretty readonly field with some stars in it * {@inheritdoc}
*/
public function getAttributes() {
$attributes = array(
'type' => 'password',
);
$autocomplete = Config::inst()->get('PasswordField', 'autocomplete');
if($autocomplete) {
$attributes['autocomplete'] = 'on';
} else {
$attributes['autocomplete'] = 'off';
}
return array_merge(
parent::getAttributes(),
$attributes
);
}
/**
* Creates a read-only version of the field.
*
* @return FormField
*/ */
public function performReadonlyTransformation() { public function performReadonlyTransformation() {
$field = $this->castedCopy('ReadonlyField'); $field = $this->castedCopy('ReadonlyField');
$field->setValue('*****'); $field->setValue('*****');
return $field; return $field;
} }
/**
* {@inheritdoc}
*/
public function Type() { public function Type() {
return 'text password'; return 'text password';
} }
} }

View File

@ -1,12 +1,11 @@
<?php <?php
/** /**
* TextareaField creates a multi-line text field, * TextareaField creates a multi-line text field,
* allowing more data to be entered than a standard * allowing more data to be entered than a standard
* text field. It creates the <textarea> tag in the * text field. It creates the <textarea> tag in the
* form HTML. * form HTML.
* *
* <b>Usage</b>
*
* <code> * <code>
* new TextareaField( * new TextareaField(
* $name = "description", * $name = "description",
@ -19,17 +18,49 @@
* @subpackage fields-basic * @subpackage fields-basic
*/ */
class TextareaField extends FormField { class TextareaField extends FormField {
/** /**
* @var int Visible number of text lines. * Visible number of text lines.
*
* @var int
*/ */
protected $rows = 5; protected $rows = 5;
/** /**
* @var int Width of the text area (in average character widths) * Visible number of text columns.
*
* @var int
*/ */
protected $cols = 20; protected $cols = 20;
/**
* Set the number of rows in the textarea
*
* @param int $rows
*
* @return $this
*/
public function setRows($rows) {
$this->rows = $rows;
return $this;
}
/**
* Set the number of columns in the textarea
*
* @param int $cols
*
* @return $this
*/
public function setColumns($cols) {
$this->cols = $cols;
return $this;
}
/**
* {@inheritdoc}
*/
public function getAttributes() { public function getAttributes() {
return array_merge( return array_merge(
parent::getAttributes(), parent::getAttributes(),
@ -42,30 +73,23 @@ class TextareaField extends FormField {
); );
} }
/**
* {@inheritdoc}
*/
public function Type() { public function Type() {
return parent::Type() . ($this->readonly ? ' readonly' : ''); $parent = parent::Type();
if($this->readonly) {
return $parent . ' readonly';
}
return $parent;
} }
/** /**
* Set the number of rows in the textarea * @return string
*
* @param int
*/ */
public function setRows($rows) {
$this->rows = $rows;
return $this;
}
/**
* Set the number of columns in the textarea
*
* @return int
*/
public function setColumns($cols) {
$this->cols = $cols;
return $this;
}
public function Value() { public function Value() {
return htmlentities($this->value, ENT_COMPAT, 'UTF-8'); return htmlentities($this->value, ENT_COMPAT, 'UTF-8');
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Allows visibility of a group of fields to be toggled. * Allows visibility of a group of fields to be toggled.
* *
@ -6,17 +7,21 @@
* @subpackage fields-structural * @subpackage fields-structural
*/ */
class ToggleCompositeField extends CompositeField { class ToggleCompositeField extends CompositeField {
/** /**
* @var bool * @var bool
*/ */
protected $startClosed = true; protected $startClosed = true;
/** /**
* @var $int * @var int
*/ */
protected $headingLevel = 3; protected $headingLevel = 3;
/**
* @param string $name
* @param string $title
* @param array|FieldList $children
*/
public function __construct($name, $title, $children) { public function __construct($name, $title, $children) {
$this->name = $name; $this->name = $name;
$this->title = $title; $this->title = $title;
@ -24,30 +29,46 @@ class ToggleCompositeField extends CompositeField {
parent::__construct($children); parent::__construct($children);
} }
/**
* @param array $properties
*
* @return HTMLText
*/
public function FieldHolder($properties = array()) { public function FieldHolder($properties = array()) {
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js'); Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-ui/jquery-ui.js'); Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-ui/jquery-ui.js');
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-entwine/dist/jquery.entwine-dist.js'); Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-entwine/dist/jquery.entwine-dist.js');
Requirements::javascript(FRAMEWORK_DIR . '/javascript/ToggleCompositeField.js'); Requirements::javascript(FRAMEWORK_DIR . '/javascript/ToggleCompositeField.js');
Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery.ui.css'); Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery.ui.css');
$obj = $properties ? $this->customise($properties) : $this; $context = $this;
return $obj->renderWith($this->getTemplates());
if(count($properties)) {
$context = $this->customise($properties);
}
return $context->renderWith($this->getTemplates());
} }
/**
* {@inheritdoc}
*/
public function getAttributes() { public function getAttributes() {
$attributes = array(
'id' => $this->id(),
'class' => $this->extraClass(),
);
if($this->getStartClosed()) { if($this->getStartClosed()) {
$class = 'ss-toggle ss-toggle-start-closed'; $attributes['class'] .= ' ss-toggle ss-toggle-start-closed';
} else { } else {
$class = 'ss-toggle'; $attributes['class'] .= ' ss-toggle';
} }
return array_merge( return array_merge(
$this->attributes, $this->attributes,
array( $attributes
'id' => $this->id(),
'class' => $class . ' ' . $this->extraClass()
)
); );
} }
@ -59,13 +80,15 @@ class ToggleCompositeField extends CompositeField {
} }
/** /**
* Controls whether the field is open or closed by default. By default the * Controls whether the field is open or closed by default. By default the field is closed.
* field is closed.
* *
* @param bool $bool * @param bool $startClosed
*
* @return $this
*/ */
public function setStartClosed($bool) { public function setStartClosed($startClosed) {
$this->startClosed = (bool) $bool; $this->startClosed = (bool) $startClosed;
return $this; return $this;
} }
@ -77,12 +100,13 @@ class ToggleCompositeField extends CompositeField {
} }
/** /**
* @param int $level * @param int $headingLevel
*
* @return $this
*/ */
public function setHeadingLevel($level) { public function setHeadingLevel($headingLevel) {
$this->headingLevel = $level; $this->headingLevel = $headingLevel;
return $this; return $this;
} }
} }

View File

@ -217,15 +217,20 @@ class TreeDropdownField extends FormField {
Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery-ui.css'); Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery-ui.css');
Requirements::css(FRAMEWORK_DIR . '/css/TreeDropdownField.css'); Requirements::css(FRAMEWORK_DIR . '/css/TreeDropdownField.css');
if($this->showSearch) {
$emptyTitle = _t('DropdownField.CHOOSESEARCH', '(Choose or Search)', 'start value of a dropdown');
} else {
$emptyTitle = _t('DropdownField.CHOOSE', '(Choose)', 'start value of a dropdown');
}
$record = $this->Value() ? $this->objectForKey($this->Value()) : null; $record = $this->Value() ? $this->objectForKey($this->Value()) : null;
if($record instanceof ViewableData) { if($record instanceof ViewableData) {
$title = $record->obj($this->labelField)->forTemplate(); $title = $record->obj($this->labelField)->forTemplate();
} elseif($record) { } elseif($record) {
$title = Convert::raw2xml($record->{$this->labelField}); $title = Convert::raw2xml($record->{$this->labelField});
} else if($this->showSearch) { }
$title = _t('DropdownField.CHOOSESEARCH', '(Choose or Search)', 'start value of a dropdown'); else {
} else { $title = $emptyTitle;
$title = _t('DropdownField.CHOOSE', '(Choose)', 'start value of a dropdown');
} }
// TODO Implement for TreeMultiSelectField // TODO Implement for TreeMultiSelectField
@ -238,6 +243,7 @@ class TreeDropdownField extends FormField {
$properties, $properties,
array( array(
'Title' => $title, 'Title' => $title,
'EmptyTitle' => $emptyTitle,
'Metadata' => ($metadata) ? Convert::raw2json($metadata) : null, 'Metadata' => ($metadata) ? Convert::raw2json($metadata) : null,
) )
); );

View File

@ -873,8 +873,8 @@ class UploadField extends FileField {
return $file->getThumbnail($width, $height)->getURL(); return $file->getThumbnail($width, $height)->getURL();
} elseif ($file->hasMethod('getThumbnailURL')) { } elseif ($file->hasMethod('getThumbnailURL')) {
return $file->getThumbnailURL($width, $height); return $file->getThumbnailURL($width, $height);
} elseif ($file->hasMethod('SetRatioSize')) { } elseif ($file->hasMethod('Fit')) {
return $file->SetRatioSize($width, $height)->getURL(); return $file->Fit($width, $height)->getURL();
} else { } else {
return $file->Icon(); return $file->Icon();
} }
@ -1182,20 +1182,25 @@ class UploadField extends FileField {
$name = $this->getName(); $name = $this->getName();
$postVars = $request->postVar($name); $postVars = $request->postVar($name);
// Save the temporary file into a File object // Extract uploaded files from Form data
$uploadedFiles = $this->extractUploadedFileData($postVars); $uploadedFiles = $this->extractUploadedFileData($postVars);
$firstFile = reset($uploadedFiles); $return = array();
$file = $this->saveTemporaryFile($firstFile, $error);
if(empty($file)) { // Save the temporary files into a File objects
$return = array('error' => $error); // and save data/error on a per file basis
} else { foreach ($uploadedFiles as $tempFile) {
$return = $this->encodeFileAttributes($file); $file = $this->saveTemporaryFile($tempFile, $error);
if(empty($file)) {
array_push($return, array('error' => $error));
} else {
array_push($return, $this->encodeFileAttributes($file));
}
$this->upload->clearErrors();
} }
// Format response with json // Format response with json
$response = new SS_HTTPResponse(Convert::raw2json(array($return))); $response = new SS_HTTPResponse(Convert::raw2json($return));
$response->addHeader('Content-Type', 'text/plain'); $response->addHeader('Content-Type', 'text/plain');
if (!empty($return['error'])) $response->setStatusCode(403);
return $response; return $response;
} }

View File

@ -495,21 +495,39 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
return $backlink; return $backlink;
} }
/**
* Get the list of extra data from the $record as saved into it by
* {@see Form::saveInto()}
*
* Handles detection of falsey values explicitly saved into the
* DataObject by formfields
*
* @param DataObject $record
* @param SS_List $list
* @return array List of data to write to the relation
*/
protected function getExtraSavedData($record, $list) {
// Skip extra data if not ManyManyList
if(!($list instanceof ManyManyList)) {
return null;
}
$data = array();
foreach($list->getExtraFields() as $field => $dbSpec) {
$savedField = "ManyMany[{$field}]";
if($record->hasField($savedField)) {
$data[$field] = $record->getField($savedField);
}
}
return $data;
}
public function doSave($data, $form) { public function doSave($data, $form) {
$new_record = $this->record->ID == 0; $new_record = $this->record->ID == 0;
$controller = $this->getToplevelController(); $controller = $this->getToplevelController();
$list = $this->gridField->getList(); $list = $this->gridField->getList();
if($list instanceof ManyManyList) {
// Data is escaped in ManyManyList->add()
$extraData = (isset($data['ManyMany'])) ? $data['ManyMany'] : null;
} else {
$extraData = null;
}
if(!$this->record->canEdit()) { if(!$this->record->canEdit()) {
return $controller->httpError(403); return $controller->httpError(403);
} }
@ -527,6 +545,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
try { try {
$form->saveInto($this->record); $form->saveInto($this->record);
$this->record->write(); $this->record->write();
$extraData = $this->getExtraSavedData($this->record, $list);
$list->add($this->record, $extraData); $list->add($this->record, $extraData);
} catch(ValidationException $e) { } catch(ValidationException $e) {
$form->sessionMessage($e->getResult()->message(), 'bad', false); $form->sessionMessage($e->getResult()->message(), 'bad', false);

View File

@ -332,8 +332,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
}, },
redraw: function() { redraw: function() {
// Using a global config (generated through HTMLEditorConfig PHP logic) // Using textarea config ID from global config object (generated through HTMLEditorConfig PHP logic)
var config = ssTinyMceConfig, self = this, ed = this.getEditor(); var config = ssTinyMceConfig[this.data('config')], self = this, ed = this.getEditor();
ed.init(config); ed.init(config);
@ -498,8 +498,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
this.find(':input:not(:submit)[data-skip-autofocus!="true"]').filter(':visible:enabled').eq(0).focus(); this.find(':input:not(:submit)[data-skip-autofocus!="true"]').filter(':visible:enabled').eq(0).focus();
this.updateFromEditor();
this.redraw(); this.redraw();
this.updateFromEditor();
}, },
onssdialogclose: function(){ onssdialogclose: function(){
@ -573,6 +573,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
this.addAnchorSelector(); this.addAnchorSelector();
this.resetFileField();
// Toggle field visibility depending on the link type. // Toggle field visibility depending on the link type.
this.find('div.content .field').hide(); this.find('div.content .field').hide();
this.find('.field[id$="LinkType"]').show(); this.find('.field[id$="LinkType"]').show();
@ -623,7 +625,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
break; break;
case 'file': case 'file':
href = '[file_link,id=' + this.find(':input[name=file]').val() + ']'; href = '[file_link,id=' + this.find('.ss-uploadfield .ss-uploadfield-item').attr('data-fileid') + ']';
target = '_blank'; target = '_blank';
break; break;
@ -653,17 +655,27 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
this.modifySelection(function(ed){ this.modifySelection(function(ed){
ed.insertLink(this.getLinkAttributes()); ed.insertLink(this.getLinkAttributes());
}); });
this.updateFromEditor();
}, },
removeLink: function() { removeLink: function() {
this.modifySelection(function(ed){ this.modifySelection(function(ed){
ed.removeLink(); ed.removeLink();
}); });
this.resetFileField();
this.close(); this.close();
}, },
resetFileField: function() {
// If there's an attached item, remove it
var fileField = this.find('#file'),
fileUpload = fileField.data('fileupload'),
currentItem = fileField.find('.ss-uploadfield-item[data-fileid]');
if(currentItem.length) {
fileUpload._trigger('destroy', null, {content: currentItem});
}
},
/** /**
* Builds an anchor selector element and injects it into the DOM next to the anchor field. * Builds an anchor selector element and injects it into the DOM next to the anchor field.
*/ */
@ -810,6 +822,18 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
el.prop('checked', selected).change(); el.prop('checked', selected).change();
} else if(el.is(':radio')) { } else if(el.is(':radio')) {
el.val([selected]).change(); el.val([selected]).change();
} else if(fieldName == 'file') {
// Can't rely on fieldName, ad UploadFields have different naming convention
el = $('#' + fieldName);
// We have to wait for the UploadField to initialise
(function attach(el, selected) {
if( ! el.getConfig()) {
setTimeout(function(){ attach(el, selected); }, 50);
} else {
el.attachFiles([selected]);
}
})(el, selected);
} else { } else {
el.val(selected).change(); el.val(selected).change();
} }

View File

@ -154,17 +154,21 @@
var updateFn = function() { var updateFn = function() {
var val = self.getValue(); var val = self.getValue();
if(val) { if(val) {
var node = tree.find('*[data-id="' + val + '"]'), var node = tree.find('*[data-id="' + val + '"]'),
title = node.children('a').find("span.jstree_pageicon")?node.children('a').find("span.item").html():null; title = node.children('a').find("span.jstree_pageicon")?node.children('a').find("span.item").html():null;
if(!title) title=(node.length > 0) ? tree.jstree('get_text', node[0]) : null; if(!title) title=(node.length > 0) ? tree.jstree('get_text', node[0]) : null;
if(title) { if(title) {
self.setTitle(title); self.setTitle(title);
self.data('title', title); self.data('title', title);
} }
if(node) tree.jstree('select_node', node); if(node) tree.jstree('select_node', node);
} }
else {
self.setTitle(self.data('empty-title'));
self.removeData('title');
}
}; };
// Load the tree if its not already present // Load the tree if its not already present

View File

@ -258,8 +258,7 @@ fi:
many_many_Members: Jäsenet many_many_Members: Jäsenet
GroupImportForm: GroupImportForm:
Help1: '<p>Tuo yksi tai useampi ryhmä <em>CSV</em>-muotoisena (arvot pilkulla erotettuina). <small><a href="#" class="toggle-advanced">Näytä edistyksellinen käyttö</a></small></p>' Help1: '<p>Tuo yksi tai useampi ryhmä <em>CSV</em>-muotoisena (arvot pilkulla erotettuina). <small><a href="#" class="toggle-advanced">Näytä edistyksellinen käyttö</a></small></p>'
Help2: "<div class=\"advanced\">\n\t<h4>Edistynyt käyttö</h4>\n\t<ul>\n\t<li>Sallitut palstat: <em>%s</em></li>\n\t<li>Olemassa olevat ryhmät kohdistetaan niiden uniikin <em>Code</em> arvolla, ja päivitetään uudet arvot tuodusta tiedostosta</li>\n\t<li>Oikeustasot voidaan luoda käyttämällä <em>ParentCode</em> palstaa.</li>\n\t<li>Oikeustasokoodit voidaan kohdistaa <em>PermissionCode</em> palstassa. Olemassaolevia oikeusia ei tyhjennetä.</li>\n\t</ul>\n\ Help2: "<div class=\"advanced\">\n<h4>Edistynyt käyttö</h4>\n<ul>\n<li>Sallitut sarakkeet: <em>%s</em></li>\n<li>Olemassa olevat rhymes kohdistetaan niiden uniikin <em>Code</em> arvolla, ja päivitetään arvot tuodusta tiedostosta</li>\n<li>Ryhmien hierarkiat voidaan luoda <em>ParentCode</em> sarakkeessa.</li>\n<li>Oikeustasokoodit voidaan kohdistaa <em>PermissionCode</em> sarakkeessa. Olemassa olevia oikeuksia ei tyhjennetä.</li>\n</ul>\n</div>"
</div>"
ResultCreated: 'Luotiin {count} ryhmä(ä)' ResultCreated: 'Luotiin {count} ryhmä(ä)'
ResultDeleted: 'Poistettu %d ryhmää' ResultDeleted: 'Poistettu %d ryhmää'
ResultUpdated: 'Päivitetty %d ryhmää' ResultUpdated: 'Päivitetty %d ryhmää'

View File

@ -191,9 +191,7 @@ lt:
TEXT2: 'slaptažodžio atstatymo nuoroda' TEXT2: 'slaptažodžio atstatymo nuoroda'
TEXT3: svetainei TEXT3: svetainei
Form: Form:
CSRF_FAILED_MESSAGE: 'Iškilo techninė problema. Prašome paspausti mygtuką Atgal, CSRF_FAILED_MESSAGE: 'Iškilo techninė problema. Prašome paspausti mygtuką Atgal, perkraukite naršyklės langą ir bandykite vėl.'
perkraukite naršyklės langą ir bandykite vėl.'
FIELDISREQUIRED: '{name} yra privalomas' FIELDISREQUIRED: '{name} yra privalomas'
SubmitBtnLabel: Vykdyti SubmitBtnLabel: Vykdyti
VALIDATIONCREDITNUMBER: 'Prašome įsitikinti, ar teisingai suvedėte kreditinės kortelės numerį {number}' VALIDATIONCREDITNUMBER: 'Prašome įsitikinti, ar teisingai suvedėte kreditinės kortelės numerį {number}'
@ -260,8 +258,7 @@ lt:
many_many_Members: Vartotojai many_many_Members: Vartotojai
GroupImportForm: GroupImportForm:
Help1: '<p>Importuoti vieną ar kelias grupes <em>CSV</em> formatu (kableliu atskirtos reikšmės). <small><a href="#" class="toggle-advanced">Rodyti detalesnį aprašymą</a></small></p>' Help1: '<p>Importuoti vieną ar kelias grupes <em>CSV</em> formatu (kableliu atskirtos reikšmės). <small><a href="#" class="toggle-advanced">Rodyti detalesnį aprašymą</a></small></p>'
Help2: "<div class=\"advanced\">\n\t<h4>Sudėtingesni pasirinkimai</h4>\n\t<ul>\n\t<li>Galimi stulpeliai: <em>%s</em></li>\n\t<li>Esamos grupės yra surišamos su jų unikalia <em>Code</em> reikšme ir atnaujinamos duomenimis iš importuojamos bylos</li>\n\t<li>Grupių hierarchija gali būti sukurta naudojant <em>ParentCode</em> stulpelį.</li>\n\t<li>Leidimų kodai gali būti priskirti naudojant <em>PermissionCode</em> stulpelį. Esami leidimai nebus pakeisti.</li>\n\t</ul>\n\ Help2: "<div class=\"advanced\">\n<h4>Sudėtingesni pasirinkimai</h4>\n<ul>\n<li>Galimi stulpeliai: <em>%s</em></li>\n<li>Esamos grupės yra surišamos su jų unikalia <em>Code</em> reikšme ir atnaujinamos duomenimis iš importuojamos bylos</li>\n<li>Grupių hierarchija gali būti sukurta naudojant <em>ParentCode</em> stulpelį.</li>\n<li>Leidimų kodai gali būti priskirti naudojant <em>PermissionCode</em> stulpelį. Esami leidimai nebus pakeisti.</li>\n</ul>\n</div>"
</div>"
ResultCreated: 'Sukurta {count} grupių' ResultCreated: 'Sukurta {count} grupių'
ResultDeleted: 'Ištrinta %d grupių' ResultDeleted: 'Ištrinta %d grupių'
ResultUpdated: 'Atnaujinta %d grupių' ResultUpdated: 'Atnaujinta %d grupių'

View File

@ -392,7 +392,7 @@ nl:
Toggle: 'Toon opmaak hulp' Toggle: 'Toon opmaak hulp'
MemberImportForm: MemberImportForm:
Help1: '<p>Importeer leden in <em>CSV</em>-formaat (comma-separated values). <small><a href="#" class="toggle-advanced">Toon geavanceerd gebruik</a></small></p>' Help1: '<p>Importeer leden in <em>CSV</em>-formaat (comma-separated values). <small><a href="#" class="toggle-advanced">Toon geavanceerd gebruik</a></small></p>'
Help2: "<div class=\"advanced\">\n\t<h4>Advanced usage</h4>\n\t<ul>\n\t<li>Allowed columns: <em>%s</em></li>\n\t<li>Existing users are matched by their unique <em>Code</em> property, and updated with any new values from\n\tthe imported file.</li>\n\t<li>Groups can be assigned by the <em>Groups</em> column. Groups are identified by their <em>Code</em> property,\n\tmultiple groups can be separated by comma. Existing group memberships are not cleared.</li>\n\t</ul>\n</div>" Help2: "<div class=\"advanced\">\n<h4>Geavanceerd gebruik</h4>\n<ul>\n<li>Toegestane kolommen: <em>%s</em></li>\n<li>Bestaande groepen worden geïdentificeerd door middel van hun unieke <em>Code</em>-waarde, en aangepast met de nieuwe waarden vanuit het geïmporteerde bestand</li>\n<li>Groepshiërarchiën kunnen aangemaakt worden door een <em>ParentCode</em>-kolom te gebruiken</li>\n<li>Toegangscodeskunnen toegewezen worden met de <em>PermissionCode</em> kolom. Bestaande toegangscodes worden niet verwijderd.</li>\n</ul>\n</div>"
ResultCreated: '{count} leden aangemaakt' ResultCreated: '{count} leden aangemaakt'
ResultDeleted: '%d leden verwijderd' ResultDeleted: '%d leden verwijderd'
ResultNone: 'Geen wijzingen' ResultNone: 'Geen wijzingen'

View File

@ -410,8 +410,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(!is_string($fieldClass)) continue; if(!is_string($fieldClass)) continue;
// Strip off any parameters // Strip off any parameters
$bPos = strpos('(', $fieldClass); $bPos = strpos($fieldClass, '(');
if($bPos !== FALSE) $fieldClass = substr(0,$bPos, $fieldClass); if($bPos !== FALSE) $fieldClass = substr($fieldClass, 0, $bPos);
// Test to see if it implements CompositeDBField // Test to see if it implements CompositeDBField
if(ClassInfo::classImplements($fieldClass, 'CompositeDBField')) { if(ClassInfo::classImplements($fieldClass, 'CompositeDBField')) {
@ -1063,12 +1063,22 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @see {@link ValidationResult} * @see {@link ValidationResult}
* @return ValidationResult * @return ValidationResult
*/ */
public function validate() { protected function validate() {
$result = ValidationResult::create(); $result = ValidationResult::create();
$this->extend('validate', $result); $this->extend('validate', $result);
return $result; return $result;
} }
/**
* Public accessor for {@see DataObject::validate()}
*
* @return ValidationResult
*/
public function doValidate() {
// validate will be public in 4.0
return $this->validate();
}
/** /**
* Event handler called before writing to the database. * Event handler called before writing to the database.
* You can overload this to clean up or otherwise process data before writing it to the * You can overload this to clean up or otherwise process data before writing it to the
@ -2584,6 +2594,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return DataObject $this * @return DataObject $this
*/ */
public function setField($fieldName, $val) { public function setField($fieldName, $val) {
//if it's a has_one component, destroy the cache
if (substr($fieldName, -2) == 'ID') {
unset($this->components[substr($fieldName, 0, -2)]);
}
// Situation 1: Passing an DBField // Situation 1: Passing an DBField
if($val instanceof DBField) { if($val instanceof DBField) {
$val->Name = $fieldName; $val->Name = $fieldName;

View File

@ -215,6 +215,260 @@ class Image extends File implements Flushable {
} }
} }
/**
* Scale image proportionally to fit within the specified bounds
*
* @param integer $width The width to size within
* @param integer $height The height to size within
* @return Image
*/
public function Fit($width, $height) {
// Prevent divide by zero on missing/blank file
if(!$this->getWidth() || !$this->getHeight()) return null;
// Check if image is already sized to the correct dimension
$widthRatio = $width / $this->getWidth();
$heightRatio = $height / $this->getHeight();
if( $widthRatio < $heightRatio ) {
// Target is higher aspect ratio than image, so check width
if($this->isWidth($width) && !Config::inst()->get('Image', 'force_resample')) return $this;
} else {
// Target is wider or same aspect ratio as image, so check height
if($this->isHeight($height) && !Config::inst()->get('Image', 'force_resample')) return $this;
}
// Item must be regenerated
return $this->getFormattedImage('Fit', $width, $height);
}
/**
* Scale image proportionally to fit within the specified bounds
*
* @param Image_Backend $backend
* @param integer $width The width to size within
* @param integer $height The height to size within
* @return Image_Backend
*/
public function generateFit(Image_Backend $backend, $width, $height) {
return $backend->resizeRatio($width, $height);
}
/**
* Proportionally scale down this image if it is wider or taller than the specified dimensions.
* Similar to Fit but without up-sampling. Use in templates with $FitMax.
*
* @uses Image::Fit()
* @param integer $width The maximum width of the output image
* @param integer $height The maximum height of the output image
* @return Image
*/
public function FitMax($width, $height) {
// Temporary $force_resample support for 3.x, to be removed in 4.0
if (Config::inst()->get('Image', 'force_resample') && $this->getWidth() <= $width && $this->getHeight() <= $height) return $this->Fit($this->getWidth(),$this->getHeight());
return $this->getWidth() > $width || $this->getHeight() > $height
? $this->Fit($width,$height)
: $this;
}
/**
* Resize and crop image to fill specified dimensions.
* Use in templates with $Fill
*
* @param integer $width Width to crop to
* @param integer $height Height to crop to
* @return Image
*/
public function Fill($width, $height) {
return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample')
? $this
: $this->getFormattedImage('Fill', $width, $height);
}
/**
* Resize and crop image to fill specified dimensions.
* Use in templates with $Fill
*
* @param Image_Backend $backend
* @param integer $width Width to crop to
* @param integer $height Height to crop to
* @return Image_Backend
*/
public function generateFill(Image_Backend $backend, $width, $height) {
return $backend->croppedResize($width, $height);
}
/**
* Crop this image to the aspect ratio defined by the specified width and height,
* then scale down the image to those dimensions if it exceeds them.
* Similar to Fill but without up-sampling. Use in templates with $FillMax.
*
* @uses Image::Fill()
* @param integer $width The relative (used to determine aspect ratio) and maximum width of the output image
* @param integer $height The relative (used to determine aspect ratio) and maximum height of the output image
* @return Image
*/
public function FillMax($width, $height) {
// Prevent divide by zero on missing/blank file
if(!$this->getWidth() || !$this->getHeight()) return null;
// Temporary $force_resample support for 3.x, to be removed in 4.0
if (Config::inst()->get('Image', 'force_resample') && $this->isSize($width, $height)) return $this->Fill($width, $height);
// Is the image already the correct size?
if ($this->isSize($width, $height)) return $this;
// If not, make sure the image isn't upsampled
$imageRatio = $this->getWidth() / $this->getHeight();
$cropRatio = $width / $height;
// If cropping on the x axis compare heights
if ($cropRatio < $imageRatio && $this->getHeight() < $height) return $this->Fill($this->getHeight()*$cropRatio, $this->getHeight());
// Otherwise we're cropping on the y axis (or not cropping at all) so compare widths
if ($this->getWidth() < $width) return $this->Fill($this->getWidth(), $this->getWidth()/$cropRatio);
return $this->Fill($width, $height);
}
/**
* Fit image to specified dimensions and fill leftover space with a solid colour (default white). Use in templates with $Pad.
*
* @param integer $width The width to size to
* @param integer $height The height to size to
* @return Image
*/
public function Pad($width, $height, $backgroundColor='FFFFFF') {
return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample')
? $this
: $this->getFormattedImage('Pad', $width, $height, $backgroundColor);
}
/**
* Fit image to specified dimensions and fill leftover space with a solid colour (default white). Use in templates with $Pad.
*
* @param Image_Backend $backend
* @param integer $width The width to size to
* @param integer $height The height to size to
* @return Image_Backend
*/
public function generatePad(Image_Backend $backend, $width, $height, $backgroundColor='FFFFFF') {
return $backend->paddedResize($width, $height, $backgroundColor);
}
/**
* Scale image proportionally by width. Use in templates with $ScaleWidth.
*
* @param integer $width The width to set
* @return Image
*/
public function ScaleWidth($width) {
return $this->isWidth($width) && !Config::inst()->get('Image', 'force_resample')
? $this
: $this->getFormattedImage('ScaleWidth', $width);
}
/**
* Scale image proportionally by width. Use in templates with $ScaleWidth.
*
* @param Image_Backend $backend
* @param int $width The width to set
* @return Image_Backend
*/
public function generateScaleWidth(Image_Backend $backend, $width) {
return $backend->resizeByWidth($width);
}
/**
* Proportionally scale down this image if it is wider than the specified width.
* Similar to ScaleWidth but without up-sampling. Use in templates with $ScaleMaxWidth.
*
* @uses Image::ScaleWidth()
* @param integer $width The maximum width of the output image
* @return Image
*/
public function ScaleMaxWidth($width) {
// Temporary $force_resample support for 3.x, to be removed in 4.0
if (Config::inst()->get('Image', 'force_resample') && $this->getWidth() <= $width) return $this->ScaleWidth($this->getWidth());
return $this->getWidth() > $width
? $this->ScaleWidth($width)
: $this;
}
/**
* Scale image proportionally by height. Use in templates with $ScaleHeight.
*
* @param integer $height The height to set
* @return Image
*/
public function ScaleHeight($height) {
return $this->isHeight($height) && !Config::inst()->get('Image', 'force_resample')
? $this
: $this->getFormattedImage('ScaleHeight', $height);
}
/**
* Scale image proportionally by height. Use in templates with $ScaleHeight.
*
* @param Image_Backend $backend
* @param integer $height The height to set
* @return Image_Backend
*/
public function generateScaleHeight(Image_Backend $backend, $height){
return $backend->resizeByHeight($height);
}
/**
* Proportionally scale down this image if it is taller than the specified height.
* Similar to ScaleHeight but without up-sampling. Use in templates with $ScaleMaxHeight.
*
* @uses Image::ScaleHeight()
* @param integer $height The maximum height of the output image
* @return Image
*/
public function ScaleMaxHeight($height) {
// Temporary $force_resample support for 3.x, to be removed in 4.0
if (Config::inst()->get('Image', 'force_resample') && $this->getHeight() <= $height) return $this->ScaleHeight($this->getHeight());
return $this->getHeight() > $height
? $this->ScaleHeight($height)
: $this;
}
/**
* Crop image on X axis if it exceeds specified width. Retain height.
* Use in templates with $CropWidth. Example: $Image.ScaleHeight(100).$CropWidth(100)
*
* @uses Image::Fill()
* @param integer $width The maximum width of the output image
* @return Image
*/
public function CropWidth($width) {
// Temporary $force_resample support for 3.x, to be removed in 4.0
if (Config::inst()->get('Image', 'force_resample') && $this->getWidth() <= $width) return $this->Fill($this->getWidth(), $this->getHeight());
return $this->getWidth() > $width
? $this->Fill($width, $this->getHeight())
: $this;
}
/**
* Crop image on Y axis if it exceeds specified height. Retain width.
* Use in templates with $CropHeight. Example: $Image.ScaleWidth(100).CropHeight(100)
*
* @uses Image::Fill()
* @param integer $height The maximum height of the output image
* @return Image
*/
public function CropHeight($height) {
// Temporary $force_resample support for 3.x, to be removed in 4.0
if (Config::inst()->get('Image', 'force_resample') && $this->getHeight() <= $height) return $this->Fill($this->getWidth(), $this->getHeight());
return $this->getHeight() > $height
? $this->Fill($this->getWidth(), $height)
: $this;
}
/** /**
* Resize the image by preserving aspect ratio, keeping the image inside the * Resize the image by preserving aspect ratio, keeping the image inside the
* $width and $height * $width and $height
@ -222,25 +476,11 @@ class Image extends File implements Flushable {
* @param integer $width The width to size within * @param integer $width The width to size within
* @param integer $height The height to size within * @param integer $height The height to size within
* @return Image * @return Image
* @deprecated 4.0 Use Fit instead
*/ */
public function SetRatioSize($width, $height) { public function SetRatioSize($width, $height) {
Deprecation::notice('4.0', 'Use Fit instead');
// Prevent divide by zero on missing/blank file return $this->Fit($width, $height);
if(empty($this->width) || empty($this->height)) return null;
// Check if image is already sized to the correct dimension
$widthRatio = $width / $this->width;
$heightRatio = $height / $this->height;
if( $widthRatio < $heightRatio ) {
// Target is higher aspect ratio than image, so check width
if($this->isWidth($width) && !Config::inst()->get('Image', 'force_resample')) return $this;
} else {
// Target is wider aspect ratio than image, so check height
if($this->isHeight($height) && !Config::inst()->get('Image', 'force_resample')) return $this;
}
// Item must be regenerated
return $this->getFormattedImage('SetRatioSize', $width, $height);
} }
/** /**
@ -251,8 +491,10 @@ class Image extends File implements Flushable {
* @param integer $width The width to size within * @param integer $width The width to size within
* @param integer $height The height to size within * @param integer $height The height to size within
* @return Image_Backend * @return Image_Backend
* @deprecated 4.0 Use generateFit instead
*/ */
public function generateSetRatioSize(Image_Backend $backend, $width, $height) { public function generateSetRatioSize(Image_Backend $backend, $width, $height) {
Deprecation::notice('4.0', 'Use generateFit instead');
return $backend->resizeRatio($width, $height); return $backend->resizeRatio($width, $height);
} }
@ -261,11 +503,11 @@ class Image extends File implements Flushable {
* *
* @param integer $width The width to set * @param integer $width The width to set
* @return Image * @return Image
* @deprecated 4.0 Use ScaleWidth instead
*/ */
public function SetWidth($width) { public function SetWidth($width) {
return $this->isWidth($width) && !Config::inst()->get('Image', 'force_resample') Deprecation::notice('4.0', 'Use ScaleWidth instead');
? $this return $this->ScaleWidth($width);
: $this->getFormattedImage('SetWidth', $width);
} }
/** /**
@ -274,8 +516,10 @@ class Image extends File implements Flushable {
* @param Image_Backend $backend * @param Image_Backend $backend
* @param int $width The width to set * @param int $width The width to set
* @return Image_Backend * @return Image_Backend
* @deprecated 4.0 Use generateScaleWidth instead
*/ */
public function generateSetWidth(Image_Backend $backend, $width) { public function generateSetWidth(Image_Backend $backend, $width) {
Deprecation::notice('4.0', 'Use generateScaleWidth instead');
return $backend->resizeByWidth($width); return $backend->resizeByWidth($width);
} }
@ -284,11 +528,11 @@ class Image extends File implements Flushable {
* *
* @param integer $height The height to set * @param integer $height The height to set
* @return Image * @return Image
* @deprecated 4.0 Use ScaleHeight instead
*/ */
public function SetHeight($height) { public function SetHeight($height) {
return $this->isHeight($height) && !Config::inst()->get('Image', 'force_resample') Deprecation::notice('4.0', 'Use ScaleHeight instead');
? $this return $this->ScaleHeight($height);
: $this->getFormattedImage('SetHeight', $height);
} }
/** /**
@ -297,8 +541,10 @@ class Image extends File implements Flushable {
* @param Image_Backend $backend * @param Image_Backend $backend
* @param integer $height The height to set * @param integer $height The height to set
* @return Image_Backend * @return Image_Backend
* @deprecated 4.0 Use generateScaleHeight instead
*/ */
public function generateSetHeight(Image_Backend $backend, $height){ public function generateSetHeight(Image_Backend $backend, $height){
Deprecation::notice('4.0', 'Use generateScaleHeight instead');
return $backend->resizeByHeight($height); return $backend->resizeByHeight($height);
} }
@ -309,11 +555,11 @@ class Image extends File implements Flushable {
* @param integer $width The width to size to * @param integer $width The width to size to
* @param integer $height The height to size to * @param integer $height The height to size to
* @return Image * @return Image
* @deprecated 4.0 Use Pad instead
*/ */
public function SetSize($width, $height) { public function SetSize($width, $height) {
return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample') Deprecation::notice('4.0', 'Use Pad instead');
? $this return $this->Pad($width, $height);
: $this->getFormattedImage('SetSize', $width, $height);
} }
/** /**
@ -323,8 +569,10 @@ class Image extends File implements Flushable {
* @param integer $width The width to size to * @param integer $width The width to size to
* @param integer $height The height to size to * @param integer $height The height to size to
* @return Image_Backend * @return Image_Backend
* @deprecated 4.0 Use generatePad instead
*/ */
public function generateSetSize(Image_Backend $backend, $width, $height) { public function generateSetSize(Image_Backend $backend, $width, $height) {
Deprecation::notice('4.0', 'Use generatePad instead');
return $backend->paddedResize($width, $height); return $backend->paddedResize($width, $height);
} }
@ -371,11 +619,11 @@ class Image extends File implements Flushable {
* @param integer $width The width to size to * @param integer $width The width to size to
* @param integer $height The height to size to * @param integer $height The height to size to
* @return Image * @return Image
* @deprecated 4.0 Use Pad instead
*/ */
public function PaddedImage($width, $height, $backgroundColor='FFFFFF') { public function PaddedImage($width, $height, $backgroundColor='FFFFFF') {
return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample') Deprecation::notice('4.0', 'Use Pad instead');
? $this return $this->Pad($width, $height, $backgroundColor);
: $this->getFormattedImage('PaddedImage', $width, $height, $backgroundColor);
} }
/** /**
@ -385,8 +633,10 @@ class Image extends File implements Flushable {
* @param integer $width The width to size to * @param integer $width The width to size to
* @param integer $height The height to size to * @param integer $height The height to size to
* @return Image_Backend * @return Image_Backend
* @deprecated 4.0 Use generatePad instead
*/ */
public function generatePaddedImage(Image_Backend $backend, $width, $height, $backgroundColor='FFFFFF') { public function generatePaddedImage(Image_Backend $backend, $width, $height, $backgroundColor='FFFFFF') {
Deprecation::notice('4.0', 'Use generatePad instead');
return $backend->paddedResize($width, $height, $backgroundColor); return $backend->paddedResize($width, $height, $backgroundColor);
} }
@ -510,7 +760,8 @@ class Image extends File implements Flushable {
/** /**
* Generate a resized copy of this image with the given width & height. * Generate a resized copy of this image with the given width & height.
* Use in templates with $ResizedImage. * This can be used in templates with $ResizedImage but should be avoided,
* as it's the only image manipulation function which can skew an image.
* *
* @param integer $width Width to resize to * @param integer $width Width to resize to
* @param integer $height Height to resize to * @param integer $height Height to resize to
@ -547,11 +798,11 @@ class Image extends File implements Flushable {
* @param integer $width Width to crop to * @param integer $width Width to crop to
* @param integer $height Height to crop to * @param integer $height Height to crop to
* @return Image * @return Image
* @deprecated 4.0 Use Fill instead
*/ */
public function CroppedImage($width, $height) { public function CroppedImage($width, $height) {
return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample') Deprecation::notice('4.0', 'Use Fill instead');
? $this return $this->Fill($width, $height);
: $this->getFormattedImage('CroppedImage', $width, $height);
} }
/** /**
@ -562,8 +813,10 @@ class Image extends File implements Flushable {
* @param integer $width Width to crop to * @param integer $width Width to crop to
* @param integer $height Height to crop to * @param integer $height Height to crop to
* @return Image_Backend * @return Image_Backend
* @deprecated 4.0 Use generateFill instead
*/ */
public function generateCroppedImage(Image_Backend $backend, $width, $height) { public function generateCroppedImage(Image_Backend $backend, $width, $height) {
Deprecation::notice('4.0', 'Use generateFill instead');
return $backend->croppedResize($width, $height); return $backend->croppedResize($width, $height);
} }
@ -648,9 +901,9 @@ class Image extends File implements Flushable {
public function regenerateFormattedImages() { public function regenerateFormattedImages() {
if(!$this->Filename) return 0; if(!$this->Filename) return 0;
// Without this, not a single file would be written // Without this, not a single file would be written
// caused by a check in getFormattedImage() // caused by a check in getFormattedImage()
$_GET['flush'] = 1; $this->flush();
$numGenerated = 0; $numGenerated = 0;
$generatedImages = $this->getGeneratedImages(); $generatedImages = $this->getGeneratedImages();

View File

@ -230,37 +230,29 @@ class ManyManyList extends RelationList {
$hasExisting = false; $hasExisting = false;
} }
$manipulation = array(); // Blank manipulation
$manipulation = array(
$this->joinTable => array(
'command' => $hasExisting ? 'update' : 'insert',
'fields' => array()
)
);
if($hasExisting) { if($hasExisting) {
$manipulation[$this->joinTable]['command'] = 'update';
$manipulation[$this->joinTable]['where'] = array( $manipulation[$this->joinTable]['where'] = array(
"\"{$this->joinTable}\".\"{$this->foreignKey}\"" => $foreignID, "\"{$this->joinTable}\".\"{$this->foreignKey}\"" => $foreignID,
"\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID "\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID
); );
} else {
$manipulation[$this->joinTable]['command'] = 'insert';
} }
if($extraFields) { if($extraFields && $this->extraFields) {
foreach($extraFields as $fieldName => $fieldValue) { // Write extra field to manipluation in the same way
if(is_null($fieldValue)) { // that DataObject::prepareManipulationTable writes fields
$manipulation[$this->joinTable]['fields'][$fieldName] = null; foreach($this->extraFields as $fieldName => $fieldSpec) {
} elseif($fieldValue instanceof DBField) { // Skip fields without an assignment
// rely on writeToManipulation to manage the changes if(array_key_exists($fieldName, $extraFields)) {
// required for this field. $fieldObject = Object::create_from_string($fieldSpec, $fieldName);
$working = array('fields' => array()); $fieldObject->setValue($extraFields[$fieldName]);
$fieldObject->writeToManipulation($manipulation[$this->joinTable]);
// create a new instance of the field so we can
// modify the field name to the correct version.
$field = DBField::create_field(get_class($fieldValue), $fieldValue);
$field->setName($fieldName);
$field->writeToManipulation($working);
foreach($working['fields'] as $extraName => $extraValue) {
$manipulation[$this->joinTable]['fields'][$extraName] = $extraValue;
}
} else {
$manipulation[$this->joinTable]['fields'][$fieldName] = $fieldValue;
} }
} }
} }
@ -357,24 +349,32 @@ class ManyManyList extends RelationList {
*/ */
public function getExtraData($componentName, $itemID) { public function getExtraData($componentName, $itemID) {
$result = array(); $result = array();
// Skip if no extrafields or unsaved record
if(empty($this->extraFields) || empty($itemID)) {
return $result;
}
if(!is_numeric($itemID)) { if(!is_numeric($itemID)) {
user_error('ComponentSet::getExtraData() passed a non-numeric child ID', E_USER_ERROR); user_error('ComponentSet::getExtraData() passed a non-numeric child ID', E_USER_ERROR);
} }
if($this->extraFields) { $cleanExtraFields = array();
$cleanExtraFields = array(); foreach ($this->extraFields as $fieldName => $dbFieldSpec) {
foreach ($this->extraFields as $fieldName => $dbFieldSpec) { $cleanExtraFields[] = "\"{$fieldName}\"";
$cleanExtraFields[] = "\"{$fieldName}\""; }
} $query = new SQLQuery($cleanExtraFields, "\"{$this->joinTable}\"");
$query = new SQLSelect($cleanExtraFields, "\"{$this->joinTable}\""); $filter = $this->foreignIDWriteFilter($this->getForeignID());
if($filter = $this->foreignIDWriteFilter($this->getForeignID())) { if($filter) {
$query->setWhere($filter); $query->setWhere($filter);
} else { } else {
user_error("Can't call ManyManyList::getExtraData() until a foreign ID is set", E_USER_WARNING); user_error("Can't call ManyManyList::getExtraData() until a foreign ID is set", E_USER_WARNING);
} }
$query->addWhere("\"{$this->localKey}\" = {$itemID}"); $query->addWhere(array(
$queryResult = $query->execute()->current(); "\"{$this->localKey}\"" => $itemID
));
$queryResult = $query->execute()->current();
if ($queryResult) {
foreach ($queryResult as $fieldName => $value) { foreach ($queryResult as $fieldName => $value) {
$result[$fieldName] = $value; $result[$fieldName] = $value;
} }

View File

@ -489,34 +489,34 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
if(!$isRootClass && ($versionedTables = ClassInfo::dataClassesFor($table))) { if(!$isRootClass && ($versionedTables = ClassInfo::dataClassesFor($table))) {
foreach($versionedTables as $child) { foreach($versionedTables as $child) {
if($table == $child) break; // only need subclasses if($table === $child) break; // only need subclasses
$count = DB::query(" // Select all orphaned version records
SELECT COUNT(*) FROM \"{$table}_versions\" $orphanedQuery = SQLSelect::create()
LEFT JOIN \"{$child}_versions\" ->selectField("\"{$table}_versions\".\"ID\"")
ON \"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\" ->setFrom("\"{$table}_versions\"");
AND \"{$child}_versions\".\"Version\" = \"{$table}_versions\".\"Version\"
WHERE \"{$child}_versions\".\"ID\" IS NULL // If we have a parent table limit orphaned records
")->value(); // to only those that exist in this
if(DB::get_schema()->hasTable("{$child}_versions")) {
$orphanedQuery
->addLeftJoin(
"{$child}_versions",
"\"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\"
AND \"{$child}_versions\".\"Version\" = \"{$table}_versions\".\"Version\""
)
->addWhere("\"{$child}_versions\".\"ID\" IS NULL");
}
$count = $orphanedQuery->count();
if($count > 0) { if($count > 0) {
DB::alteration_message("Removing orphaned versioned records", "deleted"); DB::alteration_message("Removing {$count} orphaned versioned records", "deleted");
$ids = $orphanedQuery->execute()->column();
$affectedIDs = DB::query(" foreach($ids as $id) {
SELECT \"{$table}_versions\".\"ID\" FROM \"{$table}_versions\" DB::prepared_query(
LEFT JOIN \"{$child}_versions\" "DELETE FROM \"{$table}_versions\" WHERE \"ID\" = ?",
ON \"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\" array($id)
AND \"{$child}_versions\".\"Version\" = \"{$table}_versions\".\"Version\" );
WHERE \"{$child}_versions\".\"ID\" IS NULL
")->column();
if(is_array($affectedIDs)) {
foreach($affectedIDs as $key => $value) {
DB::prepared_query(
"DELETE FROM \"{$table}_versions\" WHERE \"ID\" = ?",
array($value)
);
}
} }
} }
} }

View File

@ -336,7 +336,7 @@ class Group extends DataObject {
$this->setField("Code", Convert::raw2url($val)); $this->setField("Code", Convert::raw2url($val));
} }
public function validate() { protected function validate() {
$result = parent::validate(); $result = parent::validate();
// Check if the new group hierarchy would add certain "privileged permissions", // Check if the new group hierarchy would add certain "privileged permissions",

View File

@ -1487,7 +1487,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
/** /**
* Validate this member object. * Validate this member object.
*/ */
public function validate() { protected function validate() {
$valid = parent::validate(); $valid = parent::validate();
if(!$this->ID || $this->isChanged('Password')) { if(!$this->ID || $this->isChanged('Password')) {

View File

@ -20,7 +20,7 @@ class PermissionRoleCode extends DataObject {
"Role" => "PermissionRole", "Role" => "PermissionRole",
); );
public function validate() { protected function validate() {
$result = parent::validate(); $result = parent::validate();
// Check that new code doesn't increase privileges, unless an admin is editing. // Check that new code doesn't increase privileges, unless an admin is editing.

View File

@ -3,7 +3,7 @@
<div class="ss-uploadfield-item-preview"> <div class="ss-uploadfield-item-preview">
<% if $Width %> <% if $Width %>
<span>$Preview.SetRatioSize(30, 40)</span> <span>$Preview.Fit(30, 40)</span>
<% else %> <% else %>
<span class="no-preview"></span> <span class="no-preview"></span>
<% end_if %> <% end_if %>

View File

@ -2,6 +2,7 @@
class="TreeDropdownField <% if $extraClass %> $extraClass<% end_if %><% if $ShowSearch %> searchable<% end_if %>" class="TreeDropdownField <% if $extraClass %> $extraClass<% end_if %><% if $ShowSearch %> searchable<% end_if %>"
data-url-tree="$Link('tree')" data-url-tree="$Link('tree')"
data-title="$Title.ATT" data-title="$Title.ATT"
data-empty-title="$EmptyTitle.ATT"
<% if $Description %>title="$Description.ATT"<% end_if %> <% if $Description %>title="$Description.ATT"<% end_if %>
<% if $Metadata %>data-metadata="$Metadata.ATT"<% end_if %> tabindex="0"> <% if $Metadata %>data-metadata="$Metadata.ATT"<% end_if %> tabindex="0">
<input id="$ID" type="hidden" name="$Name.ATT" value="$Value.ATT" /> <input id="$ID" type="hidden" name="$Name.ATT" value="$Value.ATT" />

View File

@ -53,8 +53,20 @@ class CookieJarTest extends SapphireTest {
//make sure it was set //make sure it was set
$this->assertEquals('testVal', $cookieJar->get('testCookie')); $this->assertEquals('testVal', $cookieJar->get('testCookie'));
//make sure we can distinguise it from ones that were "existing" //make sure we can distinguish it from ones that were "existing"
$this->assertEmpty($cookieJar->get('testCookie', false)); $this->assertEmpty($cookieJar->get('testCookie', false));
//PHP will replace an incoming COOKIE called 'var.with.dots' to 'var_with_dots'
$cookieJar = new CookieJar(array(
'var_with_dots' => 'value',
));
$cookieJar->set('test.dots', 'dots');
//check we can access with '.' and with '_'
$this->assertEquals('value', $cookieJar->get('var.with.dots'));
$this->assertEquals('value', $cookieJar->get('var_with_dots'));
$this->assertEquals('dots', $cookieJar->get('test.dots'));
} }
/** /**
@ -120,14 +132,14 @@ class CookieJarTest extends SapphireTest {
//check we can add a new cookie and remove it and it doesn't leave any phantom values //check we can add a new cookie and remove it and it doesn't leave any phantom values
$cookieJar->set('newCookie', 'i am new'); $cookieJar->set('newCookie', 'i am new');
//check it's set by not recieved //check it's set by not received
$this->assertEquals('i am new', $cookieJar->get('newCookie')); $this->assertEquals('i am new', $cookieJar->get('newCookie'));
$this->assertEmpty($cookieJar->get('newCookie', false)); $this->assertEmpty($cookieJar->get('newCookie', false));
//remove it //remove it
$cookieJar->forceExpiry('newCookie'); $cookieJar->forceExpiry('newCookie');
//check it's neither set nor reveived //check it's neither set nor received
$this->assertEmpty($cookieJar->get('newCookie')); $this->assertEmpty($cookieJar->get('newCookie'));
$this->assertEmpty($cookieJar->get('newCookie', false)); $this->assertEmpty($cookieJar->get('newCookie', false));
} }

View File

@ -28,6 +28,7 @@ class CookieTest extends SapphireTest {
'cookie1' => 1, 'cookie1' => 1,
'cookie2' => 'cookies', 'cookie2' => 'cookies',
'cookie3' => 'test', 'cookie3' => 'test',
'cookie_4' => 'value',
); );
Injector::inst()->unregisterNamedObject('Cookie_Backend'); Injector::inst()->unregisterNamedObject('Cookie_Backend');
@ -35,6 +36,8 @@ class CookieTest extends SapphireTest {
$this->assertEquals($_COOKIE['cookie1'], Cookie::get('cookie1')); $this->assertEquals($_COOKIE['cookie1'], Cookie::get('cookie1'));
$this->assertEquals($_COOKIE['cookie2'], Cookie::get('cookie2')); $this->assertEquals($_COOKIE['cookie2'], Cookie::get('cookie2'));
$this->assertEquals($_COOKIE['cookie3'], Cookie::get('cookie3')); $this->assertEquals($_COOKIE['cookie3'], Cookie::get('cookie3'));
$this->assertEquals($_COOKIE['cookie_4'], Cookie::get('cookie.4'));
$this->assertEquals($_COOKIE['cookie_4'], Cookie::get('cookie_4'));
//for good measure check the CookieJar hasn't stored anything extra //for good measure check the CookieJar hasn't stored anything extra
$this->assertEquals($_COOKIE, Cookie::get_inst()->getAll(false)); $this->assertEquals($_COOKIE, Cookie::get_inst()->getAll(false));

View File

@ -103,18 +103,18 @@ class FileTest extends SapphireTest {
// Invalid ext // Invalid ext
$file->Name = 'asdf.php'; $file->Name = 'asdf.php';
$v = $file->validate(); $v = $file->doValidate();
$this->assertFalse($v->valid()); $this->assertFalse($v->valid());
$this->assertContains('Extension is not allowed', $v->message()); $this->assertContains('Extension is not allowed', $v->message());
// Valid ext // Valid ext
$file->Name = 'asdf.txt'; $file->Name = 'asdf.txt';
$v = $file->validate(); $v = $file->doValidate();
$this->assertTrue($v->valid()); $this->assertTrue($v->valid());
// Capital extension is valid as well // Capital extension is valid as well
$file->Name = 'asdf.TXT'; $file->Name = 'asdf.TXT';
$v = $file->validate(); $v = $file->doValidate();
$this->assertTrue($v->valid()); $this->assertTrue($v->valid());
Config::inst()->remove('File', 'allowed_extensions'); Config::inst()->remove('File', 'allowed_extensions');

View File

@ -161,7 +161,7 @@ class GDTest extends SapphireTest {
$fullPath = realpath(dirname(__FILE__) . '/gdtest/test_jpg.jpg'); $fullPath = realpath(dirname(__FILE__) . '/gdtest/test_jpg.jpg');
try { try {
$gdFailure = new GDBackend_Failure($fullPath, array('SetWidth', 123)); $gdFailure = new GDBackend_Failure($fullPath, array('ScaleWidth', 123));
$this->fail('GDBackend_Failure should throw an exception when setting image resource'); $this->fail('GDBackend_Failure should throw an exception when setting image resource');
} catch (GDBackend_Failure_Exception $e) { } catch (GDBackend_Failure_Exception $e) {
$cache = SS_Cache::factory('GDBackend_Manipulations'); $cache = SS_Cache::factory('GDBackend_Manipulations');
@ -169,8 +169,8 @@ class GDTest extends SapphireTest {
$data = unserialize($cache->load($key)); $data = unserialize($cache->load($key));
$this->assertArrayHasKey('SetWidth|123', $data); $this->assertArrayHasKey('ScaleWidth|123', $data);
$this->assertTrue($data['SetWidth|123']); $this->assertTrue($data['ScaleWidth|123']);
} }
} }
@ -183,12 +183,12 @@ class GDTest extends SapphireTest {
$fullPath = realpath(dirname(__FILE__) . '/gdtest/test_jpg.jpg'); $fullPath = realpath(dirname(__FILE__) . '/gdtest/test_jpg.jpg');
try { try {
$gdFailure = new GDBackend_Failure($fullPath, array('SetWidth-failed', 123)); $gdFailure = new GDBackend_Failure($fullPath, array('ScaleWidth-failed', 123));
$this->fail('GDBackend_Failure should throw an exception when setting image resource'); $this->fail('GDBackend_Failure should throw an exception when setting image resource');
} catch (GDBackend_Failure_Exception $e) { } catch (GDBackend_Failure_Exception $e) {
$gd = new GDBackend($fullPath, array('SetWidth', 123)); $gd = new GDBackend($fullPath, array('ScaleWidth', 123));
$this->assertTrue($gd->failedResample($fullPath, 'SetWidth-failed|123')); $this->assertTrue($gd->failedResample($fullPath, 'ScaleWidth-failed|123'));
$this->assertFalse($gd->failedResample($fullPath, 'SetWidth-not-failed|123')); $this->assertFalse($gd->failedResample($fullPath, 'ScaleWidth-not-failed|123'));
} }
} }

View File

@ -67,12 +67,26 @@ class HtmlEditorConfigTest extends SapphireTest {
$this->assertNotContains('plugin2', array_keys($plugins)); $this->assertNotContains('plugin2', array_keys($plugins));
} }
public function testGenerateJSWritesPlugins() { public function testRequireJSIncludesAllExternalPlugins() {
$c = new HtmlEditorConfig(); $c = HtmlEditorConfig::get('config');
$c->enablePlugins(array('plugin1')); $c->enablePlugins(array('plugin1' => '/mypath/plugin1'));
$c->enablePlugins(array('plugin2' => '/mypath/plugin2')); $c->enablePlugins(array('plugin2' => '/mypath/plugin2'));
$this->assertContains('plugin1', $c->generateJS()); HtmlEditorConfig::require_js();
$this->assertContains('tinymce.PluginManager.load("plugin2", "/mypath/plugin2");', $c->generateJS()); $js = Requirements::get_custom_scripts();
$this->assertContains('tinymce.PluginManager.load("plugin1", "/mypath/plugin1");', $js);
$this->assertContains('tinymce.PluginManager.load("plugin2", "/mypath/plugin2");', $js);
}
public function testRequireJSIncludesAllConfigs() {
$c = HtmlEditorConfig::get('configA');
$c = HtmlEditorConfig::get('configB');
HtmlEditorConfig::require_js();
$js = Requirements::get_custom_scripts();
$this->assertContains('"configA":{', $js);
$this->assertContains('"configB":{', $js);
} }
} }

View File

@ -174,11 +174,15 @@ class GridFieldDetailFormTest extends FunctionalTest {
$manyManyField = $parser->getByXpath('//*[@id="Form_ItemEditForm"]//input[@name="ManyMany[IsPublished]"]'); $manyManyField = $parser->getByXpath('//*[@id="Form_ItemEditForm"]//input[@name="ManyMany[IsPublished]"]');
$this->assertTrue((bool)$manyManyField); $this->assertTrue((bool)$manyManyField);
// Test save of IsPublished field
$response = $this->post( $response = $this->post(
$editformurl, $editformurl,
array( array(
'Name' => 'Updated Category', 'Name' => 'Updated Category',
'ManyMany' => array('IsPublished' => 1), 'ManyMany' => array(
'IsPublished' => 1,
'PublishedBy' => 'Richard'
),
'action_doSave' => 1 'action_doSave' => 1
) )
); );
@ -187,7 +191,33 @@ class GridFieldDetailFormTest extends FunctionalTest {
$person = GridFieldDetailFormTest_Person::get()->sort('FirstName')->First(); $person = GridFieldDetailFormTest_Person::get()->sort('FirstName')->First();
$category = $person->Categories()->filter(array('Name' => 'Updated Category'))->First(); $category = $person->Categories()->filter(array('Name' => 'Updated Category'))->First();
$this->assertEquals( $this->assertEquals(
array('IsPublished' => 1), array(
'IsPublished' => 1,
'PublishedBy' => 'Richard'
),
$person->Categories()->getExtraData('', $category->ID)
);
// Test update of value with falsey value
$response = $this->post(
$editformurl,
array(
'Name' => 'Updated Category',
'ManyMany' => array(
'PublishedBy' => ''
),
'action_doSave' => 1
)
);
$this->assertFalse($response->isError());
$person = GridFieldDetailFormTest_Person::get()->sort('FirstName')->First();
$category = $person->Categories()->filter(array('Name' => 'Updated Category'))->First();
$this->assertEquals(
array(
'IsPublished' => 0,
'PublishedBy' => ''
),
$person->Categories()->getExtraData('', $category->ID) $person->Categories()->getExtraData('', $category->ID)
); );
} }
@ -316,7 +346,8 @@ class GridFieldDetailFormTest_Person extends DataObject implements TestOnly {
private static $many_many_extraFields = array( private static $many_many_extraFields = array(
'Categories' => array( 'Categories' => array(
'IsPublished' => 'Boolean' 'IsPublished' => 'Boolean',
'PublishedBy' => 'Varchar'
) )
); );
@ -464,7 +495,10 @@ class GridFieldDetailFormTest_CategoryController extends Controller implements T
// GridField lists categories for a specific person // GridField lists categories for a specific person
$person = GridFieldDetailFormTest_Person::get()->sort('FirstName')->First(); $person = GridFieldDetailFormTest_Person::get()->sort('FirstName')->First();
$detailFields = singleton('GridFieldDetailFormTest_Category')->getCMSFields(); $detailFields = singleton('GridFieldDetailFormTest_Category')->getCMSFields();
$detailFields->addFieldToTab('Root.Main', new CheckboxField('ManyMany[IsPublished]')); $detailFields->addFieldsToTab('Root.Main', array(
new CheckboxField('ManyMany[IsPublished]'),
new TextField('ManyMany[PublishedBy]'))
);
$field = new GridField('testfield', 'testfield', $person->Categories()); $field = new GridField('testfield', 'testfield', $person->Categories());
$field->getConfig()->addComponent($gridFieldForm = new GridFieldDetailForm($this, 'Form')); $field->getConfig()->addComponent($gridFieldForm = new GridFieldDetailForm($this, 'Form'));
$gridFieldForm->setFields($detailFields); $gridFieldForm->setFields($detailFields);

View File

@ -179,8 +179,9 @@ class UploadFieldTest extends FunctionalTest {
'UploadFieldTest_Controller/Form/field/AllowedExtensionsField/upload', 'UploadFieldTest_Controller/Form/field/AllowedExtensionsField/upload',
array('AllowedExtensionsField' => $this->getUploadFile($invalidFile)) array('AllowedExtensionsField' => $this->getUploadFile($invalidFile))
); );
$this->assertTrue($response->isError()); $response = json_decode($response->getBody(), true);
$this->assertContains('Extension is not allowed', $response->getBody()); $this->assertTrue(array_key_exists('error', $response[0]));
$this->assertContains('Extension is not allowed', $response[0]['error']);
// Test valid file // Test valid file
$validFile = 'valid.txt'; $validFile = 'valid.txt';
@ -189,8 +190,8 @@ class UploadFieldTest extends FunctionalTest {
'UploadFieldTest_Controller/Form/field/AllowedExtensionsField/upload', 'UploadFieldTest_Controller/Form/field/AllowedExtensionsField/upload',
array('AllowedExtensionsField' => $this->getUploadFile($validFile)) array('AllowedExtensionsField' => $this->getUploadFile($validFile))
); );
$this->assertFalse($response->isError()); $response = json_decode($response->getBody(), true);
$this->assertNotContains('Extension is not allowed', $response->getBody()); $this->assertFalse(array_key_exists('error', $response[0]));
// Test that setAllowedExtensions rejects extensions explicitly denied by File.allowed_extensions // Test that setAllowedExtensions rejects extensions explicitly denied by File.allowed_extensions
// Relies on File::validate failing to allow this extension // Relies on File::validate failing to allow this extension
@ -200,8 +201,10 @@ class UploadFieldTest extends FunctionalTest {
'UploadFieldTest_Controller/Form/field/InvalidAllowedExtensionsField/upload', 'UploadFieldTest_Controller/Form/field/InvalidAllowedExtensionsField/upload',
array('InvalidAllowedExtensionsField' => $this->getUploadFile($invalidFile)) array('InvalidAllowedExtensionsField' => $this->getUploadFile($invalidFile))
); );
$this->assertTrue($response->isError()); $response = json_decode($response->getBody(), true);
$this->assertContains('Extension is not allowed', $response->getBody()); $this->assertTrue(array_key_exists('error', $response[0]));
$this->assertContains('Extension is not allowed', $response[0]['error']);
} }
/** /**

View File

@ -55,8 +55,8 @@ class DataDifferencerTest extends SapphireTest {
$differ = new DataDifferencer($obj1v1, $obj1v2); $differ = new DataDifferencer($obj1v1, $obj1v2);
$obj1Diff = $differ->diffedData(); $obj1Diff = $differ->diffedData();
$this->assertContains($image1->Filename, $obj1Diff->getField('Image')); $this->assertContains($image1->Name, $obj1Diff->getField('Image'));
$this->assertContains($image2->Filename, $obj1Diff->getField('Image')); $this->assertContains($image2->Name, $obj1Diff->getField('Image'));
$this->assertContains('<ins>obj2</ins><del>obj1</del>', $this->assertContains('<ins>obj2</ins><del>obj1</del>',
str_replace(' ','',$obj1Diff->getField('HasOneRelationID'))); str_replace(' ','',$obj1Diff->getField('HasOneRelationID')));
} }

View File

@ -443,6 +443,7 @@ class DataObjectTest extends SapphireTest {
public function testHasOneRelationship() { public function testHasOneRelationship() {
$team1 = $this->objFromFixture('DataObjectTest_Team', 'team1'); $team1 = $this->objFromFixture('DataObjectTest_Team', 'team1');
$player1 = $this->objFromFixture('DataObjectTest_Player', 'player1'); $player1 = $this->objFromFixture('DataObjectTest_Player', 'player1');
$player2 = $this->objFromFixture('DataObjectTest_Player', 'player2');
$fan1 = $this->objFromFixture('DataObjectTest_Fan', 'fan1'); $fan1 = $this->objFromFixture('DataObjectTest_Fan', 'fan1');
// Test relation probing // Test relation probing
@ -466,6 +467,15 @@ class DataObjectTest extends SapphireTest {
$this->assertEquals($team1->getComponent('Captain')->FirstName, 'Player 1', $this->assertEquals($team1->getComponent('Captain')->FirstName, 'Player 1',
'Player 1 is the captain'); 'Player 1 is the captain');
$team1->CaptainID = $player2->ID;
$team1->write();
$this->assertEquals($player2->ID, $team1->Captain()->ID);
$this->assertEquals($player2->ID, $team1->getComponent('Captain')->ID);
$this->assertEquals('Player 2', $team1->Captain()->FirstName);
$this->assertEquals('Player 2', $team1->getComponent('Captain')->FirstName);
// Set the favourite team for fan1 // Set the favourite team for fan1
$fan1->setField('FavouriteID', $team1->ID); $fan1->setField('FavouriteID', $team1->ID);
$fan1->setField('FavouriteClass', $team1->class); $fan1->setField('FavouriteClass', $team1->class);
@ -1811,7 +1821,7 @@ class DataObjectTest_ValidatedObject extends DataObject implements TestOnly {
'Name' => 'Varchar(50)' 'Name' => 'Varchar(50)'
); );
public function validate() { protected function validate() {
if(!empty($this->Name)) { if(!empty($this->Name)) {
return new ValidationResult(); return new ValidationResult();
} else { } else {

View File

@ -10,22 +10,9 @@ class GDImageTest extends ImageTest {
return; return;
} }
parent::setUp();
Image::set_backend("GDBackend"); Image::set_backend("GDBackend");
// Create a test files for each of the fixture references parent::setUp();
$fileIDs = $this->allFixtureIDs('Image');
foreach($fileIDs as $fileID) {
$file = DataObject::get_by_id('Image', $fileID);
$image = imagecreatetruecolor(300,300);
imagepng($image, BASE_PATH."/{$file->Filename}");
imagedestroy($image);
$file->write();
}
} }
public function tearDown() { public function tearDown() {
@ -47,13 +34,13 @@ class GDImageTest extends ImageTest {
try { try {
// Simluate a failed manipulation // Simluate a failed manipulation
$gdFailure = new GDBackend_Failure($fullPath, array('SetWidth', 123)); $gdFailure = new GDBackend_Failure($fullPath, array('ScaleWidth', 123));
$this->fail('GDBackend_Failure should throw an exception when setting image resource'); $this->fail('GDBackend_Failure should throw an exception when setting image resource');
} catch (GDBackend_Failure_Exception $e) { } catch (GDBackend_Failure_Exception $e) {
// Check that the cache has stored the manipulation failure // Check that the cache has stored the manipulation failure
$data = unserialize($cache->load($key)); $data = unserialize($cache->load($key));
$this->assertArrayHasKey('SetWidth|123', $data); $this->assertArrayHasKey('ScaleWidth|123', $data);
$this->assertTrue($data['SetWidth|123']); $this->assertTrue($data['ScaleWidth|123']);
// Delete the image object // Delete the image object
$image->delete(); $image->delete();

View File

@ -30,19 +30,30 @@ class ImageTest extends SapphireTest {
if(!file_exists(BASE_PATH."/$folder->Filename")) mkdir(BASE_PATH."/$folder->Filename"); if(!file_exists(BASE_PATH."/$folder->Filename")) mkdir(BASE_PATH."/$folder->Filename");
} }
// Copy test images for each of the fixture references
$imageIDs = $this->allFixtureIDs('Image');
foreach($imageIDs as $imageID) {
$image = DataObject::get_by_id('Image', $imageID);
$filePath = BASE_PATH."/$image->Filename";
$sourcePath = str_replace('assets/ImageTest/', 'framework/tests/model/testimages/', $filePath);
if(!file_exists($filePath)) {
if (!copy($sourcePath, $filePath)) user_error('Failed to copy test images', E_USER_ERROR);
}
}
} }
public function tearDown() { public function tearDown() {
if($this->origBackend) Image::set_backend($this->origBackend); if($this->origBackend) Image::set_backend($this->origBackend);
/* Remove the test files that we've created */ // Remove the test files that we've created
$fileIDs = $this->allFixtureIDs('Image'); $fileIDs = $this->allFixtureIDs('Image');
foreach($fileIDs as $fileID) { foreach($fileIDs as $fileID) {
$file = DataObject::get_by_id('Image', $fileID); $file = DataObject::get_by_id('Image', $fileID);
if($file && file_exists(BASE_PATH."/$file->Filename")) unlink(BASE_PATH."/$file->Filename"); if($file && file_exists(BASE_PATH."/$file->Filename")) unlink(BASE_PATH."/$file->Filename");
} }
/* Remove the test folders that we've crated */ // Remove the test folders that we've created
$folderIDs = $this->allFixtureIDs('Folder'); $folderIDs = $this->allFixtureIDs('Folder');
foreach($folderIDs as $folderID) { foreach($folderIDs as $folderID) {
$folder = DataObject::get_by_id('Folder', $folderID); $folder = DataObject::get_by_id('Folder', $folderID);
@ -89,7 +100,7 @@ class ImageTest extends SapphireTest {
public function testMultipleGenerateManipulationCalls() { public function testMultipleGenerateManipulationCalls() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle'); $image = $this->objFromFixture('Image', 'imageWithoutTitle');
$imageFirst = $image->SetWidth(200); $imageFirst = $image->ScaleWidth(200);
$this->assertNotNull($imageFirst); $this->assertNotNull($imageFirst);
$expected = 200; $expected = 200;
$actual = $imageFirst->getWidth(); $actual = $imageFirst->getWidth();
@ -113,27 +124,27 @@ class ImageTest extends SapphireTest {
$this->assertTrue($image->isSize(300, 300)); $this->assertTrue($image->isSize(300, 300));
// Set width to 300 pixels // Set width to 300 pixels
$imageSetWidth = $image->SetWidth(300); $imageScaleWidth = $image->ScaleWidth(300);
$this->assertEquals($imageSetWidth->getWidth(), 300); $this->assertEquals($imageScaleWidth->getWidth(), 300);
$this->assertEquals($image->Filename, $imageSetWidth->Filename); $this->assertEquals($image->Filename, $imageScaleWidth->Filename);
// Set height to 300 pixels // Set height to 300 pixels
$imageSetHeight = $image->SetHeight(300); $imageScaleHeight = $image->ScaleHeight(300);
$this->assertEquals($imageSetHeight->getHeight(), 300); $this->assertEquals($imageScaleHeight->getHeight(), 300);
$this->assertEquals($image->Filename, $imageSetHeight->Filename); $this->assertEquals($image->Filename, $imageScaleHeight->Filename);
// Crop image to 300 x 300 // Crop image to 300 x 300
$imageCropped = $image->CroppedImage(300, 300); $imageCropped = $image->Fill(300, 300);
$this->assertTrue($imageCropped->isSize(300, 300)); $this->assertTrue($imageCropped->isSize(300, 300));
$this->assertEquals($image->Filename, $imageCropped->Filename); $this->assertEquals($image->Filename, $imageCropped->Filename);
// Resize (padded) to 300 x 300 // Resize (padded) to 300 x 300
$imageSized = $image->SetSize(300, 300); $imageSized = $image->Pad(300, 300);
$this->assertTrue($imageSized->isSize(300, 300)); $this->assertTrue($imageSized->isSize(300, 300));
$this->assertEquals($image->Filename, $imageSized->Filename); $this->assertEquals($image->Filename, $imageSized->Filename);
// Padded image 300 x 300 (same as above) // Padded image 300 x 300 (same as above)
$imagePadded = $image->PaddedImage(300, 300); $imagePadded = $image->Pad(300, 300);
$this->assertTrue($imagePadded->isSize(300, 300)); $this->assertTrue($imagePadded->isSize(300, 300));
$this->assertEquals($image->Filename, $imagePadded->Filename); $this->assertEquals($image->Filename, $imagePadded->Filename);
@ -142,16 +153,16 @@ class ImageTest extends SapphireTest {
$this->assertTrue($imageStretched->isSize(300, 300)); $this->assertTrue($imageStretched->isSize(300, 300));
$this->assertEquals($image->Filename, $imageStretched->Filename); $this->assertEquals($image->Filename, $imageStretched->Filename);
// SetRatioSize (various options) // Fit (various options)
$imageSetRatioSize = $image->SetRatioSize(300, 600); $imageFit = $image->Fit(300, 600);
$this->assertTrue($imageSetRatioSize->isSize(300, 300)); $this->assertTrue($imageFit->isSize(300, 300));
$this->assertEquals($image->Filename, $imageSetRatioSize->Filename); $this->assertEquals($image->Filename, $imageFit->Filename);
$imageSetRatioSize = $image->SetRatioSize(600, 300); $imageFit = $image->Fit(600, 300);
$this->assertTrue($imageSetRatioSize->isSize(300, 300)); $this->assertTrue($imageFit->isSize(300, 300));
$this->assertEquals($image->Filename, $imageSetRatioSize->Filename); $this->assertEquals($image->Filename, $imageFit->Filename);
$imageSetRatioSize = $image->SetRatioSize(300, 300); $imageFit = $image->Fit(300, 300);
$this->assertTrue($imageSetRatioSize->isSize(300, 300)); $this->assertTrue($imageFit->isSize(300, 300));
$this->assertEquals($image->Filename, $imageSetRatioSize->Filename); $this->assertEquals($image->Filename, $imageFit->Filename);
} }
/** /**
@ -167,27 +178,27 @@ class ImageTest extends SapphireTest {
Config::inst()->update('Image', 'force_resample', true); Config::inst()->update('Image', 'force_resample', true);
// Set width to 300 pixels // Set width to 300 pixels
$imageSetWidth = $image->SetWidth(300); $imageScaleWidth = $image->ScaleWidth(300);
$this->assertEquals($imageSetWidth->getWidth(), 300); $this->assertEquals($imageScaleWidth->getWidth(), 300);
$this->assertNotEquals($image->Filename, $imageSetWidth->Filename); $this->assertNotEquals($image->Filename, $imageScaleWidth->Filename);
// Set height to 300 pixels // Set height to 300 pixels
$imageSetHeight = $image->SetHeight(300); $imageScaleHeight = $image->ScaleHeight(300);
$this->assertEquals($imageSetHeight->getHeight(), 300); $this->assertEquals($imageScaleHeight->getHeight(), 300);
$this->assertNotEquals($image->Filename, $imageSetHeight->Filename); $this->assertNotEquals($image->Filename, $imageScaleHeight->Filename);
// Crop image to 300 x 300 // Crop image to 300 x 300
$imageCropped = $image->CroppedImage(300, 300); $imageCropped = $image->Fill(300, 300);
$this->assertTrue($imageCropped->isSize(300, 300)); $this->assertTrue($imageCropped->isSize(300, 300));
$this->assertNotEquals($image->Filename, $imageCropped->Filename); $this->assertNotEquals($image->Filename, $imageCropped->Filename);
// Resize (padded) to 300 x 300 // Resize (padded) to 300 x 300
$imageSized = $image->SetSize(300, 300); $imageSized = $image->Pad(300, 300);
$this->assertTrue($imageSized->isSize(300, 300)); $this->assertTrue($imageSized->isSize(300, 300));
$this->assertNotEquals($image->Filename, $imageSized->Filename); $this->assertNotEquals($image->Filename, $imageSized->Filename);
// Padded image 300 x 300 (same as above) // Padded image 300 x 300 (same as above)
$imagePadded = $image->PaddedImage(300, 300); $imagePadded = $image->Pad(300, 300);
$this->assertTrue($imagePadded->isSize(300, 300)); $this->assertTrue($imagePadded->isSize(300, 300));
$this->assertNotEquals($image->Filename, $imagePadded->Filename); $this->assertNotEquals($image->Filename, $imagePadded->Filename);
@ -196,16 +207,16 @@ class ImageTest extends SapphireTest {
$this->assertTrue($imageStretched->isSize(300, 300)); $this->assertTrue($imageStretched->isSize(300, 300));
$this->assertNotEquals($image->Filename, $imageStretched->Filename); $this->assertNotEquals($image->Filename, $imageStretched->Filename);
// SetRatioSize (various options) // Fit (various options)
$imageSetRatioSize = $image->SetRatioSize(300, 600); $imageFit = $image->Fit(300, 600);
$this->assertTrue($imageSetRatioSize->isSize(300, 300)); $this->assertTrue($imageFit->isSize(300, 300));
$this->assertNotEquals($image->Filename, $imageSetRatioSize->Filename); $this->assertNotEquals($image->Filename, $imageFit->Filename);
$imageSetRatioSize = $image->SetRatioSize(600, 300); $imageFit = $image->Fit(600, 300);
$this->assertTrue($imageSetRatioSize->isSize(300, 300)); $this->assertTrue($imageFit->isSize(300, 300));
$this->assertNotEquals($image->Filename, $imageSetRatioSize->Filename); $this->assertNotEquals($image->Filename, $imageFit->Filename);
$imageSetRatioSize = $image->SetRatioSize(300, 300); $imageFit = $image->Fit(300, 300);
$this->assertTrue($imageSetRatioSize->isSize(300, 300)); $this->assertTrue($imageFit->isSize(300, 300));
$this->assertNotEquals($image->Filename, $imageSetRatioSize->Filename); $this->assertNotEquals($image->Filename, $imageFit->Filename);
Config::inst()->update('Image', 'force_resample', $origForceResample); Config::inst()->update('Image', 'force_resample', $origForceResample);
} }
@ -214,20 +225,52 @@ class ImageTest extends SapphireTest {
$this->assertTrue($image->isSize(300, 300)); $this->assertTrue($image->isSize(300, 300));
// Test normal resize // Test normal resize
$resized = $image->SetSize(150, 100); $resized = $image->Pad(150, 100);
$this->assertTrue($resized->isSize(150, 100)); $this->assertTrue($resized->isSize(150, 100));
// Test cropped resize // Test cropped resize
$cropped = $image->CroppedImage(100, 200); $cropped = $image->Fill(100, 200);
$this->assertTrue($cropped->isSize(100, 200)); $this->assertTrue($cropped->isSize(100, 200));
// Test padded resize // Test padded resize
$padded = $image->PaddedImage(200, 100); $padded = $image->Pad(200, 100);
$this->assertTrue($padded->isSize(200, 100)); $this->assertTrue($padded->isSize(200, 100));
// Test SetRatioSize // Test Fit
$ratio = $image->SetRatioSize(80, 160); $ratio = $image->Fit(80, 160);
$this->assertTrue($ratio->isSize(80, 80)); $this->assertTrue($ratio->isSize(80, 80));
// Test FitMax
$fitMaxDn = $image->FitMax(200, 100);
$this->assertTrue($fitMaxDn->isSize(100, 100));
$fitMaxUp = $image->FitMax(500, 400);
$this->assertTrue($fitMaxUp->isSize(300, 300));
//Test ScaleMax
$scaleMaxWDn = $image->ScaleMaxWidth(200);
$this->assertTrue($scaleMaxWDn->isSize(200, 200));
$scaleMaxWUp = $image->ScaleMaxWidth(400);
$this->assertTrue($scaleMaxWUp->isSize(300, 300));
$scaleMaxHDn = $image->ScaleMaxHeight(200);
$this->assertTrue($scaleMaxHDn->isSize(200, 200));
$scaleMaxHUp = $image->ScaleMaxHeight(400);
$this->assertTrue($scaleMaxHUp->isSize(300, 300));
// Test FillMax
$cropMaxDn = $image->FillMax(200, 100);
$this->assertTrue($cropMaxDn->isSize(200, 100));
$cropMaxUp = $image->FillMax(400, 200);
$this->assertTrue($cropMaxUp->isSize(300, 150));
// Test Clip
$clipWDn = $image->CropWidth(200);
$this->assertTrue($clipWDn->isSize(200, 300));
$clipWUp = $image->CropWidth(400);
$this->assertTrue($clipWUp->isSize(300, 300));
$clipHDn = $image->CropHeight(200);
$this->assertTrue($clipHDn->isSize(300, 200));
$clipHUp = $image->CropHeight(400);
$this->assertTrue($clipHUp->isSize(300, 300));
} }
/** /**
@ -236,15 +279,15 @@ class ImageTest extends SapphireTest {
public function testGenerateImageWithInvalidParameters() { public function testGenerateImageWithInvalidParameters() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle'); $image = $this->objFromFixture('Image', 'imageWithoutTitle');
$image->setHeight('String'); $image->setHeight('String');
$image->PaddedImage(600,600,'XXXXXX'); $image->Pad(600,600,'XXXXXX');
} }
public function testCacheFilename() { public function testCacheFilename() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle'); $image = $this->objFromFixture('Image', 'imageWithoutTitle');
$imageFirst = $image->SetSize(200,200); $imageFirst = $image->Pad(200,200,'CCCCCC');
$imageFilename = $imageFirst->getFullPath(); $imageFilename = $imageFirst->getFullPath();
// Encoding of the arguments is duplicated from cacheFilename // Encoding of the arguments is duplicated from cacheFilename
$neededPart = 'SetSize' . base64_encode(json_encode(array(200,200))); $neededPart = 'Pad' . base64_encode(json_encode(array(200,200,'CCCCCC')));
$this->assertContains($neededPart, $imageFilename, 'Filename for cached image is correctly generated'); $this->assertContains($neededPart, $imageFilename, 'Filename for cached image is correctly generated');
} }
@ -252,7 +295,7 @@ class ImageTest extends SapphireTest {
$image = $this->objFromFixture('Image', 'imageWithoutTitle'); $image = $this->objFromFixture('Image', 'imageWithoutTitle');
$folder = new SS_FileFinder(); $folder = new SS_FileFinder();
$imageFirst = $image->SetSize(200,200); $imageFirst = $image->Pad(200,200);
$this->assertNotNull($imageFirst); $this->assertNotNull($imageFirst);
$expected = 200; $expected = 200;
$actual = $imageFirst->getWidth(); $actual = $imageFirst->getWidth();
@ -265,7 +308,7 @@ class ImageTest extends SapphireTest {
$actual = $imageSecond->getHeight(); $actual = $imageSecond->getHeight();
$this->assertEquals($expected, $actual); $this->assertEquals($expected, $actual);
$imageThird = $imageSecond->PaddedImage(600,600,'0F0F0F'); $imageThird = $imageSecond->Pad(600,600,'0F0F0F');
// Encoding of the arguments is duplicated from cacheFilename // Encoding of the arguments is duplicated from cacheFilename
$argumentString = base64_encode(json_encode(array(600,600,'0F0F0F'))); $argumentString = base64_encode(json_encode(array(600,600,'0F0F0F')));
$this->assertNotNull($imageThird); $this->assertNotNull($imageThird);
@ -289,7 +332,7 @@ class ImageTest extends SapphireTest {
public function testRegenerateImages() { public function testRegenerateImages() {
$image = $this->objFromFixture('Image', 'imageWithMetacharacters'); $image = $this->objFromFixture('Image', 'imageWithMetacharacters');
$image_generated = $image->SetWidth(200); $image_generated = $image->ScaleWidth(200);
$p = $image_generated->getFullPath(); $p = $image_generated->getFullPath();
$this->assertTrue(file_exists($p), 'Resized image exists after creation call'); $this->assertTrue(file_exists($p), 'Resized image exists after creation call');
$this->assertEquals(1, $image->regenerateFormattedImages(), 'Cached images were regenerated correct'); $this->assertEquals(1, $image->regenerateFormattedImages(), 'Cached images were regenerated correct');
@ -298,9 +341,13 @@ class ImageTest extends SapphireTest {
$this->assertTrue(file_exists($p), 'Resized image exists after regeneration call'); $this->assertTrue(file_exists($p), 'Resized image exists after regeneration call');
} }
/**
* Tests that cached images are regenerated properly after a cached file is renamed with new arguments
* ToDo: This doesn't seem like something that is worth testing - what is the point of this?
*/
public function testRegenerateImagesWithRenaming() { public function testRegenerateImagesWithRenaming() {
$image = $this->objFromFixture('Image', 'imageWithMetacharacters'); $image = $this->objFromFixture('Image', 'imageWithMetacharacters');
$image_generated = $image->SetWidth(200); $image_generated = $image->ScaleWidth(200);
$p = $image_generated->getFullPath(); $p = $image_generated->getFullPath();
$this->assertTrue(file_exists($p), 'Resized image exists after creation call'); $this->assertTrue(file_exists($p), 'Resized image exists after creation call');
@ -311,8 +358,8 @@ class ImageTest extends SapphireTest {
$newPath = str_replace($oldArgumentString, $newArgumentString, $p); $newPath = str_replace($oldArgumentString, $newArgumentString, $p);
$newRelative = str_replace($oldArgumentString, $newArgumentString, $image_generated->getFileName()); $newRelative = str_replace($oldArgumentString, $newArgumentString, $image_generated->getFileName());
rename($p, $newPath); rename($p, $newPath);
$this->assertFalse(file_exists($p), 'Resized image does not exist after movement call under old name'); $this->assertFalse(file_exists($p), 'Resized image does not exist at old path after renaming');
$this->assertTrue(file_exists($newPath), 'Resized image exists after movement call under new name'); $this->assertTrue(file_exists($newPath), 'Resized image exists at new path after renaming');
$this->assertEquals(1, $image->regenerateFormattedImages(), $this->assertEquals(1, $image->regenerateFormattedImages(),
'Cached images were regenerated in the right number'); 'Cached images were regenerated in the right number');
@ -322,7 +369,7 @@ class ImageTest extends SapphireTest {
public function testGeneratedImageDeletion() { public function testGeneratedImageDeletion() {
$image = $this->objFromFixture('Image', 'imageWithMetacharacters'); $image = $this->objFromFixture('Image', 'imageWithMetacharacters');
$image_generated = $image->SetWidth(200); $image_generated = $image->ScaleWidth(200);
$p = $image_generated->getFullPath(); $p = $image_generated->getFullPath();
$this->assertTrue(file_exists($p), 'Resized image exists after creation call'); $this->assertTrue(file_exists($p), 'Resized image exists after creation call');
$numDeleted = $image->deleteFormattedImages(); $numDeleted = $image->deleteFormattedImages();
@ -336,11 +383,11 @@ class ImageTest extends SapphireTest {
public function testMultipleGenerateManipulationCallsImageDeletion() { public function testMultipleGenerateManipulationCallsImageDeletion() {
$image = $this->objFromFixture('Image', 'imageWithMetacharacters'); $image = $this->objFromFixture('Image', 'imageWithMetacharacters');
$firstImage = $image->SetWidth(200); $firstImage = $image->ScaleWidth(200);
$firstImagePath = $firstImage->getFullPath(); $firstImagePath = $firstImage->getFullPath();
$this->assertTrue(file_exists($firstImagePath)); $this->assertTrue(file_exists($firstImagePath));
$secondImage = $firstImage->SetHeight(100); $secondImage = $firstImage->ScaleHeight(100);
$secondImagePath = $secondImage->getFullPath(); $secondImagePath = $secondImage->getFullPath();
$this->assertTrue(file_exists($secondImagePath)); $this->assertTrue(file_exists($secondImagePath));
@ -354,11 +401,11 @@ class ImageTest extends SapphireTest {
*/ */
public function testPathPropertiesCachedImage() { public function testPathPropertiesCachedImage() {
$image = $this->objFromFixture('Image', 'imageWithMetacharacters'); $image = $this->objFromFixture('Image', 'imageWithMetacharacters');
$firstImage = $image->SetWidth(200); $firstImage = $image->ScaleWidth(200);
$firstImagePath = $firstImage->getRelativePath(); $firstImagePath = $firstImage->getRelativePath();
$this->assertEquals($firstImagePath, $firstImage->Filename); $this->assertEquals($firstImagePath, $firstImage->Filename);
$secondImage = $firstImage->SetHeight(100); $secondImage = $firstImage->ScaleHeight(100);
$secondImagePath = $secondImage->getRelativePath(); $secondImagePath = $secondImage->getRelativePath();
$this->assertEquals($secondImagePath, $secondImage->Filename); $this->assertEquals($secondImagePath, $secondImage->Filename);
} }

View File

@ -1,6 +1,6 @@
Folder: Folder:
folder1: folder1:
Filename: assets/ImageTest Filename: assets/ImageTest/
Image: Image:
imageWithTitle: imageWithTitle:
Title: This is a image Title Title: This is a image Title
@ -16,3 +16,11 @@ Image:
Title: This is a/an image Title Title: This is a/an image Title
Filename: assets/ImageTest/test_image.png Filename: assets/ImageTest/test_image.png
Parent: =>Folder.folder1 Parent: =>Folder.folder1
lowQualityJPEG:
Title: This is a low quality JPEG
Filename: assets/ImageTest/test_image_low-quality.jpg
Parent: =>Folder.folder1
highQualityJPEG:
Title: This is a high quality JPEG
Filename: assets/ImageTest/test_image_high-quality.jpg
Parent: =>Folder.folder1

View File

@ -8,22 +8,8 @@ class ImagickImageTest extends ImageTest {
return; return;
} }
parent::setUp();
Image::set_backend("ImagickBackend"); Image::set_backend("ImagickBackend");
// Create a test files for each of the fixture references parent::setUp();
$fileIDs = $this->allFixtureIDs('Image');
foreach($fileIDs as $fileID) {
$file = DataObject::get_by_id('Image', $fileID);
$image = new Imagick();
$image->newImage(300,300, new ImagickPixel("white"));
$image->setImageFormat("png");
$image->writeImage(BASE_PATH."/{$file->Filename}");
$file->write();
}
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1089,7 +1089,6 @@ after')
// Let's throw something random in there. // Let's throw something random in there.
$self->setExpectedException('InvalidArgumentException'); $self->setExpectedException('InvalidArgumentException');
$templates = SSViewer::get_templates_by_class(array()); $templates = SSViewer::get_templates_by_class(array());
$this->assertCount(0, $templates);
}); });
} }