Merge remote-tracking branch 'origin/3.1'

Conflicts:
	docs/en/topics/testing/create-silverstripe-test.md
	forms/Form.php
	i18n/i18n.php
	model/Image.php
This commit is contained in:
Ingo Schommer 2013-09-27 19:06:56 +02:00
commit 455e550d9a
124 changed files with 4497 additions and 698 deletions

View File

@ -35,4 +35,3 @@ After:
Director:
rules:
'admin': 'AdminRootController'
'dev/buildcache/$Action': 'RebuildStaticCacheTask'

View File

@ -34,6 +34,7 @@ HtmlEditorConfig::get('cms')->enablePlugins('media', 'fullscreen', 'inlinepopups
HtmlEditorConfig::get('cms')->enablePlugins(array(
'ssbuttons' => sprintf('../../../%s/tinymce_ssbuttons/editor_plugin_src.js', THIRDPARTY_DIR)
));
HtmlEditorConfig::get('cms')->enablePlugins('advimagescale');
HtmlEditorConfig::get('cms')->insertButtonsBefore('formatselect', 'styleselect');
HtmlEditorConfig::get('cms')->addButtonsToLine(2,

View File

@ -37,4 +37,9 @@ class CMSForm extends Form {
return $this->responseNegotiator;
}
public function FormName() {
if($this->htmlID) return $this->htmlID;
else return 'Form_' . str_replace(array('.', '/'), '', $this->name);
}
}

View File

@ -1379,7 +1379,8 @@ class LeftAndMain extends Controller implements PermissionProvider {
);
$form->addExtraClass('cms-batch-actions nostyle');
$form->unsetValidator();
$this->extend('updateBatchActionsForm', $form);
return $form;
}

View File

