diff --git a/.travis.yml b/.travis.yml index d8e4471e7..b09f51393 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,7 +43,7 @@ matrix: - sudo apt-get install -y tidy before_script: - - composer self-update + - composer self-update || true - phpenv rehash - 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" diff --git a/_config/config.yml b/_config/config.yml index a6fa72c8d..31dbd1682 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -5,4 +5,9 @@ Upload: # Replace an existing file rather than renaming the new one. replaceFile: false MySQLDatabase: - connection_charset: utf8 \ No newline at end of file + connection_charset: utf8 +HTTP: + cache_control: + max-age: 0 + must-revalidate: "true" + no-transform: "true" \ No newline at end of file diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index d15c46596..389f60c03 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -339,8 +339,6 @@ class LeftAndMain extends Controller implements PermissionProvider { if (Director::isDev()) Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/leaktools.js'); - HTMLEditorField::include_js(); - $leftAndMainIncludes = array_unique(array_merge( array( FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Layout.js', @@ -832,16 +830,19 @@ class LeftAndMain extends Controller implements PermissionProvider { $record = ($rootID) ? $this->getRecord($rootID) : null; $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 if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction); $obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod); // Ensure current page is exposed - // This call flushes the Hierarchy::$marked cache when the current node is deleted - // @see CMSMain::getRecord() - // This will make it impossible to show children under a deleted parent page - // if($p = $this->currentPage()) $obj->markToExpose($p); + if($currentPage) $obj->markToExpose($currentPage); // NOTE: SiteTree/CMSMain coupling :-( if(class_exists('SiteTree')) { diff --git a/admin/css/screen.css b/admin/css/screen.css index 3381ba5f4..41853c695 100644 --- a/admin/css/screen.css +++ b/admin/css/screen.css @@ -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-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; } /** 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: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-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: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; } @@ -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 .middleColumn { 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; } /** ---------------------------------------------------- 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-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 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:after, .cms-tree.jstree span.comment-count:after { content: ""; position: absolute; border-style: solid; /* reduce the damage in FF3.0 */ display: block; width: 0; bottom: -3px; /* value = - border-top-width - border-bottom-width */ left: 4px; /* value = (:before left) + (:before border-left) - (:after border-left) */ border-width: 3px 3px 0; border-color: #FFF0BC transparent; } .tree-holder.jstree .jstree-hovered, .cms-tree.jstree .jstree-hovered { text-shadow: none; text-decoration: none; } -.tree-holder.jstree .jstree-closed > ins, .cms-tree.jstree .jstree-closed > ins { background-position: 0 0; } -.tree-holder.jstree .jstree-open > ins, .cms-tree.jstree .jstree-open > ins { background-position: -20px 0; } +.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: -18px -1px; } .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-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-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 a { padding-left: 12px; } +.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-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 */ .cms-content-tools .cms-tree.jstree li { min-width: 159px; } diff --git a/admin/images/spinner.gif b/admin/images/spinner.gif index cec3f8389..bc8cf92e5 100644 Binary files a/admin/images/spinner.gif and b/admin/images/spinner.gif differ diff --git a/admin/scss/_forms.scss b/admin/scss/_forms.scss index a753745e3..2f2f2dd9f 100644 --- a/admin/scss/_forms.scss +++ b/admin/scss/_forms.scss @@ -681,6 +681,10 @@ form.small .field, .field.small { label { float: none; margin-left: 0; + + &.ss-ui-button { + float: left; + } } .description { margin-left: 0; diff --git a/admin/scss/_style.scss b/admin/scss/_style.scss index 8cc272c47..d8a33d823 100644 --- a/admin/scss/_style.scss +++ b/admin/scss/_style.scss @@ -1520,6 +1520,11 @@ body.cms-dialog { .step2 { margin-bottom: $grid-x*2; } + .ss-uploadfield { + .middleColumn { + width: auto; + } + } } .htmleditorfield-mediaform { diff --git a/admin/scss/_tree.scss b/admin/scss/_tree.scss index 0885b85ca..35c89e729 100644 --- a/admin/scss/_tree.scss +++ b/admin/scss/_tree.scss @@ -542,10 +542,10 @@ text-decoration: none; } .jstree-closed > ins { - background-position:0 0; + background-position:2px -1px; } .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 -.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; + } - .jstree-hovered, - .jstree-clicked, - a:focus { - padding-left: 3px; - } + .jstree-hovered, + .jstree-clicked, + a:focus { + padding-left: 0; + } - .jstree-hovered, - .jstree-clicked, - a:focus { - .jstree-icon { - display: block; - } + .jstree-hovered, + .jstree-clicked, + a:focus { + .jstree-icon { + display: block; } } } @@ -592,7 +590,7 @@ .jstree-default-rtl a .jstree-icon, .jstree-classic a .jstree-icon, .jstree-apple a .jstree-icon { - background-position:-62px -19px; + background-position:-60px -19px; } /* ensure status is visible in sidebar */ diff --git a/control/CookieJar.php b/control/CookieJar.php index 8e7211375..ab4912558 100644 --- a/control/CookieJar.php +++ b/control/CookieJar.php @@ -3,7 +3,7 @@ /** * 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 * * @todo Create a config array for defaults (eg: httpOnly, secure, path, domain, expiry) @@ -14,7 +14,7 @@ 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) * * @var array Existing cookies sent by the browser @@ -30,7 +30,7 @@ class CookieJar implements Cookie_Backend { 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 * * @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 - * "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) * * @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 * + * 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 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])) { return $cookies[$name]; } + + //Normalise cookie names by replacing '.' with '_' + $safeName = str_replace('.', '_', $name); + if (isset($cookies[$safeName])) { + return $cookies[$safeName]; + } } /** diff --git a/control/HTTP.php b/control/HTTP.php index 83b5e6310..9993c8e52 100644 --- a/control/HTTP.php +++ b/control/HTTP.php @@ -330,10 +330,15 @@ class HTTP { // Popuplate $responseHeaders with all the headers that we want to build $responseHeaders = array(); + $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 if(function_exists('apache_request_headers') && $config->get(get_called_class(), 'cache_ajax_requests')) { $requestHeaders = apache_request_headers(); + if(isset($requestHeaders['X-Requested-With']) && $requestHeaders['X-Requested-With']=='XMLHttpRequest') { $cacheAge = 0; } @@ -344,7 +349,7 @@ class HTTP { } if($cacheAge > 0) { - $responseHeaders["Cache-Control"] = "max-age={$cacheAge}, must-revalidate, no-transform"; + $cacheControlHeaders['max-age'] = self::$cache_age; $responseHeaders["Pragma"] = ""; // 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 // (http://support.microsoft.com/kb/323308) // 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"] = ""; } 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; } } diff --git a/docs/en/00_Getting_Started/03_Environment_Management.md b/docs/en/00_Getting_Started/03_Environment_Management.md index 5b6f2e943..b3f50ad7e 100644 --- a/docs/en/00_Getting_Started/03_Environment_Management.md +++ b/docs/en/00_Getting_Started/03_Environment_Management.md @@ -105,21 +105,21 @@ This is my `_ss_environment.php` file. I have it placed in `/var`, as each of th | 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* | -| `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_USERNAME`| The database username (mandatory)| -| `SS_DATABASE_PASSWORD`| The database password (mandatory)| -| `SS_DATABASE_PORT`| The database port| +| `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_SERVER`| The database server to use, defaulting to localhost.| +| `SS_DATABASE_USERNAME`| The database username (mandatory).| +| `SS_DATABASE_PASSWORD`| The database password (mandatory).| +| `SS_DATABASE_PORT`| The database port.| | `SS_DATABASE_SUFFIX`| A suffix 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_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_DEFAULT_ADMIN_USERNAME`| The username of the default admin. This is a user with administrative privileges.| | `SS_DEFAULT_ADMIN_PASSWORD`| The password of the default admin. This will not be stored in the database.| | `SS_USE_BASIC_AUTH`| Protect the site with basic auth (good for test sites).
When using CGI/FastCGI with Apache, you will have to add the `RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]` rewrite rule to your `.htaccess` file| -| `SS_SEND_ALL_EMAILS_TO`| If you set this define, all emails will be redirected to this address.| -| `SS_SEND_ALL_EMAILS_FROM`| If you set this define, all emails will be send from this address.| -| `SS_ERROR_LOG` | Relative path to the log file | +| `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 define this constant, all emails will be sent from this address.| +| `SS_ERROR_LOG` | Relative path to the log file. | diff --git a/docs/en/01_Tutorials/02_Extending_A_Basic_Site.md b/docs/en/01_Tutorials/02_Extending_A_Basic_Site.md index 499dea2be..36b484a3c 100644 --- a/docs/en/01_Tutorials/02_Extending_A_Basic_Site.md +++ b/docs/en/01_Tutorials/02_Extending_A_Basic_Site.md @@ -512,7 +512,7 @@ The staff section templates aren't too difficult to create, thanks to the utilit <% loop $Children %>

$Title

- $Photo.SetWidth(150) + $Photo.ScaleWidth(150)

$Content.FirstParagraph

Read more >>
@@ -521,7 +521,7 @@ The staff section templates aren't too difficult to create, thanks to the utilit -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 resize the image every time the page is viewed. @@ -537,13 +537,13 @@ The *StaffPage* template is also very straight forward.

$Title

- $Photo.SetWidth(433) + $Photo.ScaleWidth(433) $Content
$Form -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. ![](../_images/tutorial2_einstein.jpg) diff --git a/docs/en/01_Tutorials/index.md b/docs/en/01_Tutorials/index.md index de5afce94..8403c74e9 100644 --- a/docs/en/01_Tutorials/index.md +++ b/docs/en/01_Tutorials/index.md @@ -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) * [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 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) @@ -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 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 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 diff --git a/docs/en/02_Developer_Guides/03_Forms/Field_types/03_HTMLEditorField.md b/docs/en/02_Developer_Guides/03_Forms/Field_types/03_HTMLEditorField.md index 3ef4c9421..13b276dde 100644 --- a/docs/en/02_Developer_Guides/03_Forms/Field_types/03_HTMLEditorField.md +++ b/docs/en/02_Developer_Guides/03_Forms/Field_types/03_HTMLEditorField.md @@ -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 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()`.
-By default, a config named 'cms' is used in any field created throughout the CMS interface.
diff --git a/docs/en/02_Developer_Guides/05_Extending/How_Tos/03_Track_member_logins.md b/docs/en/02_Developer_Guides/05_Extending/How_Tos/03_Track_member_logins.md index cf2eaa688..c3a00d601 100644 --- a/docs/en/02_Developer_Guides/05_Extending/How_Tos/03_Track_member_logins.md +++ b/docs/en/02_Developer_Guides/05_Extending/How_Tos/03_Track_member_logins.md @@ -37,7 +37,7 @@ explicitly logging in or by invoking the "remember me" functionality. DB::query(sprintf( 'UPDATE "Member" SET "LastVisited" = %s, "NumVisit" = "NumVisit" + 1 WHERE "ID" = %d', - DB::getConn()->now(), + DB::get_conn()->now(), $this->owner->ID )); } diff --git a/docs/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md b/docs/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md index ed8d69764..9bae22450 100644 --- a/docs/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md +++ b/docs/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md @@ -15,7 +15,7 @@ how you can load default records into the test database. /** * Defines the fixture file to use for this test class * - / + */ 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:SapphireTest] -* [api:FunctionalTest] \ No newline at end of file +* [api:FunctionalTest] diff --git a/docs/en/02_Developer_Guides/09_Security/04_Secure_Coding.md b/docs/en/02_Developer_Guides/09_Security/04_Secure_Coding.md index 29bb57bd5..974791c93 100644 --- a/docs/en/02_Developer_Guides/09_Security/04_Secure_Coding.md +++ b/docs/en/02_Developer_Guides/09_Security/04_Secure_Coding.md @@ -27,7 +27,7 @@ come from user input. Example: :::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 = 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()') ->execute(); - DB::preparedQuery( + DB::prepared_query( 'INSERT INTO "MyClass" ("Name", "Position", "Age", "Created") VALUES(?, ?, GREATEST(0,?,?), NOW())' array('Daniel', 'Accountant', 24, 28) ); @@ -100,7 +100,7 @@ and [datamodel](/developer_guides/model) for ways to parameterise, cast, and con * `SQLQuery` * `DB::query()` -* `DB::preparedQuery()` +* `DB::prepared_query()` * `Director::urlParams()` * `Controller->requestParams`, `Controller->urlParams` * `SS_HTTPRequest` data diff --git a/docs/en/02_Developer_Guides/14_Files/01_File_Management.md b/docs/en/02_Developer_Guides/14_Files/01_File_Management.md new file mode 100644 index 000000000..f1d957a29 --- /dev/null +++ b/docs/en/02_Developer_Guides/14_Files/01_File_Management.md @@ -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. \ No newline at end of file diff --git a/docs/en/02_Developer_Guides/14_Files/01_Image.md b/docs/en/02_Developer_Guides/14_Files/01_Image.md deleted file mode 100644 index bd3b4537a..000000000 --- a/docs/en/02_Developer_Guides/14_Files/01_Image.md +++ /dev/null @@ -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]` diff --git a/docs/en/02_Developer_Guides/14_Files/02_Images.md b/docs/en/02_Developer_Guides/14_Files/02_Images.md new file mode 100644 index 000000000..46babf387 --- /dev/null +++ b/docs/en/02_Developer_Guides/14_Files/02_Images.md @@ -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 + + +### 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]` diff --git a/docs/en/02_Developer_Guides/14_Files/index.md b/docs/en/02_Developer_Guides/14_Files/index.md index daff8a2cb..4c2c14c9b 100644 --- a/docs/en/02_Developer_Guides/14_Files/index.md +++ b/docs/en/02_Developer_Guides/14_Files/index.md @@ -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: - -| 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. \ No newline at end of file +* [api:File] +* [api:Image] +* [api:Folder] \ No newline at end of file diff --git a/docs/en/02_Developer_Guides/16_Execution_Pipeline/index.md b/docs/en/02_Developer_Guides/16_Execution_Pipeline/index.md index b9b7ea418..8d8ca4b6f 100644 --- a/docs/en/02_Developer_Guides/16_Execution_Pipeline/index.md +++ b/docs/en/02_Developer_Guides/16_Execution_Pipeline/index.md @@ -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. 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). 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 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). The ["Request Filters" documentation](../controllers/requestfilters) shows you how. diff --git a/docs/en/04_Changelogs/3.2.0.md b/docs/en/04_Changelogs/3.2.0.md index a03bb09b0..53f84e11d 100644 --- a/docs/en/04_Changelogs/3.2.0.md +++ b/docs/en/04_Changelogs/3.2.0.md @@ -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 - * `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 `
` 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. +## Major changes -#### 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. - * `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. - * `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. +## Deprecated classes/methods removed + +* `ToggleField` was deprecated in 3.1, and has been removed. Use custom Javascript with `ReadonlyField` instead. +* `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. +* `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 - * `Cookie::forceExpiry()` was removed. Use `Cookie::force_expiry()` instead - * `Object` statics removal: `get_static()`, `set_static()`, `uninherited_static()`, `combined_static()`, +* `Cookie::forceExpiry()` was removed. Use `Cookie::force_expiry()` instead +* `Object` statics removal: `get_static()`, `set_static()`, `uninherited_static()`, `combined_static()`, `addStaticVars()` and `add_static_var()` removed. Use the Config methods 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 +* `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 statics on the class directly. - * `DataList::getRange()` removed. Use `limit()` instead. - * `SQLMap` removed. Call `map()` on a `DataList` or use `SS_Map` directly 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')` - * `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. +* `DataList::getRange()` removed. Use `limit()` instead. +* `SQLMap` removed. Call `map()` on a `DataList` or use `SS_Map` directly 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')` +* `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. Use `set*()` and `add*()` methods instead. - * Template `<% control $MyList %>` syntax removed. Use `<% loop $MyList %>` instead. - * Object::singleton() method for better type-friendly singleton generation +* Template `<% control $MyList %>` syntax removed. Use `<% loop $MyList %>` instead. +* 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 `
` 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 - -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() { - ... - } - ... - } +## Upgrading Notes ### 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), use `setDisplayFolderName()` with a folder path relative to `assets/`: + + :::php UploadField::create('MyField')->setDisplayFolderName('Uploads'); + ### UploadField won't display an overwrite warning unless Upload:replaceFile is true The configuration setting `UploadField:overwriteWarning` is dependent on `Upload:replaceFile` @@ -84,6 +216,7 @@ To display a warning before overwriting a file: Via config: + ::yaml Upload: # 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) overwriteWarning: true + Or per instance: + ::php $uploadField->getUpload()->setReplaceFile(true); $uploadField->setOverwriteWarning(true); + ### File.allowed_extensions restrictions 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 `Cookie::set()`. -### Bugfixes - * Migration of code to use new parameterised framework +### API: Removed URL routing by controller name -### 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 - 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` - * 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 +Please access any custom controllers exclusively through self-defined +[routes](/reference/director). For controllers extending `Page_Controller`, +simply use the provided page URLs. -## Bugfixes - * 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. + :::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: + + + :::html +
+ + +Now: + + + :::html + +
+ + + +#### Namespaced FormField ID's + +Form Field ID values will now be namespaced with the parent form ID. + +Before: + + + :::html +
+ + +Now: + + + :::html +
+ + +#### 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 +
+ + + +After: + + + :::html +
+ + +#### 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 @@ -173,6 +434,7 @@ although now a new SQLDelete object should be created from the original SQLQuery Before: + :::php setDelete(true); $query->execute(); + After: + :::php setWhere(array('"SiteTree"."ShowInMenus"' => 0)); $query->execute(); + Alternatively: + :::php toDelete(); $query->execute(); + ### Update code that interacts with SQL strings to use parameters 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: -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 - 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 - $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 + where('"Name" = \''.Convert::raw2sql($name).'\''); + $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 - setFrom('"SiteTree"') - ->setWhere(array("\"SiteTree\".\"Title\" LIKE '" . Convert::raw2sql($argument) . "'")); + :::php + sql(); - $sql = preg_replace('/LIKE \'(.+)\'/', 'LIKE \'%${1}%\'', $sql); // Adds %% around the argument + $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 + ); - // Pass new query to database connector - DB::query($sql); - After: +#### 3. Interaction with `DataList::sql()`, `DataQuery::sql()`, `SQLQuery::sql()`, or `SQLQuery::getJoins()` methods - :::php - setFrom('"SiteTree"') - ->setWhere(array('"SiteTree"."Title" LIKE ?' => $argument)); +User code that assumes parameterless queries will likely fail, and need to be +updated to handle this case properly. - // Inspect elements of the query - $sql = $query->sql($parameters); - foreach($parameters as $key => $value) { - // Adds %% around arguments - $parameters[$key] = "%{$value}%"; +Before: + + + :::php + 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 + 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 + 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 + getWhere(); + $new = array(); + foreach($conditions as $condition) { + if(preg_match('/\"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 - may be forced to be cast as a certain type (where supported by the current API). +After: - E.g. - :::php - 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 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: + :::php endSchemaUpdate(); + After: + :::php '"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. diff --git a/docs/en/04_Changelogs/rc/3.2.0.md b/docs/en/04_Changelogs/rc/3.2.0.md deleted file mode 100644 index 2f6f57909..000000000 --- a/docs/en/04_Changelogs/rc/3.2.0.md +++ /dev/null @@ -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: - - - Now: - -
- -#### Namespaced FormField ID's - -Form Field ID values will now be namespaced with the parent form ID. - - Before: -
- - Now: -
- -#### 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: -
- - - After: -
- -#### 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 \ No newline at end of file diff --git a/docs/en/05_Contributing/03_Documentation.md b/docs/en/05_Contributing/03_Documentation.md index f8e4ecf18..d920a3677 100644 --- a/docs/en/05_Contributing/03_Documentation.md +++ b/docs/en/05_Contributing/03_Documentation.md @@ -3,45 +3,36 @@ summary: Writing guide for contributing to SilverStripe developer and CMS user h # Contributing documentation -Documentation for a software project is a continued and collaborative effort, we encourage everybody to contribute, from -simply fixing spelling mistakes, to writing recipes, reviewing existing documentation, and translating the whole thing. +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. Modifying documentation requires basic [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) and [Markdown](http://daringfireball.net/projects/markdown/) knowledge, and a GitHub user account. ## 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 -page you want to edit. Alternatively, you can find the appropriate .md file in the -[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**. +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, locate the appropriate .md file in the +[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**. - * After you have made your change, describe it in the "commit summary" and "extended description" fields below, and - press "Commit Changes". - * 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 - will be sent to the core committers for approval. + * After editing the documentation, describe your changes in the "commit summary" and "extended description" fields below then 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 + * documentation changes are for and then submit the form. Your changes will be sent to the core committers for approval.
-You should make the 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*. +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*.
## 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-cms](http://github.com/silverstripe/silverstripe-cms) repositories and send us -"[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. +[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. -The documentation is kept alongside the source code in the `docs/` subfolder of any SilverStripe module, framework or -CMS folder. +The documentation is kept alongside the source code in the `docs/` subfolder of any SilverStripe module, framework or CMS folder.
-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. +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.
## Repositories @@ -52,44 +43,38 @@ documentation. This helps prevent our documentation from getting out of date. ## Source control -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)). +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)). ## What to write -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, and -[producing OSS: "documentation"](http://producingoss.com/en/getting-started.html#documentation) for good rules of thumb +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 for documenting open source software. ## Structure * 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 -of text somewhere? +* 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? * 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. -* Provide context: Give API documentation 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 -inaccessible island. Your page should at least be linked on the index page in the same folder. It can also appear +* API and developer guides are two forms of source code documentation that complement each other. +* API documentation should provide context, ie, the "bigger picture", by referring to developer guides inside your PHPDoc. +* 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 as "related content" on other resource (e.g. `/tutorials/site_search` might link to `/developer_guides/forms/introduction`). ## 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. * Write in an active and direct voice. -* Mark up correctly: Use preformatted text, emphasis and bold to make technical writing more "scannable". -* Avoid FAQs: FAQs are not a replacement of a coherent, well explained documentation. If you've done a good job +* Mark up correctly: Use preformatted text. Emphasis and bold make technical writing more easily "scannable". +* 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. -* "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 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. -* URLs: is the end of your sentence is a URL, 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. +* 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 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 @@ -97,8 +82,7 @@ There are several built-in block styles for highlighting a paragraph of text. Pl sparingly.
-"Tip box": Adds, deepens or accents information in the main text. Can be used for background knowledge, or "see also" -links. +"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).
Code: @@ -108,8 +92,7 @@ Code:
-"Notification box": Technical notifications relating to the main text. For example, notifying users about a deprecated -feature. +"Notification box": A notification box is good for technical notifications relating to the main text. For example, notifying users about a deprecated feature.
Code: @@ -119,8 +102,7 @@ Code:
-"Warning box": Highlight a severe bug or technical issue requiring a users attention. For example, a code block with -destructive functionality might not have its URL actions secured to keep the code shorter. +"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.
Code: @@ -129,14 +111,12 @@ Code: ...
-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. ## Translating documentation -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 or the whole body of documentation. German documentation -would for example live in `framework/docs/de/`. The +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 [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. diff --git a/docs/en/_images/image-methods.jpg b/docs/en/_images/image-methods.jpg new file mode 100644 index 000000000..c1a2e8b00 Binary files /dev/null and b/docs/en/_images/image-methods.jpg differ diff --git a/filesystem/File.php b/filesystem/File.php index ce428b4f3..35bd2da18 100644 --- a/filesystem/File.php +++ b/filesystem/File.php @@ -368,7 +368,7 @@ class File extends DataObject { // Preview if($this instanceof Image) { $formattedImage = $this->getFormattedImage( - 'SetWidth', + 'ScaleWidth', Config::inst()->get('Image', 'asset_preview_width') ); $thumbnail = $formattedImage ? $formattedImage->URL : ''; @@ -917,7 +917,7 @@ class File extends DataObject { return $labels; } - public function validate() { + protected function validate() { if($this->config()->apply_restrictions_to_admin || !Permission::check('ADMIN')) { // Extension validation // TODO Merge this with Upload_Validator diff --git a/filesystem/Folder.php b/filesystem/Folder.php index 5d34a9511..630128493 100644 --- a/filesystem/Folder.php +++ b/filesystem/Folder.php @@ -326,7 +326,7 @@ class Folder extends File { } } - public function validate() { + protected function validate() { return new ValidationResult(true); } diff --git a/filesystem/Upload.php b/filesystem/Upload.php index 56b733901..8a59b6fa6 100644 --- a/filesystem/Upload.php +++ b/filesystem/Upload.php @@ -285,9 +285,11 @@ class Upload extends Controller { /** * Clear out all errors (mostly set by {loadUploaded()}) + * including the validator's errors */ public function clearErrors() { $this->errors = array(); + $this->validator->clearErrors(); } /** @@ -367,6 +369,13 @@ class Upload_Validator { return $this->errors; } + /** + * Clear out all errors + */ + public function clearErrors() { + $this->errors = array(); + } + /** * Set information about temporary file produced by PHP. * @param array $tmpFile diff --git a/forms/EmailField.php b/forms/EmailField.php index 8e540c0f6..b1593bcb2 100644 --- a/forms/EmailField.php +++ b/forms/EmailField.php @@ -1,54 +1,59 @@ '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.ietf.org/rfc/rfc2822.txt * * @param Validator $validator - * @return String + * + * @return string */ public function validate($validator) { $this->value = trim($this->value); - $pcrePattern = '^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*' - . '@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[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])?$'; - // PHP uses forward slash (/) to delimit start/end of pattern, so it must be escaped - $pregSafePattern = str_replace('/', '\\/', $pcrePattern); + // Escape delimiter characters. + $safePattern = str_replace('/', '\\/', $pattern); - if($this->value && !preg_match('/' . $pregSafePattern . '/i', $this->value)){ + if($this->value && !preg_match('/' . $safePattern . '/i', $this->value)) { $validator->validationError( $this->name, - _t('EmailField.VALIDATION', "Please enter an email address"), - "validation" + _t('EmailField.VALIDATION', 'Please enter an email address'), + 'validation' ); - return false; - } else{ - return true; - } - } + return false; + } + + return true; + } } diff --git a/forms/Form.php b/forms/Form.php index 6f0469e38..647389156 100644 --- a/forms/Form.php +++ b/forms/Form.php @@ -35,7 +35,7 @@ * "admin/EditForm". This URL will render the form without its surrounding * 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. * For example, the "URLSegment" field in a standard CMS form would be * accessible through "admin/EditForm/field/URLSegment/FieldHolder". @@ -54,19 +54,34 @@ class Form extends RequestHandler { */ public $IncludeFormTag = true; + /** + * @var FieldList|null + */ protected $fields; + /** + * @var FieldList|null + */ protected $actions; /** - * @var Controller + * @var Controller|null */ protected $controller; + /** + * @var string|null + */ protected $name; + /** + * @var Validator|null + */ protected $validator; + /** + * @var string + */ protected $formMethod = "POST"; /** @@ -74,16 +89,21 @@ class Form extends RequestHandler { */ protected $strictFormMethodCheck = false; + /** + * @var string|null + */ protected static $current_action; /** - * @var Dataobject $record Populated by {@link loadDataFrom()}. + * @var DataObject|null $record Populated by {@link loadDataFrom()}. */ protected $record; /** * Keeps track of whether this form has a default action or not. * Set to false by $this->disableDefaultAction(); + * + * @var boolean */ protected $hasDefaultAction = true; @@ -92,7 +112,7 @@ class Form extends RequestHandler { * Useful to open a new window upon * form submission. * - * @var string + * @var string|null */ protected $target; @@ -101,7 +121,7 @@ class Form extends RequestHandler { * element before the
* in Form.ss template. * - * @var string + * @var string|null */ protected $legend; @@ -111,14 +131,23 @@ class Form extends RequestHandler { * another template for customisation. * * @see Form->setTemplate() - * @var string + * @var string|null */ protected $template; + /** + * @var callable|null + */ protected $buttonClickedFunc; + /** + * @var string|null + */ protected $message; + /** + * @var string|null + */ protected $messageType; /** @@ -129,10 +158,13 @@ class Form extends RequestHandler { */ protected $redirectToFormOnValidationError = false; + /** + * @var bool + */ protected $security = true; /** - * @var SecurityToken + * @var SecurityToken|null */ protected $securityToken = null; @@ -148,7 +180,7 @@ class Form extends RequestHandler { private static $default_classes = array(); /** - * @var string + * @var string|null */ protected $encType; @@ -184,11 +216,16 @@ class Form extends RequestHandler { */ private $formActionPath = false; + /** + * @var bool + */ + protected $securityTokenAdded = false; + /** * 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 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 $actions All of the action buttons in the form - a {@link FieldLis} of * {@link FormAction} objects @@ -239,6 +276,9 @@ class Form extends RequestHandler { $this->setupDefaultClasses(); } + /** + * @var array + */ private static $url_handlers = array( 'field/$FieldName!' => 'handleField', 'POST ' => 'httpSubmission', @@ -249,6 +289,8 @@ class Form extends RequestHandler { /** * Set up current form errors in session to * the current form if appropriate. + * + * @return $this */ public function setupFormErrors() { $errorInfo = Session::get("FormInfo.{$this->FormName()}"); @@ -294,6 +336,9 @@ class Form extends RequestHandler { * Populates the form with {@link loadDataFrom()}, calls {@link validate()}, * and only triggers the requested form action/method * if the form is valid. + * + * @param SS_HTTPRequest $request + * @throws SS_HTTPResponse_Exception */ public function httpSubmission($request) { // 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()){ $funcName = $defaultAction->actionName(); } @@ -427,6 +472,10 @@ class Form extends RequestHandler { return $this->httpError(404); } + /** + * @param string $action + * @return bool + */ public function checkAccessAction($action) { return ( parent::checkAccessAction($action) @@ -444,7 +493,7 @@ class Form extends RequestHandler { * Returns the appropriate response up the controller chain * if {@link validate()} fails (which is checked prior to executing any form actions). * 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}. * * @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 * + * @param SS_List|array $fields + * @param callable $funcName * @return FormField */ 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 * 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) { $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 * 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) { 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) { $newFields = new FieldList(); foreach($this->fields as $field) { @@ -591,8 +650,10 @@ class Form extends RequestHandler { /** * Set the {@link Validator} on this form. + * @param Validator $validator + * @return $this */ - public function setValidator( Validator $validator ) { + public function setValidator(Validator $validator ) { if($validator) { $this->validator = $validator; $this->validator->setForm($this); @@ -629,6 +690,7 @@ class Form extends RequestHandler { /** * Convert this form to another format. + * @param FormTransformation $format */ public function transformTo(FormTransformation $format) { $newFields = new FieldList(); @@ -702,6 +764,7 @@ class Form extends RequestHandler { * Setter for the form fields. * * @param FieldList $fields + * @return $this */ public function setFields($fields) { $this->fields = $fields; @@ -721,6 +784,7 @@ class Form extends RequestHandler { * Setter for the form actions. * * @param FieldList $actions + * @return $this */ public function setActions($actions) { $this->actions = $actions; @@ -736,8 +800,9 @@ class Form extends RequestHandler { } /** - * @param String - * @param String + * @param string $name + * @param string $value + * @return $this */ public function setAttribute($name, $value) { $this->attributes[$name] = $value; @@ -745,12 +810,15 @@ class Form extends RequestHandler { } /** - * @return String + * @return string $name */ public function getAttribute($name) { if(isset($this->attributes[$name])) return $this->attributes[$name]; } + /** + * @return array + */ public function getAttributes() { $attrs = array( 'id' => $this->FormName(), @@ -774,9 +842,10 @@ class Form extends RequestHandler { /** * 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. - * @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) { $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 - */ + */ public function setTemplateHelper($helper) { $this->templateHelper = $helper; } @@ -855,8 +925,7 @@ class Form extends RequestHandler { * contents in a new window or refreshing another frame. * * @param target $target The value of the target - * - * @return FormField + * @return $this */ public function setTarget($target) { $this->target = $target; @@ -867,6 +936,8 @@ class Form extends RequestHandler { /** * Set the legend value to be inserted into * the element in the Form.ss template. + * @param string $legend + * @return $this */ public function setLegend($legend) { $this->legend = $legend; @@ -878,6 +949,7 @@ class Form extends RequestHandler { * to render with. The default is "Form". * * @param string $template The name of the template (without the .ss extension) + * @return $this */ public function setTemplate($template) { $this->template = $template; @@ -921,7 +993,8 @@ class Form extends RequestHandler { * Sets the form encoding type. The most common encoding types are defined * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}. * - * @param string $enctype + * @param string $encType + * @return $this */ public function setEncType($encType) { $this->encType = $encType; @@ -961,8 +1034,9 @@ class Form extends RequestHandler { /** * Set the form method: GET, POST, PUT, DELETE. * - * @param $method string - * @param $strict If non-null, pass value to {@link setStrictFormMethodCheck()}. + * @param string $method + * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}. + * @return $this */ public function setFormMethod($method, $strict = null) { $this->formMethod = strtoupper($method); @@ -981,6 +1055,7 @@ class Form extends RequestHandler { * form. * * @param $bool boolean + * @return $this */ public function setStrictFormMethodCheck($bool) { $this->strictFormMethodCheck = (bool)$bool; @@ -1017,9 +1092,8 @@ class Form extends RequestHandler { * recommended only for situations where you have two relatively distinct * parts of the system trying to communicate via a form post. * - * @param string - * - * @return Form + * @param string $path + * @return $this */ public function setFormAction($path) { $this->formActionPath = $path; @@ -1040,8 +1114,7 @@ class Form extends RequestHandler { * Set the HTML ID attribute of the form. * * @param string $id - * - * @return FormField + * @return $this */ public function setHTMLID($id) { $this->htmlID = $id; @@ -1123,8 +1196,9 @@ class Form extends RequestHandler { } /** - * The next functions store and modify the forms message attributes. - * messages are stored in session under $_SESSION[formname][message]; + * The next functions store and modify the forms + * message attributes. messages are stored in session under + * $_SESSION[formname][message]; * * @return string */ @@ -1159,12 +1233,13 @@ class Form extends RequestHandler { /** * Set a status message for the form. - * + * * @param string $message the text of the message * @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. * In that case, you might want to use {@link Convert::raw2xml()} to escape any * user supplied data in the message. + * @return $this */ public function setMessage($message, $type, $escapeHtml = true) { $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. - * + * * @param string $message the text of the message * @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. @@ -1183,7 +1258,7 @@ class Form extends RequestHandler { */ public function sessionMessage($message, $type, $escapeHtml = true) { Session::set( - "FormInfo.{$this->FormName()}.formError.message", + "FormInfo.{$this->FormName()}.formError.message", $escapeHtml ? Convert::raw2xml($message) : $message ); 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) { Session::set( - "FormInfo.{$formName}.formError.message", + "FormInfo.{$formName}.formError.message", $escapeHtml ? Convert::raw2xml($message) : $message ); 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 * potential existing values. * - * Passed data should not be escaped, and is saved to the FormField - * instances unescaped. + * Passed data should not be escaped, and is saved to the FormField instances unescaped. + * Escaping happens automatically on saving the data through {@link saveInto()}. * * Escaping happens automatically on saving the data through * {@link saveInto()}. @@ -1293,8 +1368,7 @@ class Form extends RequestHandler { * * @param array|DataObject $data * @param int $mergeStrategy - * For every field, {@link $data} is interogated whether it contains a - * relevant property/key, and + * For every field, {@link $data} is interrogated whether it contains a relevant property/key, and * 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 @@ -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 * 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. * @return Form */ @@ -1338,7 +1412,7 @@ class Form extends RequestHandler { if($dataFields) foreach($dataFields as $field) { $name = $field->getName(); - // Skip fields that have been exlcuded + // Skip fields that have been excluded if($fieldList && !in_array($name, $fieldList)) continue; // 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. * It will make use of setCastedField() to do this. * - * @param $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 DataObjectInterface $dataObject The object to save data into + * @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. */ public function saveInto(DataObjectInterface $dataObject, $fieldList = null) { @@ -1425,11 +1499,11 @@ class Form extends RequestHandler { /** * Get the submitted data from this form through - * {@link FieldList->dataFields()}, which filters out any form-specific data - * like form-actions. - * - * Calls {@link FormField->dataValue()} on each field, which returns a value - * suitable for insertion into a DataObject property. + * {@link FieldList->dataFields()}, which filters out + * any form-specific data like form-actions. + * Calls {@link FormField->dataValue()} on each field, + * which returns a value suitable for insertion into a DataObject + * property. * * @return array */ @@ -1451,11 +1525,8 @@ class Form extends RequestHandler { /** * Call the given method on the given field. * - * This is used by Ajax-savvy form fields. By putting '&action=callfieldmethod' - * to the end of the form action, they can access server-side data. - * - * @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] + * @param array $data + * @return mixed */ public function callfieldmethod($data) { $fieldName = $data['fieldName']; @@ -1478,7 +1549,6 @@ class Form extends RequestHandler { } else { 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 - * string. - * - * You can pass either an SSViewer or a template name. - * - * @param SSViewer|string $template - * - * @return HTML + * Render this form using the given template, and return the result as a string + * You can pass either an SSViewer or a template name + * @param string|array $template + * @return HTMLText */ public function renderWithoutActionButton($template) { $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 - * {@link Controller} + * Sets the button that was clicked. This should only be called by the Controller. * - * @param string $funcName The name of the action method that will be called - * - * @return Form + * @param callable $funcName The name of the action method that will be called. + * @return $this */ public function setButtonClicked($funcName) { $this->buttonClickedFunc = $funcName; @@ -1713,9 +1777,8 @@ class Form extends RequestHandler { * be added by delimiting a string with spaces. * * @param string $class A string containing a classname or several class - * names delimited by a single space. - * - * @return Form + * names delimited by a single space. + * @return $this */ public function addExtraClass($class) { //split at white space @@ -1732,6 +1795,7 @@ class Form extends RequestHandler { * be passed through as a space delimited string * * @param string $class + * @return $this */ public function removeExtraClass($class) { //split at white space @@ -1763,8 +1827,11 @@ class Form extends RequestHandler { /** * 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 * your unit test. + * @throws SS_HTTPResponse_Exception */ public function testSubmission($action, $data) { $data['action_' . $action] = true; @@ -1774,6 +1841,9 @@ class Form extends RequestHandler { /** * 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 * your unit test. */ @@ -1797,10 +1867,9 @@ class Form_FieldMap extends ViewableData { } /** - * Ensure that all potential method calls get passed to __call(), therefore - * to dataFieldByName. - * - * @param string + * Ensure that all potential method calls get passed to __call(), therefore to dataFieldByName + * @param string $method + * @return bool */ public function hasMethod($method) { return true; diff --git a/forms/FormField.php b/forms/FormField.php index 448f5b393..718a9c50b 100644 --- a/forms/FormField.php +++ b/forms/FormField.php @@ -919,7 +919,7 @@ class FormField extends RequestHandler { * Validation method each {@link FormField} subclass should implement, * determining whether the field is valid or not based on the value. * - * @param Validator + * @param Validator $validator * @return boolean */ public function validate($validator) { diff --git a/forms/HeaderField.php b/forms/HeaderField.php index 1dc205d80..705e5b71e 100644 --- a/forms/HeaderField.php +++ b/forms/HeaderField.php @@ -1,4 +1,5 @@ to
HTML tag. Default: 2 + * The level of the

to

HTML tag. + * + * @var int */ protected $headingLevel = 2; + /** + * @param string $name + * @param null|string $title + * @param int $headingLevel + */ public function __construct($name, $title = null, $headingLevel = 2) { - // legacy handling for old parameters: $title, $heading, ... - // instead of new handling: $name, $title, $heading, ... + // legacy handling: + // $title, $headingLevel... $args = func_get_args(); + if(!isset($args[1]) || is_numeric($args[1])) { - $title = (isset($args[0])) ? $args[0] : null; - // Use "HeaderField(title)" as the default field name for a HeaderField; if it's just set to title then we - // risk causing accidental duplicate-field creation. - // this means i18nized fields won't be easily accessible through fieldByName() + if(isset($args[0])) { + $title = $args[0]; + } + + // Prefix name to avoid collisions. $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); } + /** + * @return int + */ public function getHeadingLevel() { return $this->headingLevel; } - public function setHeadingLevel($level) { - $this->headingLevel = $level; + /** + * @param int $headingLevel + * + * @return $this + */ + public function setHeadingLevel($headingLevel) { + $this->headingLevel = $headingLevel; + return $this; } + /** + * {@inheritdoc} + */ public function getAttributes() { return array_merge( + parent::getAttributes(), array( 'id' => $this->ID(), - 'class' => $this->extraClass() - ), - $this->attributes + 'class' => $this->extraClass(), + ) ); } + /** + * @return null + */ public function Type() { return null; } - } diff --git a/forms/HtmlEditorConfig.php b/forms/HtmlEditorConfig.php index 58cb41b08..c0cb7bea3 100644 --- a/forms/HtmlEditorConfig.php +++ b/forms/HtmlEditorConfig.php @@ -39,12 +39,20 @@ class HtmlEditorConfig { 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 * @return HtmlEditorConfig - the active configuration object */ public static function get_active() { - $identifier = self::$current ? self::$current : 'default'; + $identifier = self::get_active_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 - * @return string - the javascript + * Generate the JavaScript that will set TinyMCE's configuration: + * - Parse all configurations into JSON objects to be used in JavaScript + * - Includes TinyMCE and configurations using the {@link Requirements} system */ - public function generateJS() { - $config = $this->settings; + public static function require_js() { + require_once 'tinymce/tiny_mce_gzip.php'; + $useGzip = Config::inst()->get('HtmlEditorField', 'use_gzip'); - // plugins + $configs = array(); + $externalPlugins = array(); $internalPlugins = array(); - $externalPluginsJS = ''; - foreach($this->plugins as $plugin => $path) { - if(!$path) { - $internalPlugins[] = $plugin; - } else { - $internalPlugins[] = '-' . $plugin; - $externalPluginsJS .= sprintf( - 'tinymce.PluginManager.load("%s", "%s");' . "\n", - $plugin, - $path - ); + $languages = array(); + + foreach (self::$configs as $configID => $config) { + $settings = $config->settings; + // parse plugins + $configPlugins = array(); + foreach($config->plugins as $plugin => $path) { + if(!$path) { + $configPlugins[] = $plugin; + $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) { - $config['theme_advanced_buttons'.$i] = implode(',', $buttons); + // save config plugins settings + $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')) { - $externalPluginsJS - var ssTinyMceConfig = " . Convert::raw2json($config) . "; -} -"; + $externalPlugins + var ssTinyMceConfig = " . Convert::raw2json($configs) . "; +}"; + Requirements::customScript($configsJS, 'htmlEditorConfig'); } } diff --git a/forms/HtmlEditorField.php b/forms/HtmlEditorField.php index 7cda6f3a9..9817431e8 100644 --- a/forms/HtmlEditorField.php +++ b/forms/HtmlEditorField.php @@ -27,41 +27,31 @@ class HtmlEditorField extends TextareaField { private static $sanitise_server_side = false; 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() { - require_once 'tinymce/tiny_mce_gzip.php'; - - $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'); + Deprecation::notice('4.0', 'Use HtmlEditorConfig::require_js() instead'); + HtmlEditorConfig::require_js(); } + + protected $editorConfig = null; + /** + * Creates a new HTMLEditorField. * @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); - self::include_js(); + $this->editorConfig = $config ? $config : HtmlEditorConfig::get_active_identifier(); } public function getAttributes() { @@ -71,6 +61,7 @@ class HtmlEditorField extends TextareaField { 'tinymce' => 'true', 'style' => 'width: 97%; height: ' . ($this->rows * 16) . 'px', // prevents horizontal scrollbars '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-entwine/dist/jquery.entwine-dist.js'); Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/ssui.core.js'); + + HtmlEditorConfig::require_js(); Requirements::javascript(FRAMEWORK_DIR ."/javascript/HtmlEditorField.js"); Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css'); @@ -258,7 +251,7 @@ class HtmlEditorField_Toolbar extends RequestHandler { $siteTree, TextField::create('external', _t('HtmlEditorField.URL', 'URL'), 'http://'), 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('Subject', _t('HtmlEditorField.SUBJECT', 'Email subject')), 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 '); $contentComposite->addExtraClass('ss-insert-link content'); + $fileField->setAllowedMaxFileNumber(1); $form->unsetValidator(); $form->loadDataFrom($this); @@ -679,7 +673,7 @@ class HtmlEditorField_Toolbar extends RequestHandler { */ protected function getFieldsForImage($url, $file) { if($file->File instanceof Image) { - $formattedImage = $file->File->generateFormattedImage('SetWidth', + $formattedImage = $file->File->generateFormattedImage('ScaleWidth', Config::inst()->get('Image', 'asset_preview_width')); $thumbnailURL = Convert::raw2att($formattedImage ? $formattedImage->URL : $url); } else { diff --git a/forms/LiteralField.php b/forms/LiteralField.php index 2217b5e71..c2b576329 100644 --- a/forms/LiteralField.php +++ b/forms/LiteralField.php @@ -1,9 +1,8 @@ Usage - * * * new LiteralField ( * $name = "literalfield", @@ -15,40 +14,59 @@ * @subpackage fields-dataless */ class LiteralField extends DatalessField { - /** - * @var string $content + * @var string|FormField */ protected $content; + /** + * @param string $name + * @param string|FormField $content + */ public function __construct($name, $content) { - $this->content = $content; + $this->setContent($content); parent::__construct($name); } + /** + * @param array $properties + * + * @return string + */ public function FieldHolder($properties = array()) { - if(is_object($this->content)) { - $obj = $this->content; - if($properties) - $obj = $obj->customise($properties); - return $obj->forTemplate(); - } else { - return $this->content; + if($this->content instanceof ViewableData) { + $context = $this->content; + + if($properties) { + $context = $context->customise($properties); + } + + return $context->forTemplate(); } + + return $this->content; } + /** + * @param array $properties + * + * @return string + */ public function Field($properties = array()) { 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) { $this->content = $content; + return $this; } @@ -61,16 +79,25 @@ class LiteralField extends DatalessField { /** * Synonym of {@link setContent()} so that LiteralField is more compatible with other field types. + * + * @param string|FormField $content + * + * @return $this */ - public function setValue($value) { - $this->setContent($value); + public function setValue($content) { + $this->setContent($content); + return $this; } + /** + * @return static + */ public function performReadonlyTransformation() { $clone = clone $this; + $clone->setReadonly(true); + return $clone; } - } diff --git a/forms/PasswordField.php b/forms/PasswordField.php index c8e67ae9a..c230ee9bf 100644 --- a/forms/PasswordField.php +++ b/forms/PasswordField.php @@ -1,11 +1,12 @@ 'password') - ); - - $autocomplete = Config::inst()->get('PasswordField', 'autocomplete'); - if (isset($autocomplete)) { - $attributes['autocomplete'] = $autocomplete ? 'on' : 'off'; + /** + * Returns an input field. + * + * @param string $name + * @param null|string $title + * @param string $value + */ + public function __construct($name, $title = null, $value = '') { + 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() { $field = $this->castedCopy('ReadonlyField'); + $field->setValue('*****'); return $field; } + /** + * {@inheritdoc} + */ public function Type() { return 'text password'; } } - diff --git a/forms/TextareaField.php b/forms/TextareaField.php index 3567c3b51..876138e2b 100644 --- a/forms/TextareaField.php +++ b/forms/TextareaField.php @@ -1,12 +1,11 @@ tag in the * form HTML. * - * Usage - * * * new TextareaField( * $name = "description", @@ -19,17 +18,49 @@ * @subpackage fields-basic */ class TextareaField extends FormField { - /** - * @var int Visible number of text lines. + * Visible number of text lines. + * + * @var int */ protected $rows = 5; /** - * @var int Width of the text area (in average character widths) + * Visible number of text columns. + * + * @var int */ 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() { return array_merge( parent::getAttributes(), @@ -42,30 +73,23 @@ class TextareaField extends FormField { ); } + + /** + * {@inheritdoc} + */ 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 - * - * @param int + * @return string */ - 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() { return htmlentities($this->value, ENT_COMPAT, 'UTF-8'); } diff --git a/forms/ToggleCompositeField.php b/forms/ToggleCompositeField.php index 8231c2d62..225c222dd 100644 --- a/forms/ToggleCompositeField.php +++ b/forms/ToggleCompositeField.php @@ -1,4 +1,5 @@ name = $name; $this->title = $title; @@ -24,30 +29,46 @@ class ToggleCompositeField extends CompositeField { parent::__construct($children); } + /** + * @param array $properties + * + * @return HTMLText + */ public function FieldHolder($properties = array()) { Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.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 . '/javascript/ToggleCompositeField.js'); + Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery.ui.css'); - $obj = $properties ? $this->customise($properties) : $this; - return $obj->renderWith($this->getTemplates()); + $context = $this; + + if(count($properties)) { + $context = $this->customise($properties); + } + + return $context->renderWith($this->getTemplates()); } + /** + * {@inheritdoc} + */ public function getAttributes() { + $attributes = array( + 'id' => $this->id(), + 'class' => $this->extraClass(), + ); + if($this->getStartClosed()) { - $class = 'ss-toggle ss-toggle-start-closed'; + $attributes['class'] .= ' ss-toggle ss-toggle-start-closed'; } else { - $class = 'ss-toggle'; + $attributes['class'] .= ' ss-toggle'; } return array_merge( $this->attributes, - array( - 'id' => $this->id(), - 'class' => $class . ' ' . $this->extraClass() - ) + $attributes ); } @@ -59,13 +80,15 @@ class ToggleCompositeField extends CompositeField { } /** - * Controls whether the field is open or closed by default. By default the - * field is closed. + * Controls whether the field is open or closed by default. By default the field is closed. * - * @param bool $bool + * @param bool $startClosed + * + * @return $this */ - public function setStartClosed($bool) { - $this->startClosed = (bool) $bool; + public function setStartClosed($startClosed) { + $this->startClosed = (bool) $startClosed; + return $this; } @@ -77,12 +100,13 @@ class ToggleCompositeField extends CompositeField { } /** - * @param int $level + * @param int $headingLevel + * + * @return $this */ - public function setHeadingLevel($level) { - $this->headingLevel = $level; + public function setHeadingLevel($headingLevel) { + $this->headingLevel = $headingLevel; + return $this; } - } - diff --git a/forms/TreeDropdownField.php b/forms/TreeDropdownField.php index 60c651abd..e8436eced 100644 --- a/forms/TreeDropdownField.php +++ b/forms/TreeDropdownField.php @@ -217,15 +217,20 @@ class TreeDropdownField extends FormField { Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery-ui.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; if($record instanceof ViewableData) { $title = $record->obj($this->labelField)->forTemplate(); } elseif($record) { $title = Convert::raw2xml($record->{$this->labelField}); - } else if($this->showSearch) { - $title = _t('DropdownField.CHOOSESEARCH', '(Choose or Search)', 'start value of a dropdown'); - } else { - $title = _t('DropdownField.CHOOSE', '(Choose)', 'start value of a dropdown'); + } + else { + $title = $emptyTitle; } // TODO Implement for TreeMultiSelectField @@ -238,6 +243,7 @@ class TreeDropdownField extends FormField { $properties, array( 'Title' => $title, + 'EmptyTitle' => $emptyTitle, 'Metadata' => ($metadata) ? Convert::raw2json($metadata) : null, ) ); diff --git a/forms/UploadField.php b/forms/UploadField.php index d09a6379f..17c92eb50 100644 --- a/forms/UploadField.php +++ b/forms/UploadField.php @@ -873,8 +873,8 @@ class UploadField extends FileField { return $file->getThumbnail($width, $height)->getURL(); } elseif ($file->hasMethod('getThumbnailURL')) { return $file->getThumbnailURL($width, $height); - } elseif ($file->hasMethod('SetRatioSize')) { - return $file->SetRatioSize($width, $height)->getURL(); + } elseif ($file->hasMethod('Fit')) { + return $file->Fit($width, $height)->getURL(); } else { return $file->Icon(); } @@ -1182,20 +1182,25 @@ class UploadField extends FileField { $name = $this->getName(); $postVars = $request->postVar($name); - // Save the temporary file into a File object + // Extract uploaded files from Form data $uploadedFiles = $this->extractUploadedFileData($postVars); - $firstFile = reset($uploadedFiles); - $file = $this->saveTemporaryFile($firstFile, $error); - if(empty($file)) { - $return = array('error' => $error); - } else { - $return = $this->encodeFileAttributes($file); + $return = array(); + + // Save the temporary files into a File objects + // and save data/error on a per file basis + foreach ($uploadedFiles as $tempFile) { + $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 - $response = new SS_HTTPResponse(Convert::raw2json(array($return))); + $response = new SS_HTTPResponse(Convert::raw2json($return)); $response->addHeader('Content-Type', 'text/plain'); - if (!empty($return['error'])) $response->setStatusCode(403); return $response; } diff --git a/forms/gridfield/GridFieldDetailForm.php b/forms/gridfield/GridFieldDetailForm.php index 9baf1cea7..5eeabcfcb 100644 --- a/forms/gridfield/GridFieldDetailForm.php +++ b/forms/gridfield/GridFieldDetailForm.php @@ -495,21 +495,39 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler { 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) { $new_record = $this->record->ID == 0; $controller = $this->getToplevelController(); $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()) { return $controller->httpError(403); } @@ -527,6 +545,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler { try { $form->saveInto($this->record); $this->record->write(); + $extraData = $this->getExtraSavedData($this->record, $list); $list->add($this->record, $extraData); } catch(ValidationException $e) { $form->sessionMessage($e->getResult()->message(), 'bad', false); diff --git a/javascript/HtmlEditorField.js b/javascript/HtmlEditorField.js index d72948265..b992d08b1 100644 --- a/javascript/HtmlEditorField.js +++ b/javascript/HtmlEditorField.js @@ -332,8 +332,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; }, redraw: function() { - // Using a global config (generated through HTMLEditorConfig PHP logic) - var config = ssTinyMceConfig, self = this, ed = this.getEditor(); + // Using textarea config ID from global config object (generated through HTMLEditorConfig PHP logic) + var config = ssTinyMceConfig[this.data('config')], self = this, ed = this.getEditor(); 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.updateFromEditor(); this.redraw(); + this.updateFromEditor(); }, onssdialogclose: function(){ @@ -573,6 +573,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; this.addAnchorSelector(); + this.resetFileField(); + // Toggle field visibility depending on the link type. this.find('div.content .field').hide(); this.find('.field[id$="LinkType"]').show(); @@ -623,7 +625,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; break; 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'; break; @@ -653,17 +655,27 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; this.modifySelection(function(ed){ ed.insertLink(this.getLinkAttributes()); }); - - this.updateFromEditor(); }, removeLink: function() { this.modifySelection(function(ed){ ed.removeLink(); }); + this.resetFileField(); 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. */ @@ -810,6 +822,18 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; el.prop('checked', selected).change(); } else if(el.is(':radio')) { 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 { el.val(selected).change(); } diff --git a/javascript/TreeDropdownField.js b/javascript/TreeDropdownField.js index 448b95365..eb5887565 100644 --- a/javascript/TreeDropdownField.js +++ b/javascript/TreeDropdownField.js @@ -154,17 +154,21 @@ var updateFn = function() { var val = self.getValue(); if(val) { - + var node = tree.find('*[data-id="' + val + '"]'), 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) { self.setTitle(title); self.data('title', title); } 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 diff --git a/lang/fi.yml b/lang/fi.yml index 2ca08f5cf..6619a76a1 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -258,8 +258,7 @@ fi: many_many_Members: Jäsenet GroupImportForm: Help1: '

Tuo yksi tai useampi ryhmä CSV-muotoisena (arvot pilkulla erotettuina). Näytä edistyksellinen käyttö

' - Help2: "
\n\t

Edistynyt käyttö

\n\t
    \n\t
  • Sallitut palstat: %s
  • \n\t
  • Olemassa olevat ryhmät kohdistetaan niiden uniikin Code arvolla, ja päivitetään uudet arvot tuodusta tiedostosta
  • \n\t
  • Oikeustasot voidaan luoda käyttämällä ParentCode palstaa.
  • \n\t
  • Oikeustasokoodit voidaan kohdistaa PermissionCode palstassa. Olemassaolevia oikeusia ei tyhjennetä.
  • \n\t
\n\ -
" + Help2: "
\n

Edistynyt käyttö

\n
    \n
  • Sallitut sarakkeet: %s
  • \n
  • Olemassa olevat rhymes kohdistetaan niiden uniikin Code arvolla, ja päivitetään arvot tuodusta tiedostosta
  • \n
  • Ryhmien hierarkiat voidaan luoda ParentCode sarakkeessa.
  • \n
  • Oikeustasokoodit voidaan kohdistaa PermissionCode sarakkeessa. Olemassa olevia oikeuksia ei tyhjennetä.
  • \n
\n
" ResultCreated: 'Luotiin {count} ryhmä(ä)' ResultDeleted: 'Poistettu %d ryhmää' ResultUpdated: 'Päivitetty %d ryhmää' diff --git a/lang/lt.yml b/lang/lt.yml index 227386d46..e7c543d2e 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -191,9 +191,7 @@ lt: TEXT2: 'slaptažodžio atstatymo nuoroda' TEXT3: svetainei Form: - CSRF_FAILED_MESSAGE: 'Iškilo techninė problema. Prašome paspausti mygtuką Atgal, - - perkraukite naršyklės langą ir bandykite vėl.' + CSRF_FAILED_MESSAGE: 'Iškilo techninė problema. Prašome paspausti mygtuką Atgal, perkraukite naršyklės langą ir bandykite vėl.' FIELDISREQUIRED: '{name} yra privalomas' SubmitBtnLabel: Vykdyti VALIDATIONCREDITNUMBER: 'Prašome įsitikinti, ar teisingai suvedėte kreditinės kortelės numerį {number}' @@ -260,8 +258,7 @@ lt: many_many_Members: Vartotojai GroupImportForm: Help1: '

Importuoti vieną ar kelias grupes CSV formatu (kableliu atskirtos reikšmės). Rodyti detalesnį aprašymą

' - Help2: "
\n\t

Sudėtingesni pasirinkimai

\n\t
    \n\t
  • Galimi stulpeliai: %s
  • \n\t
  • Esamos grupės yra surišamos su jų unikalia Code reikšme ir atnaujinamos duomenimis iš importuojamos bylos
  • \n\t
  • Grupių hierarchija gali būti sukurta naudojant ParentCode stulpelį.
  • \n\t
  • Leidimų kodai gali būti priskirti naudojant PermissionCode stulpelį. Esami leidimai nebus pakeisti.
  • \n\t
\n\ -
" + Help2: "
\n

Sudėtingesni pasirinkimai

\n
    \n
  • Galimi stulpeliai: %s
  • \n
  • Esamos grupės yra surišamos su jų unikalia Code reikšme ir atnaujinamos duomenimis iš importuojamos bylos
  • \n
  • Grupių hierarchija gali būti sukurta naudojant ParentCode stulpelį.
  • \n
  • Leidimų kodai gali būti priskirti naudojant PermissionCode stulpelį. Esami leidimai nebus pakeisti.
  • \n
\n
" ResultCreated: 'Sukurta {count} grupių' ResultDeleted: 'Ištrinta %d grupių' ResultUpdated: 'Atnaujinta %d grupių' diff --git a/lang/nl.yml b/lang/nl.yml index de4171f3b..241715533 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -392,7 +392,7 @@ nl: Toggle: 'Toon opmaak hulp' MemberImportForm: Help1: '

Importeer leden in CSV-formaat (comma-separated values). Toon geavanceerd gebruik

' - Help2: "
\n\t

Advanced usage

\n\t
    \n\t
  • Allowed columns: %s
  • \n\t
  • Existing users are matched by their unique Code property, and updated with any new values from\n\tthe imported file.
  • \n\t
  • Groups can be assigned by the Groups column. Groups are identified by their Code property,\n\tmultiple groups can be separated by comma. Existing group memberships are not cleared.
  • \n\t
\n
" + Help2: "
\n

Geavanceerd gebruik

\n
    \n
  • Toegestane kolommen: %s
  • \n
  • Bestaande groepen worden geïdentificeerd door middel van hun unieke Code-waarde, en aangepast met de nieuwe waarden vanuit het geïmporteerde bestand
  • \n
  • Groepshiërarchiën kunnen aangemaakt worden door een ParentCode-kolom te gebruiken
  • \n
  • Toegangscodeskunnen toegewezen worden met de PermissionCode kolom. Bestaande toegangscodes worden niet verwijderd.
  • \n
\n
" ResultCreated: '{count} leden aangemaakt' ResultDeleted: '%d leden verwijderd' ResultNone: 'Geen wijzingen' diff --git a/model/DataObject.php b/model/DataObject.php index dd9c08c01..d59e2cb66 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -410,8 +410,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if(!is_string($fieldClass)) continue; // Strip off any parameters - $bPos = strpos('(', $fieldClass); - if($bPos !== FALSE) $fieldClass = substr(0,$bPos, $fieldClass); + $bPos = strpos($fieldClass, '('); + if($bPos !== FALSE) $fieldClass = substr($fieldClass, 0, $bPos); // Test to see if it implements CompositeDBField if(ClassInfo::classImplements($fieldClass, 'CompositeDBField')) { @@ -1063,12 +1063,22 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @see {@link ValidationResult} * @return ValidationResult */ - public function validate() { + protected function validate() { $result = ValidationResult::create(); $this->extend('validate', $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. * 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 */ 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 if($val instanceof DBField) { $val->Name = $fieldName; diff --git a/model/Image.php b/model/Image.php index 55c2f632d..d488c689a 100644 --- a/model/Image.php +++ b/model/Image.php @@ -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 * $width and $height @@ -222,25 +476,11 @@ class Image extends File implements Flushable { * @param integer $width The width to size within * @param integer $height The height to size within * @return Image + * @deprecated 4.0 Use Fit instead */ public function SetRatioSize($width, $height) { - - // Prevent divide by zero on missing/blank file - 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); + Deprecation::notice('4.0', 'Use Fit instead'); + return $this->Fit($width, $height); } /** @@ -251,8 +491,10 @@ class Image extends File implements Flushable { * @param integer $width The width to size within * @param integer $height The height to size within * @return Image_Backend + * @deprecated 4.0 Use generateFit instead */ public function generateSetRatioSize(Image_Backend $backend, $width, $height) { + Deprecation::notice('4.0', 'Use generateFit instead'); return $backend->resizeRatio($width, $height); } @@ -261,11 +503,11 @@ class Image extends File implements Flushable { * * @param integer $width The width to set * @return Image + * @deprecated 4.0 Use ScaleWidth instead */ public function SetWidth($width) { - return $this->isWidth($width) && !Config::inst()->get('Image', 'force_resample') - ? $this - : $this->getFormattedImage('SetWidth', $width); + Deprecation::notice('4.0', 'Use ScaleWidth instead'); + return $this->ScaleWidth($width); } /** @@ -274,8 +516,10 @@ class Image extends File implements Flushable { * @param Image_Backend $backend * @param int $width The width to set * @return Image_Backend + * @deprecated 4.0 Use generateScaleWidth instead */ public function generateSetWidth(Image_Backend $backend, $width) { + Deprecation::notice('4.0', 'Use generateScaleWidth instead'); return $backend->resizeByWidth($width); } @@ -284,11 +528,11 @@ class Image extends File implements Flushable { * * @param integer $height The height to set * @return Image + * @deprecated 4.0 Use ScaleHeight instead */ public function SetHeight($height) { - return $this->isHeight($height) && !Config::inst()->get('Image', 'force_resample') - ? $this - : $this->getFormattedImage('SetHeight', $height); + Deprecation::notice('4.0', 'Use ScaleHeight instead'); + return $this->ScaleHeight($height); } /** @@ -297,8 +541,10 @@ class Image extends File implements Flushable { * @param Image_Backend $backend * @param integer $height The height to set * @return Image_Backend + * @deprecated 4.0 Use generateScaleHeight instead */ public function generateSetHeight(Image_Backend $backend, $height){ + Deprecation::notice('4.0', 'Use generateScaleHeight instead'); return $backend->resizeByHeight($height); } @@ -309,11 +555,11 @@ class Image extends File implements Flushable { * @param integer $width The width to size to * @param integer $height The height to size to * @return Image + * @deprecated 4.0 Use Pad instead */ public function SetSize($width, $height) { - return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample') - ? $this - : $this->getFormattedImage('SetSize', $width, $height); + Deprecation::notice('4.0', 'Use Pad instead'); + return $this->Pad($width, $height); } /** @@ -323,8 +569,10 @@ class Image extends File implements Flushable { * @param integer $width The width to size to * @param integer $height The height to size to * @return Image_Backend + * @deprecated 4.0 Use generatePad instead */ public function generateSetSize(Image_Backend $backend, $width, $height) { + Deprecation::notice('4.0', 'Use generatePad instead'); 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 $height The height to size to * @return Image + * @deprecated 4.0 Use Pad instead */ public function PaddedImage($width, $height, $backgroundColor='FFFFFF') { - return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample') - ? $this - : $this->getFormattedImage('PaddedImage', $width, $height, $backgroundColor); + Deprecation::notice('4.0', 'Use Pad instead'); + return $this->Pad($width, $height, $backgroundColor); } /** @@ -385,8 +633,10 @@ class Image extends File implements Flushable { * @param integer $width The width to size to * @param integer $height The height to size to * @return Image_Backend + * @deprecated 4.0 Use generatePad instead */ public function generatePaddedImage(Image_Backend $backend, $width, $height, $backgroundColor='FFFFFF') { + Deprecation::notice('4.0', 'Use generatePad instead'); 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. - * 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 $height Height to resize to @@ -547,11 +798,11 @@ class Image extends File implements Flushable { * @param integer $width Width to crop to * @param integer $height Height to crop to * @return Image + * @deprecated 4.0 Use Fill instead */ public function CroppedImage($width, $height) { - return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample') - ? $this - : $this->getFormattedImage('CroppedImage', $width, $height); + Deprecation::notice('4.0', 'Use Fill instead'); + return $this->Fill($width, $height); } /** @@ -562,8 +813,10 @@ class Image extends File implements Flushable { * @param integer $width Width to crop to * @param integer $height Height to crop to * @return Image_Backend + * @deprecated 4.0 Use generateFill instead */ public function generateCroppedImage(Image_Backend $backend, $width, $height) { + Deprecation::notice('4.0', 'Use generateFill instead'); return $backend->croppedResize($width, $height); } @@ -648,9 +901,9 @@ class Image extends File implements Flushable { public function regenerateFormattedImages() { if(!$this->Filename) return 0; - // Without this, not a single file would be written - // caused by a check in getFormattedImage() - $_GET['flush'] = 1; + // Without this, not a single file would be written + // caused by a check in getFormattedImage() + $this->flush(); $numGenerated = 0; $generatedImages = $this->getGeneratedImages(); diff --git a/model/ManyManyList.php b/model/ManyManyList.php index f702694d0..44e3df353 100644 --- a/model/ManyManyList.php +++ b/model/ManyManyList.php @@ -230,37 +230,29 @@ class ManyManyList extends RelationList { $hasExisting = false; } - $manipulation = array(); + // Blank manipulation + $manipulation = array( + $this->joinTable => array( + 'command' => $hasExisting ? 'update' : 'insert', + 'fields' => array() + ) + ); if($hasExisting) { - $manipulation[$this->joinTable]['command'] = 'update'; $manipulation[$this->joinTable]['where'] = array( "\"{$this->joinTable}\".\"{$this->foreignKey}\"" => $foreignID, "\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID ); - } else { - $manipulation[$this->joinTable]['command'] = 'insert'; } - if($extraFields) { - foreach($extraFields as $fieldName => $fieldValue) { - if(is_null($fieldValue)) { - $manipulation[$this->joinTable]['fields'][$fieldName] = null; - } elseif($fieldValue instanceof DBField) { - // rely on writeToManipulation to manage the changes - // required for this field. - $working = array('fields' => array()); - - // 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; + if($extraFields && $this->extraFields) { + // Write extra field to manipluation in the same way + // that DataObject::prepareManipulationTable writes fields + foreach($this->extraFields as $fieldName => $fieldSpec) { + // Skip fields without an assignment + if(array_key_exists($fieldName, $extraFields)) { + $fieldObject = Object::create_from_string($fieldSpec, $fieldName); + $fieldObject->setValue($extraFields[$fieldName]); + $fieldObject->writeToManipulation($manipulation[$this->joinTable]); } } } @@ -357,24 +349,32 @@ class ManyManyList extends RelationList { */ public function getExtraData($componentName, $itemID) { $result = array(); + + // Skip if no extrafields or unsaved record + if(empty($this->extraFields) || empty($itemID)) { + return $result; + } if(!is_numeric($itemID)) { user_error('ComponentSet::getExtraData() passed a non-numeric child ID', E_USER_ERROR); } - if($this->extraFields) { - $cleanExtraFields = array(); - foreach ($this->extraFields as $fieldName => $dbFieldSpec) { - $cleanExtraFields[] = "\"{$fieldName}\""; - } - $query = new SQLSelect($cleanExtraFields, "\"{$this->joinTable}\""); - if($filter = $this->foreignIDWriteFilter($this->getForeignID())) { - $query->setWhere($filter); - } else { - user_error("Can't call ManyManyList::getExtraData() until a foreign ID is set", E_USER_WARNING); - } - $query->addWhere("\"{$this->localKey}\" = {$itemID}"); - $queryResult = $query->execute()->current(); + $cleanExtraFields = array(); + foreach ($this->extraFields as $fieldName => $dbFieldSpec) { + $cleanExtraFields[] = "\"{$fieldName}\""; + } + $query = new SQLQuery($cleanExtraFields, "\"{$this->joinTable}\""); + $filter = $this->foreignIDWriteFilter($this->getForeignID()); + if($filter) { + $query->setWhere($filter); + } else { + user_error("Can't call ManyManyList::getExtraData() until a foreign ID is set", E_USER_WARNING); + } + $query->addWhere(array( + "\"{$this->localKey}\"" => $itemID + )); + $queryResult = $query->execute()->current(); + if ($queryResult) { foreach ($queryResult as $fieldName => $value) { $result[$fieldName] = $value; } diff --git a/model/Versioned.php b/model/Versioned.php index afec0bbb0..877e0e41b 100644 --- a/model/Versioned.php +++ b/model/Versioned.php @@ -489,34 +489,34 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { if(!$isRootClass && ($versionedTables = ClassInfo::dataClassesFor($table))) { foreach($versionedTables as $child) { - if($table == $child) break; // only need subclasses - - $count = DB::query(" - SELECT COUNT(*) FROM \"{$table}_versions\" - LEFT JOIN \"{$child}_versions\" - ON \"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\" - AND \"{$child}_versions\".\"Version\" = \"{$table}_versions\".\"Version\" - WHERE \"{$child}_versions\".\"ID\" IS NULL - ")->value(); + if($table === $child) break; // only need subclasses + + // Select all orphaned version records + $orphanedQuery = SQLSelect::create() + ->selectField("\"{$table}_versions\".\"ID\"") + ->setFrom("\"{$table}_versions\""); + + // If we have a parent table limit orphaned records + // 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) { - DB::alteration_message("Removing orphaned versioned records", "deleted"); - - $affectedIDs = DB::query(" - SELECT \"{$table}_versions\".\"ID\" FROM \"{$table}_versions\" - LEFT JOIN \"{$child}_versions\" - ON \"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\" - 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) - ); - } + DB::alteration_message("Removing {$count} orphaned versioned records", "deleted"); + $ids = $orphanedQuery->execute()->column(); + foreach($ids as $id) { + DB::prepared_query( + "DELETE FROM \"{$table}_versions\" WHERE \"ID\" = ?", + array($id) + ); } } } diff --git a/security/Group.php b/security/Group.php index d7bddcae9..27ce75829 100755 --- a/security/Group.php +++ b/security/Group.php @@ -336,7 +336,7 @@ class Group extends DataObject { $this->setField("Code", Convert::raw2url($val)); } - public function validate() { + protected function validate() { $result = parent::validate(); // Check if the new group hierarchy would add certain "privileged permissions", diff --git a/security/Member.php b/security/Member.php index a83a2c62a..4b1dd7aed 100644 --- a/security/Member.php +++ b/security/Member.php @@ -1487,7 +1487,7 @@ class Member extends DataObject implements TemplateGlobalProvider { /** * Validate this member object. */ - public function validate() { + protected function validate() { $valid = parent::validate(); if(!$this->ID || $this->isChanged('Password')) { diff --git a/security/PermissionRoleCode.php b/security/PermissionRoleCode.php index 0982b96bd..f954c6fd4 100644 --- a/security/PermissionRoleCode.php +++ b/security/PermissionRoleCode.php @@ -20,7 +20,7 @@ class PermissionRoleCode extends DataObject { "Role" => "PermissionRole", ); - public function validate() { + protected function validate() { $result = parent::validate(); // Check that new code doesn't increase privileges, unless an admin is editing. diff --git a/templates/Includes/HtmlEditorField_viewfile.ss b/templates/Includes/HtmlEditorField_viewfile.ss index a1cf734cb..b2c3251dd 100644 --- a/templates/Includes/HtmlEditorField_viewfile.ss +++ b/templates/Includes/HtmlEditorField_viewfile.ss @@ -3,7 +3,7 @@
<% if $Width %> - $Preview.SetRatioSize(30, 40) + $Preview.Fit(30, 40) <% else %> <% end_if %> diff --git a/templates/forms/TreeDropdownField.ss b/templates/forms/TreeDropdownField.ss index 78c5dc617..09ee0e3ed 100644 --- a/templates/forms/TreeDropdownField.ss +++ b/templates/forms/TreeDropdownField.ss @@ -2,6 +2,7 @@ class="TreeDropdownField <% if $extraClass %> $extraClass<% end_if %><% if $ShowSearch %> searchable<% end_if %>" data-url-tree="$Link('tree')" data-title="$Title.ATT" + data-empty-title="$EmptyTitle.ATT" <% if $Description %>title="$Description.ATT"<% end_if %> <% if $Metadata %>data-metadata="$Metadata.ATT"<% end_if %> tabindex="0"> diff --git a/tests/control/CookieJarTest.php b/tests/control/CookieJarTest.php index 326b2507a..1619bbbec 100644 --- a/tests/control/CookieJarTest.php +++ b/tests/control/CookieJarTest.php @@ -53,8 +53,20 @@ class CookieJarTest extends SapphireTest { //make sure it was set $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)); + + //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 $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->assertEmpty($cookieJar->get('newCookie', false)); //remove it $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', false)); } diff --git a/tests/control/CookieTest.php b/tests/control/CookieTest.php index 8f2fdfa79..7b1b14a5d 100644 --- a/tests/control/CookieTest.php +++ b/tests/control/CookieTest.php @@ -28,6 +28,7 @@ class CookieTest extends SapphireTest { 'cookie1' => 1, 'cookie2' => 'cookies', 'cookie3' => 'test', + 'cookie_4' => 'value', ); Injector::inst()->unregisterNamedObject('Cookie_Backend'); @@ -35,6 +36,8 @@ class CookieTest extends SapphireTest { $this->assertEquals($_COOKIE['cookie1'], Cookie::get('cookie1')); $this->assertEquals($_COOKIE['cookie2'], Cookie::get('cookie2')); $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 $this->assertEquals($_COOKIE, Cookie::get_inst()->getAll(false)); diff --git a/tests/filesystem/FileTest.php b/tests/filesystem/FileTest.php index 0c6f11da8..0fd26f237 100644 --- a/tests/filesystem/FileTest.php +++ b/tests/filesystem/FileTest.php @@ -103,18 +103,18 @@ class FileTest extends SapphireTest { // Invalid ext $file->Name = 'asdf.php'; - $v = $file->validate(); + $v = $file->doValidate(); $this->assertFalse($v->valid()); $this->assertContains('Extension is not allowed', $v->message()); // Valid ext $file->Name = 'asdf.txt'; - $v = $file->validate(); + $v = $file->doValidate(); $this->assertTrue($v->valid()); // Capital extension is valid as well $file->Name = 'asdf.TXT'; - $v = $file->validate(); + $v = $file->doValidate(); $this->assertTrue($v->valid()); Config::inst()->remove('File', 'allowed_extensions'); diff --git a/tests/filesystem/GDTest.php b/tests/filesystem/GDTest.php index dfc3b79af..1cca549c8 100644 --- a/tests/filesystem/GDTest.php +++ b/tests/filesystem/GDTest.php @@ -161,7 +161,7 @@ class GDTest extends SapphireTest { $fullPath = realpath(dirname(__FILE__) . '/gdtest/test_jpg.jpg'); 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'); } catch (GDBackend_Failure_Exception $e) { $cache = SS_Cache::factory('GDBackend_Manipulations'); @@ -169,8 +169,8 @@ class GDTest extends SapphireTest { $data = unserialize($cache->load($key)); - $this->assertArrayHasKey('SetWidth|123', $data); - $this->assertTrue($data['SetWidth|123']); + $this->assertArrayHasKey('ScaleWidth|123', $data); + $this->assertTrue($data['ScaleWidth|123']); } } @@ -183,12 +183,12 @@ class GDTest extends SapphireTest { $fullPath = realpath(dirname(__FILE__) . '/gdtest/test_jpg.jpg'); 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'); } catch (GDBackend_Failure_Exception $e) { - $gd = new GDBackend($fullPath, array('SetWidth', 123)); - $this->assertTrue($gd->failedResample($fullPath, 'SetWidth-failed|123')); - $this->assertFalse($gd->failedResample($fullPath, 'SetWidth-not-failed|123')); + $gd = new GDBackend($fullPath, array('ScaleWidth', 123)); + $this->assertTrue($gd->failedResample($fullPath, 'ScaleWidth-failed|123')); + $this->assertFalse($gd->failedResample($fullPath, 'ScaleWidth-not-failed|123')); } } diff --git a/tests/forms/HtmlEditorConfigTest.php b/tests/forms/HtmlEditorConfigTest.php index 0c2830798..0c7a1754a 100644 --- a/tests/forms/HtmlEditorConfigTest.php +++ b/tests/forms/HtmlEditorConfigTest.php @@ -67,12 +67,26 @@ class HtmlEditorConfigTest extends SapphireTest { $this->assertNotContains('plugin2', array_keys($plugins)); } - public function testGenerateJSWritesPlugins() { - $c = new HtmlEditorConfig(); - $c->enablePlugins(array('plugin1')); + public function testRequireJSIncludesAllExternalPlugins() { + $c = HtmlEditorConfig::get('config'); + $c->enablePlugins(array('plugin1' => '/mypath/plugin1')); $c->enablePlugins(array('plugin2' => '/mypath/plugin2')); - $this->assertContains('plugin1', $c->generateJS()); - $this->assertContains('tinymce.PluginManager.load("plugin2", "/mypath/plugin2");', $c->generateJS()); + HtmlEditorConfig::require_js(); + $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); } } diff --git a/tests/forms/gridfield/GridFieldDetailFormTest.php b/tests/forms/gridfield/GridFieldDetailFormTest.php index caea5ac0e..a3bac5dfc 100644 --- a/tests/forms/gridfield/GridFieldDetailFormTest.php +++ b/tests/forms/gridfield/GridFieldDetailFormTest.php @@ -174,11 +174,15 @@ class GridFieldDetailFormTest extends FunctionalTest { $manyManyField = $parser->getByXpath('//*[@id="Form_ItemEditForm"]//input[@name="ManyMany[IsPublished]"]'); $this->assertTrue((bool)$manyManyField); + // Test save of IsPublished field $response = $this->post( $editformurl, array( 'Name' => 'Updated Category', - 'ManyMany' => array('IsPublished' => 1), + 'ManyMany' => array( + 'IsPublished' => 1, + 'PublishedBy' => 'Richard' + ), 'action_doSave' => 1 ) ); @@ -187,7 +191,33 @@ class GridFieldDetailFormTest extends FunctionalTest { $person = GridFieldDetailFormTest_Person::get()->sort('FirstName')->First(); $category = $person->Categories()->filter(array('Name' => 'Updated Category'))->First(); $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) ); } @@ -316,7 +346,8 @@ class GridFieldDetailFormTest_Person extends DataObject implements TestOnly { private static $many_many_extraFields = 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 $person = GridFieldDetailFormTest_Person::get()->sort('FirstName')->First(); $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->getConfig()->addComponent($gridFieldForm = new GridFieldDetailForm($this, 'Form')); $gridFieldForm->setFields($detailFields); diff --git a/tests/forms/uploadfield/UploadFieldTest.php b/tests/forms/uploadfield/UploadFieldTest.php index b2400188a..d9e42d029 100644 --- a/tests/forms/uploadfield/UploadFieldTest.php +++ b/tests/forms/uploadfield/UploadFieldTest.php @@ -179,8 +179,9 @@ class UploadFieldTest extends FunctionalTest { 'UploadFieldTest_Controller/Form/field/AllowedExtensionsField/upload', array('AllowedExtensionsField' => $this->getUploadFile($invalidFile)) ); - $this->assertTrue($response->isError()); - $this->assertContains('Extension is not allowed', $response->getBody()); + $response = json_decode($response->getBody(), true); + $this->assertTrue(array_key_exists('error', $response[0])); + $this->assertContains('Extension is not allowed', $response[0]['error']); // Test valid file $validFile = 'valid.txt'; @@ -189,8 +190,8 @@ class UploadFieldTest extends FunctionalTest { 'UploadFieldTest_Controller/Form/field/AllowedExtensionsField/upload', array('AllowedExtensionsField' => $this->getUploadFile($validFile)) ); - $this->assertFalse($response->isError()); - $this->assertNotContains('Extension is not allowed', $response->getBody()); + $response = json_decode($response->getBody(), true); + $this->assertFalse(array_key_exists('error', $response[0])); // Test that setAllowedExtensions rejects extensions explicitly denied by File.allowed_extensions // Relies on File::validate failing to allow this extension @@ -200,8 +201,10 @@ class UploadFieldTest extends FunctionalTest { 'UploadFieldTest_Controller/Form/field/InvalidAllowedExtensionsField/upload', array('InvalidAllowedExtensionsField' => $this->getUploadFile($invalidFile)) ); - $this->assertTrue($response->isError()); - $this->assertContains('Extension is not allowed', $response->getBody()); + $response = json_decode($response->getBody(), true); + $this->assertTrue(array_key_exists('error', $response[0])); + $this->assertContains('Extension is not allowed', $response[0]['error']); + } /** diff --git a/tests/model/DataDifferencerTest.php b/tests/model/DataDifferencerTest.php index b0c9d7490..45ebd87c0 100644 --- a/tests/model/DataDifferencerTest.php +++ b/tests/model/DataDifferencerTest.php @@ -55,8 +55,8 @@ class DataDifferencerTest extends SapphireTest { $differ = new DataDifferencer($obj1v1, $obj1v2); $obj1Diff = $differ->diffedData(); - $this->assertContains($image1->Filename, $obj1Diff->getField('Image')); - $this->assertContains($image2->Filename, $obj1Diff->getField('Image')); + $this->assertContains($image1->Name, $obj1Diff->getField('Image')); + $this->assertContains($image2->Name, $obj1Diff->getField('Image')); $this->assertContains('obj2obj1', str_replace(' ','',$obj1Diff->getField('HasOneRelationID'))); } diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index dc14e6bb0..58d5f9c70 100644 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -443,6 +443,7 @@ class DataObjectTest extends SapphireTest { public function testHasOneRelationship() { $team1 = $this->objFromFixture('DataObjectTest_Team', 'team1'); $player1 = $this->objFromFixture('DataObjectTest_Player', 'player1'); + $player2 = $this->objFromFixture('DataObjectTest_Player', 'player2'); $fan1 = $this->objFromFixture('DataObjectTest_Fan', 'fan1'); // Test relation probing @@ -466,6 +467,15 @@ class DataObjectTest extends SapphireTest { $this->assertEquals($team1->getComponent('Captain')->FirstName, 'Player 1', '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 $fan1->setField('FavouriteID', $team1->ID); $fan1->setField('FavouriteClass', $team1->class); @@ -1811,7 +1821,7 @@ class DataObjectTest_ValidatedObject extends DataObject implements TestOnly { 'Name' => 'Varchar(50)' ); - public function validate() { + protected function validate() { if(!empty($this->Name)) { return new ValidationResult(); } else { diff --git a/tests/model/GDImageTest.php b/tests/model/GDImageTest.php index 92e00546c..f095e3ceb 100644 --- a/tests/model/GDImageTest.php +++ b/tests/model/GDImageTest.php @@ -10,22 +10,9 @@ class GDImageTest extends ImageTest { return; } - parent::setUp(); - Image::set_backend("GDBackend"); - // Create a test files for each of the fixture references - $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(); - } + parent::setUp(); } public function tearDown() { @@ -47,13 +34,13 @@ class GDImageTest extends ImageTest { try { // 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'); } catch (GDBackend_Failure_Exception $e) { // Check that the cache has stored the manipulation failure $data = unserialize($cache->load($key)); - $this->assertArrayHasKey('SetWidth|123', $data); - $this->assertTrue($data['SetWidth|123']); + $this->assertArrayHasKey('ScaleWidth|123', $data); + $this->assertTrue($data['ScaleWidth|123']); // Delete the image object $image->delete(); diff --git a/tests/model/ImageTest.php b/tests/model/ImageTest.php index a8b5b2124..e9d87d8dc 100644 --- a/tests/model/ImageTest.php +++ b/tests/model/ImageTest.php @@ -30,19 +30,30 @@ class ImageTest extends SapphireTest { 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() { 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'); foreach($fileIDs as $fileID) { $file = DataObject::get_by_id('Image', $fileID); 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'); foreach($folderIDs as $folderID) { $folder = DataObject::get_by_id('Folder', $folderID); @@ -89,7 +100,7 @@ class ImageTest extends SapphireTest { public function testMultipleGenerateManipulationCalls() { $image = $this->objFromFixture('Image', 'imageWithoutTitle'); - $imageFirst = $image->SetWidth(200); + $imageFirst = $image->ScaleWidth(200); $this->assertNotNull($imageFirst); $expected = 200; $actual = $imageFirst->getWidth(); @@ -113,27 +124,27 @@ class ImageTest extends SapphireTest { $this->assertTrue($image->isSize(300, 300)); // Set width to 300 pixels - $imageSetWidth = $image->SetWidth(300); - $this->assertEquals($imageSetWidth->getWidth(), 300); - $this->assertEquals($image->Filename, $imageSetWidth->Filename); + $imageScaleWidth = $image->ScaleWidth(300); + $this->assertEquals($imageScaleWidth->getWidth(), 300); + $this->assertEquals($image->Filename, $imageScaleWidth->Filename); // Set height to 300 pixels - $imageSetHeight = $image->SetHeight(300); - $this->assertEquals($imageSetHeight->getHeight(), 300); - $this->assertEquals($image->Filename, $imageSetHeight->Filename); + $imageScaleHeight = $image->ScaleHeight(300); + $this->assertEquals($imageScaleHeight->getHeight(), 300); + $this->assertEquals($image->Filename, $imageScaleHeight->Filename); // Crop image to 300 x 300 - $imageCropped = $image->CroppedImage(300, 300); + $imageCropped = $image->Fill(300, 300); $this->assertTrue($imageCropped->isSize(300, 300)); $this->assertEquals($image->Filename, $imageCropped->Filename); // Resize (padded) to 300 x 300 - $imageSized = $image->SetSize(300, 300); + $imageSized = $image->Pad(300, 300); $this->assertTrue($imageSized->isSize(300, 300)); $this->assertEquals($image->Filename, $imageSized->Filename); // 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->assertEquals($image->Filename, $imagePadded->Filename); @@ -142,16 +153,16 @@ class ImageTest extends SapphireTest { $this->assertTrue($imageStretched->isSize(300, 300)); $this->assertEquals($image->Filename, $imageStretched->Filename); - // SetRatioSize (various options) - $imageSetRatioSize = $image->SetRatioSize(300, 600); - $this->assertTrue($imageSetRatioSize->isSize(300, 300)); - $this->assertEquals($image->Filename, $imageSetRatioSize->Filename); - $imageSetRatioSize = $image->SetRatioSize(600, 300); - $this->assertTrue($imageSetRatioSize->isSize(300, 300)); - $this->assertEquals($image->Filename, $imageSetRatioSize->Filename); - $imageSetRatioSize = $image->SetRatioSize(300, 300); - $this->assertTrue($imageSetRatioSize->isSize(300, 300)); - $this->assertEquals($image->Filename, $imageSetRatioSize->Filename); + // Fit (various options) + $imageFit = $image->Fit(300, 600); + $this->assertTrue($imageFit->isSize(300, 300)); + $this->assertEquals($image->Filename, $imageFit->Filename); + $imageFit = $image->Fit(600, 300); + $this->assertTrue($imageFit->isSize(300, 300)); + $this->assertEquals($image->Filename, $imageFit->Filename); + $imageFit = $image->Fit(300, 300); + $this->assertTrue($imageFit->isSize(300, 300)); + $this->assertEquals($image->Filename, $imageFit->Filename); } /** @@ -167,27 +178,27 @@ class ImageTest extends SapphireTest { Config::inst()->update('Image', 'force_resample', true); // Set width to 300 pixels - $imageSetWidth = $image->SetWidth(300); - $this->assertEquals($imageSetWidth->getWidth(), 300); - $this->assertNotEquals($image->Filename, $imageSetWidth->Filename); + $imageScaleWidth = $image->ScaleWidth(300); + $this->assertEquals($imageScaleWidth->getWidth(), 300); + $this->assertNotEquals($image->Filename, $imageScaleWidth->Filename); // Set height to 300 pixels - $imageSetHeight = $image->SetHeight(300); - $this->assertEquals($imageSetHeight->getHeight(), 300); - $this->assertNotEquals($image->Filename, $imageSetHeight->Filename); + $imageScaleHeight = $image->ScaleHeight(300); + $this->assertEquals($imageScaleHeight->getHeight(), 300); + $this->assertNotEquals($image->Filename, $imageScaleHeight->Filename); // Crop image to 300 x 300 - $imageCropped = $image->CroppedImage(300, 300); + $imageCropped = $image->Fill(300, 300); $this->assertTrue($imageCropped->isSize(300, 300)); $this->assertNotEquals($image->Filename, $imageCropped->Filename); // Resize (padded) to 300 x 300 - $imageSized = $image->SetSize(300, 300); + $imageSized = $image->Pad(300, 300); $this->assertTrue($imageSized->isSize(300, 300)); $this->assertNotEquals($image->Filename, $imageSized->Filename); // 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->assertNotEquals($image->Filename, $imagePadded->Filename); @@ -196,16 +207,16 @@ class ImageTest extends SapphireTest { $this->assertTrue($imageStretched->isSize(300, 300)); $this->assertNotEquals($image->Filename, $imageStretched->Filename); - // SetRatioSize (various options) - $imageSetRatioSize = $image->SetRatioSize(300, 600); - $this->assertTrue($imageSetRatioSize->isSize(300, 300)); - $this->assertNotEquals($image->Filename, $imageSetRatioSize->Filename); - $imageSetRatioSize = $image->SetRatioSize(600, 300); - $this->assertTrue($imageSetRatioSize->isSize(300, 300)); - $this->assertNotEquals($image->Filename, $imageSetRatioSize->Filename); - $imageSetRatioSize = $image->SetRatioSize(300, 300); - $this->assertTrue($imageSetRatioSize->isSize(300, 300)); - $this->assertNotEquals($image->Filename, $imageSetRatioSize->Filename); + // Fit (various options) + $imageFit = $image->Fit(300, 600); + $this->assertTrue($imageFit->isSize(300, 300)); + $this->assertNotEquals($image->Filename, $imageFit->Filename); + $imageFit = $image->Fit(600, 300); + $this->assertTrue($imageFit->isSize(300, 300)); + $this->assertNotEquals($image->Filename, $imageFit->Filename); + $imageFit = $image->Fit(300, 300); + $this->assertTrue($imageFit->isSize(300, 300)); + $this->assertNotEquals($image->Filename, $imageFit->Filename); Config::inst()->update('Image', 'force_resample', $origForceResample); } @@ -214,20 +225,52 @@ class ImageTest extends SapphireTest { $this->assertTrue($image->isSize(300, 300)); // Test normal resize - $resized = $image->SetSize(150, 100); + $resized = $image->Pad(150, 100); $this->assertTrue($resized->isSize(150, 100)); // Test cropped resize - $cropped = $image->CroppedImage(100, 200); + $cropped = $image->Fill(100, 200); $this->assertTrue($cropped->isSize(100, 200)); // Test padded resize - $padded = $image->PaddedImage(200, 100); + $padded = $image->Pad(200, 100); $this->assertTrue($padded->isSize(200, 100)); - // Test SetRatioSize - $ratio = $image->SetRatioSize(80, 160); + // Test Fit + $ratio = $image->Fit(80, 160); $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() { $image = $this->objFromFixture('Image', 'imageWithoutTitle'); $image->setHeight('String'); - $image->PaddedImage(600,600,'XXXXXX'); + $image->Pad(600,600,'XXXXXX'); } public function testCacheFilename() { $image = $this->objFromFixture('Image', 'imageWithoutTitle'); - $imageFirst = $image->SetSize(200,200); + $imageFirst = $image->Pad(200,200,'CCCCCC'); $imageFilename = $imageFirst->getFullPath(); // 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'); } @@ -252,7 +295,7 @@ class ImageTest extends SapphireTest { $image = $this->objFromFixture('Image', 'imageWithoutTitle'); $folder = new SS_FileFinder(); - $imageFirst = $image->SetSize(200,200); + $imageFirst = $image->Pad(200,200); $this->assertNotNull($imageFirst); $expected = 200; $actual = $imageFirst->getWidth(); @@ -265,7 +308,7 @@ class ImageTest extends SapphireTest { $actual = $imageSecond->getHeight(); $this->assertEquals($expected, $actual); - $imageThird = $imageSecond->PaddedImage(600,600,'0F0F0F'); + $imageThird = $imageSecond->Pad(600,600,'0F0F0F'); // Encoding of the arguments is duplicated from cacheFilename $argumentString = base64_encode(json_encode(array(600,600,'0F0F0F'))); $this->assertNotNull($imageThird); @@ -289,7 +332,7 @@ class ImageTest extends SapphireTest { public function testRegenerateImages() { $image = $this->objFromFixture('Image', 'imageWithMetacharacters'); - $image_generated = $image->SetWidth(200); + $image_generated = $image->ScaleWidth(200); $p = $image_generated->getFullPath(); $this->assertTrue(file_exists($p), 'Resized image exists after creation call'); $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'); } + /** + * 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() { $image = $this->objFromFixture('Image', 'imageWithMetacharacters'); - $image_generated = $image->SetWidth(200); + $image_generated = $image->ScaleWidth(200); $p = $image_generated->getFullPath(); $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); $newRelative = str_replace($oldArgumentString, $newArgumentString, $image_generated->getFileName()); rename($p, $newPath); - $this->assertFalse(file_exists($p), 'Resized image does not exist after movement call under old name'); - $this->assertTrue(file_exists($newPath), 'Resized image exists after movement call under new name'); + $this->assertFalse(file_exists($p), 'Resized image does not exist at old path after renaming'); + $this->assertTrue(file_exists($newPath), 'Resized image exists at new path after renaming'); $this->assertEquals(1, $image->regenerateFormattedImages(), 'Cached images were regenerated in the right number'); @@ -322,7 +369,7 @@ class ImageTest extends SapphireTest { public function testGeneratedImageDeletion() { $image = $this->objFromFixture('Image', 'imageWithMetacharacters'); - $image_generated = $image->SetWidth(200); + $image_generated = $image->ScaleWidth(200); $p = $image_generated->getFullPath(); $this->assertTrue(file_exists($p), 'Resized image exists after creation call'); $numDeleted = $image->deleteFormattedImages(); @@ -336,11 +383,11 @@ class ImageTest extends SapphireTest { public function testMultipleGenerateManipulationCallsImageDeletion() { $image = $this->objFromFixture('Image', 'imageWithMetacharacters'); - $firstImage = $image->SetWidth(200); + $firstImage = $image->ScaleWidth(200); $firstImagePath = $firstImage->getFullPath(); $this->assertTrue(file_exists($firstImagePath)); - $secondImage = $firstImage->SetHeight(100); + $secondImage = $firstImage->ScaleHeight(100); $secondImagePath = $secondImage->getFullPath(); $this->assertTrue(file_exists($secondImagePath)); @@ -354,11 +401,11 @@ class ImageTest extends SapphireTest { */ public function testPathPropertiesCachedImage() { $image = $this->objFromFixture('Image', 'imageWithMetacharacters'); - $firstImage = $image->SetWidth(200); + $firstImage = $image->ScaleWidth(200); $firstImagePath = $firstImage->getRelativePath(); $this->assertEquals($firstImagePath, $firstImage->Filename); - $secondImage = $firstImage->SetHeight(100); + $secondImage = $firstImage->ScaleHeight(100); $secondImagePath = $secondImage->getRelativePath(); $this->assertEquals($secondImagePath, $secondImage->Filename); } diff --git a/tests/model/ImageTest.yml b/tests/model/ImageTest.yml index ac1dfcaf4..b677f2db8 100644 --- a/tests/model/ImageTest.yml +++ b/tests/model/ImageTest.yml @@ -1,6 +1,6 @@ Folder: folder1: - Filename: assets/ImageTest + Filename: assets/ImageTest/ Image: imageWithTitle: Title: This is a image Title @@ -16,3 +16,11 @@ Image: Title: This is a/an image Title Filename: assets/ImageTest/test_image.png 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 \ No newline at end of file diff --git a/tests/model/ImagickImageTest.php b/tests/model/ImagickImageTest.php index f63827d0b..7e44de3bd 100644 --- a/tests/model/ImagickImageTest.php +++ b/tests/model/ImagickImageTest.php @@ -8,22 +8,8 @@ class ImagickImageTest extends ImageTest { return; } - parent::setUp(); - Image::set_backend("ImagickBackend"); - // Create a test files for each of the fixture references - $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(); - } + parent::setUp(); } } diff --git a/tests/model/testimages/test.image.with.dots.png b/tests/model/testimages/test.image.with.dots.png index a95602a86..21df58520 100644 Binary files a/tests/model/testimages/test.image.with.dots.png and b/tests/model/testimages/test.image.with.dots.png differ diff --git a/tests/model/testimages/test_image.png b/tests/model/testimages/test_image.png index a95602a86..abe33e5f5 100644 Binary files a/tests/model/testimages/test_image.png and b/tests/model/testimages/test_image.png differ diff --git a/tests/model/testimages/test_image_high-quality.jpg b/tests/model/testimages/test_image_high-quality.jpg new file mode 100644 index 000000000..f90315784 Binary files /dev/null and b/tests/model/testimages/test_image_high-quality.jpg differ diff --git a/tests/model/testimages/test_image_low-quality.jpg b/tests/model/testimages/test_image_low-quality.jpg new file mode 100644 index 000000000..ccc8346ab Binary files /dev/null and b/tests/model/testimages/test_image_low-quality.jpg differ diff --git a/tests/view/SSViewerTest.php b/tests/view/SSViewerTest.php index 647d7cb0c..9487232f2 100644 --- a/tests/view/SSViewerTest.php +++ b/tests/view/SSViewerTest.php @@ -1089,7 +1089,6 @@ after') // Let's throw something random in there. $self->setExpectedException('InvalidArgumentException'); $templates = SSViewer::get_templates_by_class(array()); - $this->assertCount(0, $templates); }); }