@ -67,7 +67,8 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
false,
Member::get(),
$memberListConfig = GridFieldConfig_RecordEditor::create()
->addComponent(new GridFieldExportButton())
->addComponent(new GridFieldButtonRow('after'))
->addComponent(new GridFieldExportButton('buttons-after-left'))
)->addExtraClass("members_grid");
$memberListConfig->getComponentByType('GridFieldDetailForm')->setValidator(new Member_Validator());
@ -83,10 +84,10 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
));
$columns->setFieldFormatting(array(
'Breadcrumbs' => function($val, $item) {
return $item->getBreadcrumbs(' > ');
return Convert::raw2xml($item->getBreadcrumbs(' > '));
}
));
$fields = new FieldList(
$root = new TabSet(
'Root',
@ -100,34 +101,42 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
. ' database'
)
)
),
new HeaderField(_t('SecurityAdmin.IMPORTUSERS', 'Import users'), 3),
new LiteralField(
'MemberImportFormIframe',
sprintf(
'<iframe src="%s" id="MemberImportFormIframe" width="100%%" height="250px" frameBorder="0">'
. '</iframe>',
$this->Link('memberimport')
)
)
),
$groupsTab = new Tab('Groups', singleton('Group')->i18n_plural_name(),
$groupList,
new HeaderField(_t('SecurityAdmin.IMPORTGROUPS', 'Import groups'), 3),
new LiteralField(
'GroupImportFormIframe',
sprintf(
'<iframe src="%s" id="GroupImportFormIframe" width="100%%" height="250px" frameBorder="0">'
. '</iframe>',
$this->Link('groupimport')
)
)
$groupList
)
),
// necessary for tree node selection in LeftAndMain.EditForm.js
new HiddenField('ID', false, 0)
);
// Add import capabilities. Limit to admin since the import logic can affect assigned permissions
if(Permission::check('ADMIN')) {
$fields->addFieldsToTab('Root.Users', array(
new HeaderField(_t('SecurityAdmin.IMPORTUSERS', 'Import users'), 3),
new LiteralField(
'MemberImportFormIframe',
sprintf(
'<iframe src="%s" id="MemberImportFormIframe" width="100%%" height="250px" frameBorder="0">'
. '</iframe>',
$this->Link('memberimport')
)
)
));
$fields->addFieldsToTab('Root.Groups', array(
new HeaderField(_t('SecurityAdmin.IMPORTGROUPS', 'Import groups'), 3),
new LiteralField(
'GroupImportFormIframe',
sprintf(
'<iframe src="%s" id="GroupImportFormIframe" width="100%%" height="250px" frameBorder="0">'
. '</iframe>',
$this->Link('groupimport')
)
)
));
}
// Tab nav in CMS is rendered through separate template
$root->setTemplate('CMSTabSet');
@ -195,6 +204,8 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
* @return Form
*/
public function MemberImportForm() {
if(!Permission::check('ADMIN')) return false;
$group = $this->currentPage();
$form = new MemberImportForm(
$this,
@ -225,6 +236,8 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
* @return Form
*/
public function GroupImportForm() {
if(!Permission::check('ADMIN')) return false;
$form = new GroupImportForm(
$this,
'GroupImportForm'
@ -306,7 +319,7 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
/**
* The permissions represented in the $codes will not appearing in the form
* containing {@link PermissionCheckboxSetField} so as not to be checked / unchecked.
*
*
* @deprecated 3.1 Use "Permission.hidden_permissions" config setting instead
* @param $codes String|Array
*/

View File

@ -449,6 +449,10 @@ body.cms { overflow: hidden; }
.cms-edit-form .message { margin: 16px; }
.cms-edit-form .ui-tabs-panel .message { margin: 16px 0; }
.notice-item { border: 0; -webkit-border-radius: 3px; -moz-border-radius: 3px; -ms-border-radius: 3px; -o-border-radius: 3px; border-radius: 3px; font-family: inherit; font-size: inherit; padding: 8px 10px 8px 10px; }
.notice-item-close { color: #333333; background: url(../images/filter-icons.png) no-repeat 0 -20px; width: 1px; height: 1px; overflow: hidden; padding: 0px 0 20px 15px; }
/** -------------------------------------------- Page icons -------------------------------------------- */
.page-icon, a .jstree-pageicon { display: block; width: 16px; height: 16px; background: transparent url(../images/sitetree_ss_pageclass_icons_default.png) no-repeat; }
@ -874,11 +878,11 @@ li.class-ErrorPage > a .jstree-pageicon { background-position: 0 -112px; }
.cms-menu.collapsed.cms-panel .cms-panel-content { display: block; }
.cms-menu-list li { /* Style applied to the menu flyout only when the collapsed setting */ }
.cms-menu-list li a { display: block; height: 24px; line-height: 24px; font-size: 12px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; text-shadow: #bfcad2 1px 1px 0; color: #1f1f1f; padding: 7px 5px 7px 8px; background-color: #b0bec7; cursor: pointer; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2IwYmVjNyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzkyYTViMiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b0bec7), color-stop(100%, #92a5b2)); background-image: -webkit-linear-gradient(#b0bec7, #92a5b2); background-image: -moz-linear-gradient(#b0bec7, #92a5b2); background-image: -o-linear-gradient(#b0bec7, #92a5b2); background-image: linear-gradient(#b0bec7, #92a5b2); border-top: 1px solid #c2cdd4; border-bottom: 1px solid #748d9d; }
.cms-menu-list li a { display: block; line-height: 16px; min-height: 16px; font-size: 12px; text-shadow: #bfcad2 1px 1px 0; color: #1f1f1f; padding: 11px 5px 11px 8px; background-color: #b0bec7; cursor: pointer; position: relative; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2IwYmVjNyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzkyYTViMiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b0bec7), color-stop(100%, #92a5b2)); background-image: -webkit-linear-gradient(#b0bec7, #92a5b2); background-image: -moz-linear-gradient(#b0bec7, #92a5b2); background-image: -o-linear-gradient(#b0bec7, #92a5b2); background-image: linear-gradient(#b0bec7, #92a5b2); border-top: 1px solid #c2cdd4; border-bottom: 1px solid #748d9d; }
.cms-menu-list li a:hover { text-decoration: none; background-color: #b6c3cb; border-bottom: 1px solid #8399a7; color: #2c2c2c; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2JmY2FkMiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2IwYmVjNyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #bfcad2), color-stop(100%, #b0bec7)); background-image: -webkit-linear-gradient(#bfcad2, #b0bec7); background-image: -moz-linear-gradient(#bfcad2, #b0bec7); background-image: -o-linear-gradient(#bfcad2, #b0bec7); background-image: linear-gradient(#bfcad2, #b0bec7); }
.cms-menu-list li a:focus, .cms-menu-list li a:active { border-top: 1px solid #a1b2bc; text-decoration: none; background-color: #a1b2bc; color: #393939; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzkyYTViMiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2ExYjJiYyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #92a5b2), color-stop(100%, #a1b2bc)); background-image: -webkit-linear-gradient(#92a5b2, #a1b2bc); background-image: -moz-linear-gradient(#92a5b2, #a1b2bc); background-image: -o-linear-gradient(#92a5b2, #a1b2bc); background-image: linear-gradient(#92a5b2, #a1b2bc); }
.cms-menu-list li a .icon { display: inline-block; float: left; margin: 4px 10px 0 4px; filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); opacity: 0.7; }
.cms-menu-list li a .text { display: inline-block; float: left; }
.cms-menu-list li a .icon { display: block; position: absolute; top: 50%; margin-left: 4px; margin-top: -8px; filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); opacity: 0.7; }
.cms-menu-list li a .text { display: block; margin-left: 30px; }
.cms-menu-list li a .toggle-children { display: inline-block; float: right; width: 20px; height: 100%; cursor: pointer; }
.cms-menu-list li a .toggle-children .toggle-children-icon { display: inline-block; width: 8px; height: 8px; background: url('../images/sprites-32x32-s972e408b05.png') 0 -349px no-repeat; vertical-align: middle; }
.cms-menu-list li a .toggle-children.opened .toggle-children-icon { background: url('../images/sprites-32x32-s972e408b05.png') 0 -333px no-repeat; }

View File

@ -224,15 +224,31 @@
return this;
},
/**
* Return a style element we can use in IE8 to fix fonts (see readystatechange binding in onadd below)
*/
getOrAppendFontFixStyleElement: function() {
var style = $('#FontFixStyleElement');
if (!style.length) {
style = $(
'<style type="text/css" id="FontFixStyleElement" disabled="disabled">'+
':before,:after{content:none !important}'+
'</style>'
).appendTo('head');
}
return style;
},
/**
* Initialise the preview element.
*/
onadd: function() {
var self = this, layoutContainer = this.parent();
var self = this, layoutContainer = this.parent(), iframe = this.find('iframe');
// Create layout and controls
this.find('iframe').addClass('center');
this.find('iframe').bind('load', function() {
iframe.addClass('center');
iframe.bind('load', function() {
self._adjustIframeForPreview();
// Load edit view for new page, but only if the preview is activated at the moment.
@ -241,7 +257,17 @@
$(this).removeClass('loading');
});
// If there's any webfonts in the preview, IE8 will start glitching. This fixes that.
if ($.browser.msie && 8 === parseInt($.browser.version, 10)) {
iframe.bind('readystatechange', function(e) {
if(iframe[0].readyState == 'interactive') {
self.getOrAppendFontFixStyleElement().removeAttr('disabled');
setTimeout(function(){ self.getOrAppendFontFixStyleElement().attr('disabled', 'disabled'); }, 0);
}
});
}
// Preview might not be available in all admin interfaces - block/disable when necessary
this.append('<div class="cms-preview-overlay ui-widget-overlay-light"></div>');
this.find('.cms-preview-overlay').hide();

View File

@ -542,6 +542,8 @@ jQuery.noConflict();
// Store the fragment request so we can abort later, should we get a duplicate request.
fragmentXHR[pjaxFragments] = xhr;
return xhr;
},
/**

View File

@ -155,18 +155,16 @@
li {
a {
display: block;
height: $grid-y * 3;
line-height: $grid-y * 3;
line-height: $grid-y * 2;
min-height: $grid-y * 2;
font-size: $font-base-size;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-shadow: lighten($color-base, 5%) 1px 1px 0;
color: $color-text-dark;
padding: ($grid-y - 1) 5px ($grid-y - 1) 8px;
padding: (1.5 * $grid-y - 1) 5px (1.5 * $grid-y - 1) 8px;
background-color: $color-base;
cursor: pointer;
position: relative;
@include background-image(linear-gradient(
$color-base,
darken($color-base, 10%)
@ -200,16 +198,18 @@
}
.icon {
display: inline-block;
float: left;
margin: 4px 10px 0 4px;
display: block;
position: absolute;
top: 50%;
margin-left: $grid-x / 2;
margin-top: -8px;
@include opacity(0.7);
}
.text {
display: inline-block;
float: left;
display: block;
margin-left: 30px;
}
.toggle-children {

View File

@ -479,6 +479,26 @@ body.cms {
}
}
.notice-item {
border: 0;
@include border-radius(3px);
font-family: inherit;
font-size: inherit;
padding: 8px 10px 8px 10px;
}
.notice-item-close {
color: #333333;
background: url(../images/filter-icons.png) no-repeat 0 -20px;
width: 1px;
height: 1px;
overflow: hidden;
padding: 0px 0 20px 15px;
}
/** --------------------------------------------
* Page icons
* -------------------------------------------- */

View File

@ -224,8 +224,11 @@ class RestfulService extends ViewableData {
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
if(!ini_get('open_basedir')) curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
//include headers in the response
curl_setopt($ch, CURLOPT_HEADER, true);
// Write headers to a temporary file
$headerfd = tmpfile();
curl_setopt($ch, CURLOPT_WRITEHEADER, $headerfd);
// Add headers
if($this->customHeaders) {
@ -260,8 +263,13 @@ class RestfulService extends ViewableData {
curl_setopt_array($ch, $curlOptions);
// Run request
$rawResponse = curl_exec($ch);
$response = $this->extractResponse($ch, $rawResponse);
$body = curl_exec($ch);
rewind($headerfd);
$headers = stream_get_contents($headerfd);
fclose($headerfd);
$response = $this->extractResponse($ch, $headers, $body);
curl_close($ch);
return $response;
@ -315,22 +323,19 @@ class RestfulService extends ViewableData {
*
* @return RestfulService_Response The response object
*/
protected function extractResponse($ch, $rawResponse) {
protected function extractResponse($ch, $rawHeaders, $rawBody) {
//get the status code
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
//get a curl error if there is one
$curlError = curl_error($ch);
//normalise the status code
if(curl_error($ch) !== '' || $statusCode == 0) $statusCode = 500;
//calculate the length of the header and extract it
$headerLength = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$rawHeaders = substr($rawResponse, 0, $headerLength);
//extract the body
$body = substr($rawResponse, $headerLength);
//parse the headers
$headers = $this->parseRawHeaders($rawHeaders);
$parts = array_filter(explode("\r\n\r\n", $rawHeaders));
$lastHeaders = array_pop($parts);
$headers = $this->parseRawHeaders($lastHeaders);
//return the response object
return new RestfulService_Response($body, $statusCode, $headers);
return new RestfulService_Response($rawBody, $statusCode, $headers);
}
/**

View File

@ -552,8 +552,9 @@ class Controller extends RequestHandler implements TemplateGlobalProvider {
public static function join_links() {
$args = func_get_args();
$result = "";
$querystrings = array();
$queryargs = array();
$fragmentIdentifier = null;
foreach($args as $arg) {
// Find fragment identifier - keep the last one
if(strpos($arg,'#') !== false) {
@ -562,7 +563,8 @@ class Controller extends RequestHandler implements TemplateGlobalProvider {
// Find querystrings
if(strpos($arg,'?') !== false) {
list($arg, $suffix) = explode('?',$arg,2);
$querystrings[] = $suffix;
parse_str($suffix, $localargs);
$queryargs = array_merge($queryargs, $localargs);
}
if((is_string($arg) && $arg) || is_numeric($arg)) {
$arg = (string)$arg;
@ -571,7 +573,8 @@ class Controller extends RequestHandler implements TemplateGlobalProvider {
}
}
if($querystrings) $result .= '?' . implode('&', $querystrings);
if($queryargs) $result .= '?' . http_build_query($queryargs);
if($fragmentIdentifier) $result .= "#$fragmentIdentifier";
return $result;

View File

@ -147,11 +147,22 @@ class Director implements TemplateGlobalProvider {
// Return code for a redirection request
if(is_string($result) && substr($result,0,9) == 'redirect:') {
$response = new SS_HTTPResponse();
$response->redirect(substr($result, 9));
$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
if ($res !== false) {
$response->output();
$url = substr($result, 9);
if(Director::is_cli()) {
// on cli, follow SilverStripe redirects automatically
return Director::direct(
str_replace(Director::absoluteBaseURL(), '', $url),
DataModel::inst()
);
} else {
$response = new SS_HTTPResponse();
$response->redirect($url);
$res = Injector::inst()->get('RequestProcessor')->postRequest($req, $response, $model);
if ($res !== false) {
$response->output();
}
}
// Handle a controller
} else if($result) {

View File

@ -343,7 +343,26 @@ class HTTP {
$responseHeaders['Vary'] = 'Cookie, X-Forwarded-Protocol, User-Agent, Accept';
}
else {
$responseHeaders["Cache-Control"] = "no-cache, max-age=0, must-revalidate, no-transform";
if($body) {
// Grab header for checking. Unfortunately HTTPRequest uses a mistyped variant.
$contentDisposition = $body->getHeader('Content-disposition');
if (!$contentDisposition) $contentDisposition = $body->getHeader('Content-Disposition');
}
if(
$body &&
Director::is_https() &&
strstr($_SERVER["HTTP_USER_AGENT"], 'MSIE')==true &&
strstr($contentDisposition, 'attachment;')==true
) {
// 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";
$responseHeaders["Pragma"] = "";
} else {
$responseHeaders["Cache-Control"] = "no-cache, max-age=0, must-revalidate, no-transform";
}
}
if(self::$modification_date && self::$cache_age > 0) {

View File

@ -393,13 +393,9 @@ class SS_HTTPRequest implements ArrayAccess {
}
$response = new SS_HTTPResponse($fileData);
$response->addHeader("Content-Type", "$mimeType; name=\"" . addslashes($fileName) . "\"");
// Note a IE-only fix that inspects this header in HTTP::add_cache_headers().
$response->addHeader("Content-disposition", "attachment; filename=" . addslashes($fileName));
$response->addHeader("Content-Length", strlen($fileData));
$response->addHeader("Pragma", ""); // Necessary because IE has issues sending files over SSL
if(strstr($_SERVER["HTTP_USER_AGENT"],"MSIE") == true) {
$response->addHeader('Cache-Control', 'max-age=3, must-revalidate'); // Workaround for IE6 and 7
}
return $response;
}

View File

@ -1,5 +1,10 @@
<?php
require_once dirname(__FILE__) . '/InjectionCreator.php';
require_once dirname(__FILE__) . '/SilverStripeInjectionCreator.php';
require_once dirname(__FILE__) . '/ServiceConfigurationLocator.php';
require_once dirname(__FILE__) . '/SilverStripeServiceConfigurationLocator.php';
/**
* A simple injection manager that manages creating objects and injecting
* dependencies between them. It borrows quite a lot from ideas taken from
@ -830,94 +835,4 @@ class Injector {
public function createWithArgs($name, $constructorArgs) {
return $this->get($name, false, $constructorArgs);
}
}
/**
* A class for creating new objects by the injector
*/
class InjectionCreator {
/**
*
* @param string $object
* A string representation of the class to create
* @param array $params
* An array of parameters to be passed to the constructor
*/
public function create($class, $params = array()) {
$reflector = new ReflectionClass($class);
if (count($params)) {
return $reflector->newInstanceArgs($params);
}
return $reflector->newInstance();
}
}
class SilverStripeInjectionCreator {
/**
*
* @param string $object
* A string representation of the class to create
* @param array $params
* An array of parameters to be passed to the constructor
*/
public function create($class, $params = array()) {
$class = Object::getCustomClass($class);
$reflector = new ReflectionClass($class);
return $reflector->newInstanceArgs($params);
}
}
/**
* Used to locate configuration for a particular named service.
*
* If it isn't found, return null
*/
class ServiceConfigurationLocator {
public function locateConfigFor($name) {
}
}
/**
* Use the SilverStripe configuration system to lookup config for a particular service
*/
class SilverStripeServiceConfigurationLocator {
private $configs = array();
public function locateConfigFor($name) {
if (isset($this->configs[$name])) {
return $this->configs[$name];
}
$config = Config::inst()->get('Injector', $name);
if ($config) {
$this->configs[$name] = $config;
return $config;
}
// do parent lookup if it's a class
if (class_exists($name)) {
$parents = array_reverse(array_keys(ClassInfo::ancestry($name)));
array_shift($parents);
foreach ($parents as $parent) {
// have we already got for this?
if (isset($this->configs[$parent])) {
return $this->configs[$parent];
}
$config = Config::inst()->get('Injector', $parent);
if ($config) {
$this->configs[$name] = $config;
return $config;
} else {
$this->configs[$parent] = false;
}
}
// there is no parent config, so we'll record that as false so we don't do the expensive
// lookup through parents again
$this->configs[$name] = false;
}
}
}

View File

@ -91,8 +91,12 @@ class PaginatedList extends SS_ListDecorator {
*/
public function getPageStart() {
if ($this->pageStart === null) {
if ($this->request && isset($this->request[$this->getPaginationGetVar()])) {
$this->pageStart = (int) $this->request[$this->getPaginationGetVar()];
if(
$this->request
&& isset($this->request[$this->getPaginationGetVar()])
&& $this->request[$this->getPaginationGetVar()] > 0
) {
$this->pageStart = (int)$this->request[$this->getPaginationGetVar()];
} else {
$this->pageStart = 0;
}
@ -417,4 +421,4 @@ class PaginatedList extends SS_ListDecorator {
}
}
}
}

View File

@ -40,7 +40,7 @@ body.cms.ss-uploadfield-edit-iframe .fieldholder-small label, .composite.ss-asse
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info { background-color: #c11f1d; padding-right: 130px; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #c11f1d), color-stop(4%, #bf1d1b), color-stop(8%, #b71b1c), color-stop(15%, #b61e1d), color-stop(27%, #b11d1d), color-stop(31%, #ab1d1c), color-stop(42%, #a51b1b), color-stop(46%, #9f1b19), color-stop(50%, #9f1b19), color-stop(54%, #991c1a), color-stop(58%, #971a18), color-stop(62%, #911b1b), color-stop(65%, #911b1b), color-stop(88%, #7e1816), color-stop(92%, #771919), color-stop(100%, #731817)); background-image: -webkit-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: -moz-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: -o-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); }
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info .ss-uploadfield-item-name { width: 100%; cursor: default; background: #bcb9b9; background: rgba(201, 198, 198, 0.9); }
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info .ss-uploadfield-item-name .name { text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.7); }
.ss-assetuploadfield .ss-uploadfield-files .ui-state-warning .ss-uploadfield-item-info { background-color: #e9d104; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #e5d33b), color-stop(8%, #e2ce24), color-stop(50%, #d1be1c), color-stop(54%, #d1bc1b), color-stop(96%, #d09a1a), color-stop(100%, #ce8719)); background-image: -webkit-linear-gradient(top, #e5d33b 0%, #e2ce24 8%, #d1be1c 50%, #d1bc1b 54%, #d09a1a 96%, #ce8719 100%); background-image: -moz-linear-gradient(top, #e5d33b 0%, #e2ce24 8%, #d1be1c 50%, #d1bc1b 54%, #d09a1a 96%, #ce8719 100%); background-image: -o-linear-gradient(top, #e5d33b 0%, #e2ce24 8%, #d1be1c 50%, #d1bc1b 54%, #d09a1a 96%, #ce8719 100%); background-image: linear-gradient(top, #e5d33b 0%, #e2ce24 8%, #d1be1c 50%, #d1bc1b 54%, #d09a1a 96%, #ce8719 100%); }
.ss-assetuploadfield .ss-uploadfield-files .ui-state-warning .ss-uploadfield-item-info { background-color: #ff9300; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #eba547), color-stop(8%, #e89a30), color-stop(50%, #e68f19), color-stop(54%, #e68e19), color-stop(96%, #e17519), color-stop(100%, #dc6718)); background-image: -webkit-linear-gradient(top, #eba547 0%, #e89a30 8%, #e68f19 50%, #e68e19 54%, #e17519 96%, #dc6718 100%); background-image: -moz-linear-gradient(top, #eba547 0%, #e89a30 8%, #e68f19 50%, #e68e19 54%, #e17519 96%, #dc6718 100%); background-image: -o-linear-gradient(top, #eba547 0%, #e89a30 8%, #e68f19 50%, #e68e19 54%, #e17519 96%, #dc6718 100%); background-image: linear-gradient(top, #eba547 0%, #e89a30 8%, #e68f19 50%, #e68e19 54%, #e17519 96%, #dc6718 100%); }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name { position: relative; z-index: 1; margin: 3px 0 3px 50px; width: 50%; color: #5e5e5e; background: #eeeded; background: rgba(255, 255, 255, 0.8); -webkit-border-radius: 3px; -moz-border-radius: 3px; -ms-border-radius: 3px; -o-border-radius: 3px; border-radius: 3px; line-height: 24px; height: 22px; padding: 0 5px; text-align: left; cursor: pointer; display: table; table-layout: fixed; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name .name { text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.5); display: inline; float: left; max-width: 50%; font-weight: normal; padding: 0 5px 0 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name .ss-uploadfield-item-status { position: relative; float: right; padding: 0 0 0 5px; max-width: 30%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.5); }

View File

@ -14,6 +14,10 @@ Used in side panels and action tabs
.cms .ss-gridfield > div { margin-bottom: 36px; }
.cms .ss-gridfield > div.addNewGridFieldButton { margin-bottom: 0; }
.cms .ss-gridfield > div.addNewGridFieldButton .action { margin-bottom: 12px; }
.cms .ss-gridfield > div.ss-gridfield-buttonrow-before { margin-bottom: 0; }
.cms .ss-gridfield > div.ss-gridfield-buttonrow-before .action { margin-bottom: 12px; }
.cms .ss-gridfield > div.ss-gridfield-buttonrow-after { margin-bottom: 0; }
.cms .ss-gridfield > div.ss-gridfield-buttonrow-after .action { margin-top: 12px; }
.cms .ss-gridfield[data-selectable] tr.ui-selected, .cms .ss-gridfield[data-selectable] tr.ui-selecting { background: #FFFAD6 !important; }
.cms .ss-gridfield[data-selectable] td { cursor: pointer; }
.cms .ss-gridfield span button#action_gridfield_relationfind { display: none; }
@ -29,7 +33,7 @@ Used in side panels and action tabs
.cms .ss-gridfield .add-existing-autocompleter { width: 500px; }
.cms .ss-gridfield .add-existing-autocompleter span { display: -moz-inline-stack; display: inline-block; vertical-align: top; *vertical-align: auto; zoom: 1; *display: inline; }
.cms .ss-gridfield .add-existing-autocompleter input.relation-search { width: 270px; margin-bottom: 12px; }
.cms .ss-gridfield .grid-csv-button, .cms .ss-gridfield .grid-print-button { margin-bottom: 12px; display: -moz-inline-stack; display: inline-block; vertical-align: middle; *vertical-align: auto; zoom: 1; *display: inline; }
.cms .ss-gridfield .grid-csv-button, .cms .ss-gridfield .grid-print-button { font-size: 12px; margin-bottom: 0; display: -moz-inline-stack; display: inline-block; vertical-align: middle; *vertical-align: auto; zoom: 1; *display: inline; }
.cms table.ss-gridfield-table { display: table; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; padding: 0; border-collapse: separate; border-bottom: 0 none; width: 100%; }
.cms table.ss-gridfield-table thead { color: #323e46; background: transparent; }
.cms table.ss-gridfield-table thead tr.filter-header .fieldgroup { max-width: 512px; }
@ -127,4 +131,3 @@ Used in side panels and action tabs
.cms table.ss-gridfield-table tr.last td { border-bottom: 0 none; }
.cms table.ss-gridfield-table td:first-child { border-left: 1px solid rgba(0, 0, 0, 0.1); }
.cms table.ss-gridfield-table td:last-child { border-right: 1px solid rgba(0, 0, 0, 0.1); }
.cms .grid-bottom-button { margin-top: 12px; }

View File

@ -1,8 +1,18 @@
/*Mixin used to generate slightly smaller text and forms
Used in side panels and action tabs
*/
div.TreeDropdownField { width: 400px; background: #fff; border: 1px solid #aaa; cursor: pointer; overflow: visible; position: relative; }
div.TreeDropdownField input { border: none; background: none; padding: 0; margin: 0; }
div.TreeDropdownField .treedropdownfield-title { float: left; padding: 7px; width: 90%; line-height: 16px; overflow: hidden; outline: none; }
div.TreeDropdownField .treedropdownfield-panel { clear: left; position: absolute; overflow: auto; display: none; cursor: default; border: 1px solid #aaa; border-top: none; margin: 1px 0 0 -1px; /* account for border on container div */ max-height: 200px; background-color: #fff; z-index: 50; -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); -o-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); }
div.TreeDropdownField .treedropdownfield-panel.loading { min-height: 30px; background: white url("../images/network-save.gif") 7px 7px no-repeat; }
div.TreeDropdownField .treedropdownfield-title, div.TreeDropdownField .treedropdownfield-search { float: left; padding: 7px; width: 90%; line-height: 16px; overflow: hidden; outline: none; z-index: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; }
div.TreeDropdownField .treedropdownfield-search { background: url("../admin/thirdparty/chosen/chosen/chosen-sprite.png") no-repeat 100% -22px; background: url("../admin/thirdparty/chosen/chosen/chosen-sprite.png") no-repeat 100% -22px, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff)); background: url("../admin/thirdparty/chosen/chosen/chosen-sprite.png") no-repeat 100% -22px, -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%); background: url("../admin/thirdparty/chosen/chosen/chosen-sprite.png") no-repeat 100% -22px, -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%); background: url("../admin/thirdparty/chosen/chosen/chosen-sprite.png") no-repeat 100% -22px, -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%); background: url("../admin/thirdparty/chosen/chosen/chosen-sprite.png") no-repeat 100% -22px, linear-gradient(top, #eeeeee 1%, #ffffff 15%); -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; position: relative; z-index: 1100; border: 1px solid #aaa; display: inline-block; font-family: sans-serif; font-size: 1em; margin: 1.5%; outline: 0; padding: 4px 20px 4px 5px; width: 97%; }
div.TreeDropdownField.searchable .treedropdownfield-panel.loading { min-height: 64px; background-position: 98% 39px; }
div.TreeDropdownField .treedropdownfield-panel { clear: left; position: absolute; display: none; cursor: default; border: 1px solid #aaa; border-top: none; margin: 1px 0 0 -1px; /* account for border on container div */ background-color: #fff; z-index: 70; -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); -o-box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); }
div.TreeDropdownField .treedropdownfield-panel.loading { min-height: 30px; background: white url("../images/network-save.gif") 98% 7px no-repeat; }
div.TreeDropdownField .treedropdownfield-panel .tree-holder { position: relative; z-index: 1; }
div.TreeDropdownField .treedropdownfield-panel .tree-holder > ul { position: relative; max-height: 200px; overflow-y: auto; }
div.TreeDropdownField .treedropdownfield-panel ul { overflow-x: hidden; float: left; width: 100%; }
div.TreeDropdownField .treedropdownfield-panel ul .jstree-icon { margin-left: 5px; }
div.TreeDropdownField .treedropdownfield-panel ul .jstree-open > ins { background-position: -18px 0; }
div.TreeDropdownField .treedropdownfield-panel ul.tree { margin: 0; }
div.TreeDropdownField .treedropdownfield-panel ul.tree a { font-size: 12px; }
div.TreeDropdownField .treedropdownfield-toggle-panel-link { border: none; margin: 0; z-index: 0; padding: 7px 3px; overflow: hidden; -webkit-border-radius: 0 4px 4px 0; -moz-border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0; }

View File

@ -1,6 +1,6 @@
body { background-color: #eee; margin: 0; overflow-x: hidden; padding: 0; font-family: Helvetica,Arial,sans-serif; }
body { background: #eee !important; margin: 0; overflow-x: hidden; padding: 0; font-family: Helvetica,Arial,sans-serif; }
.info { margin: 0 0 6px 0; padding: 18px; background-color: #003050; position: relative; line-height: 24px; color: #fff; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #002137), color-stop(10%, #003050), color-stop(90%, #003050), color-stop(100%, #002137)); background-image: -webkit-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: -moz-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: -o-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: linear-gradient(#002137, #003050 10%, #003050 90%, #002137); }
.info { margin: 0 0 6px 0; padding: 18px; background-color: #003050; position: relative; line-height: 24px; color: #fff; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #002137), color-stop(10%, #003050), color-stop(90%, #003050), color-stop(100%, #002137)); background-image: -webkit-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: -moz-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: -o-linear-gradient(#002137, #003050 10%, #003050 90%, #002137); background-image: linear-gradient(#002137, #003050 10%, #003050 90%, #002137); z-index: 9999; }
.info h1 { margin: 0 0 6px 0; padding: 0 32px 0 0; color: #fff; font-size: 24px; text-shadow: 0 1px #002137; line-height: 30px; background: url(../admin/images/logo_small.png) no-repeat right 3px; }
.info h3 { color: #7da4be; font-size: 16px; line-height: 18px; font-weight: normal; }
.info p { margin: 0; font-size: 14px; color: #fff; }
@ -9,7 +9,7 @@ body { background-color: #eee; margin: 0; overflow-x: hidden; padding: 0; font-f
.header { margin: 0; border-bottom: 6px solid #ccdef3; height: 23px; background-color: #666673; padding: 4px 0 2px 6px; }
.trace, .build, .options { padding: 6px 12px; }
.trace, .build, .options { padding: 6px 12px; background: #eee !important; position: relative; z-index: 9999; }
.trace li, .build li, .options li { font-size: 14px; margin: 6px 0; }
a { color: #666; }

View File

@ -1,4 +1,4 @@
# 3.0.6 (Not yet released)
# 3.0.6
## Overview
@ -7,21 +7,37 @@
## Details
### Security: Require ADMIN for ?flush=1
### Security: Require ADMIN for ?flush=1 (SS-2013-001)
Flushing the various manifests (class, template, config) is performed through a GET
parameter (`flush=1`). Since this action requires more server resources than normal requests,
it can facilitate [denial-of-service attacks](https://en.wikipedia.org/wiki/Denial-of-service_attack).
See [announcement](http://www.silverstripe.org/ss-2013-001-require-admin-for-flush1/)
To prevent this, main.php now checks and only allows the flush parameter in the following cases:
### Security: Privilege escalation through Group hierarchy setting (SS-2013-003)
* The [environment](/topics/environment-management) is in "dev mode"
* A user is logged in with ADMIN permissions
* An error occurs during startup
See [announcement](http://www.silverstripe.org/ss-2013-003-privilege-escalation-through-group-hierarchy-setting/)
This applies to both `flush=1` and `flush=all` (technically we only check for the existence of any parameter value)
but only through web requests made through main.php - CLI requests, or any other request that goes through
a custom start up script will still process all flush requests as normal.
### Security: Privilege escalation through Group and Member CSV upload (SS-2013-004)
See [announcement](http://www.silverstripe.org/ss-2013-004-privilege-escalation-through-group-and-member-csv-upload/)
### Security: Privilege escalation through APPLY_ROLES assignment (SS-2013-005)
See [announcement](http://www.silverstripe.org/ss-2013-005-privilege-escalation-through-apply-roles-assignment/)
### Security: Information disclosure in Versioned.php (SS-2013-006)
See [announcement](http://www.silverstripe.org/ss-2013-006-information-disclosure-in-versioned/)
### Security: Privilege escalation through Group hierarchy setting (SS-2013-003)
See [announcement](http://www.silverstripe.org/ss-2013-003-privilege-escalation-through-group-hierarchy-setting/)
### Security: Privilege escalation through Group and Member CSV upload (SS-2013-004)
See [announcement](http://www.silverstripe.org/ss-2013-004-privilege-escalation-through-group-and-member-csv-upload/)
### Security: Privilege escalation through APPLY_ROLES assignment (SS-2013-005)
See [announcement](http://www.silverstripe.org/ss-2013-005-privilege-escalation-through-apply-roles-assignment/)
## Upgrading
@ -34,4 +50,4 @@ a custom start up script will still process all flush requests as normal.
Before: `BackLink_Button.ss.Back`, after `BackLink_Button_ss.Back`. Please fix any custom language
files or uses of those entities in custom code.
* If using "Māori/Te Reo" (mi_NZ) as your CMS locale, please re-select it in `admin/myprofile`
to ensure correct operation (it has changed its locale identifier)
to ensure correct operation (it has changed its locale identifier)

View File

@ -0,0 +1,17 @@
# 3.0.7
## Overview
### Security: XSS in form validation errors (SS-2013-008)
See [announcement](http://www.silverstripe.org/ss-2013-008-xss-in-numericfield-validation/)
### Security: XSS in CMS "Pages" section (SS-2013-009)
See [announcement](http://www.silverstripe.org/ss-2013-009-xss-in-cms-pages-section/)
### API: Form validation message no longer allow HTML
Due to cross-site scripting concerns when user data is used for form messages,
it is no longer possible to use HTML in `Form->sessionMessage()`, and consequently
in the `FormField->validate()` API.

View File

@ -9,7 +9,6 @@
* Decluttered "Edit Page" buttons by moving minor actions into a "more options" panel
* Auto-detect CMS changes and highlight the save button for better informancy
* New context action "Show children as list" on tree for better management on large sites
* Display "last edited" and "last published" data for pages in CMS
* CMS form fields now support help text through `setDescription()`, both inline and as tooltips
* Removed SiteTree "MetaTitle" and "MetaKeywords" fields
* More legible and simplified tab and menu styling in the CMS
@ -23,22 +22,21 @@
* Static properties are immutable and private, you must use Config API
* Statics in custom Page classes need to be "private"
* `$default_cast` is now `Text` instead of `HTMLText`, to secure templates from XSS by default
* Shortcodes are no longer supported in template files. They continue to work in DB fields and other
HTMLText-cast fields.
* Shortcodes are no longer supported in template files (still works in DB fields and through HTMLText casting)
* `DataList` and `ArrayList` are now immutable, they'll return cloned instances on modification
* Behaviour testing support through [Behat](http://behat.org), with CMS test coverage
(see the [SilverStripe Behat Extension]() for details)
* Removed legacy table APIs (e.g. `TableListField`), use GridField instead
* Deny URL access if `Controller::$allowed_actions` is undefined
* Removed support for "*" rules in `Controller::$allowed_actions`
* Removed support for overriding rules on parent classes through `Controller::$allowed_actions`
* `RestfulService` verifies SSL peers by default
* UploadField functions on new records
* Editing of relation table data (`$many_many_extraFields`) in `GridField`
* Optional integration with ImageMagick as a new image manipulation backend
* Support for PHP 5.4's built-in webserver
* Support for [Composer](http://getcomposer.org) dependency manager (also works with 3.0)
* Added support for filtering incoming HTML from TinyMCE (disabled by default, see [security](/topics/security))
* More secure forms by limiting HTTP submissions to GET or POST only (optional)
* Behaviour testing support through [Behat](http://behat.org), with CMS test coverage
(see the [SilverStripe Behat Extension]() for details)
## Details

View File

@ -0,0 +1,26 @@
# 3.1.1 (unreleased)
## Overview ##
### CMS
### Framework
* Treedropdownfield showsearch defaults to true
## Details
## Upgrading
### Treedropdownfield showsearch defaults to true
The showSearch option of TreedropdownField is now set to true by default. This is to provide a fallback ui for when children of a tree node fail to render (due to too many children). You may set search as false when initializing a TreedropdownField, or afterwards:
:::php
$treedropdownfield->setShowSearch(false);
If your data requires a specialized search function, you may specify it within:
:::php
$treedropdownfield->setSearchFunction();

View File

@ -0,0 +1,17 @@
# 3.0.6-rc2
# Overview
TODO
### Bugfixes
* 2013-08-30 [f803704](https://github.com/silverstripe/sapphire/commit/f803704) Disallow permissions assign for APPLY_ROLES (SS-2013-005) (Ingo Schommer)
* 2013-08-30 [05757ef](https://github.com/silverstripe/sapphire/commit/05757ef) Privilege escalation through APPLY_ROLES assignment (SS-2013-005) (Ingo Schommer)
* 2013-08-30 [6cff967](https://github.com/silverstripe/sapphire/commit/6cff967) Privilege escalation through Group and Member CSV upload (SS-2013-004) (Ingo Schommer)
* 2013-08-30 [720c149](https://github.com/silverstripe/sapphire/commit/720c149) Privilege escalation through Group hierarchy setting (SS-2013-003) (Ingo Schommer)
* 2013-08-26 [65ad510](https://github.com/silverstripe/sapphire/commit/65ad510) fixed grammatical errors and formatting issues (jbridson)
* 2013-08-19 [4a7aef0](https://github.com/silverstripe/sapphire/commit/4a7aef0) Double slashes in ParameterConfirmationToken (Hamish Friedlander)
* 2013-08-09 [b1664f8](https://github.com/silverstripe/silverstripe-cms/commit/b1664f8) Check for stage and drafts in SiteTree::canView() (Simon Welsh)
* 2013-08-08 [2fae928](https://github.com/silverstripe/silverstripe-cms/commit/2fae928) ArchiveDate enforcement (Hamish Friedlander)

View File

@ -0,0 +1,17 @@
# 3.0.7-rc1
## Overview
### Security: XSS in form validation errors (SS-2013-008)
See [announcement](http://www.silverstripe.org/ss-2013-008-xss-in-numericfield-validation/)
### Security: XSS in CMS "Pages" section (SS-2013-009)
See [announcement](http://www.silverstripe.org/ss-2013-009-xss-in-cms-pages-section/)
### API: Form validation message no longer allow HTML
Due to cross-site scripting concerns when user data is used for form messages,
it is no longer possible to use HTML in `Form->sessionMessage()`, and consequently
in the `FormField->validate()` API.

View File

@ -0,0 +1,38 @@
# 3.1.0-rc2
## Overview
### Security: Privilege escalation through Group hierarchy setting (SS-2013-003)
See [announcement](http://www.silverstripe.org/ss-2013-003-privilege-escalation-through-group-hierarchy-setting/)
### Security: Privilege escalation through Group and Member CSV upload (SS-2013-004)
See [announcement](http://www.silverstripe.org/ss-2013-004-privilege-escalation-through-group-and-member-csv-upload/)
### Security: Privilege escalation through APPLY_ROLES assignment (SS-2013-005)
See [announcement](http://www.silverstripe.org/ss-2013-005-privilege-escalation-through-apply-roles-assignment/)
## Changelog
### Bugfixes
* 2013-08-30 [091c096](https://github.com/silverstripe/sapphire/commit/091c096) Disallow permissions assign for APPLY_ROLES (SS-2013-005) (Ingo Schommer)
* 2013-08-30 [cfa88ad](https://github.com/silverstripe/sapphire/commit/cfa88ad) Privilege escalation through APPLY_ROLES assignment (SS-2013-005) (Ingo Schommer)
* 2013-08-30 [46556b6](https://github.com/silverstripe/sapphire/commit/46556b6) Privilege escalation through Group and Member CSV upload (SS-2013-004) (Ingo Schommer)
* 2013-08-30 [68ca47b](https://github.com/silverstripe/sapphire/commit/68ca47b) Privilege escalation through Group hierarchy setting (SS-2013-003) (Ingo Schommer)
* 2013-08-23 [1461ae9](https://github.com/silverstripe/sapphire/commit/1461ae9) Fix regression in IE no-cache https file downloads. (Mateusz Uzdowski)
* 2013-08-22 [45c1d2b](https://github.com/silverstripe/sapphire/commit/45c1d2b) webfonts in preview iframe breaking admin fonts (Hamish Friedlander)
* 2013-08-21 [a2026ad](https://github.com/silverstripe/sapphire/commit/a2026ad) flushing on non-dev when Session::cookie_secure is true (Hamish Friedlander)
* 2013-08-20 [1c31c09](https://github.com/silverstripe/sapphire/commit/1c31c09) Correct Zend_Locale fallbacks in i18n/DateField/DateTimeField (Ingo Schommer)
* 2013-08-20 [68d8ec3](https://github.com/silverstripe/sapphire/commit/68d8ec3) Memory leaks in jstree drag & drop (Hamish Friedlander)
* 2013-08-20 [fda4b91](https://github.com/silverstripe/sapphire/commit/fda4b91) Make sure CurrentXHR is set back to null on completion (Hamish Friedlander)
* 2013-08-20 [e282f0b](https://github.com/silverstripe/sapphire/commit/e282f0b) Two memory leaks with HtmlEditorField (Hamish Friedlander)
* 2013-08-19 [4a7aef0](https://github.com/silverstripe/sapphire/commit/4a7aef0) Double slashes in ParameterConfirmationToken (Hamish Friedlander)
* 2013-08-15 [0ca4969](https://github.com/silverstripe/sapphire/commit/0ca4969) Dont update preview iframe if hidden (Hamish Friedlander)
* 2013-08-12 [c59305d](https://github.com/silverstripe/sapphire/commit/c59305d) Multiple redraw calls on navigation (Hamish Friedlander)
* 2013-08-09 [b1664f8](https://github.com/silverstripe/silverstripe-cms/commit/b1664f8) Check for stage and drafts in SiteTree::canView() (Simon Welsh)
* 2013-08-08 [2fae928](https://github.com/silverstripe/silverstripe-cms/commit/2fae928) ArchiveDate enforcement (Hamish Friedlander)
* 2013-08-07 [fb67181](https://github.com/silverstripe/sapphire/commit/fb67181) Context menu too long - CSS only (Fixes CMS #811) (Naomi Guyer)
* 2013-08-07 [71608f0](https://github.com/silverstripe/silverstripe-cms/commit/71608f0) Add SiteTree link tracking as an extension, and apply to SiteTree itself (Hamish Friedlander)

View File

@ -0,0 +1,21 @@
# 3.1.0-rc3
# Overview
### Security: XSS in CMS "Security" section (SS-2013-007)
See [announcement](http://www.silverstripe.org/ss-2013-007-xss-in-cms-security-section/)
### Security: XSS in form validation errors (SS-2013-008)
See [announcement](http://www.silverstripe.org/ss-2013-008-xss-in-numericfield-validation/)
### Security: XSS in CMS "Pages" section (SS-2013-009)
See [announcement](http://www.silverstripe.org/ss-2013-009-xss-in-cms-pages-section/)
### API: Form validation message no longer allow HTML
Due to cross-site scripting concerns when user data is used for form messages,
it is no longer possible to use HTML in `Form->sessionMessage()`, and consequently
in the `FormField->validate()` API.

View File

@ -19,7 +19,7 @@ add a `.cms-description-tooltip` class.
:::php
TextField::create('MyText', 'My Text Label')
->setDescription('More <strong>detailed</strong> help')
->addExtraClass('cms-help-tooltip');
->addExtraClass('cms-description-tooltip');
Tooltips are only supported
for native, focusable input elements, which excludes
@ -27,4 +27,4 @@ more complex fields like `GridField`, `UploadField`
or `DropdownField` with the chosen.js behaviour applied.
Note: For more advanced help text we recommend using
[Custom form field templates](/topics/forms#custom-form-field-templates);
[Custom form field templates](/topics/forms#custom-form-field-templates);

View File

@ -0,0 +1,140 @@
# How to add a custom action to a GridField row
In a [GridField](/reference/grid-field) instance each table row can have a
number of actions located the end of the row such as edit or delete actions.
Each action is represented as a instance of a specific class
(e.g [api:GridFieldEditButton]) which has been added to the `GridFieldConfig`
for that `GridField`
As a developer, you can create your own custom actions to be located alongside
the built in buttons.
For example let's create a custom action on the GridField to allow the user to
perform custom operations on a row.
## Basic GridFieldCustomAction boilerplate
A basic outline of our new `GridFieldCustomAction.php` will look like something
below:
:::php
<?php
class GridFieldCustomAction implements GridField_ColumnProvider, GridField_ActionProvider {
public function augmentColumns($gridField, &$columns) {
if(!in_array('Actions', $columns)) {
$columns[] = 'Actions';
}
}
public function getColumnAttributes($gridField, $record, $columnName) {
return array('class' => 'col-buttons');
}
public function getColumnMetadata($gridField, $columnName) {
if($columnName == 'Actions') {
return array('title' => '');
}
}
public function getColumnsHandled($gridField) {
return array('Actions');
}
public function getColumnContent($gridField, $record, $columnName) {
if(!$record->canEdit()) return;
$field = GridField_FormAction::create(
$gridField,
'CustomAction'.$record->ID,
'Do Action',
"docustomaction",
array('RecordID' => $record->ID)
);
return $field->Field();
}
public function getActions($gridField) {
return array('docustomaction');
}
public function handleAction(GridField $gridField, $actionName, $arguments, $data) {
if($actionName == 'docustomaction') {
// perform your action here
// output a success message to the user
Controller::curr()->getResponse()->setStatusCode(
200,
'Do Custom Action Done.'
);
}
}
}
## Add the GridFieldCustomAction to the current `GridFieldConfig`
While we're working on the code, to add this new action to the `GridField`, add
a new instance of the class to the [api:GridFieldConfig] object. The `GridField`
[Reference](/reference/grid-field) documentation has more information about
manipulating the `GridFieldConfig` instance if required.
:::php
$config = GridFieldConfig::create();
$config->addComponent(new GridFieldCustomAction());
$gridField = new GridField('Teams', 'Teams', $this->Teams(), $config);
Now let's go back and dive through the `GridFieldCustomAction` class in more
detail.
First thing to note is that our new class implements two interfaces,
[api:GridField_ColumnProvider] and [api:GridField_ActionProvider].
Each interface allows our class to define particular behaviors and is a core
concept of the modular `GridFieldConfig` system.
The `GridField_ColumnProvider` implementation tells SilverStripe that this class
will provide the `GridField` with an additional column of information. By
implementing this interface we're required to define several methods to explain
where we want the column to exist and how we need it to be formatted. This is
done via the following methods:
* `augmentColumns`
* `getColumnAttributes`
* `getColumnMetadata`
* `getColumnsHandled`
* `getColumnContent`
In this example, we simply add the new column to the existing `Actions` column
located at the end of the table. Our `getColumnContent` implementation produces
a custom button for the user to click on the page.
The second interface we add is `GridField_ActionProvider`. This interface is
used as we're providing a custom action for the user to take (`docustomaction`).
This action is triggered when a user clicks on the button defined in
`getColumnContent`. As with the `GridField_ColumnProvider` interface, by adding
this interface we have to define two methods to describe the behavior of the
action:
* `getActions` returns an array of all the custom actions we want this class to
handle (i.e `docustomaction`) .
* `handleAction` method which will contain the logic for performing the
specific action (e.g publishing the row to a thirdparty service).
Inside `handleAction` we have access to the current GridField and GridField row
through the `$arguments`. If your column provides more than one action (e.g two
links) both actions will be handled through the one `handleAction` method. The
called method is available as a parameter.
To finish off our basic example, the `handleAction` method simply returns a
message to the user interface indicating a successful message.
## Related
* [GridField Reference](/reference/grid-field)
* [ModelAdmin: A UI driven by GridField](/reference/modeladmin)
* [Tutorial 5: Dataobject Relationship Management](/tutorials/5-dataobject-relationship-management)

View File

@ -19,3 +19,4 @@ the language and functions which are used in the guides.
* [How to create a navigation menu](navigation-menu). Create primary navigation for your website.
* [Paginating A List](pagination). Add pagination for an SS_List object.
* [How to make a simple contact form](simple-contact-form).
* [How to add a custom action to a GridField row](gridfield-rowaction)

View File

@ -12,7 +12,11 @@ Please register a free translator account to get started, even if you just feel
We provide a GUI for translations through [transifex.com](http://transifex.com). If you don't have an account yet, please follow the links there to sign up. Select a project from the [list of translatable modules](https://www.transifex.com/accounts/profile/silverstripe/) and start translating online!
For all modules listed there, we automatically import new master strings as they get committed to the various codebases, so you're always translating on the latest and greatest version.
For all modules listed there, we automatically import new master strings
as they get committed to the various codebases (via a nightly task),
so you're always translating on the latest and greatest version.
You can check the last successful push of the translation master strings in our
[public continuous integration server](http://teamcity.silverstripe.com/viewType.html?buildTypeId=bt112) (select "log in as guest").
## FAQ

View File

@ -319,7 +319,7 @@ Upon the receipt of the response, the fragment will be injected into DOM where a
has been found on an element (this element will get completely replaced). Afterwards a `afterloadfragment` event
will be triggered. In case of a request error a `loadfragmenterror` will be raised and DOM will not be touched.
You can hook up a response handler that obtains all the details of the XHR request like this:
You can hook up a response handler that obtains all the details of the XHR request via Entwine handler:
'from .cms-container': {
onafterloadfragment: function(e, data) {
@ -328,6 +328,15 @@ You can hook up a response handler that obtains all the details of the XHR reque
}
}
Alternatively you can use the jQuery deferred API:
$('.cms-container')
.loadFragment('admin/foobar/', 'Fragment1')
.success(function(data, status, xhr) {
// Say 'success'!
alert(status);
});
## Ajax Redirects
Sometimes, a server response represents a new URL state, e.g. when submitting an "add record" form,

View File

@ -333,3 +333,4 @@ records, use the `DataObject->can...()` methods
* [ModelAdmin: A UI driven by GridField](/reference/modeladmin)
* [Tutorial 5: Dataobject Relationship Management](/tutorials/5-dataobject-relationship-management)
* [How to add a custom action to a GridField row](/howto/gridfield-rowaction)

View File

@ -15,23 +15,19 @@ The simple usage, Permission::check("PERM_CODE") will detect if the currently lo
**Group ACLs**
* Call **Permission::check("MY_PERMISSION_CODE")** to see if the current user has MY_PERMISSION_CODE.
* MY_PERMISSION_CODE can be loaded into the Security admin on the appropriate group, using the "Permissions" tab.
You can use whatever codes you like, but for the sanity of developers and users, it would be worth listing the codes in
[permissions:codes](/reference/permission)
* MY_PERMISSION_CODE can be loaded into the Security admin on the appropriate group, using the "Permissions" tab.
## PermissionProvider
`[api:PermissionProvider]` is an interface which lets you define a method *providePermissions()*. This method should return a
map of permission code names with a human readable explanation of its purpose (see
[permissions:codes](/reference/permission)).
`[api:PermissionProvider]` is an interface which lets you define a method *providePermissions()*.
This method should return a map of permission code names with a human readable explanation of its purpose.
:::php
class Page_Controller implements PermissionProvider {
public function init() {
if(!Permission::check("VIEW_SITE")) Security::permissionFailure();
}
public function providePermissions() {
return array(
"VIEW_SITE" => "Access the site",
@ -53,7 +49,7 @@ By default, permissions are used in the following way:
* If not logged in, the 'View' permissions must be 'anyone logged in' for a page to be displayed in a menu
* If logged in, you must be allowed to view a page for it to be displayed in a menu
**NOTE:** Should the canView() method on SiteTree be updated to call Permission::check("SITETREE_VIEW", $this->ID)?
**NOTE:** Should the canView() method on SiteTree be updated to call Permission::check("SITETREE_VIEW", $this->ID)?
Making this work well is a subtle business and should be discussed with a few developers.
## Setting up permissions

View File

@ -91,7 +91,7 @@ JavaScript in a separate file and instead load, via search and replace, several
:::php
$vars = array(
"EditorCSS" => "mot/css/editor.css",
)
);
Requirements::javascriptTemplate("cms/javascript/editor.template.js", $vars);
@ -185,4 +185,4 @@ slightly different JS/CSS requirements, the whole lot will be refetched.
nature of an ajax-request. Needs some more research
## API Documentation
`[api:Requirements]`
`[api:Requirements]`

View File

@ -417,7 +417,7 @@ have to repeat it on each reference of a property.
properties of the collection itself, instead of iterating over it. For example:
:::ss
$Children.Length
$Children.Count
returns the number of items in the $Children collection.

View File

@ -38,7 +38,7 @@ a description of the configuration setting.
Each named class configuration property can contain either an array or a non-array value.
If the value is an array, each value in the array may also be one of those three types
As mentioned, this value of any specific class configuration property comes from several sources. These sources do not
As mentioned, the value of any specific class configuration property comes from several sources. These sources do not
override each other (except in one specific circumstance) - instead the values from each source are merged together
to give the final configuration value, using these rules:
@ -49,10 +49,10 @@ to give the final configuration value, using these rules:
- If the value is not an array, the highest priority value is used without any attempt to merge
It is an error to have mixed types of the same named property in different locations (but an error will not necessarily
be raised due to optimisations in the lookup code)
be raised due to optimisations in the lookup code).
The exception to this is "false-ish" values - empty arrays, empty strings, etc. When merging a non-false-ish value with a
false-ish value, the result will be the non-false-ish value regardless of priority. When merging two false-sh values
false-ish value, the result will be the non-false-ish value regardless of priority. When merging two false-ish values
the result will be the higher priority false-ish value.
The locations that configuration values are taken from in highest -> lowest priority order are:
@ -85,7 +85,7 @@ done by calling the static method `[api:Config::inst()]`, like so:
$config = Config::inst();
There are then three public methods available on the instance so obtained
There are then three public methods available on the instance so obtained:
- Config#get() returns the value of a specified classes' property
- Config#remove() removes information from the value of a specified classes' property.
@ -136,7 +136,7 @@ is only one set of values the header can be omitted.
### The header
Each value section of a YAML file has
Each value section of a YAML file has:
- A reference path, made up of the module name, the config file name, and a fragment identifier
- A set of rules for the value section's priority relative to other value sections
@ -150,13 +150,13 @@ value section in the header section that immediately preceeds the value section.
Each value section has a reference path. Each path looks a little like a URL,
and is of this form: `module/file#fragment`.
- "module" is the name of the module this YAML file is in
- "file" is the name of this YAML file, stripped of the extension (so for routes.yml, it would be routes)
- "fragment" is a specified identifier. It is specified by putting a `Name: {fragment}` key / value pair into the header
- "module" is the name of the module this YAML file is in.
- "file" is the name of this YAML file, stripped of the extension (so for routes.yml, it would be routes).
- "fragment" is a specified identifier. It is specified by putting a `Name: {fragment}` key / value pair into the header.
section. If you don't specify a name, a random one will be assigned.
This reference path has no affect on the value section itself, but is how other header sections refer to this value
section in their priority chain rules
section in their priority chain rules.
#### Priorities
@ -190,7 +190,7 @@ one value section can not be both before _and_ after another. However when you h
was a difference in how many wildcards were used, the one with the least wildcards will be kept and the other one
ignored.
A more complex example, taken from framework/_config/routes.yml
A more complex example, taken from framework/_config/routes.yml:
:::yml
---
@ -212,8 +212,8 @@ The value section above has two rules:
In this case there would appear to be a problem - adminroutes can not be both before all other value sections _and_
after value sections with a name of `rootroutes`. However because `\*` has three wildcards
(it is the equivalent of `\*/\*#\*`) but `#rootroutes` only has two (it is the equivalent of `\*/\*#rootroutes`),
`\*` in this case means "every value section _except_ ones that have a fragment name of rootroutes"
(it is the equivalent of `\*/\*#\*`) but `#rootroutes` only has two (it is the equivalent of `\*/\*#rootroutes`).
In this case `\*` means "every value section _except_ ones that have a fragment name of rootroutes".
One important thing to note: it is possible to create chains that are unsolvable. For instance, A must be before B,
B must be before C, C must be before A. In this case you will get an error when accessing your site.
@ -221,7 +221,7 @@ B must be before C, C must be before A. In this case you will get an error when
#### Exclusionary rules
Some value sections might only make sense under certain environmental conditions - a class exists, a module is installed,
an environment variable or constant is set, or SilverStripe is running in a certain environment mode (live, dev, etc)
an environment variable or constant is set, or SilverStripe is running in a certain environment mode (live, dev, etc).
To accommodate this, value sections can be filtered to only be used when either a rule matches or doesn't match the
current environment.
@ -267,12 +267,12 @@ will result in only the latter coming through.
### The values
The values section of YAML configuration files is quite simple - it is simply a nested key / value pair structure
The values section of a YAML configuration file is quite simple - it is simply a nested key / value pair structure
where the top level key is the class name to set the property on, and the sub key / value pairs are the properties
and values themselves (where values of course can themselves be nested hashes).
and values themselves (where values, of course, can themselves be nested hashes).
A simple example setting a property called "foo" to the scalar "bar" on class "MyClass", and a property called "baz"
to a nested array on class "MyOtherClass".
to a nested array on class "MyOtherClass":
:::yml
MyClass:
@ -319,8 +319,9 @@ classes (see [common-problems](/installation/common-problems)).
## Configuration through the CMS
SilverStripe framework does not provide a method to set most system-level configuration via a web panel.
This lack of a configuration GUI is on purpose, as we'd like to keep developer-level options where they belong (into
SilverStripe framework does not provide a method to set configuration via a web panel.
This lack of a configuration-GUI is on purpose, as we'd like to keep developer-level options where they belong (into
code), without cluttering up the interface. See this core forum discussion ["The role of the
CMS"](http://www.silverstripe.org/archive/show/532) for further reasoning.

View File

@ -584,6 +584,35 @@ object type.
}
### belongs_to
Defines a 1-to-1 relationship with another object, which declares the other end
of the relationship with a corresponding $has_one. A single database column named
`<relationship-name>ID` will be created in the object with the $has_one, but
the $belongs_to by itself will not create a database field. This field will hold
the ID of the object declaring the $belongs_to.
Similarly with $has_many, dot notation can be used to explicitly specify the $has_one
which refers to this relation. This is not mandatory unless the relationship would
be otherwise ambiguous.
:::php
class Torso extends DataObject {
// HeadID will be generated on the Torso table
private static $has_one = array(
'Head' => 'Head'
);
}
class Head extends DataObject {
// No database field created. The '.Head' suffix could be omitted
private static $belongs_to = array(
'Torso' => 'Torso.Head'
);
}
### many_many
Defines many-to-many joins. A new table, (this-class)_(relationship-name), will

View File

@ -131,6 +131,21 @@ An alternative (or additional) approach to validation is to place it directly
on the model. SilverStripe provides a `[api:DataObject->validate()]` method for this purpose.
Refer to the ["datamodel" topic](/topics/datamodel#validation-and-constraints) for more information.
## Validation in the CMS
Since you're not creating the forms for editing CMS records,
SilverStripe provides you with a `getCMSValidator()` method on your models
to return a `[api:Validator]` instance.
:::php
class Page extends SiteTree {
private static $db = array('MyRequiredField' => 'Text');
public function getCMSValidator() {
return new RequiredFields(array('MyRequiredField'));
}
}
## Subclassing Validator
To create your own validator, you need to subclass validator and define two methods:

View File

@ -156,7 +156,7 @@ data.
// Template method
public function HelloForm() {
return new MyForm($this, 'MyCustomForm');
return new MyForm($this, 'HelloForm');
}
}

View File

@ -323,7 +323,11 @@ match the hash stored in the users session, the request is discarded.
You can disable this behaviour through `[api:Form->disableSecurityToken()]`.
It is also recommended to limit form submissions to the intended HTTP verb (mostly `GET` or `POST`)
through `[api:Form->setStrictFormMethodCheck()]`.
through `[api:Form->setStrictFormMethodCheck()]`.
Sometimes you need to handle state-changing HTTP submissions which aren't handled through
SilverStripe's form system. In this case, you can also check the current HTTP request
for a valid token through `[api:SecurityToken::checkRequest()]`.
## Casting user input

View File

@ -1,69 +0,0 @@
# How To Create a SilverStripe Test
A unit test class will test the behaviour of one of your `[api:DataObjects]`. This simple fragment of `[api:SiteTreeTest]`
provides us the basics of creating unit tests.
:::php
<?php
class SiteTreeTest extends SapphireTest {
// Define the fixture file to use for this test class
private static $fixture_file = 'SiteTreeTest.yml';
/**
* Test generation of the URLSegment values.
* - Turns things into lowercase-hyphen-format
* - Generates from Title by default, unless URLSegment is explicitly set
* - Resolves duplicates by appending a number
*/
public function testURLGeneration() {
$expectedURLs = array(
'home' => 'home',
'staff' => 'my-staff',
'about' => 'about-us',
'staffduplicate' => 'my-staff-2',
'product1' => '1-1-test-product',
'product2' => 'another-product',
'product3' => 'another-product-2',
'product4' => 'another-product-3',
);
foreach($expectedURLs as $fixture => $urlSegment) {
$obj = $this->objFromFixture('Page', $fixture);
$this->assertEquals($urlSegment, $obj->URLSegment);
}
}
}
There are a number of points to note in this code fragment:
* Your test is a **subclass of SapphireTest**. Both unit tests and functional tests are a subclass of `[api:SapphireTest]`.
* **static $fixture_file** is defined. The testing framework will automatically set up a new database for **each** of
your tests. The initial database content will be sourced from the YML file that you list in $fixture_file. The property can take an array of fixture paths.
* Each **method that starts with the word "test"** will be executed by the TestRunner. Define as many as you like; the
database will be rebuilt for each of these.
* **$this->objFromFixture($className, $identifier)** can be used to select one of the objects named in your fixture
file. To identify to the object, we provide a class name and an identifier. The identifier is specified in the YML
file but not saved in the database anywhere. objFromFixture() looks the `[api:DataObject]` up in memory rather than using the
database. This means that you can use it to test the functions responsible for looking up content in the database.
## Assertion commands
**$this->assertEquals()** is an example of an assertion function.
These functions form the basis of our tests - a test
fails if and only if one or more of the assertions fail.
See [the PHPUnit manual](http://www.phpunit.de/manual/current/en/api.html#api.assert)
for a listing of all PHPUnit's built-in assertions.
The `[api:SapphireTest]` class comes with additional assertions which are more
specific to the framework, e.g. `[api:SapphireTest->assertEmailSent()]`
which can simulate sending emails through the `[api:Email->send()]` API without actually
using a mail server (see the [testing emails](email-sending)) guide.
## Fixtures
Often you need to test your functionality with some existing data, so called "fixtures".
These records are inserted on a fresh test database automatically.
[Read more about fixtures](fixtures).

View File

@ -1,4 +1,4 @@
# Writing functional tests
# Creating a functional tests
Functional tests test your controllers. The core of these are the same as unit tests:
@ -13,7 +13,7 @@ URLs. Here is an example from the subsites module:
:::php
class SubsiteAdminTest extends SapphireTest {
private static $fixture_file = 'subsites/tests/SubsiteTest.yml';
/**
* Return a session that has a user logged in as an administrator
*/
@ -22,27 +22,27 @@ URLs. Here is an example from the subsites module:
'loggedInAs' => $this->idFromFixture('Member', 'admin')
));
}
/**
* Test generation of the view
*/
public function testBasicView() {
// Open the admin area logged in as admin
$response1 = Director::test('admin/subsites/', null, $this->adminLoggedInSession());
// Confirm that this URL gets you the entire page, with the edit form loaded
$response2 = Director::test('admin/subsites/show/1', null, $this->adminLoggedInSession());
$this->assertTrue(strpos($response2->getBody(), 'id="Root_Configuration"') !== false);
$this->assertTrue(strpos($response2->getBody(), '<head') !== false);
// Confirm that this URL gets you just the form content, with the edit form loaded
$response3 = Director::test('admin/subsites/show/1', array('ajax' => 1), $this->adminLoggedInSession());
$this->assertTrue(strpos($response3->getBody(), 'id="Root_Configuration"') !== false);
$this->assertTrue(strpos($response3->getBody(), '<form') === false);
$this->assertTrue(strpos($response3->getBody(), '<head') === false);
}
}
We are using a new static method here: **Director::test($url, $postVars, $sessionObj)**
@ -64,5 +64,6 @@ We can use string processing on the body of the response to then see if it fits
If you're testing for natural language responses like error messages, make sure to use [i18n](/topics/i18n) translations through
the *_t()* method to avoid tests failing when i18n is enabled.
Note that for a more highlevel testing approach, SilverStripe also supports
[behaviour-driven testing through Behat](https://github.com/silverstripe-labs/silverstripe-behat-extension). It interacts directly with your website or CMS interface by remote controlling an actual browser, driven by natural language assertions.
Note that for a more highlevel testing approach, SilverStripe also supports
[behaviour-driven testing through Behat](https://github.com/silverstripe-labs/silverstripe-behat-extension). It interacts
directly with your website or CMS interface by remote controlling an actual browser, driven by natural language assertions.

View File

@ -0,0 +1,64 @@
# Creating a SilverStripe Test
A test is created by extending one of two classes, SapphireTest and FunctionalTest.
You would subclass `[api:SapphireTest]` to test your application logic,
for example testing the behaviour of one of your `[DataObjects](api:DataObject)`,
whereas `[api:FunctionalTest]` is extended when you want to test your application's functionality,
such as testing the results of GET and POST requests,
and validating the content of a page. FunctionalTest is a subclass of SapphireTest.
## Creating a test from SapphireTest
Here is an example of a test which extends SapphireTest:
:::php
<?php
class SiteTreeTest extends SapphireTest {
// Define the fixture file to use for this test class
private static $fixture_file = 'SiteTreeTest.yml';
/**
* Test generation of the URLSegment values.
* - Turns things into lowercase-hyphen-format
* - Generates from Title by default, unless URLSegment is explicitly set
* - Resolves duplicates by appending a number
*/
public function testURLGeneration() {
$expectedURLs = array(
'home' => 'home',
'staff' => 'my-staff',
'about' => 'about-us',
'staffduplicate' => 'my-staff-2'
);
foreach($expectedURLs as $fixture => $urlSegment) {
$obj = $this->objFromFixture('Page', $fixture);
$this->assertEquals($urlSegment, $obj->URLSegment);
}
}
}
Firstly we define a static member `$fixture_file`, this should point to a file that represents the data we want to test,
represented in YAML. When our test is run, the data from this file will be loaded into a test database for our test to use.
This property can be an array of strings pointing to many .yml files, but for our test we are just using a string on its
own. For more detail on fixtures, see [this page](fixtures).
The second part of our class is the `testURLGeneration` method. This method is our test. You can asign many tests, but
again for our purposes there is just the one. When the test is executed, methods prefixed with the word `test` will be
run. The test database is rebuilt every time one of these methods is run.
Inside our test method is the `objFromFixture` method that will generate an object for us based on data from our fixture
file. To identify to the object, we provide a class name and an identifier. The identifier is specified in the YAML file
but not saved in the database anywhere, `objFromFixture` looks the `[api:DataObject]` up in memory rather than using the
database. This means that you can use it to test the functions responsible for looking up content in the database.
The final part of our test is an assertion command, `assertEquals`. An assertion command allows us to test for something
in our test methods (in this case we are testing if two values are equal). A test method can have more than one assertion
command, and if any one of these assertions fail, so will the test method.
For more information on PHPUnit's assertions see the [PHPUnit manual](http://www.phpunit.de/manual/current/en/api.html#api.assert).
The `[api:SapphireTest]` class comes with additional assertions which are more specific to Sapphire, for example the
`assertEmailSent` method, which simulates sending emails through the `Email->send()`
API without actually using a mail server. For more details on this see the [testing emails](testing-email) guide.

View File

@ -2,80 +2,126 @@
## Overview
Often you need to test your functionality with some existing data, so called "fixtures".
The `[api:SapphireTest]` class already prepares an empty database for you,
and you have various ways to define those fixtures.
You will often find the need to test your functionality with some consistent data.
If we are testing our code with the same data each time,
we can trust our tests to yeild reliable results.
In Silverstripe we define this data via 'fixtures' (so called because of their fixed nature).
The `[api:SapphireTest]` class takes care of populating a test database with data from these fixtures -
all we have to do is define them, and we have a few ways in which we can do this.
## YAML Fixtures
YAML is a markup language which is deliberately simple and easy to read,
so ideal for our fixture generation.
so it is ideal for fixture generation.
We will begin with a sample file and talk our way through it.
Say we have the following two DataObjects:
Page:
home:
Title: Home
about:
Title: About Us
staff:
Title: Staff
URLSegment: my-staff
Parent: =>Page.about
RedirectorPage:
redirect_home:
RedirectionType: Internal
LinkTo: =>Page.home
:::php
class Player extends DataObject {
static $db = array (
'Name' => 'Varchar(255)'
);
static $has_one = array(
'Team' => 'Team'
);
}
The contents of the YAML file are broken into three levels.
class Team extends DataObject {
static $db = array (
'Name' => 'Varchar(255)',
'Origin' => 'Varchar(255)'
);
* **Top level: class names** - `Page` and `RedirectorPage`. This is the name of the dataobject class that should be created.
The fact that `RedirectorPage` is actually a subclass is irrelevant to the system populating the database. It just
instantiates the object you specify.
* **Second level: identifiers** - `home`, `about`, etc. These are the identifiers that you pass as
the second argument of SapphireTest::objFromFixture(). Each identifier you specify delimits a new database record.
This means that every record needs to have an identifier, whether you use it or not.
* **Third level: fields** - each field for the record is listed as a 3rd level entry. In most cases, the field's raw
content is provided. However, if you want to define a relationship, you can do so using "=>".
static $has_many = array(
'Players' => 'Player'
);
}
There are a couple of lines like this:
We can represent multiple instances of them in `YAML` as follows:
Parent: =>Page.about
:::yml
Player:
john:
Name: John
Team: =>Team.hurricanes
joe:
Name: Joe
Team: =>Team.crusaders
jack:
Name: Jack
Team: =>Team.crusaders
Team:
hurricanes:
Name: The Hurricanes
Origin: Wellington
crusaders:
Name: The Crusaders
Origin: Bay of Plenty
This will tell the system to set the ParentID database field to the ID of the Page object with the identifier "about".
This can be used on any has-one or many-many relationship. Note that we use the name of the relationship (Parent), and
not the name of the database field (ParentID)
Our `YAML` is broken up into three levels, signified by the indentation of each line.
In the first level of indentation, `Player` and `Team`,
represent the class names of the objects we want to be created for the test.
On many-many relationships, you should specify a comma separated list of values.
The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are identifiers.
These are what you pass as the second argument of `SapphireTest::objFromFixture()`.
Each identifier you specify represents a new object.
MyRelation: =>Class.inst1,=>Class.inst2,=>Class.inst3
The third and final level represents each individual object's fields.
A field can either be provided with raw data (such as the Names for our Players),
or we can define a relationship, as seen by the fields prefixed with `=>`.
An crucial thing to note is that **the YAML file specifies DataObjects, not database records**. The database is
populated by instantiating DataObject objects, setting the fields listed, and calling write(). This means that any
onBeforeWrite() or default value logic will be executed as part of the test. This forms the basis of our
testURLGeneration() test above.
Each one of our Players has a relationship to a Team,
this is shown with the `Team` field for each `Player` being set to `=>Team.` followed by a team name.
Take the player John for example, his team is the Hurricanes which is represented by `=>Team.hurricanes`.
This is tells the system that we want to set up a relationship for the `Player` object `john` with the `Team` object `hurricanes`.
It will populate the `Player` object's `TeamID` with the ID of `hurricanes`,
just like how a relationship is always set up.
For example, the URLSegment value of Page.staffduplicate is the same as the URLSegment value of Page.staff. When the
fixture is set up, the URLSegment value of Page.staffduplicate will actually be my-staff-2.
<div class="hint" markdown='1'>
Note that we use the name of the relationship (Team), and not the name of the database field (TeamID).
</div>
Finally, be aware that requireDefaultRecords() is **not** called by the database populator - so you will need to specify
standard pages such as 404 and home in your YAML file.
This style of relationship declaration can be used for both a `has-one` and a `many-many` relationship.
For `many-many` relationships, we specify a comma separated list of values.
For example we could just as easily write the above as:
:::yml
Player:
john:
Name: John
joe:
Name: Joe
jack:
Name: Jack
Team:
hurricanes:
Name: The Hurricanes
Origin: Wellington
Players: =>Player.john
crusaders:
Name: The Crusaders
Origin: Bay of Plenty
Players: =>Player.joe,=>Player.jack
A crucial thing to note is that **the YAML file specifies DataObjects, not database records**.
The database is populated by instantiating DataObject objects and setting the fields declared in the YML,
then calling write() on those objects.
This means that any `onBeforeWrite()` or default value logic will be executed as part of the test.
The reasoning behind this is to allow us to test the `onBeforeWrite` functionality of our objects.
You can see this kind of testing in action in the `testURLGeneration()` test from the example in
[Creating a SilverStripe Test](creating-a-silverstripe-test).
## Test Class Definition
### Manual Object Creation
## Manual Object Creation
Sometimes statically defined fixtures don't suffice, because of the complexity of the tested model,
or because the YAML format doesn't allow you to modify all model state.
Sometimes statically defined fixtures don't suffice. This could be because of the complexity of the tested model,
or because the YAML format doesn't allow you to modify all of a model's state.
One common example here is publishing pages (page fixtures aren't published by default).
You can always resort to creating objects manually in the test setup phase.
Since the test database is cleared on every test method, you'll get a fresh
set of test instances every time.
Since the test database is cleared on every test method, you'll get a fresh set of test instances every time.
:::php
class SiteTreeTest extends SapphireTest {
@ -94,20 +140,21 @@ set of test instances every time.
### Why Factories?
Manually defined fixture provide full flexibility, but very little in terms of structure and convention.
Alternatively, you can use the `[api:FixtureFactory]` class, which allows you
to set default values, callbacks on object creation, and dynamic/lazy value setting.
By the way, the `SapphireTest` YAML fixtures rely on internally on this class as well.
While manually defined fixtures provide full flexibility, they offer very little in terms of structure and convention.
Alternatively, you can use the `[api:FixtureFactory]` class, which allows you to set default values,
callbacks on object creation, and dynamic/lazy value setting.
The idea is that rather than instanciating objects directly, we'll have a factory class for them.
This factory can have so called "blueprints" defined on it, which tells the factory
how to instanciate an object of a specific type. Blueprints need a name,
which is usually set to the class it creates.
<div class="hint" markdown='1'>
SapphireTest uses FixtureFactory under the hood when it is provided with YAML based fixtures.
</div>
The idea is that rather than instantiating objects directly, we'll have a factory class for them.
This factory can have so called "blueprints" defined on it, which tells the factory how to instantiate an object of a specific type. Blueprints need a name, which is usually set to the class it creates.
### Usage
Since blueprints are auto-created for all available DataObject subclasses,
you only need to instanciate a factory to start using it.
you only need to instantiate a factory to start using it.
:::php
$factory = Injector::inst()->create('FixtureFactory');
@ -116,7 +163,7 @@ you only need to instanciate a factory to start using it.
It is important to remember that fixtures are referenced by arbitrary
identifiers ('myobj1'). These are internally mapped to their database identifiers.
:::
:::php
$databaseId = $factory->getId('MyClass', 'myobj1');
In order to create an object with certain properties, just add a second argument:
@ -124,7 +171,7 @@ In order to create an object with certain properties, just add a second argument
:::php
$obj = $factory->createObject('MyClass', 'myobj1', array('MyProperty' => 'My Value'));
### Default Properties
#### Default Properties
Blueprints can be overwritten in order to customize their behaviour,
for example with default properties in case none are passed into `createObject()`.
@ -134,11 +181,10 @@ for example with default properties in case none are passed into `createObject()
'MyProperty' => 'My Default Value'
));
### Dependent Properties
#### Dependent Properties
Values can be set on demand through anonymous functions,
which can either generate random defaults, or create
composite values based on other fixture data.
Values can be set on demand through anonymous functions, which can either generate random defaults,
or create composite values based on other fixture data.
:::php
$factory->define('Member', array(
@ -152,7 +198,7 @@ composite values based on other fixture data.
}
));
### Relations
#### Relations
Model relations can be expressed through the same notation as in the YAML fixture format
described earlier, through the `=>` prefix on data values.
@ -162,7 +208,7 @@ described earlier, through the `=>` prefix on data values.
'MyHasManyRelation' => '=>MyOtherObject.obj1,=>MyOtherObject.obj2'
));
### Callbacks
#### Callbacks
Sometimes new model instances need to be modified in ways which can't be expressed
in their properties, for example to publish a page, which requires a method call.
@ -195,7 +241,7 @@ By default, blueprint names equal the class names they manage.
$obj->Groups()->add($adminGroup);
}
});
$member = $factory->createObject('Member'); // not in admin group
$admin = $factory->createObject('AdminMember'); // in admin group

View File

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

View File

@ -2,105 +2,65 @@
The SilverStripe core contains various features designed to simplify the process of creating and managing automated tests.
* [Create a unit test](create-silverstripe-test): Writing tests to check core data objects
* [Creating a functional test](create-functional-test): An overview of functional tests and how to write a functional test
* [Email Sending](email-sending): An overview of the built-in email testing code
* [Troubleshooting](testing-guide-troubleshooting): Frequently asked questions list for testing issues
* [Why Unit Test?](why-test): Why should you test and how to start testing
If you are familiar with PHP coding but new to unit testing, you should read the [Introduction](/topics/testing) and
check out Mark's presentation [Getting to Grips with SilverStripe Testing](http://www.slideshare.net/maetl/getting-to-grips-with-silverstripe-testing).
This section's page [Why Unit Test?](why-should-i-test) will give you the reasons behind why you should be testing your
code.
You should also read over [the PHPUnit manual](http://www.phpunit.de/manual/current/en/). It provides a lot of
fundamental concepts that we build on in this documentation.
If you're more familiar with unit testing, but want a refresher of some of the concepts and terminology, you can browse
the [Testing Glossary](#glossary).
To get started now, follow the installation instructions below, and check
[Troubleshooting](/topics/testing/testing-guide-troubleshooting) in case you run into any problems.
the [Testing Glossary](glossary). To get started now, follow the installation instructions below, and check
[Troubleshooting](troubleshooting) in case you run into any problems.
## Installation
### Via Composer
Unit tests are not included in the normal SilverStripe downloads,
you are expected to work with local git repositories
Unit tests are not included in the normal SilverStripe downloads, you are expected to work with local git repositories
([installation instructions](/topics/installation/composer)).
Once you've got the project up and running,
check out the additional requirements to run unit tests:
Once you've got the project up and running, check out the additional requirements to run unit tests:
composer update --dev
The will install (among other things) the [PHPUnit](http://www.phpunit.de/) dependency,
which is the framework we're building our unit tests on.
Composer installs it alongside the required PHP classes into the `vendor/bin/` directory.
You can either use it through its full path (`vendor/bin/phpunit`), or symlink it
into the root directory of your website:
This will install (among other things) the [PHPUnit](http://www.phpunit.de/) dependency, which is the framework we're
building our unit tests on. Composer installs it alongside the required PHP classes into the `vendor/bin/` directory.
You can either use it through its full path (`vendor/bin/phpunit`), or symlink it into the root directory of your website:
ln -s vendor/bin/phpunit phpunit
### Via PEAR
Alternatively, you can check out phpunit globally via the PEAR packanage manager
Alternatively, you can check out PHPUnit globally via the PEAR packanage manager
([instructions](https://github.com/sebastianbergmann/phpunit/)).
pear config-set auto_discover 1
pear install pear.phpunit.de/PHPUnit
## Running Tests
## Configuration
### Via the "phpunit" Binary on Command Line
### phpunit.xml
The `phpunit` binary should be used from the root directory of your website.
The `phpunit` executable can be configured by commandline arguments or through an XML file. File-based configuration has
the advantage of enforcing certain rules across test executions (e.g. excluding files from code coverage reports), and
of course this information can be version controlled and shared with other team members.
# Runs all tests defined in phpunit.xml
phpunit
# Run all tests of a specific module
phpunit framework/tests/
# Run specific tests within a specific module
phpunit framework/tests/filesystem
# Run a specific test
phpunit framework/tests/filesystem/FolderTest.php
# Run tests with optional `$_GET` parameters (you need an empty second argument)
phpunit framework/tests '' flush=all
**Note: This doesn't apply for running tests through the "sake" wrapper**
All command-line arguments are documented on
[phpunit.de](http://www.phpunit.de/manual/current/en/textui.html).
SilverStripe comes with a default `phpunit.xml.dist` that you can use as a starting point. Copy the file into a new
`phpunit.xml` and customize to your needs - PHPUnit will auto-detect its existence, and prioritize it over the default
file.
### Via the "sake" Wrapper on Command Line
There's nothing stopping you from creating multiple XML files (see the `--configuration` flag in
[PHPUnit documentation](http://www.phpunit.de/manual/current/en/textui.html)). For example, you could have a
`phpunit-unit-tests.xml` and `phpunit-functional-tests.xml` file (see below).
The [sake](/topics/commandline) executable that comes with SilverStripe can trigger a customized
"[api:TestRunner]" class that handles the PHPUnit configuration and output formatting.
While the custom test runner a handy tool, its also more limited than using `phpunit` directly,
particularly around formatting test output.
### Database Permissions
# Run all tests
sake dev/tests/all
# Run all tests of a specific module (comma-separated)
sake dev/tests/module/framework,cms
# Run specific tests (comma-separated)
sake dev/tests/FolderTest,OtherTest
# Run tests with optional `$_GET` parameters
sake dev/tests/all flush=all
# Skip some tests
sake dev/tests/all SkipTests=MySkippedTest
### Via Web Browser
Executing tests from the command line is recommended, since it most closely reflects
test runs in any automated testing environments. If for some reason you don't have
access to the command line, you can also run tests through the browser.
http://localhost/dev/tests
SilverStripe tests create thier own database when they are run. Because of this the database user in your config file
should have the appropriate permissions to create new databases on your server, otherwise tests will not run.
## Writing Tests
@ -120,71 +80,60 @@ Some people may note that we have used the same naming convention as Ruby on Rai
Tutorials and recipes for creating tests using the SilverStripe framework:
* **[Create a SilverStripe Test](/topics/testing/create-silverstripe-test)**
* **[Create a Functional Test](/topics/testing/create-functional-test)**
* **[Test Outgoing Email Sending](/topics/testing/email-sending)**
* [Creating a SilverStripe test](creating-a-silverstripe-test): Writing tests to check core data objects
* [Creating a functional test](creating-a-functional-test): An overview of functional tests and how to write a functional test
* [Testing Outgoing Email](testing-email): An overview of the built-in email testing code
## Configuration
## Running Tests
### phpunit.xml
### Via the "phpunit" Binary on Command Line
The `phpunit` executable can be configured by commandline arguments or through an XML file.
File-based configuration has the advantage of enforcing certain rules across
test executions (e.g. excluding files from code coverage reports), and of course this
information can be version controlled and shared with other team members.
The `phpunit` binary should be used from the root directory of your website.
**Note: This doesn't apply for running tests through the "sake" wrapper**
# Runs all tests defined in phpunit.xml
phpunit
SilverStripe comes with a default `phpunit.xml.dist` that you can use as a starting point.
Copy the file into a new `phpunit.xml` and customize to your needs - PHPUnit will auto-detect
its existence, and prioritize it over the default file.
# Run all tests of a specific module
phpunit framework/tests/
There's nothing stopping you from creating multiple XML files (see the `--configuration` flag in [PHPUnit documentation](http://www.phpunit.de/manual/current/en/textui.html)).
For example, you could have a `phpunit-unit-tests.xml` and `phpunit-functional-tests.xml` file (see below).
# Run specific tests within a specific module
phpunit framework/tests/filesystem
## Glossary {#glossary}
# Run a specific test
phpunit framework/tests/filesystem/FolderTest.php
**Assertion:** A predicate statement that must be true when a test runs.
# Run tests with optional `$_GET` parameters (you need an empty second argument)
phpunit framework/tests '' flush=all
**Behat:** A behaviour-driven testing library used with SilverStripe as a higher-level
alternative to the `FunctionalTest` API, see [http://behat.org](http://behat.org).
All command-line arguments are documented on
[phpunit.de](http://www.phpunit.de/manual/current/en/textui.html).
**Test Case:** The atomic class type in most unit test frameworks. New unit tests are created by inheriting from the
base test case.
### Via the "sake" Wrapper on Command Line
**Test Suite:** Also known as a 'test group', a composite of test cases, used to collect individual unit tests into
packages, allowing all tests to be run at once.
The [sake](/topics/commandline) executable that comes with SilverStripe can trigger a customized
`[api:TestRunner]` class that handles the PHPUnit configuration and output formatting.
While the custom test runner a handy tool, its also more limited than using `phpunit` directly,
particularly around formatting test output.
**Fixture:** Usually refers to the runtime context of a unit test - the environment and data prerequisites that must be
in place in order to run the test and expect a particular outcome. Most unit test frameworks provide methods that can be
used to create fixtures for the duration of a test - `setUp` - and clean them up after the test is done - `tearDown'.
# Run all tests
sake dev/tests/all
**Refactoring:** A behavior preserving transformation of code. If you change the code, while keeping the actual
functionality the same, it is refactoring. If you change the behavior or add new functionality it's not.
# Run all tests of a specific module (comma-separated)
sake dev/tests/module/framework,cms
**Smell:** A code smell is a symptom of a problem. Usually refers to code that is structured in a way that will lead to
problems with maintenance or understanding.
# Run specific tests (comma-separated)
sake dev/tests/FolderTest,OtherTest
**Spike:** A limited and throwaway sketch of code or experiment to get a feel for how long it will take to implement a
certain feature, or a possible direction for how that feature might work.
# Run tests with optional `$_GET` parameters
sake dev/tests/all flush=all
**Test Double:** Also known as a 'Substitute'. A general term for a dummy object that replaces a real object with the
same interface. Substituting objects is useful when a real object is difficult or impossible to incorporate into a unit
test.
# Skip some tests
sake dev/tests/all SkipTests=MySkippedTest
**Fake Object**: A substitute object that simply replaces a real object with the same interface, and returns a
pre-determined (usually fixed) value from each method.
### Via Web Browser
**Mock Object:** A substitute object that mimicks the same behavior as a real object (some people think of mocks as
"crash test dummy" objects). Mocks differ from other kinds of substitute objects in that they must understand the
context of each call to them, setting expectations of which, and what order, methods will be invoked and what parameters
will be passed.
Executing tests from the command line is recommended, since it most closely reflects
test runs in any automated testing environments. If for some reason you don't have
access to the command line, you can also run tests through the browser.
**Test-Driven Development (TDD):** A style of programming where tests for a new feature are constructed before any code
is written. Code to implement the feature is then written with the aim of making the tests pass. Testing is used to
understand the problem space and discover suitable APIs for performing specific actions.
**Behavior Driven Development (BDD):** An extension of the test-driven programming style, where tests are used primarily
for describing the specification of how code should perform. In practice, there's little or no technical difference - it
all comes down to language. In BDD, the usual terminology is changed to reflect this change of focus, so *Specification*
is used in place of *Test Case*, and *should* is used in place of *expect* and *assert*.
http://localhost/dev/tests

View File

@ -1,11 +1,12 @@
# Email Sending
# Testing Email
SilverStripe's test system has built-in support for testing emails sent using the Email class.
SilverStripe's test system has built-in support for testing emails sent using the `[api:Email]` class.
## How it works
For this to work, you need to send emails using the `Email` class, which is generally the way that we recommend you
send emails in your SilverStripe application. Here is a simple example of how you might do this:
For this to work, you need to send emails using the `Email` class,
which is generally the way that we recommend you send emails in your SilverStripe application.
Here is a simple example of how you might do this:
:::php
$e = new Email();
@ -14,35 +15,32 @@ send emails in your SilverStripe application. Here is a simple example of how y
$e->Body = "I just really wanted to email you and say hi.";
$e->send();
Normally, the send() method would send an email using PHP's mail() function. However, if you are running a `[api:SapphireTest]`
test, then it holds off actually sending the email, and instead lets you assert that an email was sent using this method.
Normally, the `send()` method would send an email using PHP's `mail()` function.
However, if you are running a `[api:SapphireTest]` test, then it holds off actually sending the email,
and instead lets you assert that an email was sent using this method.
:::php
$this->assertEmailSent("someone@example.com", null, "/th.*e$/");
The arguments are `$to`, `$from`, `$subject`, `$body`, and can take one of the three following types:
The arguments are `$to`, `$from`, `$subject`, `$body`, and can be take one of the following three forms:
* A string: match exactly that string
* `null/false`: match anything
* A PERL regular expression (starting with '/'): match that regular expression
* A string: match exactly that string
* `null/false`: match anything
* A PERL regular expression (starting with '/'): match that regular expression
## How to use it
Given all of that, there is not a lot that you have to do in order to test emailing functionality in your application.
* Write your SilverStripe application, using the Email class to send emails.
* Write tests that trigger the email sending functionality.
* Include appropriate `$this->assertEmailSent()` calls in those tests.
Whenever we include e-mailing functionality in our application,
we simply use `$this->assertEmailSent()` to check our mail has been passed to PHP `mail` in our tests.
That's it!
## What isn't tested
It's important to realise that this email testing doesn't actually test everything that there is to do with email. The
focus of this email testing system is testing that your application is triggering emails correctly. It doesn't test
your email infrastructure outside of the webserver. For example:
It's important to realise that this email testing doesn't actually test everything that there is to do with email.
The focus of this email testing system is testing that your application is triggering emails correctly.
It doesn't test your email infrastructure outside of the webserver. For example:
* It won't test that email is correctly configured on your webserver
* It won't test whether your emails are going to be lost in someone's spam filter
@ -50,9 +48,9 @@ your email infrastructure outside of the webserver. For example:
## How it's built
For those of you who want to dig a little deeper, here's a quick run-through of how the system has been built. As well
as explaining how we built the email test, this is a good design pattern for making other "tricky external systems"
testable:
For those of you who want to dig a little deeper, here's a quick run-through of how the system has been built.
As well as explaining how we built the email test,
this is a good design pattern for making other "tricky external systems" testable:
1. The `Email::send()` method makes uses of a static object, `Email::$mailer`, to do the dirty work of calling
mail(). The default mailer is an object of type `Mailer`, which performs a normal send.

View File

@ -81,7 +81,7 @@ but also include information about when and how a record was published.
:::php
$record = MyRecord::get()->byID(99); // stage doesn't matter here
$versions = $record->allVersions();
echo $versions->First()->Version; // instance of Versioned_Versoin
echo $versions->First()->Version; // instance of Versioned_Version
### Writing Versions and Changing Stages

View File

@ -3,7 +3,7 @@
## Overview
In the [first tutorial](1-building-a-basic-site) we learnt how to create a basic site using SilverStripe. This tutorial will build on that, and explore extending SilverStripe by creating our own page types. After doing this we should have a better understanding of how SilverStripe works.
In the [first tutorial (Building a basic site)](1-building-a-basic-site) we learnt how to create a basic site using SilverStripe. This tutorial will build on that, and explore extending SilverStripe by creating our own page types. After doing this we should have a better understanding of how SilverStripe works.
## What are we working towards?
@ -54,7 +54,7 @@ A more in-depth introduction of Model-View-Controller can be found
## Creating the news section page types
To create a news section we'll need two new page types. The first one is obvious: we need an *ArticlePage* page type. The second is a little less obvious: we need an *ArticleHolder* page type to contain our article pages.
To create a News section we'll need two new page types. The first one is obvious: we need an *ArticlePage* page type. The second is a little less obvious: we need an *ArticleHolder* page type to contain our article pages.
We'll start with the *ArticlePage* page type. First we create the model, a class called "ArticlePage". We put the *ArticlePage* class into a file called "ArticlePage.php" inside *mysite/code*. All other classes relating to *ArticlePage* should be placed within "ArticlePage.php", this includes our controller (*ArticlePage_Controller*).
@ -69,7 +69,7 @@ We'll start with the *ArticlePage* page type. First we create the model, a class
Here we've created our data object/controller pair, but we haven't extended them at all. Don't worry about the *$db* and *$has_one* arrays just yet, we'll explain them shortly. SilverStripe will use the template for the *Page* page type as explained in the first tutorial, so we don't need
Here we've created our data object/controller pair, but we haven't extended them at all. SilverStripe will use the template for the *Page* page type as explained in the first tutorial, so we don't need
to specifically create the view for this page type.
Let's create the *ArticleHolder* page type.
@ -100,8 +100,8 @@ page type "News", it would conflict with the page name also called "News".
## Adding date and author fields
Now that we have an *ArticlePage* page type, let's make it a little more useful. Remember the *$db* array? We can use
this array to add extra fields to the database. It would be nice to know when each article was posted, and who posted
Now that we have an *ArticlePage* page type, let's make it a little more useful. We can use
the $db array to add extra fields to the database. It would be nice to know when each article was posted, and who posted
it. Add a *$db* property definition in the *ArticlePage* class:
:::php
@ -188,7 +188,7 @@ Now that we have created our page types, let's add some content. Go into the CMS
At the moment, your date field will look just like a text field.
This makes it confusing and doesn't give the user much help when adding a date.
To make the date field a bit more user friendly, you can add a dropdown calendar, set the date format and add better title. By default,
To make the date field a bit more user friendly, you can add a dropdown calendar, set the date format and add a better title. By default,
the date field will have the date format defined by your locale.
:::php
@ -213,7 +213,7 @@ Let's walk through these changes.
:::php
$fields->addFieldToTab('Root.Main', $dateField = new DateField('Date','Article Date (for example: 20/12/2010)'), 'Content');
*$dateField* is declared only to in order to change the configuration of the DateField.
*$dateField* is declared in order to change the configuration of the DateField.
:::php
$dateField->setConfig('showcalendar', true);
@ -303,7 +303,7 @@ Here we use the page control *Children*. As the name suggests, this control allo
### Using include files in templates
We can make our templates more modular and easier to maintain by separating commonly-used components in to *include files*. We are already familiar with the `<% include Sidebar %>` line from looking at the menu in the [first tutorial](1-building-a-basic-site).
We can make our templates more modular and easier to maintain by separating commonly-used components in to *include files*. We are already familiar with the `<% include Sidebar %>` line from looking at the menu in the [first tutorial (Building a basic site)](1-building-a-basic-site).
We'll separate the display of linked articles as we want to reuse this code later on.
@ -331,7 +331,7 @@ Paste the code that was in ArticleHolder into a new include file called ArticleT
### Changing the icons of pages in the CMS
Let's now make a purely cosmetic change that nevertheless helps to make the information presented in the CMS clearer.
Now let's make a purely cosmetic change that nevertheless helps to make the information presented in the CMS clearer.
Add the following field to the *ArticleHolder* and *ArticlePage* classes:
:::php

View File

@ -24,6 +24,10 @@ class Folder extends File {
private static $plural_name = "Folders";
private static $default_sort = "\"Name\"";
private static $casting = array (
'TreeTitle' => 'HTMLText'
);
/**
*

View File

@ -184,7 +184,7 @@ class ImagickBackend extends Imagick implements Image_Backend {
* @param int $height
* @return Image_Backend
*/
public function paddedResize($width, $height, $backgroundColor = "FFFFFF") {
public function paddedResize($width, $height, $backgroundColor = "#FFFFFF00") {
if(!$this->valid()) return;
$width = round($width);

View File

@ -35,6 +35,9 @@ class CreditCardField extends TextField {
* @return string
*/
protected function getTabIndexHTML($increment = 0) {
// we can't add a tabindex if there hasn't been one set yet.
if($this->getAttribute('tabindex') === null) return false;
$tabIndex = (int)$this->getAttribute('tabindex') + (int)$increment;
return (is_numeric($tabIndex)) ? ' tabindex = "' . $tabIndex . '"' : '';
}

View File

@ -217,6 +217,8 @@ class DateField extends TextField {
* @param String|Array $val
*/
public function setValue($val) {
$locale = new Zend_Locale($this->locale);
if(empty($val)) {
$this->value = null;
$this->valueObj = null;
@ -226,7 +228,7 @@ class DateField extends TextField {
if(is_array($val) && $this->validateArrayValue($val)) {
// set() gets confused with custom date formats when using array notation
if(!(empty($val['day']) || empty($val['month']) || empty($val['year']))) {
$this->valueObj = new Zend_Date($val, null, $this->locale);
$this->valueObj = new Zend_Date($val, null, $locale);
$this->value = $this->valueObj->toArray();
} else {
$this->value = $val;
@ -234,8 +236,8 @@ class DateField extends TextField {
}
}
// load ISO date from database (usually through Form->loadDataForm())
else if(!empty($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'), $this->locale)) {
$this->valueObj = new Zend_Date($val, $this->getConfig('datavalueformat'), $this->locale);
else if(!empty($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'), $locale)) {
$this->valueObj = new Zend_Date($val, $this->getConfig('datavalueformat'), $locale);
$this->value = $this->valueObj->toArray();
}
else {
@ -247,15 +249,15 @@ class DateField extends TextField {
// Caution: Its important to have this check *before* the ISO date fallback,
// as some dates are falsely detected as ISO by isDate(), e.g. '03/04/03'
// (en_NZ for 3rd of April, definetly not yyyy-MM-dd)
if(!empty($val) && Zend_Date::isDate($val, $this->getConfig('dateformat'), $this->locale)) {
$this->valueObj = new Zend_Date($val, $this->getConfig('dateformat'), $this->locale);
$this->value = $this->valueObj->get($this->getConfig('dateformat'), $this->locale);
if(!empty($val) && Zend_Date::isDate($val, $this->getConfig('dateformat'), $locale)) {
$this->valueObj = new Zend_Date($val, $this->getConfig('dateformat'), $locale);
$this->value = $this->valueObj->get($this->getConfig('dateformat'), $locale);
}
// load ISO date from database (usually through Form->loadDataForm())
else if(!empty($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'))) {
$this->valueObj = new Zend_Date($val, $this->getConfig('datavalueformat'));
$this->value = $this->valueObj->get($this->getConfig('dateformat'), $this->locale);
$this->value = $this->valueObj->get($this->getConfig('dateformat'), $locale);
}
else {
$this->value = $val;
@ -367,7 +369,7 @@ class DateField extends TextField {
} else {
$minDate = new Zend_Date(strftime('%Y-%m-%d', strtotime($min)), $this->getConfig('datavalueformat'));
}
if(!$this->valueObj->isLater($minDate) && !$this->valueObj->equals($minDate)) {
if(!$this->valueObj || (!$this->valueObj->isLater($minDate) && !$this->valueObj->equals($minDate))) {
$validator->validationError(
$this->name,
_t(
@ -388,7 +390,7 @@ class DateField extends TextField {
} else {
$maxDate = new Zend_Date(strftime('%Y-%m-%d', strtotime($max)), $this->getConfig('datavalueformat'));
}
if(!$this->valueObj->isEarlier($maxDate) && !$this->valueObj->equals($maxDate)) {
if(!$this->valueObj || (!$this->valueObj->isEarlier($maxDate) && !$this->valueObj->equals($maxDate))) {
$validator->validationError(
$this->name,
_t('DateField.VALIDDATEMAXDATE',

View File

@ -115,6 +115,8 @@ class DatetimeField extends FormField {
* the 'date' value may contain array notation was well (see {@link DateField->setValue()}).
*/
public function setValue($val) {
$locale = new Zend_Locale($this->locale);
// If timezones are enabled, assume user data needs to be reverted to server timezone
if($this->getConfig('usertimezone')) {
// Accept user input on timezone, but only when timezone support is enabled
@ -130,7 +132,7 @@ class DatetimeField extends FormField {
$this->timeField->setValue(null);
} else {
// Case 1: String setting from database, in ISO date format
if(is_string($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'), $this->locale)) {
if(is_string($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'), $locale)) {
$this->value = $val;
}
// Case 2: Array form submission with user date format
@ -145,13 +147,13 @@ class DatetimeField extends FormField {
$this->dateField->setValue($val['date']);
$this->timeField->setValue($val['time']);
if($this->dateField->dataValue() && $this->timeField->dataValue()) {
$userValueObj = new Zend_Date(null, null, $this->locale);
$userValueObj = new Zend_Date(null, null, $locale);
$userValueObj->setDate($this->dateField->dataValue(),
$this->dateField->getConfig('datavalueformat'));
$userValueObj->setTime($this->timeField->dataValue(),
$this->timeField->getConfig('datavalueformat'));
if($userTz) $userValueObj->setTimezone($dataTz);
$this->value = $userValueObj->get($this->getConfig('datavalueformat'), $this->locale);
$this->value = $userValueObj->get($this->getConfig('datavalueformat'), $locale);
unset($userValueObj);
} else {
// Validation happens later, so set the raw string in case Zend_Date doesn't accept it
@ -168,8 +170,8 @@ class DatetimeField extends FormField {
}
// view settings (dates might differ from $this->value based on user timezone settings)
if (Zend_Date::isDate($this->value, $this->getConfig('datavalueformat'), $this->locale)) {
$valueObj = new Zend_Date($this->value, $this->getConfig('datavalueformat'), $this->locale);
if (Zend_Date::isDate($this->value, $this->getConfig('datavalueformat'), $locale)) {
$valueObj = new Zend_Date($this->value, $this->getConfig('datavalueformat'), $locale);
if($userTz) $valueObj->setTimezone($userTz);
// Set view values in sub-fields
@ -177,9 +179,9 @@ class DatetimeField extends FormField {
$this->dateField->setValue($valueObj->toArray());
} else {
$this->dateField->setValue(
$valueObj->get($this->dateField->getConfig('dateformat'), $this->locale));
$valueObj->get($this->dateField->getConfig('dateformat'), $locale));
}
$this->timeField->setValue($valueObj->get($this->timeField->getConfig('timeformat'), $this->locale));
$this->timeField->setValue($valueObj->get($this->timeField->getConfig('timeformat'), $locale));
}
}

View File

@ -155,6 +155,10 @@ class Form extends RequestHandler {
'forTemplate',
);
private static $casting = array(
'Message' => 'Text'
);
/**
* @var FormTemplateHelper
*/
@ -256,6 +260,8 @@ class Form extends RequestHandler {
if(isset($errorInfo['message']) && isset($errorInfo['type'])) {
$this->setMessage($errorInfo['message'], $errorInfo['type']);
}
return $this;
}
/**
@ -504,7 +510,7 @@ class Form extends RequestHandler {
}
/**
* Add an error message to a field on this form. It will be saved into the session
* Add a plain text error message to a field on this form. It will be saved into the session
* and used the next time this form is displayed.
*/
public function addErrorMessage($fieldName, $message, $messageType) {
@ -1425,7 +1431,7 @@ class Form extends RequestHandler {
$this->getTemplate(),
'Form'
));
$return = $view->dontRewriteHashlinks()->process($this);
// Now that we're rendered, clear message
@ -1626,14 +1632,12 @@ class Form extends RequestHandler {
* @return Form
*/
public function addExtraClass($class) {
$classes = explode(' ', $class);
//split at white space
$classes = preg_split('/\s+/', $class);
foreach($classes as $class) {
$value = trim($class);
$this->extraClasses[] = $value;
//add classes one by one
$this->extraClasses[$class] = $class;
}
return $this;
}
@ -1644,9 +1648,12 @@ class Form extends RequestHandler {
* @param string $class
*/
public function removeExtraClass($class) {
$classes = explode(' ', $class);
$this->extraClasses = array_diff($this->extraClasses, $classes);
//split at white space
$classes = preg_split('/\s+/', $class);
foreach ($classes as $class) {
//unset one by one
unset($this->extraClasses[$class]);
}
return $this;
}

View File

@ -96,6 +96,10 @@ class FormField extends RequestHandler {
*/
protected $attributes = array();
private static $casting = array(
'Message' => 'Text'
);
/**
* Takes a fieldname and converts camelcase to spaced
* words. Also resolves combined fieldnames with dot syntax
@ -223,7 +227,7 @@ class FormField extends RequestHandler {
return $this->name;
}
/**
/**
* Returns the field message, used by form validation.
*
* Use {@link setError()} to set this property.
@ -352,24 +356,32 @@ class FormField extends RequestHandler {
}
/**
* Add a CSS-class to the formfield-container.
* Add one or more CSS-classes to the formfield-container.
*
* @param $class String
*/
public function addExtraClass($class) {
//split at white space to extract all the classes
$classes = preg_split('/\s+/', $class);
foreach ($classes as $class) {
//add each class one by one
$this->extraClasses[$class] = $class;
}
return $this;
}
/**
* Remove a CSS-class from the formfield-container.
* Remove one or more CSS-classes from the formfield-container.
*
* @param $class String
*/
public function removeExtraClass($class) {
$pos = array_search($class, $this->extraClasses);
if($pos !== false) unset($this->extraClasses[$pos]);
//split at white space to extract all the classes
$classes = preg_split('/\s+/', $class);
foreach ($classes as $class) {
//unset each class one by one
unset($this->extraClasses[$class]);
}
return $this;
}
@ -419,7 +431,7 @@ class FormField extends RequestHandler {
'id' => $this->ID(),
'disabled' => $this->isDisabled(),
);
if ($this->Required()) {
$attrs['required'] = 'required';
$attrs['aria-required'] = 'true';
@ -847,8 +859,8 @@ class FormField extends RequestHandler {
$clone->setDisabled(true);
}
return $clone;
}
return $clone;
}
public function transform(FormTransformation $trans) {
return $trans->transform($this);
@ -878,7 +890,7 @@ class FormField extends RequestHandler {
public function createTag($tag, $attributes, $content = null) {
Deprecation::notice('3.2', 'Use FormField::create_tag()');
return self::create_tag($tag, $attributes, $content);
}
}
/**
* Abstract method each {@link FormField} subclass must implement,
@ -949,7 +961,7 @@ class FormField extends RequestHandler {
if(is_object($this->containerFieldList)) return $this->containerFieldList->rootFieldList();
else user_error("rootFieldList() called on $this->class object without a containerFieldList", E_USER_ERROR);
}
/**
* Returns another instance of this field, but "cast" to a different class.
* The logic tries to retain all of the instance properties,
@ -982,7 +994,7 @@ class FormField extends RequestHandler {
// of the field, e.g. its "type" attribute.
foreach($this->attributes as $k => $v) {
$field->setAttribute($k, $v);
}
}
$field->dontEscape = $this->dontEscape;
return $field;

View File

@ -86,7 +86,7 @@ class TreeDropdownField extends FormField {
* entering the text in the input field.
*/
public function __construct($name, $title = null, $sourceObject = 'Group', $keyField = 'ID',
$labelField = 'TreeTitle', $showSearch = false
$labelField = 'TreeTitle', $showSearch = true
) {
$this->sourceObject = $sourceObject;
@ -181,8 +181,12 @@ class TreeDropdownField extends FormField {
if($record) {
$title = $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');
}
}
// TODO Implement for TreeMultiSelectField
$metadata = array(
@ -265,9 +269,23 @@ class TreeDropdownField extends FormField {
$obj->markToExpose($this->objectForKey($value));
}
}
$eval = '"<li id=\"selector-' . $this->getName() . '-{$child->' . $this->keyField . '}\" data-id=\"$child->'
. $this->keyField . '\" class=\"class-$child->class"'
. ' . $child->markingClasses() . "\"><a rel=\"$child->ID\">" . $child->' . $this->labelField . ' . "</a>"';
$self = $this;
$escapeLabelField = ($obj->escapeTypeForField($this->labelField) != 'xml');
$titleFn = function(&$child) use(&$self, $escapeLabelField) {
$keyField = $self->keyField;
$labelField = $self->labelField;
return sprintf(
'<li id="selector-%s-%s" data-id="%s" class="class-%s %s"><a rel="%d">%s</a>',
Convert::raw2xml($self->getName()),
Convert::raw2xml($child->$keyField),
Convert::raw2xml($child->$keyField),
Convert::raw2xml($child->class),
Convert::raw2xml($child->markingClasses()),
(int)$child->ID,
$escapeLabelField ? Convert::raw2xml($child->$labelField) : $child->$labelField
);
};
// Limit the amount of nodes shown for performance reasons.
// Skip the check if we're filtering the tree, since its not clear how many children will
@ -290,7 +308,7 @@ class TreeDropdownField extends FormField {
if($isSubTree) {
$html = $obj->getChildrenAsUL(
"",
$eval,
$titleFn,
null,
true,
$this->childrenMethod,
@ -303,7 +321,7 @@ class TreeDropdownField extends FormField {
} else {
$html = $obj->getChildrenAsUL(
'class="tree"',
$eval,
$titleFn,
null,
true,
$this->childrenMethod,
@ -385,10 +403,32 @@ class TreeDropdownField extends FormField {
*/
protected function populateIDs() {
// get all the leaves to be displayed
if ( $this->searchCallback )
if ($this->searchCallback) {
$res = call_user_func($this->searchCallback, $this->sourceObject, $this->labelField, $this->search);
else
$res = DataObject::get($this->sourceObject, "\"$this->labelField\" LIKE '%$this->search%'");
} else {
$sourceObject = $this->sourceObject;
$wheres = array();
if(singleton($sourceObject)->hasDatabaseField($this->labelField)) {
$wheres[] = "\"$searchField\" LIKE '%$this->search%'";
} else {
if(singleton($sourceObject)->hasDatabaseField('Title')) {
$wheres[] = "\"Title\" LIKE '%$this->search%'";
}
if(singleton($sourceObject)->hasDatabaseField('Name')) {
$wheres[] = "\"Name\" LIKE '%$this->search%'";
}
}
if(!$wheres) {
throw new InvalidArgumentException(sprintf(
'Cannot query by %s.%s, not a valid database column',
$sourceObject,
$this->labelField
));
}
$res = DataObject::get($this->sourceObject, implode(' OR ', $wheres));
}
if( $res ) {
// iteratively fetch the parents in bulk, until all the leaves can be accessed using the tree control

View File

@ -231,6 +231,11 @@ class GridFieldAddExistingAutocompleter
$json = array();
foreach($results as $result) {
// Prevent a circular reference and associated error in CMS/admin
$hideFromSearch = ($gridField->getForm()->getRecord() && ($result->ID == $gridField->getForm()->getRecord()->ID));
if($hideFromSearch) {
continue;
}
$json[$result->ID] = SSViewer::fromString($this->resultsFormat)->process($result);
}
return Convert::array2json($json);

View File

@ -21,6 +21,7 @@ class GridFieldButtonRow implements GridField_HTMLProvider {
public function getHTMLFragments( $gridField) {
$data = new ArrayData(array(
"TargetFragmentName" => $this->targetFragment,
"LeftFragment" => "\$DefineFragment(buttons-{$this->targetFragment}-left)",
"RightFragment" => "\$DefineFragment(buttons-{$this->targetFragment}-right)",
));

View File

@ -95,10 +95,15 @@ class GridFieldDataColumns implements GridField_ColumnProvider {
/**
* Specify custom formatting for fields, e.g. to render a link instead of pure text.
*
* Caution: Make sure to escape special php-characters like in a normal php-statement.
* Example: "myFieldName" => '<a href=\"custom-admin/$ID\">$ID</a>'.
*
* Alternatively, pass a anonymous function, which takes two parameters:
* The value and the original list item.
* The value and the original list item.
*
* Formatting is applied after field casting, so if you're modifying the string
* to include further data through custom formatting, ensure it's correctly escaped.
*
* @param array $formatting
*/

View File

@ -53,7 +53,7 @@ class GridFieldExportButton implements GridField_HTMLProvider, GridField_ActionP
$button->setAttribute('data-icon', 'download-csv');
$button->addExtraClass('no-ajax');
return array(
$this->targetFragment => '<p class="grid-bottom-button grid-csv-button">' . $button->Field() . '</p>',
$this->targetFragment => '<p class="grid-csv-button">' . $button->Field() . '</p>',
);
}

View File

@ -2233,12 +2233,10 @@ class i18n extends Object implements TemplateGlobalProvider {
// TODO Replace with CLDR list of actually available languages/regions
// Only allow explicitly registered locales, otherwise we'll get into trouble
// if the locale doesn't exist in Zend's CLDR data
$labelLocale = str_replace('-', '_', self::get_locale_from_lang($locale));
if(isset(self::$all_locales[$locale])) {
$locales[$locale] = self::$all_locales[$locale];
} else if(isset(self::$all_locales[$labelLocale])) {
$locales[$locale] = self::$all_locales[$labelLocale];
}
$fullLocale = self::get_locale_from_lang($locale);
if(isset($allLocales[$fullLocale])) {
$locales[$fullLocale] = $allLocales[$fullLocale];
}
}
}
}
@ -2356,7 +2354,7 @@ class i18n extends Object implements TemplateGlobalProvider {
public static function get_locale_from_lang($lang) {
$subtags = Config::inst()->get('i18n', 'likely_subtags');
if(preg_match('/\-|_/', $lang)) {
return $lang;
return str_replace('-', '_', $lang);
} else if(isset($subtags[$lang])) {
return $subtags[$lang];
} else {

View File

@ -77,8 +77,8 @@
});
} else {
checkboxes.each(function() {
$(this).attr('checked', '');
$(this).attr('disabled', '');
$(this).prop('checked', false);
$(this).prop('disabled', false);
});
}
}

View File

@ -24,8 +24,8 @@
var strings = {
'openlink': 'Open',
'fieldTitle': '(choose)',
'searchFieldTitle': '(choose or search)'
'fieldTitle': '(Choose)',
'searchFieldTitle': '(Choose or Search)'
};
var _clickTestFn = function(e) {
@ -280,24 +280,14 @@
$('.TreeDropdownField.searchable').entwine({
onadd: function() {
this._super();
var title = decodeURIComponent(this.data('title'));
this.find('.treedropdownfield-title').replaceWith(
$('<input type="text" class="treedropdownfield-title search" data-skip-autofocus="true" />')
var title = ss.i18n._t(
'DropdownField.ENTERTOSEARCH',
'Press enter to search'
);
this.find('.treedropdownfield-panel').prepend(
$('<input type="text" class="search treedropdownfield-search" data-skip-autofocus="true" placeholder="' + title + '" value="" />')
);
this.setTitle(title ? title : strings.searchFieldTitle);
},
setTitle: function(title) {
if(!title && title !== '') title = strings.fieldTitle;
this.find('.treedropdownfield-title').val(title);
},
getTitle: function() {
return this.find('.treedropdownfield-title').val();
},
resetTitle: function() {
this.setTitle(decodeURIComponent(this.data('title')));
},
search: function(str, callback) {
this.openPanel();
@ -306,19 +296,10 @@
cancelSearch: function() {
this.closePanel();
this.loadTree();
this.resetTitle();
}
});
$('.TreeDropdownField.searchable input.search').entwine({
onfocusin: function(e) {
var field = this.getField();
field.setTitle('');
},
onfocusout: function(e) {
var field = this.getField();
field.resetTitle();
},
onkeydown: function(e) {
var field = this.getField();
if(e.keyCode == 13) {

View File

@ -13,12 +13,13 @@ cs:
SIZE: 'Velikost'
TITLE: Titulek
TYPE: 'Typ'
URL: URL
AssetUploadField:
ChooseFiles: 'Vyberte soubory'
DRAGFILESHERE: 'Táhni soubory sem'
DROPAREA: 'Oblast upustění'
EDITALL: 'Editovat vše'
EDITANDORGANIZE: 'Editovat & organizovat'
EDITANDORGANIZE: 'Editovat a organizovat'
EDITINFO: 'Editovat soubory'
FILES: Soubory
FROMCOMPUTER: 'Vyberte soubory z vašeho počítače'
@ -73,6 +74,7 @@ cs:
ChangePasswordEmail_ss:
CHANGEPASSWORDTEXT1: 'Vaše heslo bylo změněno pro'
CHANGEPASSWORDTEXT2: 'Nyní můžete použít následující přihlašovací údaje pro přihlášení:'
EMAIL: E-mail
HELLO: Dobrý den
PASSWORD: Heslo
ComplexTableField:
@ -304,6 +306,7 @@ cs:
LINKOPENNEWWIN: 'Otevřít odkaz v novém okně?'
LINKTO: 'Odkázat na'
PAGE: Stránku
URL: URL
URLNOTANOEMBEDRESOURCE: 'URL ''{url}'' nemůže být vloženo do zdroje médií.'
UpdateMEDIA: 'Aktualizovat média'
BUTTONADDURL: 'Přidat url'
@ -342,6 +345,7 @@ cs:
IP: 'IP adresy'
PLURALNAME: 'Pokusy přihlášení'
SINGULARNAME: 'Pokus přihlášení'
Status: Stav
Member:
ADDGROUP: 'Přidat skupinu'
BUTTONCHANGEPASSWORD: 'Změnit heslo'
@ -354,9 +358,11 @@ cs:
DATEFORMAT: 'Formát datumu'
DefaultAdminFirstname: 'Implicitní Admin'
DefaultDateTime: výchozí
EMAIL: E-mail
EMPTYNEWPASSWORD: 'Nové heslo nesmí být prázdné, zkuste to znovu'
ENTEREMAIL: 'Zadejte e-mailovou adresu pro získání odkazu na restart hesla.'
ERRORLOCKEDOUT: 'Váš účet byl dočasně zablokován, kvůli příliš velkému množství nezdařených pokusů o přihlášení. Zkuste se prosím přihlásit za 20 minut.'
ERRORLOCKEDOUT2: 'Váš účet byl dočasně zablokován, kvůli příliš velkému množství nezdařených pokusů o přihlášení. Zkuste se prosím přihlásit za {count} minut.'
ERRORNEWPASSWORD: 'Zadali jste nové heslo rozdílně, zkuste to znovu'
ERRORPASSWORDNOTMATCH: 'Váše současné heslo není správně, prosím zkuste to znovu'
ERRORWRONGCRED: 'Toto nevypadá jako správná emailová adresa nebo heslo. Prosím zkuste to znovu.'
@ -384,6 +390,7 @@ cs:
db_NumVisit: 'Počet návštěvníků'
db_Password: Heslo
db_PasswordExpiry: 'Datum vypršení hesla'
NoPassword: 'Neni zde heslo pro tohoto člena'
MemberAuthenticator:
TITLE: 'E-mail a Heslo'
MemberDatetimeOptionsetField:
@ -466,6 +473,7 @@ cs:
PermissionRole:
OnlyAdminCanApply: 'Pouze administrátor může použít'
PLURALNAME: Role
SINGULARNAME: Role
Title: Název
PermissionRoleCode:
PLURALNAME: 'Kódy role oprávnění'
@ -481,6 +489,7 @@ cs:
NOTFOUND: 'Žádné položky'
Security:
ALREADYLOGGEDIN: 'K této stránce nemáte přístup. Pokud máte jiný účet, který k ní může přistupovat, můžete se přihlásit níže'
LOSTPASSWORDHEADER: 'Ztracené heslo'
BUTTONSEND: 'Pošlete mi nulovací odkaz pro heslo'
CHANGEPASSWORDBELOW: 'Svoje heslo si můžete změnit níže.'
CHANGEPASSWORDHEADER: 'Změnit heslo'
@ -516,7 +525,9 @@ cs:
FileFieldLabel: 'Soubor CSV <small>(Povoleny přípony: *.csv)</small>'
SilverStripeNavigator:
Edit: Editovat
Auto: Auto
ChangeViewMode: 'Změnit mód zobrazení'
Desktop: Desktop
DualWindowView: 'Dualní okno'
EditView: 'Mód editace'
Mobile: Mobilní telefon
@ -524,6 +535,7 @@ cs:
PreviewView: 'Mód náhledu'
Responsive: Responzivní
SplitView: 'Mód rozdělení'
Tablet: Tablet
ViewDeviceWidth: 'Vyberte šířku náhledu'
Width: šířka
SimpleImageField:
@ -593,3 +605,11 @@ cs:
PREVIEW: 'Náhled webu'
GridFieldEditButton_ss:
EDIT: Editovat
ContentController:
NOTLOGGEDIN: 'Nepřihlášen'
GridFieldItemEditView:
Go_back: 'Jdi zpět'
PasswordValidator:
LOWCHARSTRENGTH: 'Prosím, posilněte heslo přidáním některých z následujících znaků: %s'
PREVPASSWORD: 'Již jste použil toto heslo v minulosti, vyberte nové heslo, prosím'
TOOSHORT: 'Heslo je příliš krátké, musí být %s nebo více znaků dlouhé'

View File

@ -392,6 +392,7 @@ de:
db_NumVisit: 'Anzahl der Besuche'
db_Password: Passwort
db_PasswordExpiry: 'Ablaufdatum des Passworts'
NoPassword: 'Dieser Benutzer hat kein Passwort.'
MemberAuthenticator:
TITLE: 'E-Mail &amp; Passwort'
MemberDatetimeOptionsetField:
@ -490,6 +491,7 @@ de:
NOTFOUND: 'Keine Elemente gefunden'
Security:
ALREADYLOGGEDIN: 'Sie haben keinen Zugriff auf diese Seite. Wenn Sie ein anderes Konto besitzen, mit dem Sie auf diese Seite zugreifen können, melden Sie sich bitte unten an.'
LOSTPASSWORDHEADER: 'Passwort vergessen'
BUTTONSEND: 'Senden Sie mir den Link zur Passwortrücksetzung'
CHANGEPASSWORDBELOW: 'Sie können Ihr Passwort unten ändern.'
CHANGEPASSWORDHEADER: 'Passwort ändern'
@ -531,6 +533,7 @@ de:
DualWindowView: 'Zwei Fenster'
EditView: 'Bearbeitungsmodus'
Mobile: Mobil
PreviewState: 'Vorschau Status'
PreviewView: 'Vorschaumodus'
Responsive: Responsive
SplitView: 'Geteilter Modus'
@ -573,23 +576,29 @@ de:
ATTACHFILES: 'Dateien anhängen'
AttachFile: 'Datei(en) anhängen'
DELETE: 'Aus Dateien löschen'
DELETEINFO: 'Diese Datei am Server löschen'
DOEDIT: Speichern
DROPFILE: 'Datei hier ablegen'
DROPFILES: 'Dateien hier ablegen'
Dimensions: Dimensionen
EDIT: Bearbeiten
EDITINFO: 'Diese Datei bearbeiten'
FIELDNOTSET: 'Dateiinformationen nicht gefunden'
FROMCOMPUTER: 'Von Ihrem Computer'
FROMCOMPUTERINFO: 'Aus Dateien auswählen'
FROMFILES: 'Aus Dateien'
HOTLINKINFO: 'Info: Dieses Bild wird verknüpft. Bitte vergewissere dich die Erlaubnis des Inhabers der Ursprungsseite zu haben.'
MAXNUMBEROFFILES: 'Maximale Anzahl an {count} Datei(en) überschritten'
MAXNUMBEROFFILESSHORT: 'SIe können maximal {count} Datei(en) hochladen'
MAXNUMBEROFFILESONE: 'SIe können maximal eine Datei hochladen'
REMOVE: Entfernen
REMOVEERROR: 'Fehler beim Entfernen der Datei'
REMOVEINFO: 'Diese Datei entfernen aber nicht am Server löschen'
STARTALL: 'Alle starten'
STARTALLINFO: 'Alle Uploads starten'
Saved: Gespeichert
CHOOSEANOTHERFILE: 'Andere Datei auswählen'
CHOOSEANOTHERINFO: 'Diese Datei mit einer Datei vom Server ersetzen'
OVERWRITEWARNING: 'Eine Datei mit dem selben Namen existiert bereits'
UPLOADSINTO: 'speichert nach /{path}'
Versioned:
@ -598,3 +607,11 @@ de:
PREVIEW: 'Vorschau der Webseite'
GridFieldEditButton_ss:
EDIT: Bearbeiten
ContentController:
NOTLOGGEDIN: 'Nicht eingeloggt'
GridFieldItemEditView:
Go_back: 'Zurück'
PasswordValidator:
LOWCHARSTRENGTH: 'Bitte erhöhen Sie die Sicherheit des Passworts, indem Sie auch einige der folgenden Zeichen verwenden: %s'
PREVPASSWORD: 'Sie haben dieses Passwort schon einmal verwendet. Bitte wählen Sie ein neues Passwort'
TOOSHORT: 'Das Passwort ist zu kurz, es muss mindestens %s Zeichen lang sein'

View File

@ -1,8 +1,11 @@
fr:
AssetAdmin:
ALLOWEDEXTS: 'Extensions autorisées'
NEWFOLDER: Nouveau dossier
SHOWALLOWEDEXTS: 'Montrer les extensions autorisées'
AssetTableField:
CREATED: 'Premier chargement'
DIM: Dimensions
FILENAME: Nom du fichier
FOLDER: Dossier
LASTEDIT: 'Dernière modification'
@ -10,6 +13,7 @@ fr:
SIZE: 'Taille'
TITLE: Titre
TYPE: 'Type'
URL: URL
AssetUploadField:
ChooseFiles: 'Choisissez les fichiers'
DRAGFILESHERE: 'Glissez des fichiers ici'
@ -20,6 +24,7 @@ fr:
FILES: Fichiers
FROMCOMPUTER: 'Choisissez des fichiers de votre ordinateur'
FROMCOMPUTERINFO: 'Télécharger depuis votre ordinateur'
TOTAL: Total
TOUPLOAD: 'Choisissez les fichiers à télécharger…'
UPLOADINPROGRESS: 'Patientez s''il vous plaît… téléchargement en cours '
UPLOADOR: OU
@ -35,6 +40,7 @@ fr:
COLOREDEXAMPLE: 'texte bleu'
EMAILLINK: 'Lien email'
EMAILLINKDESCRIPTION: 'Créer un lien vers une adresse email'
IMAGE: Image
IMAGEDESCRIPTION: 'Afficher une image dans votre message'
ITALIC: 'Texte en italique'
ITALICEXAMPLE: Italique
@ -68,6 +74,7 @@ fr:
ChangePasswordEmail_ss:
CHANGEPASSWORDTEXT1: 'Vous avez modifié votre mot de passe pour'
CHANGEPASSWORDTEXT2: 'Vous pouvez maintenant utiliser les identifiants suivants pour vous connecter&nbsp;:'
EMAIL: Email
HELLO: Salut
PASSWORD: Mot de passe
ComplexTableField:
@ -90,12 +97,31 @@ fr:
CreditCardField:
FIRST: premier
FOURTH: quatrième
SECOND: second
THIRD: troisième
CurrencyField:
CURRENCYSYMBOL: $
DataObject:
PLURALNAME: 'Modèles de données'
SINGULARNAME: 'Modèle de donées'
Date:
DAY: jour
DAYS: jours
HOUR: heure
HOURS: heures
MIN: min
MINS: mins
MONTH: mois
MONTHS: mois
SEC: sec
SECS: secs
TIMEDIFFAGO: 'Il y a {difference}'
TIMEDIFFIN: 'Dans {difference}'
YEAR: année
YEARS: années
LessThanMinuteAgo: 'moins d''une minute'
DateField:
NOTSET: 'pas d''ensemble'
NOTSET: 'non renseigné'
TODAY: aujourd'hui
VALIDDATEFORMAT2: 'Saisissez la date au format valide ({format})'
VALIDDATEMAXDATE: 'La date doit être antérieure ou égale à celle qui a été autorisée ({date})'
@ -169,6 +195,7 @@ fr:
TEXT2: 'lien de réinitialisation de mot de passe'
TEXT3: pour
Form:
FIELDISREQUIRED: '{name} requis'
SubmitBtnLabel: Envoyer
VALIDATIONCREDITNUMBER: 'Vérifiez que vous avez bien saisi votre numéro de carte bleue {number}.'
VALIDATIONNOTUNIQUE: 'La valeur entrée n''est pas unique'
@ -177,8 +204,10 @@ fr:
VALIDATIONSTRONGPASSWORD: 'Le mot de passe doit comporter au moins un chiffre et un caractère alphanumérique'
VALIDATOR: Validateur
VALIDCURRENCY: 'Saisissez une monnaie valide'
CSRF_FAILED_MESSAGE: 'Il semble qu''il y ait eu un problème technique. Veuillez cliquez sur le bouton Retour, raffraîchir votre navigateur, et essayer à nouveau'
FormField:
NONE: aucun
Example: 'par exemple %s'
GridAction:
DELETE_DESCRIPTION: Supprime
Delete: Supprimer
@ -200,6 +229,7 @@ fr:
ResetFilter: Réinitialiser
GridFieldAction_Delete:
DeletePermissionsFailure: 'Vous navez pas les autorisations pour supprimer'
EditPermissionsFailure: 'Pas de permissions pour délier l''enregistrement'
GridFieldDetailForm:
CancelBtn: Annuler
Create: Créer
@ -207,12 +237,16 @@ fr:
DeletePermissionsFailure: 'Vous navez pas les autorisations pour supprimer'
Deleted: '%s %s supprimés'
Save: Enregistrer
Saved: '{name} {link} sauvegardé'
GridFieldItemEditView_ss:
Go_back: 'Revenir en arrière'
Group:
AddRole: 'Ajouter un rôle pour ce groupe'
Code: 'Code de groupe'
DefaultGroupTitleAdministrators: Administrateur
DefaultGroupTitleContentAuthors: 'Auteurs du contenu'
Description: Description
GroupReminder: 'Si vous choisissez un groupe parent, ce groupe prendra tous ses rôles'
Locked: 'Verrouillé?'
NoRoles: 'Vous navez pas la permission pour faire ça'
PLURALNAME: Groupes
@ -255,6 +289,7 @@ fr:
IMAGEALT: 'Texte alternatif (alt)'
IMAGEALTTEXT: 'Texte alternatif (alt) - s''affiche si l''image ne peut être affichée.'
IMAGEALTTEXTDESC: 'Proposé aux lecteurs décran ou si limage ne peut pas être affichée'
IMAGEDIMENSIONS: Dimensions
IMAGEHEIGHTPX: Hauteur
IMAGETITLE: 'Texte du titre (tooltip) - informations à propos de l''image'
IMAGETITLETEXT: 'Texte du titre (info-bulle)'
@ -270,11 +305,16 @@ fr:
LINKINTERNAL: 'Une page du site'
LINKOPENNEWWIN: 'Ouvrir le lien dans une nouvelle fenêtre ?'
LINKTO: 'Lier à'
PAGE: Page
URL: URL
URLNOTANOEMBEDRESOURCE: 'LURL {url} na pas pu être utilisée comme ressource média.'
UpdateMEDIA: 'Mettre à jour le support audiovisuel'
BUTTONADDURL: 'Ajouter une URL'
Image:
PLURALNAME: Fichiers
SINGULARNAME: Fichier
ImageField:
IMAGE: Image
Image_Cached:
PLURALNAME: Fichiers
SINGULARNAME: Fichier
@ -283,6 +323,7 @@ fr:
LeftAndMain:
CANT_REORGANISE: 'Vous navez pas lautorisation de modifier les pages de premier niveau. Vos modifications nont pas été enregistrées.'
DELETED: Supprimé.
DropdownBatchActionsDefault: Actions
HELP: Aide
PAGETYPE: 'Type de page&nbsp;:'
PERMAGAIN: 'Vous avez été déconnecté du CMS. Si vous voulez vous reconnecter, entrez un nom d''utilisateur et un mot de passe ci-dessous.'
@ -293,6 +334,9 @@ fr:
REORGANISATIONSUCCESSFUL: 'Larbre du site a été bien réorganisé.'
SAVEDUP: Enregistré.
VersionUnknown: inconnu
ShowAsList: 'lister'
TooManyPages: 'Trop de pages'
ValidationError: 'Erreur de validation'
LeftAndMain_Menu_ss:
Hello: Bonjour
LOGOUT: 'Déconnexion'
@ -314,10 +358,12 @@ fr:
DATEFORMAT: 'Format de la date'
DefaultAdminFirstname: 'Administrateur par défaut'
DefaultDateTime: par défaut
EMAIL: Email
EMPTYNEWPASSWORD: 'Le champs nouveau mot de passe ne peut être vide, essayez de nouveau'
ENTEREMAIL: 'Veuillez écrire une adresse email pour obtenir le lien de réinitialisation du mot de passe.'
ERRORLOCKEDOUT: 'Votre compte a été désactivé temporairement à cause de multiples tentatives de connexions. Veuillez réessayer dans 20 minutes.'
ERRORNEWPASSWORD: 'Vous avez entré votre nouveau mot de passe différemment, essayez encore'
ERRORLOCKEDOUT2: 'Votre compte a été temporairement désactivé à cause de trop nombreux échecs d''identification. Veuillez réessayer dans {count} minutes.'
ERRORNEWPASSWORD: 'Vous avez entré votre nouveau mot de passe différemment, réessayez'
ERRORPASSWORDNOTMATCH: 'Votre actuel mot de passe ne correspond pas, essayez encore s''il vous plaît'
ERRORWRONGCRED: 'Il semble que ce ne soit pas le bon email ou mot de passe. Essayez encore s''il vous plaît.'
FIRSTNAME: 'Prénom'
@ -344,10 +390,11 @@ fr:
db_NumVisit: 'Nombre de visite'
db_Password: Mot de passe
db_PasswordExpiry: 'Date d''expiration du mot de passe'
NoPassword: 'Ce membre n''a pas de mot de passe'
MemberAuthenticator:
TITLE: 'Email &amp; Mot de passe'
MemberDatetimeOptionsetField:
AMORPM: 'AM (Ante meridiem) ou PM (Post meridiem)'
AMORPM: 'AM (matin) ou PM (après-midi)'
Custom: Personnalisé
DATEFORMATBAD: 'Format de date incorrect'
DAYNOLEADING: 'Le jour du mois sans zéro initial'
@ -366,6 +413,7 @@ fr:
TWODIGITMONTH: 'Le mois sur deux chiffres (p. ex. 01=janvier)'
TWODIGITSECOND: 'La seconde sur deux chiffres (de 00 à 59)'
TWODIGITYEAR: 'Lannée sur deux chiffres'
Toggle: 'Afficher laide de mise en forme'
MemberImportForm:
Help1: '<p>Importer les membres au format<em>CSV format</em> (comma-separated values). <small><a href="#" class="toggle-advanced">Afficher l''usage avancé.</a></small></p>'
Help2: "<div class=\"advanced\">\n<h4>Utilisation avancée</h4>\n<ul>\n<li>Colonnes autorisées&nbsp;: <em>%s</em></li>\n<li>Les utilisateurs existants sont retrouvés avec leur <em>Code</em> unique et les registres sont mis à jour avec les nouvelles valeurs du fichier importé.</li>\n<li>Des groupes peuvent être assignés à laide de la colonne <em>Groups</em>. Les groupes sont identifiés par leur <em>Code</em>, plusieurs groupes peuvent être indiqués en les séparant par des virgules. Lappartenance actuelle aux groupes nest pas modifiée.</li>\n</ul>\n</div>"
@ -381,6 +429,7 @@ fr:
ModelAdmin:
DELETE: Supprime
DELETEDRECORDS: '{count} enregistrements supprimés.'
EMPTYBEFOREIMPORT: 'Remplacer les données'
IMPORT: 'Importer de CSV'
IMPORTEDRECORDS: '{count} enregistrements importés.'
NOCSVFILE: 'Veuillez choisir un fichier CSV à importer'
@ -391,6 +440,7 @@ fr:
ModelAdmin_ImportSpec_ss:
IMPORTSPECFIELDS: 'Colonnes de la base de données'
IMPORTSPECLINK: 'Afficher la spécification de %s'
IMPORTSPECRELATIONS: Relations
IMPORTSPECTITLE: 'Spécification de %s'
ModelAdmin_Tools_ss:
FILTER: Filtrer
@ -406,6 +456,7 @@ fr:
NumericField:
VALIDATION: '« {value} » nest pas un chiffre, seule donnée acceptée dans ce champ '
Pagination:
Page: Page
View: Afficher
Permission:
AdminGroup: Administrateur
@ -438,6 +489,7 @@ fr:
NOTFOUND: 'Aucun élément na été trouvé'
Security:
ALREADYLOGGEDIN: 'Vous n''avez pas accès à cette page. Si vous avez un autre identifiant pouvant accéder à cette page, vous pouvez l''utiliser ci-dessous.'
LOSTPASSWORDHEADER: 'Mot de passe oublié'
BUTTONSEND: 'Envoyer moi le lien pour modifier le mot de passe'
CHANGEPASSWORDBELOW: 'Vous pouvez modifier votre mot de passe ci-dessous.'
CHANGEPASSWORDHEADER: 'Modifier votre mot de passe'
@ -457,6 +509,8 @@ fr:
EDITPERMISSIONS: 'Gérer les autorisations des groupes'
EDITPERMISSIONS_HELP: 'Possibilité d''éditer les autorisations et les adresses IP pour un groupe. Nécessite lautorisation « Accès à la section “Securité” ».'
GROUPNAME: 'Nom du group'
IMPORTGROUPS: 'Importer des groupes'
IMPORTUSERS: 'Importer des utilisateurs'
MEMBERS: Membres
MENUTITLE: Sécurité
MemberListCaution: 'Attention&nbsp;: en supprimant des membres de cette liste vous les enlèverez de tous les groupes ainsi que de la base de données'
@ -471,6 +525,19 @@ fr:
FileFieldLabel: 'Fichier CSV <small>(extension autorisée&nbsp;: *.csv)</small>'
SilverStripeNavigator:
Edit: Tout modifier
Auto: Auto
ChangeViewMode: 'Changer de mode de vue'
Desktop: Bureau
DualWindowView: 'Double fenêtre'
EditView: 'Editer le mode'
Mobile: Mobile
PreviewState: 'Aperçu'
PreviewView: 'Aperçu'
Responsive: Responsive
SplitView: 'Mode partagé'
Tablet: Tablette
ViewDeviceWidth: 'Sélectionnez une largeur d''aperçu'
Width: largeur
SimpleImageField:
NOUPLOAD: 'Aucune image chargée'
SiteTree:
@ -510,17 +577,39 @@ fr:
DELETEINFO: 'Effacer définitivement ce fichier des archives'
DOEDIT: Enregistrer
DROPFILE: 'glissez-déposez un fichier'
DROPFILES: 'glissez-déposez des fichiers'
Dimensions: Dimensions
EDIT: Modifier
EDITINFO: 'Modifier ce fichier'
FIELDNOTSET: 'Informations concernant le fichiers non-trouvées'
FROMCOMPUTER: 'Depuis votre ordinateur'
FROMCOMPUTERINFO: 'Choisir parmi les fichiers'
FROMFILES: 'Depuis les fichiers'
HOTLINKINFO: 'Note : Cette image sera liée par un « hotlink », assurez-vous davoir lautorisation des ayant-droits du site web dorigine.'
MAXNUMBEROFFILES: 'Le nombre maximal de {count} fichiers a été dépassé.'
MAXNUMBEROFFILESSHORT: 'On ne peut pas télécharger plus de {count} fichiers'
MAXNUMBEROFFILESONE: 'Vous ne pouvez pas uploader plus d''un fichier'
REMOVE: Supprimer
REMOVEERROR: 'Le fichier na pas pu être supprimé'
REMOVEINFO: 'Supprimer ce fichier ici sans leffacer des archives'
STARTALL: 'Démarrer tout'
STARTALLINFO: 'Démarrer tous les téléchargements'
Saved: Enregistré
CHOOSEANOTHERFILE: 'Choisissez un autre fichier'
CHOOSEANOTHERINFO: 'Remplacer ce fichier par un autre depuis les archives'
OVERWRITEWARNING: 'Un fichier avec le même nom existe déjà'
UPLOADSINTO: 'sauvegarder dans /{path}'
Versioned:
has_many_Versions: Versions
CMSPageHistoryController_versions_ss:
PREVIEW: 'Aperçu du site'
GridFieldEditButton_ss:
EDIT: Editer
ContentController:
NOTLOGGEDIN: 'Hors-ligne'
GridFieldItemEditView:
Go_back: 'Retour'
PasswordValidator:
LOWCHARSTRENGTH: 'Veuillez augmenter la force de votre mot de passe en ajoutant certains caractères suivants : %s'
PREVPASSWORD: 'Vous avez déjà utilisé ce mot de passe par le passé, veuillez en choisir un autre'
TOOSHORT: 'Le mot de passe est trop court, il doit contenir au moins %s caractères'

View File

@ -1,6 +1,8 @@
hi:
AssetUploadField:
ChooseFiles: 'फाइलें चुनें'
ChooseFiles: 'अपलोड करने के लिए फ़ाइलों को चुनें'
DRAGFILESHERE: 'अपलोड करने के लिए फाइलें यहाँ लाये '
DROPAREA: 'अपलोड छेत्र '
EDITALL: 'सभी संपादित करें'
EDITANDORGANIZE: 'संपादित और व्यवस्थित करें '
EDITINFO: 'फ़ाइलों को संपादित करें'
@ -28,6 +30,27 @@ hi:
ITALIC: 'तिरछे अक्षर'
ITALICEXAMPLE: तिरछे अक्षर
LINK: 'वेबसाइट लिंक'
LINKDESCRIPTION: ' किसी दुसरे वेबसाइट या यूआरएल से जोड़े '
BackLink_Button_ss:
Back: वापस
BasicAuth:
ENTERINFO: 'कृपया उपयोगकर्ता का नाम और पासवर्ड दर्ज करें'
ERRORNOTADMIN: 'वह उपयोगकर्ता एक व्यवस्थापक नहीं है.'
CMSProfileController:
MENUTITLE: 'मेरी प्रोफाइल '
ComplexTableField_popup_ss:
NEXT: अगला
PREVIOUS: पिछला
ConfirmedPasswordField:
ATLEAST: 'पासवर्डों को कम से कम {min} अक्षर लंबा होना चाहिए.'
SHOWONCLICKTITLE: 'पासवर्ड बदलें'
DataObject:
PLURALNAME: 'डेटा ऑब्जेक्ट्स'
SINGULARNAME: 'डेटा ऑब्जेक्ट'
DateField:
NOTSET: 'सेट नहीं'
TODAY: आज
VALIDDATEFORMAT2: 'कृपया एक मान्य दिनांक स्वरूप दर्ज करें ({format})'
Group:
RolesAddEditLink: 'भूमिकाओं का प्रबंधन करे '
has_many_Permissions: अनुमतियाँ

View File

@ -24,6 +24,7 @@ nb:
FILES: Filer
FROMCOMPUTER: 'Velg filer fra din pc'
FROMCOMPUTERINFO: 'Last opp fra din pc'
TOTAL: Total
TOUPLOAD: 'Velg filer til opplasting ...'
UPLOADINPROGRESS: 'Vennligst vent... opplasting pågår'
UPLOADOR: ELLER
@ -98,6 +99,8 @@ nb:
FOURTH: fjerde
SECOND: andre
THIRD: tredje
CurrencyField:
CURRENCYSYMBOL: $
DataObject:
PLURALNAME: 'Dataobjekter'
SINGULARNAME: 'Dataobjekt'
@ -140,6 +143,7 @@ nb:
AviType: 'AVI videofil'
Content: Innhold
CssType: 'CSS-fil'
DmgType: 'Apple disk image'
DocType: 'Word-dokument'
Filename: Filnavn
GifType: 'GIF bilde - bra til diagrammer'
@ -162,6 +166,7 @@ nb:
SINGULARNAME: Fil
TOOLARGE: 'Filstørrelse for stor, maksimum {size} tillatt '
TOOLARGESHORT: 'Filstørrelse overstiger {size}'
TiffType: 'Tagged image format'
Title: Tittel
WavType: 'WAV lydfil'
XlsType: 'Excel regneark'
@ -188,6 +193,7 @@ nb:
HELLO: Hei
TEXT1: 'Her er din'
TEXT2: 'lenke for nullstilling av passord'
TEXT3: for
Form:
FIELDISREQUIRED: '{name} er påkrevet'
SubmitBtnLabel: Utfør
@ -196,6 +202,7 @@ nb:
VALIDATIONPASSWORDSDONTMATCH: 'Passordene stemmer ikke overens'
VALIDATIONPASSWORDSNOTEMPTY: 'Passord kan ikke være tomt'
VALIDATIONSTRONGPASSWORD: 'Passord må inneholde minst ett siffer og en bokstav'
VALIDATOR: Validator
VALIDCURRENCY: 'Vennligst skriv inn gyldig valuta'
CSRF_FAILED_MESSAGE: 'Det ser ut til å ha oppstått et teknisk problem. Vennligst trykk på tilbakeknappen, oppdater nettsiden og prøv på nytt.'
FormField:
@ -204,16 +211,21 @@ nb:
GridAction:
DELETE_DESCRIPTION: Slett
Delete: Slett
UnlinkRelation: Koble fra
GridField:
Add: 'Legg til {name}'
Filter: Filtrer
FilterBy: 'Filtrer på '
Find: Finn
LEVELUP: 'Opp ett nivå'
LinkExisting: 'Eksisterende lenke'
NewRecord: 'Ny %s'
NoItemsFound: 'Ingen elementer ble funnet'
PRINTEDAT: 'Skrevet ut ved'
PRINTEDBY: 'Skrevet ut av'
PlaceHolder: 'Finn {type}'
PlaceHolderWithLabels: 'Finn {type} ved {name}'
RelationSearch: 'Relasjonssøk'
ResetFilter: Tilbakestille
GridFieldAction_Delete:
DeletePermissionsFailure: 'Ikke tillatt å slette'
@ -232,6 +244,7 @@ nb:
AddRole: 'Legg til en rolle for denne gruppen'
Code: 'Gruppekode'
DefaultGroupTitleAdministrators: Administratorer
DefaultGroupTitleContentAuthors: 'Innholdsforfattere'
Description: Beskrivelse
GroupReminder: 'Hvis du velger en overordnet gruppe, vil denne gruppen arve alle rollene'
Locked: 'Låst?'
@ -244,6 +257,7 @@ nb:
has_many_Permissions: Tillatelser
many_many_Members: Medlemmer
GroupImportForm:
Help1: '<p>Importer en eller flere grupper i <em>CSV</em>-format (kommaseparerte verdier). <small><a href="#" class="toggle-advanced">Vis avanserte alternativer</a></small></p>'
Help2: "<div class=\"advanced\">\n<h4>Avanserte alternativer</h4>\n<ul>\n<li>Tillatte kolonner: <em>%s</em></li>\n<li>Eksisterende grupper matches mot deres <em>Code</em>-verdi og oppdateres med nye verdier fra den importerte filen.</li>\n<li>Gruppehierarkier kan bli opprettet ved å benytte en <em>ParentCode</em>-kolonne.</li>\n<li>Tillatelseskoder kan bli angitt med <em>PermissionCode</em>-kolonnen. Eksisterende tillatelselskoder blir ikke fjernet.</li>\n</ul>\n</div>"
ResultCreated: 'Opprettet {count} grupper'
ResultDeleted: 'Slettet %d grupper'
@ -331,6 +345,7 @@ nb:
IP: 'IP-adresse'
PLURALNAME: 'Innloggingsforsøk'
SINGULARNAME: 'Innloggingsforsøk'
Status: Status
Member:
ADDGROUP: 'Legg til en gruppe'
BUTTONCHANGEPASSWORD: 'Bytt passord'
@ -341,10 +356,13 @@ nb:
CONFIRMNEWPASSWORD: 'Bekreft nytt passord'
CONFIRMPASSWORD: 'Bekreft passord'
DATEFORMAT: 'Datoformat'
DefaultAdminFirstname: 'Standard Admin'
DefaultDateTime: standard
EMAIL: Epost
EMPTYNEWPASSWORD: 'Det nye passordet kan ikke være tomt, vennligst prøv igjen'
ENTEREMAIL: 'Vennligst skriv en epostadresse så du kan bli tilsendt en lenke til å nullstille passord.'
ERRORLOCKEDOUT: 'Din konto har blitt sperret på grunn av for mange forsøk på å logge inn. Vennligst prøv igjen om 20 minutter.'
ERRORLOCKEDOUT2: 'Din konto har blitt midlertidig sperret på grunn av for mange mislykkede forsøk på å logge inn. Vennligst prøv igjen om {count} minutter.'
ERRORNEWPASSWORD: 'Du har tastet inn nye passord forskjellig, vennligst prøv igjen.'
ERRORPASSWORDNOTMATCH: 'Passordene stemmer ikke overens, vennligst prøv igjen.'
ERRORWRONGCRED: 'Det ser ikke ut til å være riktig epostadresse eller passord. Vennligst prøv igjen.'
@ -372,9 +390,12 @@ nb:
db_NumVisit: 'Antall besøk'
db_Password: Passord
db_PasswordExpiry: 'Utløpsdato for passord'
NoPassword: 'Det finnes ikke noe passord for dette medlemmet.'
MemberAuthenticator:
TITLE: 'E-post og passord'
MemberDatetimeOptionsetField:
AMORPM: 'AM (Ante meridiem) or PM (Post meridiem)'
Custom: Egendefinert
DATEFORMATBAD: 'Datoformat er ugyldig'
DAYNOLEADING: 'Dag i måneden uten ledende null'
DIGITSDECFRACTIONSECOND: 'Ett eller flere siffer som representerer en desimlbrøkdel av et sekund'
@ -411,6 +432,7 @@ nb:
EMPTYBEFOREIMPORT: 'Erstatt data'
IMPORT: 'Importer fra CSV'
IMPORTEDRECORDS: 'Importerte {count} oppføringer.'
NOCSVFILE: 'Vennligst finn en CSV-fil å importere'
NOIMPORT: 'Ingenting å importere'
RESET: Tilbakestill
Title: 'Datamodeller'
@ -437,6 +459,7 @@ nb:
Page: Side
View: Viser
Permission:
AdminGroup: Administratorer
CMS_ACCESS_CATEGORY: 'Tilgang til publiseringssystem'
FULLADMINRIGHTS: 'Fulle administrative rettigheter'
FULLADMINRIGHTS_HELP: 'Overstyrer alle andre tilordnede tillatelser.'
@ -466,6 +489,7 @@ nb:
NOTFOUND: 'Ingen elementer ble funnet'
Security:
ALREADYLOGGEDIN: 'Du har ikke adgang til denne siden. Hvis du har en annen konto som har adgang til denne siden, kan du logge inn med den under.'
LOSTPASSWORDHEADER: 'Mistet passord'
BUTTONSEND: 'Send meg en lenke for å nullstille passordet'
CHANGEPASSWORDBELOW: 'Du kan bytte passord under her.'
CHANGEPASSWORDHEADER: 'Bytt passord'
@ -485,15 +509,20 @@ nb:
EDITPERMISSIONS: 'Administrer tillatelser for grupper'
EDITPERMISSIONS_HELP: 'Mulighet for å endre tillatelser og IP-adresser for en gruppe. Brukere må ha adgang til sikkerhetsseksjon.'
GROUPNAME: 'Gruppenavn'
IMPORTGROUPS: 'Importer grupper'
IMPORTUSERS: 'Importer brukere'
MEMBERS: Medlemmer
MENUTITLE: Sikkerhet
MemberListCaution: 'Advarsel: Hvis du fjerner medlemmer fra denne listen vil det samtidig fjerne dem fra alle grupper og databasen'
NEWGROUP: 'Ny gruppe'
PERMISSIONS: Tilganger
ROLES: Roller
ROLESDESCRIPTION: 'Roller er forhåndsdefinerte lister av tillatelser og kan angis til grupper.<br />De blir arvet fra overerdnede grupper om nødvendig.'
TABROLES: Roller
Users: Brukere
SecurityAdmin_MemberImportForm:
BtnImport: 'Importer fra CSV'
FileFieldLabel: 'CSV-fil <small>(Tillatt filtype: *.csv)</small>'
SilverStripeNavigator:
Edit: Rediger
Auto: Automatisk
@ -541,6 +570,7 @@ nb:
LESS: mindre
MORE: mer
UploadField:
ATTACHFILE: 'Legg ved en fil'
ATTACHFILES: 'Legg ved filer'
AttachFile: 'Legg ved fil(er)'
DELETE: 'Slett fra filer'
@ -575,3 +605,11 @@ nb:
PREVIEW: 'Forhåndsvisning'
GridFieldEditButton_ss:
EDIT: Rediger
ContentController:
NOTLOGGEDIN: 'Ikke innlogget'
GridFieldItemEditView:
Go_back: 'Gå tilbake'
PasswordValidator:
LOWCHARSTRENGTH: 'Vennligst øk passordstyrken ved å legge til noen av følgende tegn: %s'
PREVPASSWORD: 'Du har brukt passordet tidligere, vennligst velg et nytt passord'
TOOSHORT: 'Passordet er for kort, det må være %s eller flere tegn langt'

View File

@ -13,8 +13,10 @@ sk:
SIZE: 'Veľkosť'
TITLE: Titulok
TYPE: 'Typ'
URL: URL
AssetUploadField:
DRAGFILESHERE: 'Tiahni súbory sem'
ChooseFiles: 'Vyberte súbory'
DRAGFILESHERE: 'Tiahni súbory tu'
DROPAREA: 'Oblasť upustenia'
EDITALL: 'Editovať všetko'
EDITANDORGANIZE: 'Editovať a organizovať'
@ -72,6 +74,7 @@ sk:
ChangePasswordEmail_ss:
CHANGEPASSWORDTEXT1: 'Vaše heslo bolo zmenené pre'
CHANGEPASSWORDTEXT2: 'Teraz môžete použiť nasledujúce prihlasovacie údaje na prihlásenie:'
EMAIL: E-mail
HELLO: Dobrý deň
PASSWORD: Heslo
ComplexTableField:
@ -96,6 +99,8 @@ sk:
FOURTH: štvrtý
SECOND: druhý
THIRD: tretí
CurrencyField:
CURRENCYSYMBOL: $
DataObject:
PLURALNAME: 'Datové objekty'
SINGULARNAME: 'Dátový objekt'
@ -209,6 +214,7 @@ sk:
UnlinkRelation: Odpojiť
GridField:
Add: 'Pridať {name}'
Filter: Filter
FilterBy: 'Filtrovať podľa'
Find: Vyhľadať
LEVELUP: 'Úroveň vyššie'
@ -220,6 +226,7 @@ sk:
PlaceHolder: 'Vyhľadať {type}'
PlaceHolderWithLabels: 'Vyhľadať {type} podľa {name}'
RelationSearch: 'Vzťah hľadania'
ResetFilter: Reset
GridFieldAction_Delete:
DeletePermissionsFailure: 'Žiadne oprávnenia zmazať'
EditPermissionsFailure: 'Žiadne oprávnenie pre odpojenie záznamu'
@ -261,6 +268,7 @@ sk:
ADDURL: 'Pridať URL'
ADJUSTDETAILSDIMENSIONS: 'Detaily &amp; rozmery'
ANCHORVALUE: Odkaz
BUTTONINSERT: Vložiť
BUTTONINSERTLINK: 'Vložiť odkaz'
BUTTONREMOVELINK: 'Odstrániť odkaz'
BUTTONUpdate: Aktualizovať
@ -298,6 +306,7 @@ sk:
LINKOPENNEWWIN: 'Otvoriť odkaz v novom okne?'
LINKTO: 'Odkázať na'
PAGE: Stránku
URL: URL
URLNOTANOEMBEDRESOURCE: 'URL ''{url}'' nemôže byť vložené do zdroja médií.'
UpdateMEDIA: 'Aktualizovať média'
BUTTONADDURL: 'Pridať url'
@ -314,6 +323,7 @@ sk:
LeftAndMain:
CANT_REORGANISE: 'Nemáte oprávnenie meniť stránky najvyššej úrovne. Vaša zmena nebola uložená.'
DELETED: Zmazané.
DropdownBatchActionsDefault: Akcie
HELP: Pomoc
PAGETYPE: 'Typ stránky:'
PERMAGAIN: 'Boli ste odhlásený'
@ -323,6 +333,7 @@ sk:
PreviewButton: Náhľad
REORGANISATIONSUCCESSFUL: 'Strom webu bol reorganizovaný úspešne.'
SAVEDUP: Uložené.
VersionUnknown: Neznáme
ShowAsList: 'ukázať ako zoznam'
TooManyPages: 'Príliž veľa stránok'
ValidationError: 'Chyba platnosti'
@ -334,7 +345,9 @@ sk:
IP: 'IP adreasa'
PLURALNAME: 'Pokusy o prihlásenie'
SINGULARNAME: 'Pokus o prihlásenie'
Status: Stav
Member:
ADDGROUP: 'Pridať skupinu'
BUTTONCHANGEPASSWORD: 'Zmeniť heslo'
BUTTONLOGIN: 'Prihlásiť sa'
BUTTONLOGINOTHER: 'Prihlásiť sa ako niekto iný'
@ -349,6 +362,7 @@ sk:
EMPTYNEWPASSWORD: 'Nové heslo nesmie byť prázdne, skúste to prosím znova'
ENTEREMAIL: 'Prosím zadajte emailovú adresu pre zaslanie odkazu na resetovanie hesla.'
ERRORLOCKEDOUT: 'Váš účet bol dočasne zablokovaný, kvôli množstvu neúspešných pokusov o prihlásenie. Prosí skúste to znova za 20 minút.'
ERRORLOCKEDOUT2: 'Váš účet bol dočasne zablokovaný, kvôli množstvu neúspešných pokusov o prihlásenie. Prosím skúste to znova za {count} minút.'
ERRORNEWPASSWORD: 'Zadali ste rozdielne nové heslo, skúste to znovu'
ERRORPASSWORDNOTMATCH: 'Vaše súčasné heslo nie je správne, prosím skúste to znovu'
ERRORWRONGCRED: 'Toto nevyzerá ako správna e-mailová adresa alebo heslo. Prosím skúste to znovu.'
@ -367,6 +381,7 @@ sk:
TIMEFORMAT: 'Formát času'
VALIDATIONMEMBEREXISTS: 'Člen s takýmto e-mailom už existuje'
ValidationIdentifierFailed: 'Nemôžte prepísať existujúceho člena #{id} s identickým identifikátorm ({name} = {value}))'
WELCOMEBACK: 'Vitajte späť, {firstname}'
YOUROLDPASSWORD: 'Vaše staré heslo'
belongs_many_many_Groups: Skupiny
db_LastVisited: 'Dátum poslednej navštevy'
@ -375,10 +390,12 @@ sk:
db_NumVisit: 'Počet návštev'
db_Password: Heslo
db_PasswordExpiry: 'Dátum expirácie hesla'
NoPassword: 'Nie je tu heslo pre tohto člena.'
MemberAuthenticator:
TITLE: 'E-mail &amp; Heslo'
MemberDatetimeOptionsetField:
AMORPM: 'AM (pred poludním) alebo PM (popoludní)'
Custom: Vlastné
DATEFORMATBAD: 'Formát dátumu je neplatný'
DAYNOLEADING: 'Deň mesiaca bez úvodnej nuly'
DIGITSDECFRACTIONSECOND: 'Jedna alebo viac číslic zastupujúcich desatinný zlomok sekundy'
@ -387,6 +404,7 @@ sk:
HOURNOLEADING: 'Hodina bez úvodnej nuly'
MINUTENOLEADING: 'Minúta bez úvodnej nuly'
MONTHNOLEADING: 'Číslo mesiaca bez úvodnej nuly'
Preview: Náhľad
SHORTMONTH: 'Krátky názov mesiaca (napr. jún)'
TOGGLEHELP: 'Prepnúť nápovedu formátovania'
TWODIGITDAY: 'Dvojčíslie dňa mesiaca'
@ -416,6 +434,7 @@ sk:
IMPORTEDRECORDS: 'Importovaných {count} záznamov.'
NOCSVFILE: 'Prosím vyhľadajte CSV súbor pre importovanie'
NOIMPORT: 'Nie je čo importovať'
RESET: Reset
Title: 'Dátové modely'
UPDATEDRECORDS: 'Aktualizovaných {count} záznamov.'
ModelAdmin_ImportSpec_ss:
@ -432,6 +451,8 @@ sk:
MoneyField:
FIELDLABELAMOUNT: Množstvo
FIELDLABELCURRENCY: Mena
NullableField:
IsNullLabel: 'Je Null'
NumericField:
VALIDATION: '''{value}'' nie je číslo, iba čísla môžu byť akceptované pre toto pole'
Pagination:
@ -468,6 +489,7 @@ sk:
NOTFOUND: 'Žiadne položky'
Security:
ALREADYLOGGEDIN: 'K tejto stránke nemáte prístup. Ak máte iný účet, ktorý k nej má prístup, môžete sa <a href="%s">prihlásiť</a>.'
LOSTPASSWORDHEADER: 'Stratené heslo'
BUTTONSEND: 'Pošlite mi odkaz na resetovanie hesla'
CHANGEPASSWORDBELOW: 'Svoje heslo si môžete zmeniť nižšie.'
CHANGEPASSWORDHEADER: 'Zmeniť heslo'
@ -487,6 +509,7 @@ sk:
EDITPERMISSIONS: 'Spravovať právomoci pre skupiny'
EDITPERMISSIONS_HELP: 'Schopnosť upravovať právomoci a IP adresi pre skupinu. Vyžaduje prístup do sekcie "Bezpečnosť".'
GROUPNAME: 'Názov skupiny'
IMPORTGROUPS: 'Importovať skupiny'
IMPORTUSERS: 'Importovať požívateľov'
MEMBERS: Členovia
MENUTITLE: Bezpečnosť
@ -502,7 +525,9 @@ sk:
FileFieldLabel: 'CSV súbor <small>(Povoléné koncovki súborov: *.csv)</small>'
SilverStripeNavigator:
Edit: Editovať
Auto: Auto
ChangeViewMode: 'Zmeniť nód zobrazenia'
Desktop: Desktop
DualWindowView: 'Duálne okno'
EditView: 'Mód editácie'
Mobile: Mobilný telefón
@ -510,6 +535,7 @@ sk:
PreviewView: 'Mód náhľadu'
Responsive: Responzívny
SplitView: 'Mód rozdelenia'
Tablet: Tablet
ViewDeviceWidth: 'Vyberte šírku náhľadu'
Width: šírka
SimpleImageField:
@ -544,8 +570,12 @@ sk:
LESS: menej
MORE: viac
UploadField:
ATTACHFILE: 'Pripojiť súbor'
ATTACHFILES: 'Pripojiť súbory'
AttachFile: 'Pripojiť súbor(y)'
DELETE: 'Zmazať zo súborov'
DELETEINFO: 'Trvalo zmazať tento súbor z úložiska súborov'
DOEDIT: Uložiť
DROPFILE: 'pusť súbor'
DROPFILES: 'pusť súbory'
Dimensions: Rozmery
@ -554,6 +584,7 @@ sk:
FIELDNOTSET: 'Žiadna informácia o súbore'
FROMCOMPUTER: 'Z vášho počitača'
FROMCOMPUTERINFO: 'Vyberte zo súborov'
FROMFILES: 'Zo súborov'
HOTLINKINFO: 'Info: Tento obrázok bude "hotlinkovaný". Uistete sa prosím, že máte oprávnenie od pôvodneho tvorcu webu, aby sa tak stalo.'
MAXNUMBEROFFILES: 'Maximálny počet {count} súbor(ov) prekročený.'
MAXNUMBEROFFILESSHORT: 'Môžte nahrať iba {count} súborov'
@ -563,6 +594,7 @@ sk:
REMOVEINFO: 'Odstrániť tento súbor odtiaľ, ale nezmazať z úložiska súborov'
STARTALL: 'Začni všetko'
STARTALLINFO: 'Začni všetko nahrávať'
Saved: Uložené
CHOOSEANOTHERFILE: 'Vyberte iný súbor'
CHOOSEANOTHERINFO: 'Nahradiť tento súbor iným z úložiska súborov'
OVERWRITEWARNING: 'Súbor s rovnakým názvom už existuje'
@ -573,3 +605,11 @@ sk:
PREVIEW: 'Náhľad webu'
GridFieldEditButton_ss:
EDIT: Editovať
ContentController:
NOTLOGGEDIN: 'Neprihlásený'
GridFieldItemEditView:
Go_back: 'Choď späť'
PasswordValidator:
LOWCHARSTRENGTH: 'Prosím posilnite heslo pridaním z týchto niektorých znakov: %s'
PREVPASSWORD: 'Už ste použili toto heslo v minulosti, vyberte nové hoslo, prosím'
TOOSHORT: 'Heslo je príliš krátke, musí byť %s alebo viacej znakov dlhé'

View File

@ -141,6 +141,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/**
* @config
* @var boolean Should dataobjects be validated before they are written?
* Caution: Validation can contain safeguards against invalid/malicious data,
* and check permission levels (e.g. on {@link Group}). Therefore it is recommended
* to only disable validation for very specific use cases.
*/
private static $validation_enabled = true;
@ -187,6 +190,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/**
* Set whether DataObjects should be validated before they are written.
*
* Caution: Validation can contain safeguards against invalid/malicious data,
* and check permission levels (e.g. on {@link Group}). Therefore it is recommended
* to only disable validation for very specific use cases.
*
* @param $enable bool
* @see DataObject::validate()
* @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead

View File

@ -25,7 +25,16 @@ class HasManyList extends RelationList {
$this->foreignKey = $foreignKey;
}
/**
* Gets the field name which holds the related object ID.
*
* @return string
*/
public function getForeignKey() {
return $this->foreignKey;
}
protected function foreignIDFilter($id = null) {
if ($id === null) $id = $this->getForeignID();

View File

@ -425,8 +425,8 @@ class Image extends File {
$cached = new Image_Cached($cacheFile);
// Pass through the title so the templates can use it
$cached->Title = $this->Title;
// Pass through the parent, to store cached images in correct folder.
$cached->ParentID = $this->ParentID;
$cached->Parent = $this->Parent();
return $cached;
}
}
@ -451,7 +451,7 @@ class Image extends File {
return $folder . "_resampled/" . $filename;
}
/**
* Generate an image on the specified format. It will save the image
* at the location specified by cacheFilename(). The image will be generated
@ -545,13 +545,12 @@ class Image extends File {
public function generateCroppedImage(Image_Backend $backend, $width, $height) {
return $backend->croppedResize($width, $height);
}
/**
* Generate patterns that will help to match filenames of cached images
*
* @param string $filename Filename of source image
* @return array
*/
/**
* Generate patterns that will help to match filenames of cached images
* @param string $filename Filename of source image
* @return array
*/
private function getFilenamePatterns($filename) {
$methodNames = $this->allMethodNames(true);
$generateFuncs = array();
@ -569,7 +568,7 @@ class Image extends File {
'GeneratorPattern' => "/(?P<Generator>{$generateFuncs})(?P<Args>" . $base64Match . ")\-/i"
);
}
/**
* Generate a list of images that were generated from this image
*/

View File

@ -1066,10 +1066,20 @@ class SQLQuery {
* @param $alias An optional alias for the aggregate column.
*/
public function aggregate($column, $alias = null) {
$clone = clone $this;
$clone->setLimit($this->limit);
$clone->setOrderBy($this->orderby);
// don't set an ORDER BY clause if no limit has been set. It doesn't make
// sense to add an ORDER BY if there is no limit, and it will break
// queries to databases like MSSQL if you do so. Note that the reason
// this came up is because DataQuery::initialiseQuery() introduces
// a default sort.
if($this->limit) {
$clone->setLimit($this->limit);
$clone->setOrderBy($this->orderby);
} else {
$clone->setOrderBy(array());
}
$clone->setGroupBy($this->groupby);
if($alias) {
$clone->setSelect(array());
@ -1077,7 +1087,6 @@ class SQLQuery {
} else {
$clone->setSelect($column);
}
return $clone;
}
@ -1118,10 +1127,15 @@ class SQLQuery {
// shift the first FROM table out from so we only deal with the JOINs
$baseFrom = array_shift($from);
$this->mergesort($from, function($firstJoin, $secondJoin) {
if($firstJoin['order'] == $secondJoin['order']) {
if(
!is_array($firstJoin)
|| !is_array($secondJoin)
|| $firstJoin['order'] == $secondJoin['order']
) {
return 0;
} else {
return ($firstJoin['order'] < $secondJoin['order']) ? -1 : 1;
}
return ($firstJoin['order'] < $secondJoin['order']) ? -1 : 1;
});
// Put the first FROM table back into the results

View File

@ -163,7 +163,7 @@ class HTMLText extends Text {
}
// If it's got a content tag
if(preg_match('/<(img|embed|object|iframe|meta|source)[^>]*>/i', $this->value)) {
if(preg_match('/<(img|embed|object|iframe|meta|source|link)[^>]*>/i', $this->value)) {
return true;
}

View File

@ -212,7 +212,8 @@ class Money extends DBField implements CompositeDBField {
* @return boolean
*/
public function hasAmount() {
return (int)$this->getAmount() != '0';
$a = $this->getAmount();
return (!empty($a) && is_numeric($a));
}
public function isChanged() {

View File

@ -45,6 +45,18 @@ $gf_grid_x: 16px;
.action {
margin-bottom:$gf_grid_y;
}
}
&.ss-gridfield-buttonrow-before{
margin-bottom: 0;
.action {
margin-bottom:$gf_grid_y;
}
}
&.ss-gridfield-buttonrow-after{
margin-bottom: 0;
.action {
margin-top:$gf_grid_y;
}
}
}
@ -116,7 +128,8 @@ $gf_grid_x: 16px;
width: 500px;
}
.grid-csv-button, .grid-print-button {
margin-bottom: $gf_grid_y;
margin-bottom: 0;
font-size: $font-base-size;
@include inline-block();
}
}
@ -641,8 +654,4 @@ $gf_grid_x: 16px;
border-right: 1px solid $gf_colour_border;
}
}
.grid-bottom-button {
margin-top:$gf_grid_y;
}
}

View File

@ -1,3 +1,6 @@
@import 'compass';
@import "../admin/scss/_mixins";
div.TreeDropdownField {
width: 400px;
background: #fff;
@ -20,20 +23,49 @@ div.TreeDropdownField {
line-height: 16px;
overflow:hidden;
outline: none;
z-index:1;
@include hide-text-overflow;
}
.treedropdownfield-search{
@extend .treedropdownfield-title;
//Style search box to match chosen search
$bgImage: '../admin/thirdparty/chosen/chosen/chosen-sprite.png';
background:url($bgImage) no-repeat 100% -22px; //For browers that only support 1 background
@include background(
url($bgImage) no-repeat 100% -22px,
linear-gradient(top, #eeeeee 1%, #ffffff 15%)
);
@include box-sizing(border-box);
position:relative;
z-index:1100; //Needed to work within modales in chrome
border: 1px solid #aaa;
display:inline-block;
font-family: sans-serif;
font-size: 1em;
margin:1.5%;
outline: 0;
padding: 4px 20px 4px 5px;
width:97%; //optimized for most common tree width
}
&.searchable .treedropdownfield-panel.loading{
min-height: 16px /* icon */ + 14px /* padding */ + 34px /* approx height search input */; // Ensure there's room for loading indicator
background-position: 98% 39px;
}
.treedropdownfield-panel {
clear: left;
position: absolute;
overflow: auto;
display: none;
cursor: default;
border: 1px solid #aaa;
border-top: none;
margin: 1px 0 0 -1px; /* account for border on container div */
max-height:200px;
background-color: #fff;
z-index: 50;
z-index: 70;
-webkit-box-shadow: 0 4px 5px rgba(0,0,0,.15);
-moz-box-shadow : 0 4px 5px rgba(0,0,0,.15);
-o-box-shadow : 0 4px 5px rgba(0,0,0,.15);
@ -41,12 +73,33 @@ div.TreeDropdownField {
&.loading {
min-height: 16px /* icon */ + 14px /* padding */; // Ensure there's room for loading indicator
background: #fff url("../images/network-save.gif") 7px 7px no-repeat;
background: #fff url("../images/network-save.gif") 98% 7px no-repeat;
}
.tree-holder{
position:relative;
z-index:1;
> ul{
position:relative;
max-height:200px;
overflow-y: auto;
}
}
ul{
overflow-x:hidden;
float:left;
width:100%;
.jstree-icon{
margin-left:5px; //move to align with possible search box
}
.jstree-open > ins{
background-position:-18px 0; //move to align with possible search box
}
}
ul.tree {
margin: 0;
a {
font-size: 12px;
}

View File

@ -1,7 +1,7 @@
@import "compass/css3";
body {
background-color: #eee;
background: #eee !important;
margin:0;
overflow-x: hidden;
padding:0;
@ -20,7 +20,8 @@ body {
linear-gradient(darken(#003050, 5%), #003050 10%, #003050 90%, darken(#003050, 5%))
);
// try to get the info above the template with z-index
z-index: 9999;
h1 {
margin: 0 0 6px 0;
padding: 0 32px 0 0;
@ -71,9 +72,14 @@ body {
.build,
.options {
padding:6px 12px;
background: #eee !important;
// try to get the info above the template with z-index
position: relative;
z-index: 9999;
li {
font-size:14px; margin:6px 0;
font-size:14px;
margin:6px 0;
}
}

View File

@ -96,8 +96,9 @@ class Group extends DataObject {
if($this->ID) {
$group = $this;
$config = new GridFieldConfig_RelationEditor();
$config->addComponents(new GridFieldExportButton('after'));
$config->addComponents(new GridFieldPrintButton('after'));
$config->addComponent(new GridFieldButtonRow('after'));
$config->addComponents(new GridFieldExportButton('buttons-after-left'));
$config->addComponents(new GridFieldPrintButton('buttons-after-left'));
$config->getComponentByType('GridFieldAddExistingAutocompleter')
->setResultsFormat('$Title ($Email)')->setSearchFields(array('FirstName', 'Surname', 'Email'));
$config->getComponentByType('GridFieldDetailForm')
@ -336,6 +337,31 @@ class Group extends DataObject {
public function setCode($val){
$this->setField("Code", Convert::raw2url($val));
}
public function validate() {
$result = parent::validate();
// Check if the new group hierarchy would add certain "privileged permissions",
// and require an admin to perform this change in case it does.
// This prevents "sub-admin" users with group editing permissions to increase their privileges.
if($this->Parent()->exists() && !Permission::check('ADMIN')) {
$inheritedCodes = Permission::get()
->filter('GroupID', $this->Parent()->collateAncestorIDs())
->column('Code');
$privilegedCodes = Config::inst()->get('Permission', 'privileged_permissions');
if(array_intersect($inheritedCodes, $privilegedCodes)) {
$result->error(sprintf(
_t(
'Group.HierarchyPermsError',
'Can\'t assign parent group "%s" with privileged permissions (requires ADMIN access)'
),
$this->Parent()->Title
));
}
}
return $result;
}
public function onBeforeWrite() {
parent::onBeforeWrite();

View File

@ -1169,7 +1169,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
$password->setCanBeEmpty(true);
if(!$this->ID) $password->showOnClick = false;
$mainFields->replaceField('Password', $password);
$mainFields->replaceField('Locale', new DropdownField(
"Locale",
_t('Member.INTERFACELANG', "Interface Language", 'Language of the CMS'),
@ -1231,7 +1231,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions');
if($permissionsTab) $permissionsTab->addExtraClass('readonly');
$defaultDateFormat = Zend_Locale_Format::getDateFormat($this->Locale);
$defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($this->Locale));
$dateFormatMap = array(
'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'),
'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'),
@ -1249,7 +1249,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
);
$dateFormatField->setValue($this->DateFormat);
$defaultTimeFormat = Zend_Locale_Format::getTimeFormat($this->Locale);
$defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($this->Locale));
$timeFormatMap = array(
'h:mm a' => Zend_Date::now()->toString('h:mm a'),
'H:mm' => Zend_Date::now()->toString('H:mm'),

View File

@ -86,6 +86,17 @@ class Permission extends DataObject implements TemplateGlobalProvider {
*/
private static $hidden_permissions = array();
/**
* @config These permissions can only be applied by ADMIN users, to prevent
* privilege escalation on group assignments and inheritance.
* @var array
*/
private static $privileged_permissions = array(
'ADMIN',
'APPLY_ROLES',
'EDIT_PERMISSIONS'
);
/**
* Check that the current member has the given permission.
*

View File

@ -162,6 +162,8 @@ class PermissionCheckboxSetField extends FormField {
$options = '';
$globalHidden = (array)Config::inst()->get('Permission', 'hidden_permissions');
if($this->source) {
$privilegedPermissions = Permission::config()->privileged_permissions;
// loop through all available categorized permissions and see if they're assigned for the given groups
foreach($this->source as $categoryName => $permissions) {
$options .= "<li><h5>$categoryName</h5></li>";
@ -194,6 +196,11 @@ class PermissionCheckboxSetField extends FormField {
$inheritMessage = ' (' . join(', ', $uninheritedCodes[$code]).')';
}
// Disallow modification of "privileged" permissions unless currently logged-in user is an admin
if(!Permission::check('ADMIN') && in_array($code, $privilegedPermissions)) {
$disabled = ' disabled="true"';
}
// If the field is readonly, always mark as "disabled"
if($this->readonly) $disabled = ' disabled="true"';
@ -246,6 +253,16 @@ class PermissionCheckboxSetField extends FormField {
$fieldname = $this->name;
$managedClass = $this->managedClass;
// Remove all "privileged" permissions if the currently logged-in user is not an admin
$privilegedPermissions = Permission::config()->privileged_permissions;
if(!Permission::check('ADMIN')) {
foreach($this->value as $id => $bool) {
if(in_array($id, $privilegedPermissions)) {
unset($this->value[$id]);
}
}
}
// remove all permissions and re-add them afterwards
$permissions = $record->$fieldname();
foreach ( $permissions as $permission ) {

View File

@ -76,4 +76,20 @@ class PermissionRole extends DataObject {
return $labels;
}
public function canView($member = null) {
return Permission::check('APPLY_ROLES', 'any', $member);
}
public function canCreate($member = null) {
return Permission::check('APPLY_ROLES', 'any', $member);
}
public function canEdit($member = null) {
return Permission::check('APPLY_ROLES', 'any', $member);
}
public function canDelete($member = null) {
return Permission::check('APPLY_ROLES', 'any', $member);
}
}

View File

@ -13,4 +13,38 @@ class PermissionRoleCode extends DataObject {
private static $has_one = array(
"Role" => "PermissionRole",
);
protected function validate() {
$result = parent::validate();
// Check that new code doesn't increase privileges, unless an admin is editing.
$privilegedCodes = Config::inst()->get('Permission', 'privileged_permissions');
if(
$this->Code
&& in_array($this->Code, $privilegedCodes)
&& !Permission::check('ADMIN')
) {
$result->error(sprintf(
_t(
'PermissionRoleCode.PermsError',
'Can\'t assign code "%s" with privileged permissions (requires ADMIN access)'
),
$this->Code
));
}
return $result;
}
public function canCreate($member = null) {
return Permission::check('APPLY_ROLES', 'any', $member);
}
public function canEdit($member = null) {
return Permission::check('APPLY_ROLES', 'any', $member);
}
public function canDelete($member = null) {
return Permission::check('APPLY_ROLES', 'any', $member);
}
}

View File

@ -280,6 +280,9 @@ class Security extends Controller {
$this->response->addHeader('X-Frame-Options', 'SAMEORIGIN');
}
public function index() {
return $this->httpError(404); // no-op
}
/**
* Get the login form to process according to the submitted data

View File

@ -1,4 +1,4 @@
<div class="addNewGridFieldButton ss-gridfield-buttonrow">
<div class="ss-gridfield-buttonrow ss-gridfield-buttonrow-{$TargetFragmentName}">
<div class="left">$LeftFragment</div>
<div class="right">$RightFragment</div>
</div>

View File

@ -67,4 +67,3 @@
<div class="clear"><!-- --></div>
</div>
<% end_if %>
<% if Description %><span class="description">$Description</span><% end_if %>

View File

@ -196,6 +196,74 @@ class RestfulServiceTest extends SapphireTest {
);
}
public function testExtractResponseRedirectionAndProxy() {
// This is an example of real raw response for a request via a proxy that gets redirected.
$rawHeaders =
"HTTP/1.0 200 Connection established\r\n" .
"\r\n" .
"HTTP/1.1 301 Moved Permanently\r\n" .
"Server: nginx\r\n" .
"Date: Fri, 20 Sep 2013 01:53:07 GMT\r\n" .
"Content-Type: text/html\r\n" .
"Content-Length: 178\r\n" .
"Connection: keep-alive\r\n" .
"Location: https://www.foobar.org.nz/\r\n" .
"\r\n" .
"HTTP/1.0 200 Connection established\r\n" .
"\r\n" .
"HTTP/1.1 200 OK\r\n" .
"Server: nginx\r\n" .
"Date: Fri, 20 Sep 2013 01:53:08 GMT\r\n" .
"Content-Type: text/html; charset=utf-8\r\n" .
"Transfer-Encoding: chunked\r\n" .
"Connection: keep-alive\r\n" .
"X-Frame-Options: SAMEORIGIN\r\n" .
"Cache-Control: no-cache, max-age=0, must-revalidate, no-transform\r\n" .
"Vary: Accept-Encoding\r\n" .
"\r\n"
;
$headerFunction = new ReflectionMethod('RestfulService', 'extractResponse');
$headerFunction->setAccessible(true);
$ch = curl_init();
$response = $headerFunction->invoke(
new RestfulService(Director::absoluteBaseURL(),0),
$ch,
$rawHeaders,
''
);
$this->assertEquals(
$response->getHeaders(),
array(
'Server' => "nginx",
'Date' => "Fri, 20 Sep 2013 01:53:08 GMT",
'Content-Type' => "text/html; charset=utf-8",
'Transfer-Encoding' => "chunked",
'Connection' => "keep-alive",
'X-Frame-Options' => "SAMEORIGIN",
'Cache-Control' => "no-cache, max-age=0, must-revalidate, no-transform",
'Vary' => "Accept-Encoding"
),
'Only last header is extracted and parsed.'
);
}
public function testExtractResponseNoHead() {
$headerFunction = new ReflectionMethod('RestfulService', 'extractResponse');
$headerFunction->setAccessible(true);
$ch = curl_init();
$response = $headerFunction->invoke(
new RestfulService(Director::absoluteBaseURL(),0),
$ch,
'',
''
);
$this->assertEquals($response->getHeaders(), array(), 'Headers are correctly extracted.');
}
}
class RestfulServiceTest_Controller extends Controller implements TestOnly {

View File

@ -0,0 +1,28 @@
Feature: Apply rich formatting to content
As a cms author
I want to work with content in the way I'm used to from word processing software
So that I make it more appealing by creating structure and highlights
Background:
Given a "page" "About Us" has the "Content" "<h1>My awesome headline</h1><p>Some amazing content</p>"
And I am logged in with "ADMIN" permissions
And I go to "/admin/pages"
Then I click on "About Us" in the tree
Scenario: I can control alignment of selected content
Given I select "My awesome headline" in the "Content" HTML field
When I press the "Align Right" button
Then "My awesome headline" in the "Content" HTML field should be right aligned
But "Some amazing content" in the "Content" HTML field should be left aligned
Then I press the "Save draft" button
Then "My awesome headline" in the "Content" HTML field should be right aligned
Scenario: I can bold selected content
Given I select "awesome" in the "Content" HTML field
When I press the "Bold (Ctrl+B)" button
Then "awesome" in the "Content" HTML field should be bold
But "My" in the "Content" HTML field should not be bold
When I press the "Save draft" button
Then "awesome" in the "Content" HTML field should be bold
But "My" in the "Content" HTML field should not be bold

View File

@ -11,6 +11,8 @@ use Behat\Behat\Context\ClosuredContextInterface,
Behat\Gherkin\Node\PyStringNode,
Behat\Gherkin\Node\TableNode;
use Symfony\Component\DomCrawler\Crawler;
// PHPUnit
require_once 'PHPUnit/Autoload.php';
require_once 'PHPUnit/Framework/Assert/Functions.php';
@ -89,7 +91,7 @@ class CmsFormsContext extends BehatContext
}
/**
* @Then /^the "(?P<locator>([^"]*))" HTML field should contain "(?P<html>([^"]*))"$/
* @Then /^the "(?P<locator>([^"]*))" HTML field should contain "(?P<html>.*)"$/
*/
public function theHtmlFieldShouldContain($locator, $html)
{
@ -101,15 +103,92 @@ class CmsFormsContext extends BehatContext
$regex = '/'.preg_quote($html, '/').'/ui';
if (!preg_match($regex, $actual)) {
$message = sprintf(
'The string "%s" was not found in the HTML of the element matching %s "%s".',
'The string "%s" was not found in the HTML of the element matching %s "%s". Actual content: "%s"',
$html,
'named',
$locator
$locator,
$actual
);
throw new ElementHtmlException($message, $this->getSession(), $element);
}
}
/**
* Checks formatting in the HTML field, by analyzing the HTML node surrounding
* the text for certain properties.
*
* Example: Given "my text" in the "Content" HTML field should be right aligned
* Example: Given "my text" in the "Content" HTML field should not be bold
*
* @todo Use an actual DOM parser for more accurate assertions
*
* @Given /^"(?P<text>([^"]*))" in the "(?P<field>([^"]*))" HTML field should(?P<negate>(?: not)?) be (?P<formatting>(.*))$/
*/
public function stepContentInHtmlFieldShouldHaveFormatting($text, $field, $negate, $formatting) {
$page = $this->getSession()->getPage();
$inputField = $page->findField($field);
assertNotNull($inputField, sprintf('HTML field "%s" not found', $field));
$crawler = new Crawler($inputField->getValue());
$matchedNode = null;
foreach($crawler->filterXPath('//*') as $node) {
if(
$node->firstChild
&& $node->firstChild->nodeType == XML_TEXT_NODE
&& stripos($node->firstChild->nodeValue, $text) !== FALSE
) {
$matchedNode = $node;
}
}
assertNotNull($matchedNode);
$assertFn = $negate ? 'assertNotEquals' : 'assertEquals';
if($formatting == 'bold') {
call_user_func($assertFn, 'strong', $matchedNode->nodeName);
} else if($formatting == 'left aligned') {
if($matchedNode->getAttribute('style')) {
call_user_func($assertFn, 'text-align: left;', $matchedNode->getAttribute('style'));
}
} else if($formatting == 'right aligned') {
call_user_func($assertFn, 'text-align: right;', $matchedNode->getAttribute('style'));
}
}
/**
* Selects the first textual match in the HTML editor. Does not support
* selection across DOM node boundaries.
*
* @When /^I select "(?P<text>([^"]*))" in the "(?P<field>([^"]*))" HTML field$/
*/
public function stepIHighlightTextInHtmlField($text, $field)
{
$page = $this->getSession()->getPage();
$inputField = $page->findField($field);
assertNotNull($inputField, sprintf('HTML field "%s" not found', $field));
$inputFieldId = $inputField->getAttribute('id');
$text = addcslashes($text, "'");
$js = <<<JS
// TODO <IE9 support
// TODO Allow text matches across nodes
var editor = jQuery('#$inputFieldId').entwine('ss').getEditor(),
doc = editor.getDOM().doc,
sel = editor.getInstance().selection,
rng = document.createRange(),
matched = false;
jQuery(doc).find('body *').each(function() {
if(!matched && this.firstChild && this.firstChild.nodeValue && this.firstChild.nodeValue.match('$text')) {
rng.setStart(this.firstChild, this.firstChild.nodeValue.indexOf('$text'));
rng.setEnd(this.firstChild, this.firstChild.nodeValue.indexOf('$text') + '$text'.length);
sel.setRng(rng);
editor.getInstance().nodeChanged();
matched = true;
}
});
JS;
$this->getSession()->evaluateScript($js);
}
/**
* @Given /^I should see a "([^"]*)" button$/
*/

View File

@ -140,7 +140,7 @@ class CmsUiContext extends BehatContext
}
/**
* @When /^I should see "([^"]*)" in CMS Tree$/
* @When /^I should see "([^"]*)" in the tree$/
*/
public function stepIShouldSeeInCmsTree($text)
{
@ -151,7 +151,7 @@ class CmsUiContext extends BehatContext
}
/**
* @When /^I should not see "([^"]*)" in CMS Tree$/
* @When /^I should not see "([^"]*)" in the tree$/
*/
public function stepIShouldNotSeeInCmsTree($text)
{
@ -161,6 +161,17 @@ class CmsUiContext extends BehatContext
assertNull($element, sprintf('%s found', $text));
}
/**
* @When /^I click on "([^"]*)" in the tree$/
*/
public function stepIClickOnElementInTheTree($text)
{
$treeEl = $this->getCmsTreeElement();
$treeNode = $treeEl->findLink($text);
assertNotNull($treeNode, sprintf('%s not found', $text));
$treeNode->click();
}
/**
* @When /^I expand the "([^"]*)" CMS Panel$/
*/
@ -329,7 +340,10 @@ class CmsUiContext extends BehatContext
$field = $this->fixStepArgument($field);
$value = $this->fixStepArgument($value);
$nativeField = $this->getSession()->getPage()->findField($field);
$nativeField = $this->getSession()->getPage()->find(
'named',
array('select', $this->getSession()->getSelectorsHandler()->xpathLiteral($field))
);
if($nativeField) {
$nativeField->selectOption($value);
return;
@ -340,7 +354,9 @@ class CmsUiContext extends BehatContext
// Find by label
$formField = $this->getSession()->getPage()->findField($field);
if($formField) $formFields[] = $formField;
if($formField && $formField->getTagName() == 'select') {
$formFields[] = $formField;
}
// Fall back to finding by title (for dropdowns without a label)
if(!$formFields) {
@ -358,6 +374,15 @@ class CmsUiContext extends BehatContext
$formFields = $this->getSession()->getPage()->findAll('xpath', "//*[@name='$field']");
}
// Find by label
if(!$formFields) {
$label = $this->getSession()->getPage()->find('xpath', "//label[.='$field']");
if($label && $for = $label->getAttribute('for')) {
$formField = $this->getSession()->getPage()->find('xpath', "//*[@id='$for']");
if($formField) $formFields[] = $formField;
}
}
assertGreaterThan(0, count($formFields), sprintf(
'Chosen.js dropdown named "%s" not found',
$field

View File

@ -0,0 +1,83 @@
@assets
Feature: Insert links into a page
As a cms author
I want to insert a link into my content
So that I can link to a external website or a page on my site
Background:
Given a "page" "Home"
And a "page" "About Us" has the "Content" "My awesome content"
#And a "file" "assets/folder1/file1.jpg"
And I am logged in with "ADMIN" permissions
And I go to "/admin/pages"
And I click on "About Us" in the tree
Scenario: I can link to an internal page
Given I select "awesome" in the "Content" HTML field
And I press the "Insert Link" button
When I check "Page on the site"
And I fill in the "Page" dropdown with "Home"
And I fill in "my desc" for "Link description"
And I press the "Insert" button
# TODO Dynamic DB identifiers
Then the "Content" HTML field should contain "<a title="my desc" href="[sitetree_link,id=1]">awesome</a>"
# Required to avoid "unsaved changed" browser dialog
Then I press the "Save draft" button
Scenario: I can link to an external URL
Given I select "awesome" in the "Content" HTML field
And I press the "Insert Link" button
When I check "Another website"
And I fill in "http://silverstripe.org" for "URL"
And I check "Open link in a new window"
And I press the "Insert" button
Then the "Content" HTML field should contain "<a href="http://silverstripe.org" target="_blank">awesome</a>"
# Required to avoid "unsaved changed" browser dialog
Then I press the "Save draft" button
@todo
Scenario: I can link to a file
Given I select "awesome" in the "Content" HTML field
When I press the "Insert Link" button
When I check "Download a file"
And I fill in the "File" dropdown with "file1.jpg"
And I press the "Insert link" button
Then the "Content" HTML field should contain "<a href="assets/folder1/file1.jpg">awesome</a>"
# Required to avoid "unsaved changed" browser dialog
Then I press the "Save draft" button
@todo
Scenario: I can link to an anchor
Given I fill in the "Content" HTML field with "My awesome content <a name=myanchor>"
And I select "awesome" in the "Content" HTML field
When I press the "Insert Link" button
When I check "Anchor on this page"
And I fill in the "Select an anchor" dropdown with "myanchor"
And I press the "Insert link" button
Then the "Content" HTML field should contain "<a href="#myanchor">awesome</a>"
# Required to avoid "unsaved changed" browser dialog
Then I press the "Save draft" button
@todo
Scenario: I can edit a link
Given I fill in the "Content" HTML field with "My <a href="http://silverstripe.org">awesome</a> content"
And I select "awesome"
When I press the "Insert Link" button
And the "URL" field should contain "http://silverstripe.org"
When I fill in "http://wordpress.org" for "URL"
And I press the "Insert link" button
Then the "Content" HTML field should contain "<a href="http://wordpress.org">awesome</a>"
# Required to avoid "unsaved changed" browser dialog
Then I press the "Save draft" button
@todo
Scenario: I can delete a link
Given I fill in the "Content" HTML field with "My <a href="http://silverstripe.org">awesome</a> content"
And I select "awesome"
When I press the "Insert Link" button
And I press the "Remove link" button
Then the "Content" HTML field should not contain "<a href="http://silverstripe.org">awesome</a>"
# Required to avoid "unsaved changed" browser dialog
Then I press the "Save draft" button

View File

@ -1,29 +1,95 @@
@assets
Feature: Insert an image into a page
As a cms author
I want to insert an image into a page
So that I can insert them into my content efficiently
As a cms author
I want to insert an image into a page
So that I can insert them into my content efficiently
Background:
Given a "page" "About Us"
Given I am logged in with "ADMIN" permissions
And I go to "/admin/pages"
Then I should see "About Us" in CMS Tree
Background:
Given a "page" "About Us"
#And a "file" "assets/folder1/file1.jpg"
#And a "file" "assets/folder1/file3.jpg"
#And a "file" "assets/folder1/folder1.1/file2.jpg"
#And a "folder" "assets/folder2"
And I am logged in with "ADMIN" permissions
And I go to "/admin/pages"
And I click on "About Us" in the tree
@javascript
Scenario: I can insert images into the content
When I follow "About Us"
Then I should see an edit page form
Scenario: I can insert an image from a URL
Given I press the "Insert Media" button
Then I should see "Choose files to upload..."
When I press the "Insert Media" button
Then I should see "Choose files to upload..."
When I press the "From the web" button
And I fill in "RemoteURL" with "http://www.silverstripe.com/themes/sscom/images/silverstripe_logo_web.png"
And I press the "Add url" button
Then I should see "silverstripe_logo_web.png (www.silverstripe.com)" in the ".ss-assetuploadfield span.name" element
When I press the "From the web" button
And I fill in "RemoteURL" with "http://www.silverstripe.com/themes/sscom/images/silverstripe_logo_web.png"
And I press the "Add url" button
Then I should see "silverstripe_logo_web.png (www.silverstripe.com)" in the ".ss-assetuploadfield span.name" element
When I press the "Insert" button
Then the "Content" HTML field should contain "silverstripe_logo_web.png"
# Required to avoid "unsaved changed" browser dialog
Then I press the "Save draft" button
When I press the "Insert" button
Then the "Content" HTML field should contain "silverstripe_logo_web.png"
# Required to avoid "unsaved changed" browser dialog
Then I press the "Save draft" button
@todo
Scenario: I can insert an image uploaded from my own computer
Given I press the "Insert Media" button
And I press the "From your computer" button
# TODO Figure out how to provide the file
And I attach the file "testfile.jpg" to "AssetUploadField" with HTML5
Then the upload field should have successfully uploaded "testfile.jpg"
When I press the "Insert" button
Then the "Content" HTML field should contain "testfile.jpg"
@todo
Scenario: I can insert an image from the CMS file store
Given I press the "Insert Media" button
And I press the "From the CMS" button
And I select "folder1" in the "Find in Folder" dropdown
And I select "file1.jpg"
When I press the "Insert" button
Then the "Content" HTML field should contain "file1.jpg"
@todo
Scenario: I can insert multiple images at once
Given I press the "Insert Media" button
And I press the "From the CMS" button
And I select "folder1" in the "Find in Folder" dropdown
And I select "file1.jpg"
And I select "file3.jpg"
When I press the "Insert" button
Then the "Content" HTML field should contain "file1.jpg"
And the "Content" HTML field should contain "file1.jpg"
@todo
Scenario: I can edit properties of an image before inserting it
Given I press the "Insert Media" button
And I press the "From the CMS" button
And I select "folder1" in the "Find in Folder" dropdown
And I select "file1.jpg"
And I follow "Edit"
When I fill in "Alternative text (alt)" with "My alt"
And I press the "Insert" button
Then the "Content" HTML field should contain "file1.jpg"
And the "Content" HTML field should contain "My alt"
@todo
Scenario: I can edit dimensions of an image before inserting it
Given I press the "Insert Media" button
And I press the "From the CMS" button
And I select "folder1" in the "Find in Folder" dropdown
And I select "file1.jpg"
And I follow "Edit"
When I fill in "Width" with "10"
When I fill in "Height" with "20"
And I press the "Insert" button
Then the "Content" HTML field should contain "<img src=assets/folder1/file1.jpg width=10 height=20>"
@todo
Scenario: I can edit dimensions of an existing image
Given the "page" "About us" contains "<img src=assets/folder1/file1.jpg>"
And I reload the current page
When I highlight "<img src=assets/folder1/file1.jpg>" in the "Content" HTML field
And I press the "Insert Media" button
Then I should see "file1.jpg"
When I fill in "Width" with "10"
When I fill in "Height" with "20"
And I press the "Insert" button
Then the "Content" HTML field should contain "<img src=assets/folder1/file1.jpg width=10 height=20>"

View File

@ -49,7 +49,7 @@ Feature: Manage files
Scenario: I can change the folder of a file
Given I click on "folder1" in the "Files" table
And I click on "file1" in the "folder1" table
And I fill in "folder2" for the "ParentID" dropdown
And I fill in "folder2" for the "Folder" dropdown
And I press the "Save" button
# /show/0 is to ensure that we are on top level folder
And I go to "/admin/assets/show/0"

View File

@ -1,12 +1,38 @@
Feature: My Profile
@todo
Feature: Manage my own settings
As a CMS user
I want to be able to change personal settings
In order to streamline my CMS experience
@javascript
Scenario: I can see date formatting help
Given I am logged in with "ADMIN" permissions
# Only tests this specific field and admin UI because its got built-in tooltips
When I go to "/admin/myprofile"
And I follow "Show formatting help"
Then I should see "Four-digit year"
Background:
Given a "member" "Joe" belongs to "Admin Group" with "Email"="joe@test.com" and "Password"="secret"
And the "group" "Admin Group" has permissions "Full administrative rights"
And I am logged in with "joe@test.com" and "secret"
And I navigate to "admin/myprofile"
Scenario: I can edit my personal details
Given I fill in "First Name" with "Jack"
And I fill in "Surname" with "Johnson"
And I fill in "Email" with "jack@test.com"
When I press the "Save" button
Then I should not see "John"
But I should see "Jack"
And I should see "Johnson"
And I should see "jack@test.com"
Scenario: I can change my password
Given I click "Change Password"
And I fill out "Password" with "newsecret"
And I fill out "Confirm Password" with "newsecret"
And I press the "Save" button
And I log out
When I login with "joe@test.com" and "newsecret"
Then I should see the CMS
Scenario: I can change the interface language
Given I fill in "Interface Language" with "German (Germany)"
And I press the "Save" button
Then I should see "Sprache"
# TODO Date/time format - Difficult because its not exposed anywhere in the CMS?
# TODO Group modification as ADMIN user

View File

@ -236,9 +236,9 @@ class ControllerTest extends FunctionalTest {
$this->assertEquals("admin/crm?existing=1&flush=1", Controller::join_links("admin/crm?existing=1", "?flush=1"));
$this->assertEquals("admin/crm/MyForm?a=1&b=2&c=3",
Controller::join_links("?a=1", "admin/crm", "?b=2", "MyForm?c=3"));
/* Note, however, that it doesn't deal with duplicates very well. */
$this->assertEquals("admin/crm?flush=1&flush=1", Controller::join_links("admin/crm?flush=1", "?flush=1"));
// And duplicates are handled nicely
$this->assertEquals("admin/crm?foo=2&bar=3&baz=1", Controller::join_links("admin/crm?foo=1&bar=1&baz=1", "?foo=2&bar=3"));
$this->assertEquals (
'admin/action', Controller::join_links('admin/', '/', '/action'), 'Test that multiple slashes are trimmed.'

View File

@ -21,6 +21,45 @@ class FormFieldTest extends SapphireTest {
$this->assertStringEndsWith('class2', $field->extraClass());
}
public function testAddManyExtraClasses() {
$field = new FormField('MyField');
//test we can split by a range of spaces and tabs
$field->addExtraClass('class1 class2 class3 class4 class5');
$this->assertStringEndsWith(
'class1 class2 class3 class4 class5',
$field->extraClass()
);
//test that duplicate classes don't get added
$field->addExtraClass('class1 class2');
$this->assertStringEndsWith(
'class1 class2 class3 class4 class5',
$field->extraClass()
);
}
public function testRemoveManyExtraClasses() {
$field = new FormField('MyField');
$field->addExtraClass('class1 class2 class3 class4 class5');
//test we can remove a single class we just added
$field->removeExtraClass('class3');
$this->assertStringEndsWith(
'class1 class2 class4 class5',
$field->extraClass()
);
//check we can remove many classes at once
$field->removeExtraClass('class1 class5');
$this->assertStringEndsWith(
'class2 class4',
$field->extraClass()
);
//check that removing a dud class is fine
$field->removeExtraClass('dudClass');
$this->assertStringEndsWith(
'class2 class4',
$field->extraClass()
);
}
public function testAttributes() {
$field = new FormField('MyField');
$field->setAttribute('foo', 'bar');

View File

@ -393,6 +393,61 @@ class FormTest extends FunctionalTest {
$this->assertEquals('application/x-www-form-urlencoded', $form->getEncType());
}
public function testAddExtraClass() {
$form = $this->getStubForm();
$form->addExtraClass('class1');
$form->addExtraClass('class2');
$this->assertStringEndsWith('class1 class2', $form->extraClass());
}
public function testRemoveExtraClass() {
$form = $this->getStubForm();
$form->addExtraClass('class1');
$form->addExtraClass('class2');
$this->assertStringEndsWith('class1 class2', $form->extraClass());
$form->removeExtraClass('class1');
$this->assertStringEndsWith('class2', $form->extraClass());
}
public function testAddManyExtraClasses() {
$form = $this->getStubForm();
//test we can split by a range of spaces and tabs
$form->addExtraClass('class1 class2 class3 class4 class5');
$this->assertStringEndsWith(
'class1 class2 class3 class4 class5',
$form->extraClass()
);
//test that duplicate classes don't get added
$form->addExtraClass('class1 class2');
$this->assertStringEndsWith(
'class1 class2 class3 class4 class5',
$form->extraClass()
);
}
public function testRemoveManyExtraClasses() {
$form = $this->getStubForm();
$form->addExtraClass('class1 class2 class3 class4 class5');
//test we can remove a single class we just added
$form->removeExtraClass('class3');
$this->assertStringEndsWith(
'class1 class2 class4 class5',
$form->extraClass()
);
//check we can remove many classes at once
$form->removeExtraClass('class1 class5');
$this->assertStringEndsWith(
'class2 class4',
$form->extraClass()
);
//check that removing a dud class is fine
$form->removeExtraClass('dudClass');
$this->assertStringEndsWith(
'class2 class4',
$form->extraClass()
);
}
public function testAttributes() {
$form = $this->getStubForm();

View File

@ -97,9 +97,9 @@ class i18nTest extends SapphireTest {
public function testGetExistingTranslations() {
$translations = i18n::get_existing_translations();
$this->assertTrue(isset($translations['en']), 'Checking for en translation');
$this->assertEquals($translations['en'], 'English (United States)');
$this->assertTrue(isset($translations['de']), 'Checking for de_DE translation');
$this->assertTrue(isset($translations['en_US']), 'Checking for en translation');
$this->assertEquals($translations['en_US'], 'English (United States)');
$this->assertTrue(isset($translations['de_DE']), 'Checking for de_DE translation');
}
public function testDataObjectFieldLabels() {

View File

@ -269,4 +269,37 @@ class ImageTest extends SapphireTest {
$this->assertEquals(1, $numDeleted, 'Expected one image to be deleted, but deleted ' . $numDeleted . ' images');
$this->assertFalse(file_exists($p), 'Resized image not existing after deletion call');
}
/**
* Tests that generated images with multiple image manipulations are all deleted
*/
public function testMultipleGenerateManipulationCallsImageDeletion() {
$image = $this->objFromFixture('Image', 'imageWithMetacharacters');
$firstImage = $image->SetWidth(200);
$firstImagePath = $firstImage->getFullPath();
$this->assertTrue(file_exists($firstImagePath));
$secondImage = $firstImage->SetHeight(100);
$secondImagePath = $secondImage->getFullPath();
$this->assertTrue(file_exists($secondImagePath));
$image->deleteFormattedImages();
$this->assertFalse(file_exists($firstImagePath));
$this->assertFalse(file_exists($secondImagePath));
}
/**
* Tests path properties of cached images with multiple image manipulations
*/
public function testPathPropertiesCachedImage() {
$image = $this->objFromFixture('Image', 'imageWithMetacharacters');
$firstImage = $image->SetWidth(200);
$firstImagePath = $firstImage->getRelativePath();
$this->assertEquals($firstImagePath, $firstImage->Filename);
$secondImage = $firstImage->SetHeight(100);
$secondImagePath = $secondImage->getRelativePath();
$this->assertEquals($secondImagePath, $secondImage->Filename);
}
}

View File

@ -277,6 +277,36 @@ class MoneyTest extends SapphireTest {
$this->assertEquals('£2.46', $obj->obj('MyOtherMoney')->Nice());
}
public function testHasAmount() {
$obj = new MoneyTest_DataObject();
$m = new Money();
$obj->MyMoney = $m;
$m->setValue(array('Amount' => 1));
$this->assertTrue($obj->MyMoney->hasAmount());
$m->setValue(array('Amount' => 1.00));
$this->assertTrue($obj->MyMoney->hasAmount());
$m->setValue(array('Amount' => 1.01));
$this->assertTrue($obj->MyMoney->hasAmount());
$m->setValue(array('Amount' => 0.99));
$this->assertTrue($obj->MyMoney->hasAmount());
$m->setValue(array('Amount' => 0.01));
$this->assertTrue($obj->MyMoney->hasAmount());
$m->setValue(array('Amount' => 0));
$this->assertFalse($obj->MyMoney->hasAmount());
$m->setValue(array('Amount' => 0.0));
$this->assertFalse($obj->MyMoney->hasAmount());
$m->setValue(array('Amount' => 0.00));
$this->assertFalse($obj->MyMoney->hasAmount());
}
}

View File

@ -132,10 +132,21 @@ class SQLQueryTest extends SapphireTest {
$query = new SQLQuery();
$query->setFrom("MyTable");
$query->setOrderBy('RAND()');
$this->assertEquals(
'SELECT *, RAND() AS "_SortColumn0" FROM MyTable ORDER BY "_SortColumn0" ASC',
$query->sql());
$query = new SQLQuery();
$query->setFrom("MyTable");
$query->addFrom('INNER JOIN SecondTable USING (ID)');
$query->addFrom('INNER JOIN ThirdTable USING (ID)');
$query->setOrderBy('MyName');
$this->assertEquals(
'SELECT * FROM MyTable '
. 'INNER JOIN SecondTable USING (ID) '
. 'INNER JOIN ThirdTable USING (ID) '
. 'ORDER BY MyName ASC',
$query->sql());
}
public function testNullLimit() {
@ -161,6 +172,11 @@ class SQLQueryTest extends SapphireTest {
}
public function testZeroLimitWithOffset() {
if(!(DB::getConn() instanceof MySQLDatabase || DB::getConn() instanceof SQLite3Database
|| DB::getConn() instanceof PostgreSQLDatabase)) {
$this->markTestIncomplete();
}
$query = new SQLQuery();
$query->setFrom("MyTable");
$query->setLimit(0, 99);
@ -414,6 +430,31 @@ class SQLQueryTest extends SapphireTest {
$this->assertEquals(array(2), $result->column('cnt'));
}
/**
* Tests that an ORDER BY is only added if a LIMIT is set.
*/
public function testAggregateNoOrderByIfNoLimit() {
$query = new SQLQuery();
$query->setFrom('"SQLQueryTest_DO"');
$query->setOrderBy('Common');
$query->setLimit(array());
$aggregate = $query->aggregate('MAX("ID")');
$limit = $aggregate->getLimit();
$this->assertEquals(array(), $aggregate->getOrderBy());
$this->assertEquals(array(), $limit);
$query = new SQLQuery();
$query->setFrom('"SQLQueryTest_DO"');
$query->setOrderBy('Common');
$query->setLimit(2);
$aggregate = $query->aggregate('MAX("ID")');
$limit = $aggregate->getLimit();
$this->assertEquals(array('Common' => 'ASC'), $aggregate->getOrderBy());
$this->assertEquals(array('start' => 0, 'limit' => 2), $limit);
}
/**
* Test that "_SortColumn0" is added for an aggregate in the ORDER BY
* clause, in combination with a LIMIT and GROUP BY clause.
@ -439,6 +480,20 @@ class SQLQueryTest extends SapphireTest {
$this->assertEquals('2012-05-01 09:00:00', $records['0']['_SortColumn0']);
}
/**
* Test passing in a LIMIT with OFFSET clause string.
*/
public function testLimitSetFromClauseString() {
$query = new SQLQuery();
$query->setSelect('*');
$query->setFrom('"SQLQueryTest_DO"');
$query->setLimit('20 OFFSET 10');
$limit = $query->getLimit();
$this->assertEquals(20, $limit['limit']);
$this->assertEquals(10, $limit['start']);
}
}
class SQLQueryTest_DO extends DataObject implements TestOnly {

View File

@ -109,6 +109,49 @@ class GroupTest extends FunctionalTest {
'Grandchild groups are removed');
}
public function testValidatesPrivilegeLevelOfParent() {
$nonAdminUser = $this->objFromFixture('GroupTest_Member', 'childgroupuser');
$adminUser = $this->objFromFixture('GroupTest_Member', 'admin');
$nonAdminGroup = $this->objFromFixture('Group', 'childgroup');
$adminGroup = $this->objFromFixture('Group', 'admingroup');
$nonAdminValidateMethod = new ReflectionMethod($nonAdminGroup, 'validate');
$nonAdminValidateMethod->setAccessible(true);
// Making admin group parent of a non-admin group, effectively expanding is privileges
$nonAdminGroup->ParentID = $adminGroup->ID;
$this->logInWithPermission('APPLY_ROLES');
$result = $nonAdminValidateMethod->invoke($nonAdminGroup);
$this->assertFalse(
$result->valid(),
'Members with only APPLY_ROLES can\'t assign parent groups with direct ADMIN permissions'
);
$this->logInWithPermission('ADMIN');
$result = $nonAdminValidateMethod->invoke($nonAdminGroup);
$this->assertTrue(
$result->valid(),
'Members with ADMIN can assign parent groups with direct ADMIN permissions'
);
$nonAdminGroup->write();
$newlyAdminGroup = $nonAdminGroup;
$this->logInWithPermission('ADMIN');
$inheritedAdminGroup = $this->objFromFixture('Group', 'group1');
$inheritedAdminMethod = new ReflectionMethod($inheritedAdminGroup, 'validate');
$inheritedAdminMethod->setAccessible(true);
$inheritedAdminGroup->ParentID = $adminGroup->ID;
$inheritedAdminGroup->write(); // only works with ADMIN login
$this->logInWithPermission('APPLY_ROLES');
$result = $inheritedAdminMethod->invoke($nonAdminGroup);
$this->assertFalse(
$result->valid(),
'Members with only APPLY_ROLES can\'t assign parent groups with inherited ADMIN permission'
);
}
}
class GroupTest_Member extends Member implements TestOnly {

View File

@ -16,4 +16,35 @@ class PermissionRoleTest extends FunctionalTest {
$this->assertEquals(0, DataObject::get('PermissionRoleCode',"\"RoleID\"={$role->ID}")->count(),
'Permissions removed along with the role');
}
public function testValidatesPrivilegedPermissions() {
$nonAdminCode = new PermissionRoleCode(array('Code' => 'CMS_ACCESS_CMSMain'));
$nonAdminValidateMethod = new ReflectionMethod($nonAdminCode, 'validate');
$nonAdminValidateMethod->setAccessible(true);
$adminCode = new PermissionRoleCode(array('Code' => 'ADMIN'));
$adminValidateMethod = new ReflectionMethod($adminCode, 'validate');
$adminValidateMethod->setAccessible(true);
$this->logInWithPermission('APPLY_ROLES');
$result = $nonAdminValidateMethod->invoke($nonAdminCode);
$this->assertTrue(
$result->valid(),
'Members with only APPLY_ROLES can create non-privileged permission role codes'
);
$this->logInWithPermission('APPLY_ROLES');
$result = $adminValidateMethod->invoke($adminCode);
$this->assertFalse(
$result->valid(),
'Members with only APPLY_ROLES can\'t create privileged permission role codes'
);
$this->logInWithPermission('ADMIN');
$result = $adminValidateMethod->invoke($adminCode);
$this->assertTrue(
$result->valid(),
'Members with ADMIN can create privileged permission role codes'
);
}
}

View File

@ -0,0 +1,138 @@
(function($){
// Gets all the child elements of a particular elements, stores it in an array
function getElements(store, original) {
var node, i = store.length, next = original.firstChild;
while ((node = next)) {
if (node.nodeType === 1) store[i++] = node;
next = node.firstChild || node.nextSibling;
while (!next && (node = node.parentNode) && node !== original) next = node.nextSibling;
}
}
// This might be faster? Or slower? @todo: benchmark.
function getElementsAlt(store, node) {
if (node.getElementsByTagName) {
var els = node.getElementsByTagName('*'), len = els.length, i = 0, j = store.length;
for(; i < len; i++, j++) {
store[j] = els[i];
}
}
else if (node.childNodes) {
var els = node.childNodes, len = els.length, i = 0;
for(; i < len; i++) {
getElements(store, els[i]);
}
}
}
var dontTrigger = false;
var patchDomManipCallback = function(original) {
var patched = function(elem){
var added = [];
if (!dontTrigger) {
if (elem.nodeType == 1) added[added.length] = elem;
getElements(added, elem);
}
var rv = original.apply(this, arguments);
if (!dontTrigger && added.length) {
var event = $.Event('EntwineElementsAdded');
event.targets = added;
$(document).triggerHandler(event);
}
return rv;
}
patched.patched = true;
return patched;
}
var version = $.prototype.jquery.split('.');
var callbackIdx = (version[0] > 1 || version[1] >= 10 ? 1 : 2);
// Monkey patch $.fn.domManip to catch all regular jQuery add element calls
var _domManip = $.prototype.domManip;
$.prototype.domManip = function() {
if (!arguments[callbackIdx].patched) arguments[callbackIdx] = patchDomManipCallback(arguments[callbackIdx]);
return _domManip.apply(this, arguments);
}
// Monkey patch $.fn.html to catch when jQuery sets innerHTML directly
var _html = $.prototype.html;
$.prototype.html = function(value) {
if (value === undefined) return _html.apply(this, arguments);
dontTrigger = true;
var res = _html.apply(this, arguments);
dontTrigger = false;
var added = [];
var i = 0, length = this.length;
for (; i < length; i++ ) getElements(added, this[i]);
var event = $.Event('EntwineElementsAdded');
event.targets = added;
$(document).triggerHandler(event);
return res;
}
// If this is true, we've changed something to call cleanData so that we can catch the elements, but we don't
// want to call the underlying original $.cleanData
var supressActualClean = false;
// Monkey patch $.cleanData to catch element removal
var _cleanData = $.cleanData;
$.cleanData = function( elems ) {
// By default we can assume all elements passed are legitimately being removeed
var removed = elems;
// Except if we're supressing actual clean - we might be being called by jQuery "being careful" about detaching nodes
// before attaching them. So we need to check to make sure these nodes currently are in a document
if (supressActualClean) {
var i = 0, len = elems.length, removed = [], ri = 0;
for(; i < len; i++) {
var node = elems[i], current = node;
while (current = current.parentNode) {
if (current.nodeType == 9) { removed[ri++] = node; break; }
}
}
}
if (removed.length) {
var event = $.Event('EntwineElementsRemoved');
event.targets = removed;
$(document).triggerHandler(event);
}
if (!supressActualClean) _cleanData.apply(this, arguments);
}
// Monkey patch $.fn.remove to catch when we're just detaching (keepdata == 1) -
// this doesn't call cleanData but still needs to trigger event
var _remove = $.prototype.remove;
$.prototype.remove = function(selector, keepdata) {
supressActualClean = keepdata;
var rv = _remove.call(this, selector);
supressActualClean = false;
return rv;
}
// And on DOM ready, trigger adding once
$(function(){
var added = []; getElements(added, document);
var event = $.Event('EntwineElementsAdded');
event.targets = added;
$(document).triggerHandler(event);
});
})(jQuery);

View File

@ -0,0 +1,149 @@
(function($){
/** Utility function to monkey-patch a jQuery method */
var monkey = function( /* method, method, ...., patch */){
var methods = $.makeArray(arguments);
var patch = methods.pop();
$.each(methods, function(i, method){
var old = $.fn[method];
$.fn[method] = function() {
var self = this, args = $.makeArray(arguments);
var rv = old.apply(self, args);
patch.apply(self, args);
return rv;
}
});
}
/** What to call to run a function 'soon'. Normally setTimeout, but for syncronous mode we override so soon === now */
var runSoon = window.setTimeout;
/** The timer handle for the asyncronous matching call */
var ChangeDetails = Base.extend({
init: function() {
this.global = false;
this.attrs = {};
this.classes = {};
},
/** Fire the change event. Only fires on the document node, so bind to that */
triggerEvent: function() {
// If we're not the active changes instance any more, don't trigger
if (changes != this) return;
// Cancel any pending timeout (if we're directly called in the mean time)
if (this.check_id) clearTimeout(this.check_id);
// Reset the global changes object to be a new instance (do before trigger, in case trigger fires changes itself)
changes = new ChangeDetails();
// Fire event
$(document).triggerHandler("EntwineSubtreeMaybeChanged", [this]);
},
changed: function() {
if (!this.check_id) {
var self = this;
this.check_id = runSoon(function(){ self.check_id = null; self.triggerEvent(); }, 10);
}
},
addAll: function() {
if (this.global) return this; // If we've already flagged as a global change, just skip
this.global = true;
this.changed();
return this;
},
addSubtree: function(node) {
return this.addAll();
},
/* For now we don't do this. It's expensive, and jquery.entwine.ctors doesn't use this information anyway */
addSubtreeFuture: function(node) {
if (this.global) return this; // If we've already flagged as a global change, just skip
this.subtree = this.subtree ? this.subtree.add(node) : $(node);
this.changed();
return this;
},
addAttr: function(attr, node) {
if (this.global) return this;
this.attrs[attr] = (attr in this.attrs) ? this.attrs[attr].add(node) : $(node);
this.changed();
return this;
},
addClass: function(klass, node) {
if (this.global) return this;
this.classes[klass] = (klass in this.classes) ? this.classes[klass].add(node) : $(node);
this.changed();
return this;
}
});
var changes = new ChangeDetails();
// Element add events trigger maybechanged events
$(document).bind('EntwineElementsAdded', function(e){ changes.addSubtree(e.targets); });
// Element remove events trigger maybechanged events, but we have to wait until after the nodes are actually removed
// (EntwineElementsRemoved fires _just before_ the elements are removed so the data still exists), especially in syncronous mode
var removed = null;
$(document).bind('EntwineElementsRemoved', function(e){ removed = e.targets; });
monkey('remove', 'html', 'empty', function(){
var subtree = removed; removed = null;
if (subtree) changes.addSubtree(subtree);
});
// We also need to know when an attribute, class, etc changes. Patch the relevant jQuery methods here
monkey('removeAttr', function(attr){
changes.addAttr(attr, this);
});
monkey('addClass', 'removeClass', 'toggleClass', function(klass){
if (typeof klass == 'string') changes.addClass(klass, this);
});
monkey('attr', function(a, b){
if (b !== undefined && typeof a == 'string') changes.addAttr(a, this);
else if (typeof a != 'string') { for (var k in a) changes.addAttr(k, this); }
});
// Add some usefull accessors to $.entwine
$.extend($.entwine, {
/**
* Make onmatch and onunmatch work in synchronous mode - that is, new elements will be detected immediately after
* the DOM manipulation that made them match. This is only really useful for during testing, since it's pretty slow
* (otherwise we'd make it the default).
*/
synchronous_mode: function() {
if (changes && changes.check_id) clearTimeout(changes.check_id);
changes = new ChangeDetails();
runSoon = function(func, delay){ func.call(this); return null; };
},
/**
* Trigger onmatch and onunmatch now - usefull for after DOM manipulation by methods other than through jQuery.
* Called automatically on document.ready
*/
triggerMatching: function() {
changes.addAll();
}
});
})(jQuery);

View File

@ -0,0 +1,63 @@
(function($) {
$.entwine.Namespace.addMethods({
build_addrem_proxy: function(name) {
var one = this.one(name, 'func');
return function() {
if (this.length === 0){
return;
}
else if (this.length) {
var rv, i = this.length;
while (i--) rv = one(this[i], arguments);
return rv;
}
else {
return one(this, arguments);
}
};
},
bind_addrem_proxy: function(selector, name, func) {
var rulelist = this.store[name] || (this.store[name] = $.entwine.RuleList());
var rule = rulelist.addRule(selector, name); rule.func = func;
if (!this.injectee.hasOwnProperty(name)) {
this.injectee[name] = this.build_addrem_proxy(name);
this.injectee[name].isentwinemethod = true;
}
}
});
$.entwine.Namespace.addHandler({
order: 30,
bind: function(selector, k, v) {
if ($.isFunction(v) && (k == 'onadd' || k == 'onremove')) {
this.bind_addrem_proxy(selector, k, v);
return true;
}
}
});
$(document).bind('EntwineElementsAdded', function(e){
// For every namespace
for (var k in $.entwine.namespaces) {
var namespace = $.entwine.namespaces[k];
if (namespace.injectee.onadd) namespace.injectee.onadd.call(e.targets);
}
});
$(document).bind('EntwineElementsRemoved', function(e){
for (var k in $.entwine.namespaces) {
var namespace = $.entwine.namespaces[k];
if (namespace.injectee.onremove) namespace.injectee.onremove.call(e.targets);
}
});
})(jQuery);

View File

@ -0,0 +1,242 @@
(function($) {
/* Add the methods to handle constructor & destructor binding to the Namespace class */
$.entwine.Namespace.addMethods({
bind_condesc: function(selector, name, func) {
var ctors = this.store.ctors || (this.store.ctors = $.entwine.RuleList()) ;
var rule;
for (var i = 0 ; i < ctors.length; i++) {
if (ctors[i].selector.selector == selector.selector) {
rule = ctors[i]; break;
}
}
if (!rule) {
rule = ctors.addRule(selector, 'ctors');
}
rule[name] = func;
if (!ctors[name+'proxy']) {
var one = this.one('ctors', name);
var namespace = this;
var proxy = function(els, i, func) {
var j = els.length;
while (j--) {
var el = els[j];
var tmp_i = el.i, tmp_f = el.f;
el.i = i; el.f = one;
try { func.call(namespace.$(el)); }
catch(e) { $.entwine.warn_exception(name, el, e); }
finally { el.i = tmp_i; el.f = tmp_f; }
}
};
ctors[name+'proxy'] = proxy;
}
}
});
$.entwine.Namespace.addHandler({
order: 30,
bind: function(selector, k, v) {
if ($.isFunction(v) && (k == 'onmatch' || k == 'onunmatch')) {
// When we add new matchers we need to trigger a full global recalc once, regardless of the DOM changes that triggered the event
this.matchersDirty = true;
this.bind_condesc(selector, k, v);
return true;
}
}
});
/**
* Finds all the elements that now match a different rule (or have been removed) and call onmatch on onunmatch as appropriate
*
* Because this has to scan the DOM, and is therefore fairly slow, this is normally triggered off a short timeout, so that
* a series of DOM manipulations will only trigger this once.
*
* The downside of this is that things like:
* $('#foo').addClass('tabs'); $('#foo').tabFunctionBar();
* won't work.
*/
$(document).bind('EntwineSubtreeMaybeChanged', function(e, changes){
// var start = (new Date).getTime();
// For every namespace
for (var k in $.entwine.namespaces) {
var namespace = $.entwine.namespaces[k];
// That has constructors or destructors
var ctors = namespace.store.ctors;
if (ctors) {
// Keep a record of elements that have matched some previous more specific rule.
// Not that we _don't_ actually do that until this is needed. If matched is null, it's not been calculated yet.
// We also keep track of any elements that have newly been taken or released by a specific rule
var matched = null, taken = $([]), released = $([]);
// Updates matched to contain all the previously matched elements as if we'd been keeping track all along
var calcmatched = function(j){
if (matched !== null) return;
matched = $([]);
var cache, k = ctors.length;
while ((--k) > j) {
if (cache = ctors[k].cache) matched = matched.add(cache);
}
}
// Some declared variables used in the loop
var add, rem, res, rule, sel, ctor, dtor, full;
// Stepping through each selector from most to least specific
var j = ctors.length;
while (j--) {
// Build some quick-access variables
rule = ctors[j];
sel = rule.selector.selector;
ctor = rule.onmatch;
dtor = rule.onunmatch;
/*
Rule.cache might be stale or fresh. It'll be stale if
- some more specific selector now has some of rule.cache in it
- some change has happened that means new elements match this selector now
- some change has happened that means elements no longer match this selector
The first we can just compare rules.cache with matched, removing anything that's there already.
*/
// Reset the "elements that match this selector and no more specific selector with an onmatch rule" to null.
// Staying null means this selector is fresh.
res = null;
// If this gets changed to true, it's too hard to do a delta update, so do a full update
full = false;
if (namespace.matchersDirty || changes.global) {
// For now, just fall back to old version. We need to do something like changed.Subtree.find('*').andSelf().filter(sel), but that's _way_ slower on modern browsers than the below
full = true;
}
else {
// We don't deal with attributes yet, so any attribute change means we need to do a full recalc
for (var k in changes.attrs) { full = true; break; }
/*
If a class changes, but it isn't listed in our selector, we don't care - the change couldn't affect whether or not any element matches
If it is listed on our selector
- If it is on the direct match part, it could have added or removed the node it changed on
- If it is on the context part, it could have added or removed any node that were previously included or excluded because of a match or failure to match with the context required on that node
- NOTE: It might be on _both_
*/
var method = rule.selector.affectedBy(changes);
if (method.classes.context) {
full = true;
}
else {
for (var k in method.classes.direct) {
calcmatched(j);
var recheck = changes.classes[k].not(matched);
if (res === null) {
res = rule.cache ? rule.cache.not(taken).add(released.filter(sel)) : $([]);
}
res = res.not(recheck).add(recheck.filter(sel));
}
}
}
if (full) {
calcmatched(j);
res = $(sel).not(matched);
}
else {
if (!res) {
// We weren't stale because of any changes to the DOM that affected this selector, but more specific
// onmatches might have caused stale-ness
// Do any of the previous released elements match this selector?
add = released.length && released.filter(sel);
if (add && add.length) {
// Yes, so we're stale as we need to include them. Filter for any possible taken value at the same time
res = rule.cache ? rule.cache.not(taken).add(add) : add;
}
else {
// Do we think we own any of the elements now taken by more specific rules?
rem = taken.length && rule.cache && rule.cache.filter(taken);
if (rem && rem.length) {
// Yes, so we're stale as we need to exclude them.
res = rule.cache.not(rem);
}
}
}
}
// Res will be null if we know we are fresh (no full needed, selector not affectedBy changes)
if (res === null) {
// If we are tracking matched, add ourselves
if (matched && rule.cache) matched = matched.add(rule.cache);
}
else {
// If this selector has a list of elements it matched against last time
if (rule.cache) {
// Find the ones that are extra this time
add = res.not(rule.cache);
rem = rule.cache.not(res);
}
else {
add = res; rem = null;
}
if ((add && add.length) || (rem && rem.length)) {
if (rem && rem.length) {
released = released.add(rem);
if (dtor && !rule.onunmatchRunning) {
rule.onunmatchRunning = true;
ctors.onunmatchproxy(rem, j, dtor);
rule.onunmatchRunning = false;
}
}
// Call the constructor on the newly matched ones
if (add && add.length) {
taken = taken.add(add);
released = released.not(add);
if (ctor && !rule.onmatchRunning) {
rule.onmatchRunning = true;
ctors.onmatchproxy(add, j, ctor);
rule.onmatchRunning = false;
}
}
}
// If we are tracking matched, add ourselves
if (matched) matched = matched.add(res);
// And remember this list of matching elements again this selector, so next matching we can find the unmatched ones
rule.cache = res;
}
}
namespace.matchersDirty = false;
}
}
// console.log((new Date).getTime() - start);
});
})(jQuery);

View File

@ -0,0 +1,118 @@
(function($) {
$.entwine.Namespace.addMethods({
bind_capture: function(selector, event, name, capture) {
var store = this.captures || (this.captures = {});
var rulelists = store[event] || (store[event] = {});
var rulelist = rulelists[name] || (rulelists[name] = $.entwine.RuleList());
rule = rulelist.addRule(selector, event);
rule.handler = name;
this.bind_proxy(selector, name, capture);
}
});
var bindings = $.entwine.capture_bindings = {};
var event_proxy = function(event) {
return function(e) {
var namespace, capturelists, forevent, capturelist, rule, handler, sel;
for (var k in $.entwine.namespaces) {
namespace = $.entwine.namespaces[k];
capturelists = namespace.captures;
if (capturelists && (forevent = capturelists[event])) {
for (var k in forevent) {
var capturelist = forevent[k];
var triggered = namespace.$([]);
// Stepping through each selector from most to least specific
var j = capturelist.length;
while (j--) {
rule = capturelist[j];
handler = rule.handler;
sel = rule.selector.selector;
var matching = namespace.$(sel).not(triggered);
matching[handler].apply(matching, arguments);
triggered = triggered.add(matching);
}
}
}
}
}
};
var selector_proxy = function(selector, handler, includechildren) {
var matcher = $.selector(selector);
return function(e){
if (matcher.matches(e.target)) return handler.apply(this, arguments);
}
};
var document_proxy = function(selector, handler, includechildren) {
return function(e){
if (e.target === document) return handler.apply(this, arguments);
}
};
var window_proxy = function(selector, handler, includechildren) {
return function(e){
if (e.target === window) return handler.apply(this, arguments);
}
};
var property_proxy = function(property, handler, includechildren) {
var matcher;
return function(e){
var match = this['get'+property]();
if (typeof(match) == 'string') {
var matcher = (matcher && match == matcher.selector) ? matcher : $.selector(match);
if (matcher.matches(e.target)) return handler.apply(this, arguments);
}
else {
if ($.inArray(e.target, match) !== -1) return handler.apply(this, arguments);
}
}
};
$.entwine.Namespace.addHandler({
order: 10,
bind: function(selector, k, v) {
var match;
if ($.isPlainObject(v) && (match = k.match(/^from\s*(.*)/))) {
var from = match[1];
var proxyGen;
if (from.match(/[^\w]/)) proxyGen = selector_proxy;
else if (from == 'Window' || from == 'window') proxyGen = window_proxy;
else if (from == 'Document' || from == 'document') proxyGen = document_proxy;
else proxyGen = property_proxy;
for (var onevent in v) {
var handler = v[onevent];
match = onevent.match(/^on(.*)/);
var event = match[1];
this.bind_capture(selector, event, k + '_' + event, proxyGen(from, handler));
if (!bindings[event]) {
var namespaced = event.replace(/(\s+|$)/g, '.entwine$1');
bindings[event] = event_proxy(event);
$(proxyGen == window_proxy ? window : document).bind(namespaced, bindings[event]);
}
}
return true;
}
}
});
})(jQuery);

View File

@ -0,0 +1,249 @@
(function($) {
/** Taken from jQuery 1.5.2 for backwards compatibility */
if ($.support.changeBubbles == undefined) {
$.support.changeBubbles = true;
var el = document.createElement("div");
eventName = "onchange";
if (el.attachEvent) {
var isSupported = (eventName in el);
if (!isSupported) {
el.setAttribute(eventName, "return;");
isSupported = typeof el[eventName] === "function";
}
$.support.changeBubbles = isSupported;
}
}
/* Return true if node b is the same as, or is a descendant of, node a */
if (document.compareDocumentPosition) {
var is_or_contains = function(a, b) {
return a && b && (a == b || !!(a.compareDocumentPosition(b) & 16));
};
}
else {
var is_or_contains = function(a, b) {
return a && b && (a == b || (a.contains ? a.contains(b) : true));
};
}
/* Add the methods to handle event binding to the Namespace class */
$.entwine.Namespace.addMethods({
build_event_proxy: function(name) {
var one = this.one(name, 'func');
var prxy = function(e, data) {
// For events that do not bubble we manually trigger delegation (see delegate_submit below)
// If this event is a manual trigger, the event we actually want to bubble is attached as a property of the passed event
e = e.delegatedEvent || e;
var el = e.target;
while (el && el.nodeType == 1 && !e.isPropagationStopped()) {
var ret = one(el, arguments);
if (ret !== undefined) e.result = ret;
if (ret === false) { e.preventDefault(); e.stopPropagation(); }
el = el.parentNode;
}
};
return prxy;
},
build_mouseenterleave_proxy: function(name) {
var one = this.one(name, 'func');
var prxy = function(e) {
var el = e.target;
var rel = e.relatedTarget;
while (el && el.nodeType == 1 && !e.isPropagationStopped()) {
/* We know el contained target. If it also contains relatedTarget then we didn't mouseenter / leave. What's more, every ancestor will also
contan el and rel, and so we can just stop bubbling */
if (is_or_contains(el, rel)) break;
var ret = one(el, arguments);
if (ret !== undefined) e.result = ret;
if (ret === false) { e.preventDefault(); e.stopPropagation(); }
el = el.parentNode;
}
};
return prxy;
},
build_change_proxy: function(name) {
var one = this.one(name, 'func');
/*
This change bubble emulation code is taken mostly from jQuery 1.6 - unfortunately we can't easily reuse any of
it without duplication, so we'll have to re-migrate any bugfixes
*/
// Get the value of an item. Isn't supposed to be interpretable, just stable for some value, and different
// once the value changes
var getVal = function( elem ) {
var type = elem.type, val = elem.value;
if (type === "radio" || type === "checkbox") {
val = elem.checked;
}
else if (type === "select-multiple") {
val = "";
if (elem.selectedIndex > -1) {
val = jQuery.map(elem.options, function(elem){ return elem.selected; }).join("-");
}
}
else if (jQuery.nodeName(elem, "select")) {
val = elem.selectedIndex;
}
return val;
};
// Test if a node name is a form input
var rformElems = /^(?:textarea|input|select)$/i;
// Check if this event is a change, and bubble the change event if it is
var testChange = function(e) {
var elem = e.target, data, val;
if (!rformElems.test(elem.nodeName) || elem.readOnly) return;
data = jQuery.data(elem, "_entwine_change_data");
val = getVal(elem);
// the current data will be also retrieved by beforeactivate
if (e.type !== "focusout" || elem.type !== "radio") {
jQuery.data(elem, "_entwine_change_data", val);
}
if (data === undefined || val === data) return;
if (data != null || val) {
e.type = "change";
while (elem && elem.nodeType == 1 && !e.isPropagationStopped()) {
var ret = one(elem, arguments);
if (ret !== undefined) e.result = ret;
if (ret === false) { e.preventDefault(); e.stopPropagation(); }
elem = elem.parentNode;
}
}
};
// The actual proxy - responds to several events, some of which triger a change check, some
// of which just store the value for future change checks
var prxy = function(e) {
var event = e.type, elem = e.target, type = jQuery.nodeName( elem, "input" ) ? elem.type : "";
switch (event) {
case 'focusout':
case 'beforedeactivate':
testChange.apply(this, arguments);
break;
case 'click':
if ( type === "radio" || type === "checkbox" || jQuery.nodeName( elem, "select" ) ) {
testChange.apply(this, arguments);
}
break;
// Change has to be called before submit
// Keydown will be called before keypress, which is used in submit-event delegation
case 'keydown':
if (
(e.keyCode === 13 && !jQuery.nodeName( elem, "textarea" ) ) ||
(e.keyCode === 32 && (type === "checkbox" || type === "radio")) ||
type === "select-multiple"
) {
testChange.apply(this, arguments);
}
break;
// Beforeactivate happens also before the previous element is blurred
// with this event you can't trigger a change event, but you can store
// information
case 'focusin':
case 'beforeactivate':
jQuery.data( elem, "_entwine_change_data", getVal(elem) );
break;
}
}
return prxy;
},
bind_event: function(selector, name, func, event) {
var funcs = this.store[name] || (this.store[name] = $.entwine.RuleList()) ;
var proxies = funcs.proxies || (funcs.proxies = {});
var rule = funcs.addRule(selector, name); rule.func = func;
if (!proxies[name]) {
switch (name) {
case 'onmouseenter':
proxies[name] = this.build_mouseenterleave_proxy(name);
event = 'mouseover';
break;
case 'onmouseleave':
proxies[name] = this.build_mouseenterleave_proxy(name);
event = 'mouseout';
break;
case 'onchange':
if (!$.support.changeBubbles) {
proxies[name] = this.build_change_proxy(name);
event = 'click keydown focusin focusout beforeactivate beforedeactivate';
}
break;
case 'onsubmit':
event = 'delegatedSubmit';
break;
case 'onfocus':
case 'onblur':
$.entwine.warn('Event '+event+' not supported - using focusin / focusout instead', $.entwine.WARN_LEVEL_IMPORTANT);
}
// If none of the special handlers created a proxy, use the generic proxy
if (!proxies[name]) proxies[name] = this.build_event_proxy(name);
$(document).bind(event.replace(/(\s+|$)/g, '.entwine$1'), proxies[name]);
}
}
});
$.entwine.Namespace.addHandler({
order: 40,
bind: function(selector, k, v){
var match, event;
if ($.isFunction(v) && (match = k.match(/^on(.*)/))) {
event = match[1];
this.bind_event(selector, k, v, event);
return true;
}
}
});
// Find all forms and bind onsubmit to trigger on the document too.
// This is the only event that can't be grabbed via delegation
var delegate_submit = function(e, data){
var delegationEvent = $.Event('delegatedSubmit'); delegationEvent.delegatedEvent = e;
return $(document).trigger(delegationEvent, data);
};
$(document).bind('EntwineElementsAdded', function(e){
var forms = $(e.targets).filter('form');
if (!forms.length) return;
forms.bind('submit.entwine_delegate_submit', delegate_submit);
});
})(jQuery);

View File

@ -0,0 +1,240 @@
jQuery(function($){
// Create a new style element
var styleEl = document.createElement('style');
styleEl.setAttribute('type', 'text/css');
(document.head || document.getElementsByTagName('head')[0]).appendChild(styleEl);
var inspectorCSS = [
'#entwine-inspector { position: fixed; z-index: 1000001; left: 0; right: 0; height: 400px; background: white; -webkit-box-shadow: 0 5px 40px 0 black; -moz-box-shadow: 0 5px 40px 0 black; }',
'#entwine-inspector li { list-style: none; margin: 2px 0; padding: 2px 0; }',
'#entwine-inspector li:hover { background: #eee; }',
'#entwine-inspector li.selected { background: #ddd; }',
'#ei-columns { overflow: hidden; display: -webkit-box; display: -moz-box; width: 100%; height: 380px; }',
'.ei-column { height: 380px; width: 1px; -webkit-box-flex: 1; -moz-box-flex: 1; }',
'#entwine-inspector .ei-column h1 { display: block; margin: 0; padding: 5px 2px; height: 20px; text-align: center; background: #444; color: #eee; font-size: 14px; font-weight: bold; }',
'#entwine-inspector .ei-column ul { overflow-y: scroll; height: 350px; }',
'#ei-options { overflow: hidden; height: 20px; background: #444; color: #eee; }',
'#ei-options label { padding-right: 5px; border-right: 1px solid #eee; }',
'.ei-entwined:hover, .ei-selected { background: rgba(128,0,0,0.2); }',
'.ei-hovernode { position: absolute; z-index: 1000000; background: rgba(0,0,0,0.3); border: 1px solid white; outline: 1px solid white; }',
'#ei-selectors li { color: #aaa; display: none; }',
'#ei-selectors li.matching, #entwine-inspector.show-unmatched #ei-selectors li { display: block; }',
'#ei-selectors li.matching { color: black; }'
].join("\n");
// Set the style element to style up the inspector panel
if(styleEl.styleSheet){
styleEl.styleSheet.cssText = inspectorCSS;
}else{
styleEl.appendChild(document.createTextNode(inspectorCSS));
}
var inspectorPanel = $('<div id="entwine-inspector" class="show-unmatched"></div>').appendTo('body');
var columnHolder = $('<div id="ei-columns"></div>').appendTo(inspectorPanel);
var optionsHolder = $('<div id="ei-options"></div>').appendTo(inspectorPanel);
inspectorPanel.css({
top: -400,
visibility: 'hidden'
});
$('body').bind('keypress', function(e){
if (e.ctrlKey && e.which == 96) {
if (inspectorPanel.css('visibility') != 'visible') {
inspectorPanel.css({top: 0, visibility: 'visible'});
$('body').css({marginTop: 400});
initialise();
}
else {
inspectorPanel.css({top: -400, visibility: 'hidden'});
$('body').css({marginTop: 0});
reset();
}
return false;
}
});
var showUnmatching = $('<input id="ei-option-showunmatching" type="checkbox" checked="checked" />').appendTo(optionsHolder);
var showUnmatchingLabel = $('<label>Show selectors that dont match</label>').appendTo(optionsHolder);
showUnmatching.bind('click', function(){
inspectorPanel.toggleClass('show-unmatched', $(this).val());
});
var hovernode;
var reset = function() {
$('.ei-entwined').unbind('.entwine-inspector').removeClass('ei-entwined');
if (hovernode) hovernode.remove();
}
var initialise = function(){
reset();
$.each($.entwine.namespaces, function(name, namespace){
$.each(namespace.store, function(name, list){
$.each(list, function(i, rule){
var match = $(rule.selector.selector);
match.addClass('ei-entwined').bind('click.entwine-inspector', displaydetails);
})
});
});
};
var dumpElement = function(el) {
var frag = document.createDocumentFragment();
var div = document.createElement('div'); frag.appendChild(div);
var clone = el.cloneNode(false); $(clone).removeClass('ei-entwined').removeAttr('style');
var i = clone.attributes.length;
while (i--) {
var attr = clone.attributes.item(i);
if (attr.name != 'class' && attr.name != 'id' && attr.value.length > 20) attr.value = attr.value.substr(0, 18)+'..'+attr.value.substr(-2);
}
div.appendChild(clone);
return div.innerHTML;
};
var displaydetails = function(e){
e.preventDefault(); e.stopPropagation();
columnHolder.empty();
var columns = {};
$.each(['elements', 'namespaces', 'methods', 'selectors'], function(i, col){
columns[col] = $('<div id="ei-'+col+'" class="ei-column"><h1>'+col+'</h1></div>').appendTo(columnHolder);
})
var lists = {};
var ctr = 0;
lists.elements = $('<ul></ul>').appendTo(columns.elements);
var displayelement = function(){
var target = $(this);
var li = $('<li></li>');
li.text(dumpElement(this)).attr('data-id', ++ctr).data('el', target).prependTo(lists.elements);
var namespaces = $('<ul data-element="'+ctr+'"></ul>').appendTo(columns.namespaces);
$.each($.entwine.namespaces, function(name, namespace){
var methods = $('<ul data-namespace="'+ctr+'-'+name+'"></ul>');
$.each(namespace.store, function(method, list){
if (method == 'ctors') {
var matchselectors = $('<ul data-method="'+ctr+'-'+name+'-onmatch"></ul>');
var unmatchselectors = $('<ul data-method="'+ctr+'-'+name+'-onunmatch"></ul>');
$.each(list, function(i, rule){
var matchitem = $('<li>'+rule.selector.selector+'</li>').prependTo(matchselectors);
var unmatchitem = rule.onunmatch ? $('<li>'+rule.selector.selector+'</li>').prependTo(unmatchselectors) : null;
if (target.is(rule.selector.selector)) {
matchitem.addClass('matching'); unmatchitem && unmatchitem.addClass('matching');
if (!methods.parent().length) {
$('<li data-namespace="'+ctr+'-'+name+'">'+name+'</li>').prependTo(namespaces);
methods.appendTo(columns.methods);
}
if (!matchselectors.parent().length) {
$('<li data-method="'+ctr+'-'+name+'-onmatch">onmatch</li>').prependTo(methods);
matchselectors.appendTo(columns.selectors);
}
if (rule.onunmatch && !unmatchselectors.parent().length) {
$('<li data-method="'+ctr+'-'+name+'-onunmatch">onunmatch</li>').prependTo(methods);
unmatchselectors.appendTo(columns.selectors);
}
}
});
}
else {
var selectors = $('<ul data-method="'+ctr+'-'+name+'-'+method+'"></ul>');
$.each(list, function(i, rule){
var ruleitem = $('<li>'+rule.selector.selector+'</li>').prependTo(selectors);
if (target.is(rule.selector.selector)){
ruleitem.addClass('matching');
if (!methods.parent().length) {
$('<li data-namespace="'+ctr+'-'+name+'">'+name+'</li>').prependTo(namespaces);
methods.appendTo(columns.methods);
}
if (!selectors.parent().length) {
$('<li data-method="'+ctr+'-'+name+'-'+method+'">'+method+'</li>').prependTo(methods);
selectors.appendTo(columns.selectors);
}
}
})
}
});
});
};
$.each($(e.target).parents().andSelf().filter('.ei-entwined'), displayelement);
$('#ei-elements > ul:first > li:first').click();
}
var activatelist = function(list) {
list = $(list);
list.siblings('ul').css('display', 'none');
list.css('display', 'block');
list.children().first().click();
}
$('#entwine-inspector').live('mouseleave', function(){
if (hovernode) hovernode.hide();
})
$('#entwine-inspector').live('mouseenter', function(){
if (hovernode) hovernode.show();
})
$('#ei-elements > ul > li').live('click', function(e){
var target = $(e.target), id = target.attr('data-id');
target.addClass('selected').siblings().removeClass('selected');
if (!hovernode) {
hovernode = $('<div class="ei-hovernode"></div>').appendTo('body');
}
var hover = target.data('el');
hovernode.css({width: hover.outerWidth()-2, height: hover.outerHeight()-2, top: hover.offset().top, left: hover.offset().left});
$('.ei-selected').removeClass('ei-selected');
activatelist('#ei-namespaces ul[data-element="'+id+'"]');
});
$('#ei-namespaces > ul > li').live('click', function(e){
var target = $(e.target), namespace = target.attr('data-namespace');
target.addClass('selected').siblings().removeClass('selected');
activatelist('#ei-methods ul[data-namespace="'+namespace+'"]');
});
$('#ei-methods > ul > li').live('click', function(e){
var target = $(e.target), method = target.attr('data-method');
target.addClass('selected').siblings().removeClass('selected');
activatelist('#ei-selectors ul[data-method="'+method+'"]');
});
});

View File

@ -0,0 +1,354 @@
try {
console.log;
}
catch (e) {
window.console = undefined;
}
(function($) {
/* Create a subclass of the jQuery object. This was introduced in jQuery 1.5, but removed again in 1.9 */
var sub = function() {
function jQuerySub( selector, context ) {
return new jQuerySub.fn.init( selector, context );
}
jQuery.extend( true, jQuerySub, $ );
jQuerySub.superclass = $;
jQuerySub.fn = jQuerySub.prototype = $();
jQuerySub.fn.constructor = jQuerySub;
jQuerySub.fn.init = function init( selector, context ) {
if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) {
context = jQuerySub( context );
}
return jQuery.fn.init.call( this, selector, context, rootjQuerySub );
};
jQuerySub.fn.init.prototype = jQuerySub.fn;
var rootjQuerySub = jQuerySub(document);
return jQuerySub;
};
var namespaces = {};
$.entwine = function() {
$.fn.entwine.apply(null, arguments);
};
/**
* A couple of utility functions for accessing the store outside of this closure, and for making things
* operate in a little more easy-to-test manner
*/
$.extend($.entwine, {
/**
* Get all the namespaces. Useful for introspection? Internal interface of Namespace not guaranteed consistant
*/
namespaces: namespaces,
/**
* Remove all entwine rules
*/
clear_all_rules: function() {
// Remove proxy functions
for (var k in $.fn) { if ($.fn[k].isentwinemethod) delete $.fn[k]; }
// Remove bound events - TODO: Make this pluggable, so this code can be moved to jquery.entwine.events.js
$(document).unbind('.entwine');
$(window).unbind('.entwine');
// Remove namespaces, and start over again
for (var k in namespaces) delete namespaces[k];
for (var k in $.entwine.capture_bindings) delete $.entwine.capture_bindings[k];
},
WARN_LEVEL_NONE: 0,
WARN_LEVEL_IMPORTANT: 1,
WARN_LEVEL_BESTPRACTISE: 2,
/**
* Warning level. Set to a higher level to get warnings dumped to console.
*/
warningLevel: 0,
/** Utility to optionally display warning messages depending on level */
warn: function(message, level) {
if (level <= $.entwine.warningLevel && console && console.warn) {
console.warn(message);
if (console.trace) console.trace();
}
},
warn_exception: function(where, /* optional: */ on, e) {
if ($.entwine.WARN_LEVEL_IMPORTANT <= $.entwine.warningLevel && console && console.warn) {
if (arguments.length == 2) { e = on; on = null; }
if (on) console.warn('Uncaught exception',e,'in',where,'on',on);
else console.warn('Uncaught exception',e,'in',where);
if (e.stack) console.warn("Stack Trace:\n" + e.stack);
}
}
});
/** Stores a count of definitions, so that we can sort identical selectors by definition order */
var rulecount = 0;
var Rule = Base.extend({
init: function(selector, name) {
this.selector = selector;
this.specifity = selector.specifity();
this.important = 0;
this.name = name;
this.rulecount = rulecount++;
}
});
Rule.compare = function(a, b) {
var as = a.specifity, bs = b.specifity;
return (a.important - b.important) ||
(as[0] - bs[0]) ||
(as[1] - bs[1]) ||
(as[2] - bs[2]) ||
(a.rulecount - b.rulecount) ;
};
$.entwine.RuleList = function() {
var list = [];
list.addRule = function(selector, name){
var rule = Rule(selector, name);
list[list.length] = rule;
list.sort(Rule.compare);
return rule;
};
return list;
};
var handlers = [];
/**
* A Namespace holds all the information needed for adding entwine methods to a namespace (including the _null_ namespace)
*/
$.entwine.Namespace = Base.extend({
init: function(name){
if (name && !name.match(/^[A-Za-z0-9.]+$/)) $.entwine.warn('Entwine namespace '+name+' is not formatted as period seperated identifiers', $.entwine.WARN_LEVEL_BESTPRACTISE);
name = name || '__base';
this.name = name;
this.store = {};
namespaces[name] = this;
if (name == "__base") {
this.injectee = $.fn;
this.$ = $;
}
else {
// We're in a namespace, so we build a Class that subclasses the jQuery Object Class to inject namespace functions into
this.$ = $.sub ? $.sub() : sub();
// Work around bug in sub() - subclass must share cache with root or data won't get cleared by cleanData
this.$.cache = $.cache;
this.injectee = this.$.prototype;
// We override entwine to inject the name of this namespace when defining blocks inside this namespace
var entwine_wrapper = this.injectee.entwine = function(spacename) {
var args = arguments;
if (!spacename || typeof spacename != 'string') { args = $.makeArray(args); args.unshift(name); }
else if (spacename.charAt(0) != '.') args[0] = name+'.'+spacename;
return $.fn.entwine.apply(this, args);
};
this.$.entwine = function() {
entwine_wrapper.apply(null, arguments);
};
for (var i = 0; i < handlers.length; i++) {
var handler = handlers[i], builder;
// Inject jQuery object method overrides
if (builder = handler.namespaceMethodOverrides) {
var overrides = builder(this);
for (var k in overrides) this.injectee[k] = overrides[k];
}
// Inject $.entwine function overrides
if (builder = handler.namespaceStaticOverrides) {
var overrides = builder(this);
for (var k in overrides) this.$.entwine[k] = overrides[k];
}
}
}
},
/**
* Returns a function that does selector matching against the function list for a function name
* Used by proxy for all calls, and by ctorProxy to handle _super calls
* @param {String} name - name of the function as passed in the construction object
* @param {String} funcprop - the property on the Rule object that gives the actual function to call
* @param {function} basefunc - the non-entwine function to use as the catch-all function at the bottom of the stack
*/
one: function(name, funcprop, basefunc) {
var namespace = this;
var funcs = this.store[name];
var one = function(el, args, i){
if (i === undefined) i = funcs.length;
while (i--) {
if (funcs[i].selector.matches(el)) {
var ret, tmp_i = el.i, tmp_f = el.f;
el.i = i; el.f = one;
try { ret = funcs[i][funcprop].apply(namespace.$(el), args); }
finally { el.i = tmp_i; el.f = tmp_f; }
return ret;
}
}
// If we didn't find a entwine-defined function, but there is a non-entwine function to use as a base, try that
if (basefunc) return basefunc.apply(namespace.$(el), args);
};
return one;
},
/**
* A proxy is a function attached to a callable object (either the base jQuery.fn or a subspace object) which handles
* finding and calling the correct function for each member of the current jQuery context
* @param {String} name - name of the function as passed in the construction object
* @param {function} basefunc - the non-entwine function to use as the catch-all function at the bottom of the stack
*/
build_proxy: function(name, basefunc) {
var one = this.one(name, 'func', basefunc);
var prxy = function() {
var rv, ctx = $(this);
var i = ctx.length;
while (i--) rv = one(ctx[i], arguments);
return rv;
};
return prxy;
},
bind_proxy: function(selector, name, func) {
var rulelist = this.store[name] || (this.store[name] = $.entwine.RuleList());
var rule = rulelist.addRule(selector, name); rule.func = func;
if (!this.injectee.hasOwnProperty(name) || !this.injectee[name].isentwinemethod) {
this.injectee[name] = this.build_proxy(name, this.injectee.hasOwnProperty(name) ? this.injectee[name] : null);
this.injectee[name].isentwinemethod = true;
}
if (!this.injectee[name].isentwinemethod) {
$.entwine.warn('Warning: Entwine function '+name+' clashes with regular jQuery function - entwine function will not be callable directly on jQuery object', $.entwine.WARN_LEVEL_IMPORTANT);
}
},
add: function(selector, data) {
// For every item in the hash, try ever method handler, until one returns true
for (var k in data) {
var v = data[k];
for (var i = 0; i < handlers.length; i++) {
if (handlers[i].bind && handlers[i].bind.call(this, selector, k, v)) break;
}
}
},
has: function(ctx, name) {
var rulelist = this.store[name];
if (!rulelist) return false;
/* We go forward this time, since low specifity is likely to knock out a bunch of elements quickly */
for (var i = 0 ; i < rulelist.length; i++) {
ctx = ctx.not(rulelist[i].selector);
if (!ctx.length) return true;
}
return false;
}
});
/**
* A handler is some javascript code that adds support for some time of key / value pair passed in the hash to the Namespace add method.
* The default handlers provided (and included by default) are event, ctor and properties
*/
$.entwine.Namespace.addHandler = function(handler) {
for (var i = 0; i < handlers.length && handlers[i].order < handler.order; i++) { /* Pass */ }
handlers.splice(i, 0, handler);
};
$.entwine.Namespace.addHandler({
order: 50,
bind: function(selector, k, v){
if ($.isFunction(v)) {
this.bind_proxy(selector, k, v);
return true;
}
}
});
$.extend($.fn, {
/**
* Main entwine function. Used for new definitions, calling into a namespace (or forcing the base namespace) and entering a using block
*
*/
entwine: function(spacename) {
var i = 0;
/* Don't actually work out selector until we try and define something on it - we might be opening a namespace on an function-traveresed object
which have non-standard selectors like .parents(.foo).slice(0,1) */
var selector = null;
/* By default we operator on the base namespace */
var namespace = namespaces.__base || $.entwine.Namespace();
/* If the first argument is a string, then it's the name of a namespace. Look it up */
if (typeof spacename == 'string') {
if (spacename.charAt('0') == '.') spacename = spacename.substr(1);
if (spacename) namespace = namespaces[spacename] || $.entwine.Namespace(spacename);
i=1;
}
/* All remaining arguments should either be using blocks or definition hashs */
while (i < arguments.length) {
var res = arguments[i++];
// If it's a function, call it - either it's a using block or it's a namespaced entwine definition
if ($.isFunction(res)) {
if (res.length != 1) $.entwine.warn('Function block inside entwine definition does not take $ argument properly', $.entwine.WARN_LEVEL_IMPORTANT);
res = res.call(namespace.$(this), namespace.$);
}
// If we have a entwine definition hash, inject it into namespace
if (res) {
if (selector === null) selector = this.selector ? $.selector(this.selector) : false;
if (selector) namespace.add(selector, res);
else $.entwine.warn('Entwine block given to entwine call without selector. Make sure you call $(selector).entwine when defining blocks', $.entwine.WARN_LEVEL_IMPORTANT);
}
}
/* Finally, return the jQuery object 'this' refers to, wrapped in the new namespace */
return namespace.$(this);
},
/**
* Calls the next most specific version of the current entwine method
*/
_super: function(){
var rv, i = this.length;
while (i--) {
var el = this[0];
rv = el.f(el, arguments, el.i);
}
return rv;
}
});
})(jQuery);

View File

@ -0,0 +1,21 @@
(function($) {
// Adds back concrete methods for backwards compatibility
$.concrete = $.entwine;
$.fn.concrete = $.fn.entwine;
$.fn.concreteData = $.fn.entwineData;
// Use addHandler to hack in the namespace.$.concrete equivilent to the namespace.$.entwine namespace-injection
$.entwine.Namespace.addHandler({
order: 100,
bind: function(selector, k, v) { return false; },
namespaceMethodOverrides: function(namespace){
namespace.$.concrete = namespace.$.entwine;
namespace.injectee.concrete = namespace.injectee.entwine;
namespace.injectee.concreteData = namespace.injectee.entwineData;
return {};
}
});
})(jQuery);

View File

@ -0,0 +1,85 @@
(function($) {
var entwine_prepend = '__entwine!';
var getEntwineData = function(el, namespace, property) {
return el.data(entwine_prepend + namespace + '!' + property);
};
var setEntwineData = function(el, namespace, property, value) {
return el.data(entwine_prepend + namespace + '!' + property, value);
};
var getEntwineDataAsHash = function(el, namespace) {
var hash = {};
var id = jQuery.data(el[0]);
var matchstr = entwine_prepend + namespace + '!';
var matchlen = matchstr.length;
var cache = jQuery.cache[id];
for (var k in cache) {
if (k.substr(0,matchlen) == matchstr) hash[k.substr(matchlen)] = cache[k];
}
return hash;
};
var setEntwineDataFromHash = function(el, namespace, hash) {
for (var k in hash) setEntwineData(namespace, k, hash[k]);
};
var entwineData = function(el, namespace, args) {
switch (args.length) {
case 0:
return getEntwineDataAsHash(el, namespace);
case 1:
if (typeof args[0] == 'string') return getEntwineData(el, namespace, args[0]);
else return setEntwineDataFromHash(el, namespace, args[0]);
default:
return setEntwineData(el, namespace, args[0], args[1]);
}
};
$.extend($.fn, {
entwineData: function() {
return entwineData(this, '__base', arguments);
}
});
$.entwine.Namespace.addHandler({
order: 60,
bind: function(selector, k, v) {
if (k.charAt(0) != k.charAt(0).toUpperCase()) $.entwine.warn('Entwine property '+k+' does not start with a capital letter', $.entwine.WARN_LEVEL_BESTPRACTISE);
// Create the getters and setters
var getterName = 'get'+k;
var setterName = 'set'+k;
this.bind_proxy(selector, getterName, function() { var r = this.entwineData(k); return r === undefined ? v : r; });
this.bind_proxy(selector, setterName, function(v){ return this.entwineData(k, v); });
// Get the get and set proxies we just created
var getter = this.injectee[getterName];
var setter = this.injectee[setterName];
// And bind in the jQuery-style accessor
this.bind_proxy(selector, k, function(v){ return (arguments.length == 1 ? setter : getter).call(this, v) ; });
return true;
},
namespaceMethodOverrides: function(namespace){
return {
entwineData: function() {
return entwineData(this, namespace.name, arguments);
}
};
}
});
})(jQuery);

View File

@ -0,0 +1,52 @@
(function($){
/**
* Add focusin and focusout support to bind and live for browers other than IE. Designed to be usable in a delegated fashion (like $.live)
* Copyright (c) 2007 Jörn Zaefferer
*/
if ($.support.focusinBubbles === undefined) {
$.support.focusinBubbles = !!($.browser.msie);
}
if (!$.support.focusinBubbles && !$.event.special.focusin) {
// Emulate focusin and focusout by binding focus and blur in capturing mode
$.each({focus: 'focusin', blur: 'focusout'}, function(original, fix){
$.event.special[fix] = {
setup: function(){
if (!this.addEventListener) return false;
this.addEventListener(original, $.event.special[fix].handler, true);
},
teardown: function(){
if (!this.removeEventListener) return false;
this.removeEventListener(original, $.event.special[fix].handler, true);
},
handler: function(e){
arguments[0] = $.event.fix(e);
arguments[0].type = fix;
return $.event.handle.apply(this, arguments);
}
};
});
}
(function(){
//IE has some trouble with focusout with select and keyboard navigation
var activeFocus = null;
$(document)
.bind('focusin', function(e){
var target = e.realTarget || e.target;
if (activeFocus && activeFocus !== target) {
e.type = 'focusout';
$(activeFocus).trigger(e);
e.type = 'focusin';
e.target = target;
}
activeFocus = target;
})
.bind('focusout', function(e){
activeFocus = null;
});
})();
})(jQuery);

View File

@ -0,0 +1,58 @@
(function($) {
// TODO:
// Make attributes & IDs work
var DIRECT = /DIRECT/g;
var CONTEXT = /CONTEXT/g;
var EITHER = /DIRECT|CONTEXT/g;
$.selector.SelectorBase.addMethod('affectedBy', function(props) {
this.affectedBy = new Function('props', ([
'var direct_classes, context_classes, direct_attrs, context_attrs, t;',
this.ABC_compile().replace(DIRECT, 'direct').replace(CONTEXT, 'context'),
'return {classes: {context: context_classes, direct: direct_classes}, attrs: {context: context_attrs, direct: direct_attrs}};'
]).join("\n"));
// DEBUG: Print out the compiled funciton
// console.log(this.selector, ''+this.affectedBy);
return this.affectedBy(props);
});
$.selector.SimpleSelector.addMethod('ABC_compile', function() {
var parts = [];
$.each(this.classes, function(i, cls){
parts[parts.length] = "if (t = props.classes['"+cls+"']) (DIRECT_classes || (DIRECT_classes = {}))['"+cls+"'] = t;";
});
$.each(this.nots, function(i, not){
parts[parts.length] = not.ABC_compile();
});
return parts.join("\n");
});
$.selector.Selector.addMethod('ABC_compile', function(arg){
var parts = [];
var i = this.parts.length-1;
parts[parts.length] = this.parts[i].ABC_compile();
while ((i = i - 2) >= 0) parts[parts.length] = this.parts[i].ABC_compile().replace(EITHER, 'CONTEXT');
return parts.join("\n");
});
$.selector.SelectorsGroup.addMethod('ABC_compile', function(){
var parts = [];
$.each(this.parts, function(i,part){
parts[parts.length] = part.ABC_compile();
});
return parts.join("\n");
});
})(jQuery);

View File

@ -0,0 +1 @@
(function(){function e(e,t){var n=e.dom;var r=n.getAttrib(t,"mce_advimageresize_id");if(!e.originalDimensions[r]){e.originalDimensions[r]=e.lastDimensions[r]={width:n.getAttrib(t,"width",t.width),height:n.getAttrib(t,"height",t.height)}}return true}function t(t,n){var r=t.dom;var i=r.getAttrib(n,"mce_advimageresize_id");if(!i){var i=t.id+"_"+t.dom.uniqueId();r.setAttrib(n,"mce_advimageresize_id",i);e(t,n)}return i}function n(n,o,u){var l=n.dom;var c=t(n,o);var h=l.getAttrib(o,"width")!=n.lastDimensions[c].width||l.getAttrib(o,"height")!=n.lastDimensions[c].height;if(!h)return;if(l.getAttrib(o,"mce_noresize")||l.hasClass(o,n.getParam("advimagescale_noresize_class","noresize"))||n.getParam("advimagescale_noresize_all")){l.setAttrib(o,"width",n.lastDimensions[c].width);l.setAttrib(o,"height",n.lastDimensions[c].height);if(tinymce.isGecko)i(n);return}if(n.getParam("advimagescale_fix_border_glitch",true)){r(n,o);e(n,o)}var p=n.getParam("advimagescale_filter_src");if(p){var d=new RegExp(p);if(!o.src.match(d)){return}}var v=n.getParam("advimagescale_filter_class");if(v){if(!l.hasClass(o,v)){return}}var m={width:l.getAttrib(o,"width",o.width),height:l.getAttrib(o,"height",o.height)};if(n.getParam("advimagescale_maintain_aspect_ratio",true)){m=a(n,o,m.width,m.height)}m=f(n,o,m.width,m.height);var g=l.getAttrib(o,"width",o.width)!=m.width||l.getAttrib(o,"height",o.height)!=m.height;if(g){l.setAttrib(o,"width",m.width);l.setAttrib(o,"height",m.height);if(tinymce.isGecko)i(n)}if(n.getParam("advimagescale_append_to_url")){s(n,o,l.getAttrib(o,"width",o.width),l.getAttrib(o,"height",o.height))}if(n.lastDimensions[c].width!=l.getAttrib(o,"width",o.width)||n.lastDimensions[c].height!=l.getAttrib(o,"height",o.height)){if(n.getParam("advimagescale_resize_callback")){n.getParam("advimagescale_resize_callback")(n,o)}}n.lastDimensions[c]={width:l.getAttrib(o,"width",o.width),height:l.getAttrib(o,"height",o.height)}}function r(e,t){var n=e.dom;var r=n.getAttrib(t,"mce_advimageresize_id");var s=n.getAttrib(t,"width",t.width);var o=n.getAttrib(t,"height",t.height);var u=false;if(s!=e.lastDimensions[r].width){var a=0;a+=parseInt(n.getStyle(t,"borderLeftWidth","borderLeftWidth"));a+=parseInt(n.getStyle(t,"borderRightWidth","borderRightWidth"));if(a>0){n.setAttrib(t,"width",s-a);u=true}}if(o!=e.lastDimensions[r].height){var f=0;f+=parseInt(n.getStyle(t,"borderTopWidth","borderTopWidth"));f+=parseInt(n.getStyle(t,"borderBottomWidth","borderBottomWidth"));if(f>0){n.setAttrib(t,"height",o-f);u=true}}if(u&&tinymce.isGecko)i(e)}function i(e){e.execCommand("mceRepaint",false)}function s(e,t,n,r){var i=e.dom;var s=i.getAttrib(t,"src");var a=e.getParam("advimagescale_url_width_key","w");s=u(s,a,n);var f=e.getParam("advimagescale_url_height_key","h");s=u(s,f,r);if(s==i.getAttrib(t,"src")){return}if(e.getParam("advimagescale_loading_callback")){e.getParam("advimagescale_loading_callback")(t)}if(e.getParam("advimagescale_loaded_callback")){tinymce.dom.Event.add(t,"load",o,{el:t,ed:e})}i.setAttrib(t,"src",s)}function o(e){var t=this.el;var n=this.ed;var r=n.getParam("advimagescale_loaded_callback");r(t);tinymce.dom.Event.remove(t,"load",o)}function u(e,t,n){if(!e.match(/\?/))e+="?";if(!e.match(new RegExp("([?&])"+t+"="))){if(!e.match(/[&\?]$/))e+="&";e+=t+"="+escape(n)}else{e=e.replace(new RegExp("([?&])"+t+"=[^&]*"),"$1"+t+"="+escape(n))}return e}function a(e,t,n,r){var i=e.dom.getAttrib(t,"mce_advimageresize_id");var s=e.originalDimensions[i].width/e.originalDimensions[i].height;var o=e.lastDimensions[i].width;var u=e.lastDimensions[i].height;var a=Math.abs(o-n);var f=Math.abs(u-r);var l=Math.abs(a/o);var c=Math.abs(f/u);if(a||f){if(l>c){return{width:n,height:Math.round(n/s)}}else{return{width:Math.round(r*s),height:r}}}return{width:n,height:r}}function f(e,t,n,r){var i=e.dom.getAttrib(t,"mce_advimageresize_id");var s=e.getParam("advimagescale_max_width");var o=e.getParam("advimagescale_max_height");var u=e.getParam("advimagescale_min_width");var a=e.getParam("advimagescale_min_height");var f=e.getParam("advimagescale_maintain_aspect_ratio",true);var l=e.originalDimensions[i].width;var c=e.originalDimensions[i].height;var h=l/c;if(s&&n>s){n=s;r=f?Math.round(n/h):r}if(o&&r>o){r=o;n=f?Math.round(r*h):n}if(u&&n<u){n=u;r=f?Math.round(n/h):r}if(a&&r<a){r=a;n=f?Math.round(r*h):r}return{width:n,height:r}}tinymce.create("tinymce.plugins.AdvImageScale",{init:function(e,r){e.originalDimensions=new Array;e.lastDimensions=new Array;e.edMouseDown=false;e.onMouseDown.add(function(e,n){var r=tinyMCE.activeEditor.selection.getNode();if(r!=null&&r.nodeName=="IMG"){t(e,n.target)}return true});e.onMouseUp.add(function(e,t){var r=tinyMCE.activeEditor.selection.getNode();if(r!=null&&r.nodeName=="IMG"){setTimeout(function(){n(e,r)},100)}return true});e.onPreProcess.add(function(e,t){if(!t.set)return;tinymce.each(e.dom.select("img",t.node),function(t){n(e,t)})});e.onInit.add(function(e){e.selection.onSetContent.add(function(t,r){var i=t.getNode();tinymce.each(e.dom.select("img",i),function(t){if(t.id!="__mce_tmp")n(e,t)})})});if(e.getParam("advimagescale_reject_external_dragdrop",true)){e.onMouseDown.add(function(t){e.edMouseDown=true});e.onMouseUp.add(function(t){e.edMouseDown=false});e.onInit.add(function(e,t){tinymce.dom.Event.add(e.getBody().parentNode,"dragdrop",function(t){e.edMouseDown=false})});var i=tinymce.isIE?"dragenter":"dragover";e.onInit.add(function(e,t){tinymce.dom.Event.add(e.getBody().parentNode,i,function(t){if(!e.edMouseDown){return tinymce.dom.Event.cancel(t)}})})}},getInfo:function(){return{longname:"Advanced Image Resize Helper",author:"Marc Hodgins",authorurl:"http://www.hodginsmedia.com",infourl:"http://code.google.com/p/tinymce-plugin-advimagescale",version:"1.1.3"}}});tinymce.PluginManager.add("advimagescale",tinymce.plugins.AdvImageScale)})();

View File

@ -0,0 +1,454 @@
/**
* TinyMCE Advanced Image Resize Helper Plugin
*
* Forces images to maintain aspect ratio while scaling - also optionally enforces
* min/max image dimensions, and appends width/height to the image URL for server-side
* resizing
*
* @author Marc Hodgins (modified to remove global variables: http://sourceforge.net/p/tinymce/plugins/186/)
* @link http://www.hodginsmedia.com Hodgins Media Ventures Inc.
* @copyright Copyright (C) 2008-2010 Hodgins Media Ventures Inc., All right reserved.
* @license http://www.opensource.org/licenses/lgpl-3.0.html LGPLv3
*/
(function() {
tinymce.create('tinymce.plugins.AdvImageScale', {
/**
* Initializes the plugin, this will be executed after the plugin has been created.
*
* @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.
* @param {string} url Absolute URL to where the plugin is located.
*/
init : function(ed, url) {
/**
* Stores pre-resize image dimensions
* @var {array} (w,h)
*/
ed.originalDimensions = new Array();
/**
* Stores last dimensions before a resize
* @var {array} (w,h)
*/
ed.lastDimensions = new Array();
/**
* Track mousedown status in editor
* @var {boolean}
*/
ed.edMouseDown = false;
// Watch for mousedown (as a fall through to ensure that prepareImage() definitely
// got called on an image tag before mouseup).
//
// Normally this should have happened via the onPreProcess/onSetContent listeners, but
// for completeness we check once more here in case there are edge cases we've missed.
ed.onMouseDown.add(function(ed, e) {
var el = tinyMCE.activeEditor.selection.getNode();
if (el != null && el.nodeName == 'IMG') {
// prepare image for resizing
prepareImage(ed, e.target);
}
return true;
});
// Watch for mouseup (catch image resizes)
ed.onMouseUp.add(function(ed, e) {
var el = tinyMCE.activeEditor.selection.getNode();
if (el != null && el.nodeName == 'IMG') {
// setTimeout is necessary to allow the browser to complete the resize so we have new dimensions
setTimeout(function() {
constrainSize(ed, el);
}, 100);
}
return true;
});
/*****************************************************
* ENFORCE CONSTRAINTS ON CONTENT INSERTED INTO EDITOR
*****************************************************/
// Catch editor.setContent() events via onPreProcess (because onPreProcess allows us to
// modify the DOM before it is inserted, unlike onSetContent)
ed.onPreProcess.add(function(ed, o) {
if (!o.set) return; // only 'set' operations let us modify the nodes
// loop in each img node and run constrainSize
tinymce.each(ed.dom.select('img', o.node), function(currentNode) {
constrainSize(ed, currentNode);
});
});
// To be complete, we also need to watch for setContent() calls on the selection object so that
// constraints are enforced (i.e. in case an <img> tag is inserted via mceInsertContent).
// So, catch all insertions using the editor's selection object
ed.onInit.add(function(ed) {
// http://wiki.moxiecode.com/index.php/TinyMCE:API/tinymce.dom.Selection/onSetContent
ed.selection.onSetContent.add(function(se, o) {
// @todo This seems to grab the entire editor contents - it works but could
// perform poorly on large documents
var currentNode = se.getNode();
tinymce.each(ed.dom.select('img', currentNode), function (currentNode) {
// IF condition required as tinyMCE inserts 24x24 placeholders uner some conditions
if (currentNode.id != "__mce_tmp")
constrainSize(ed, currentNode);
});
});
});
/*****************************
* DISALLOW EXTERNAL IMAGE DRAG/DROPS
*****************************/
// This is a hack. Listening for drag events wasn't working.
//
// Watches for mousedown and mouseup/dragdrop events within the editor. If a mouseup or
// dragdrop occurs in the editor without a preceeding mousedown, we assume it is an external
// dragdrop that should be rejected.
if (ed.getParam('advimagescale_reject_external_dragdrop', true)) {
// catch mousedowns mouseups and dragdrops (which are basically mouseups too..)
ed.onMouseDown.add(function(e) { ed.edMouseDown = true; });
ed.onMouseUp.add(function(e) { ed.edMouseDown = false; });
ed.onInit.add(function(ed, o) {
tinymce.dom.Event.add(ed.getBody().parentNode, 'dragdrop', function(e) { ed.edMouseDown = false; });
});
// watch for drag attempts
var evt = (tinymce.isIE) ? 'dragenter' : 'dragover'; // IE allows dragdrop reject on dragenter (more efficient)
ed.onInit.add(function(ed, o) {
// use parentNode to go above editor content, to cover entire editor area
tinymce.dom.Event.add(ed.getBody().parentNode, evt, function (e) {
if (!ed.edMouseDown) {
// disallow drop
return tinymce.dom.Event.cancel(e);
}
});
});
}
},
/**
* Returns information about the plugin as a name/value array.
* The current keys are longname, author, authorurl, infourl and version.
*
* @return {Object} Name/value array containing information about the plugin.
*/
getInfo : function() {
return {
longname : 'Advanced Image Resize Helper',
author : 'Marc Hodgins',
authorurl : 'http://www.hodginsmedia.com',
infourl : 'http://code.google.com/p/tinymce-plugin-advimagescale',
version : '1.1.3'
};
}
});
// Register plugin
tinymce.PluginManager.add('advimagescale', tinymce.plugins.AdvImageScale);
/**
* Store image dimensions, pre-resize
*
* @param {object} el HTMLDomNode
*/
function storeDimensions(ed, el) {
var dom = ed.dom;
var elId = dom.getAttrib(el, 'mce_advimageresize_id');
// store original dimensions if this is the first resize of this element
if (!ed.originalDimensions[elId]) {
ed.originalDimensions[elId] = ed.lastDimensions[elId] = {width: dom.getAttrib(el, 'width', el.width), height: dom.getAttrib(el, 'height', el.height)};
}
return true;
}
/**
* Prepare image for resizing
* Check to see if we've seen this IMG tag before; does tasks such as adding
* unique IDs to image tags, saving "original" image dimensions, etc.
* @param {object} e is optional
*/
function prepareImage(ed, el) {
var dom = ed.dom;
var elId = dom.getAttrib(el, 'mce_advimageresize_id');
// is this the first time this image tag has been seen?
if (!elId) {
var elId = ed.id + "_" + ed.dom.uniqueId();
dom.setAttrib(el, 'mce_advimageresize_id', elId);
storeDimensions(ed, el);
}
return elId;
}
/**
* Adjusts width and height to keep within min/max bounds and also maintain aspect ratio
* If mce_noresize attribute is set to image tag, then image resize is disallowed
*/
function constrainSize(ed, el, e) {
var dom = ed.dom;
var elId = prepareImage(ed, el); // also calls storeDimensions
var resized = (dom.getAttrib(el, 'width') != ed.lastDimensions[elId].width || dom.getAttrib(el, 'height') != ed.lastDimensions[elId].height);
if (!resized)
return; // nothing to do
// disallow image resize if mce_noresize or the noresize class is set on the image tag
if (dom.getAttrib(el, 'mce_noresize') || dom.hasClass(el, ed.getParam('advimagescale_noresize_class', 'noresize')) || ed.getParam('advimagescale_noresize_all')) {
dom.setAttrib(el, 'width', ed.lastDimensions[elId].width);
dom.setAttrib(el, 'height', ed.lastDimensions[elId].height);
if (tinymce.isGecko)
fixGeckoHandles(ed);
return;
}
// Both IE7 and Gecko (as of FF3.0.03) has a "expands image by border width" bug before doing anything else
if (ed.getParam('advimagescale_fix_border_glitch', true /* default to true */)) {
fixImageBorderGlitch(ed, el);
storeDimensions(ed, el); // store adjusted dimensions
}
// filter by regexp so only some images get constrained
var src_filter = ed.getParam('advimagescale_filter_src');
if (src_filter) {
var r = new RegExp(src_filter);
if (!el.src.match(r)) {
return; // skip this element
}
}
// allow filtering by classname
var class_filter = ed.getParam('advimagescale_filter_class');
if (class_filter) {
if (!dom.hasClass(el, class_filter)) {
return; // skip this element, doesn't have the class we want
}
}
// populate new dimensions object
var newDimensions = { width: dom.getAttrib(el, 'width', el.width), height: dom.getAttrib(el, 'height', el.height) };
// adjust w/h to maintain aspect ratio
if (ed.getParam('advimagescale_maintain_aspect_ratio', true /* default to true */)) {
newDimensions = maintainAspect(ed, el, newDimensions.width, newDimensions.height);
}
// enforce minW/minH/maxW/maxH
newDimensions = checkBoundaries(ed, el, newDimensions.width, newDimensions.height);
// was an adjustment made?
var adjusted = (dom.getAttrib(el, 'width', el.width) != newDimensions.width || dom.getAttrib(el, 'height', el.height) != newDimensions.height);
// apply new w/h
if (adjusted) {
dom.setAttrib(el, 'width', newDimensions.width);
dom.setAttrib(el, 'height', newDimensions.height);
if (tinymce.isGecko) fixGeckoHandles(ed);
}
if (ed.getParam('advimagescale_append_to_url')) {
appendToUri(ed, el, dom.getAttrib(el, 'width', el.width), dom.getAttrib(el, 'height', el.height));
}
// was the image resized?
if (ed.lastDimensions[elId].width != dom.getAttrib(el, 'width', el.width) || ed.lastDimensions[elId].height != dom.getAttrib(el, 'height', el.height)) {
// call "image resized" callback (if set)
if (ed.getParam('advimagescale_resize_callback')) {
ed.getParam('advimagescale_resize_callback')(ed, el);
}
}
// remember "last dimensions" for next time
ed.lastDimensions[elId] = { width: dom.getAttrib(el, 'width', el.width), height: dom.getAttrib(el, 'height', el.height) };
}
/**
* Fixes IE7 and Gecko border width glitch
*
* Both "add" the border width to an image after the resize handles have been
* dropped. This reverses it by looking at the "previous" known size and comparing
* to the current size. If they don't match, then a resize has taken place and the browser
* has (probably) messed it up. So, we reverse it. Note, this will probably need to be
* wrapped in a conditional statement if/when each browser fixes this bug.
*/
function fixImageBorderGlitch(ed, el) {
var dom = ed.dom;
var elId = dom.getAttrib(el, 'mce_advimageresize_id');
var currentWidth = dom.getAttrib(el, 'width', el.width);
var currentHeight = dom.getAttrib(el, 'height', el.height);
var adjusted = false;
// if current dimensions do not match what we last saw, then a resize has taken place
if (currentWidth != ed.lastDimensions[elId].width) {
var adjustWidth = 0;
// get computed border left/right widths
adjustWidth += parseInt(dom.getStyle(el, 'borderLeftWidth', 'borderLeftWidth'));
adjustWidth += parseInt(dom.getStyle(el, 'borderRightWidth', 'borderRightWidth'));
// reset the width height to NOT include these amounts
if (adjustWidth > 0) {
dom.setAttrib(el, 'width', (currentWidth - adjustWidth));
adjusted = true;
}
}
if (currentHeight != ed.lastDimensions[elId].height) {
var adjustHeight = 0;
// get computed border top/bottom widths
adjustHeight += parseInt(dom.getStyle(el, 'borderTopWidth', 'borderTopWidth'));
adjustHeight += parseInt(dom.getStyle(el, 'borderBottomWidth', 'borderBottomWidth'));
if (adjustHeight > 0) {
dom.setAttrib(el, 'height', (currentHeight - adjustHeight));
adjusted = true;
}
}
if (adjusted && tinymce.isGecko) fixGeckoHandles(ed);
}
/**
* Fix gecko resize handles glitch
*/
function fixGeckoHandles(ed) {
ed.execCommand('mceRepaint', false);
}
/**
* Set image dimensions on into a uri as querystring params
*/
function appendToUri(ed, el, w, h) {
var dom = ed.dom;
var uri = dom.getAttrib(el, 'src');
var wKey = ed.getParam('advimagescale_url_width_key', 'w');
uri = setQueryParam(uri, wKey, w);
var hKey = ed.getParam('advimagescale_url_height_key', 'h');
uri = setQueryParam(uri, hKey, h);
// no need to continue if URL didn't change
if (uri == dom.getAttrib(el, 'src')) {
return;
}
// trigger image loading callback (if set)
if (ed.getParam('advimagescale_loading_callback')) {
// call loading callback
ed.getParam('advimagescale_loading_callback')(el);
}
// hook image load(ed) callback (if set)
if (ed.getParam('advimagescale_loaded_callback')) {
// hook load event on the image tag to call the loaded callback
tinymce.dom.Event.add(el, 'load', imageLoadedCallback, {el: el, ed: ed});
}
// set new src
dom.setAttrib(el, 'src', uri);
}
/**
* Callback event when an image is (re)loaded
* @param {object} e Event (use e.target or this.el to access element, this.ed to access editor instance)
*/
function imageLoadedCallback(e) {
var el = this.el; // image element
var ed = this.ed; // editor
var callback = ed.getParam('advimagescale_loaded_callback'); // user specified callback
// call callback, pass img as param
callback(el);
// remove callback event
tinymce.dom.Event.remove(el, 'load', imageLoadedCallback);
}
/**
* Sets URL querystring parameters by appending or replacing existing params of same name
*/
function setQueryParam(uri, key, value) {
if (!uri.match(/\?/)) uri += '?';
if (!uri.match(new RegExp('([\?&])' + key + '='))) {
if (!uri.match(/[&\?]$/)) uri += '&';
uri += key + '=' + escape(value);
} else {
uri = uri.replace(new RegExp('([\?\&])' + key + '=[^&]*'), '$1' + key + '=' + escape(value));
}
return uri;
}
/**
* Returns w/h that maintain aspect ratio
*/
function maintainAspect(ed, el, w, h) {
var elId = ed.dom.getAttrib(el, 'mce_advimageresize_id');
// calculate aspect ratio of original so we can maintain it
var ratio = ed.originalDimensions[elId].width / ed.originalDimensions[elId].height;
// decide which dimension changed more (percentage), because that's the
// one we'll respect (the other we'll adjust to keep aspect ratio)
var lastW = ed.lastDimensions[elId].width;
var lastH = ed.lastDimensions[elId].height;
var deltaW = Math.abs(lastW - w); // absolute
var deltaH = Math.abs(lastH - h); // absolute
var pctW = Math.abs(deltaW / lastW); // percentage
var pctH = Math.abs(deltaH / lastH); // percentage
if (deltaW || deltaH) {
if (pctW > pctH) {
// width changed more - use that as the locked point and adjust height
return { width: w, height: Math.round(w / ratio) };
} else {
// height changed more - use that as the locked point and adjust width
return { width: Math.round(h * ratio), height: h };
}
}
// nothing changed
return { width: w, height: h };
}
/**
* Enforce min/max boundaries
*
* Returns true if an adjustment was made
*/
function checkBoundaries(ed, el, w, h) {
var elId = ed.dom.getAttrib(el, 'mce_advimageresize_id');
var maxW = ed.getParam('advimagescale_max_width');
var maxH = ed.getParam('advimagescale_max_height');
var minW = ed.getParam('advimagescale_min_width');
var minH = ed.getParam('advimagescale_min_height');
var maintainAspect = ed.getParam('advimagescale_maintain_aspect_ratio', true);
var oW = ed.originalDimensions[elId].width;
var oH = ed.originalDimensions[elId].height;
var ratio = oW/oH;
// max
if (maxW && w > maxW) {
w = maxW;
h = maintainAspect ? Math.round(w / ratio) : h;
}
if (maxH && h > maxH) {
h = maxH;
w = maintainAspect ? Math.round(h * ratio) : w;
}
// min
if (minW && w < minW) {
w = minW;
h = maintainAspect ? Math.round(w / ratio) : h;
}
if (minH && h < minH) {
h = minH;
w = maintainAspect ? Math.round(h * ratio) : h;
}
return { width: w, height:h };
}
})();