Merge remote-tracking branch 'origin/3.1'

Conflicts:
	.travis.yml
	forms/FormField.php
This commit is contained in:
Ingo Schommer 2013-10-20 13:52:56 +02:00
commit 25b6175e67
39 changed files with 959 additions and 216 deletions

View File

@ -4,7 +4,13 @@ php:
- 5.3
env:
- DB=MYSQL CORE_RELEASE=master
global:
- "ARTIFACTS_AWS_REGION=us-east-1"
- "ARTIFACTS_S3_BUCKET=silverstripe-travis-artifacts"
- secure: "DjwZKhY/c0wXppGmd8oEMiTV0ayfOXiCmi9Lg1aXoSXNnj+sjLmhYwhUWjehjR6IX0MRtzJG6v7V5Y+4nSGe+i+XIrBQnhPQ95Jrkm1gKofX2mznWTl9npQElNS1DXi58NLPbiB3qxHWGFBRAWmRQrsAouyZabkPnChnSa9ldOg="
- secure: "UmbXCNLK0f2Dk+7qX8bOVcgIt4QhRvccoWvMUxaPtIU+95HCbG10eeCxvfOeBax+tHcRXmeCG4vM4tcuT/WoANkAma/VX74DylFjbWhks2tsKOcr2kjTrOwe6Q9CXOBjVAlcx0lnV/a+w83KARjXGnCrIbE7p7r4EDw31rkVufg="
matrix:
- DB=MYSQL CORE_RELEASE=master
matrix:
include:
@ -16,7 +22,7 @@ matrix:
env: DB=MYSQL CORE_RELEASE=master
- php: 5.5
env: DB=MYSQL CORE_RELEASE=master
- php: 5.3
- php: 5.4
env: DB=MYSQL CORE_RELEASE=master BEHAT_TEST=1
before_script:
@ -26,16 +32,15 @@ before_script:
- "if [ \"$BEHAT_TEST\" = \"\" ]; then php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss; fi"
- "if [ \"$BEHAT_TEST\" = \"1\" ]; then php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss --require silverstripe/behat-extension; fi"
- cd ~/builds/ss
- php ~/travis-support/travis_setup_selenium.php --base-url http://localhost --if-env BEHAT_TEST
- php ~/travis-support/travis_setup_apache.php --if-env BEHAT_TEST
- php ~/travis-support/travis_setup_selenium.php --if-env BEHAT_TEST
- php ~/travis-support/travis_setup_php54_webserver.php --if-env BEHAT_TEST
script:
- "if [ \"$BEHAT_TEST\" = \"\" ]; then phpunit framework/tests; fi"
- "if [ \"$BEHAT_TEST\" = \"1\" ]; then vendor/bin/behat --tags '~@todo&&~@assets' @framework; fi"
- "if [ \"$BEHAT_TEST\" = \"1\" ]; then vendor/bin/behat @framework; fi"
after_failure:
- "if [ \"$BEHAT_TEST\" = \"1\" ]; then sudo cat /var/log/apache2/error.log; fi"
- "if [ \"$BEHAT_TEST\" = \"1\" ]; then sudo cat /var/log/apache2/access.log; fi"
- php ~/travis-support/travis_upload_artifacts.php --if-env BEHAT_TEST,ARTIFACTS_AWS_SECRET_ACCESS_KEY --target-path $TRAVIS_REPO_SLUG/$TRAVIS_BUILD_ID/$TRAVIS_JOB_ID --artifacts-base-url https://s3.amazonaws.com/$ARTIFACTS_S3_BUCKET/
branches:
except:

View File

@ -3,7 +3,11 @@ name: Oembed
Oembed:
providers:
'http://*.youtube.com/watch*':
'http://www.youtube.com/oembed/'
http: 'http://www.youtube.com/oembed/',
https: 'https://www.youtube.com/oembed/?scheme=https'
'https://*.youtube.com/watch*':
http: 'http://www.youtube.com/oembed/',
https: 'https://www.youtube.com/oembed/?scheme=https'
'http://*.flickr.com/*':
'http://www.flickr.com/services/oembed/'
'http://*.viddler.com/*':
@ -13,12 +17,18 @@ Oembed:
'http://*.hulu.com/watch/*':
'http://www.hulu.com/api/oembed.json'
'http://*.vimeo.com/*':
'http://www.vimeo.com/api/oembed.json'
'https://twitter.com/*':
'https://api.twitter.com/1/statuses/oembed.json'
http: 'http://www.vimeo.com/api/oembed.json',
https: 'https://www.vimeo.com/api/oembed.json'
'https://*.vimeo.com/*':
http: 'http://www.vimeo.com/api/oembed.json',
https: 'https://www.vimeo.com/api/oembed.json'
'http://twitter.com/*':
'https://api.twitter.com/1/statuses/oembed.json'
http: 'https://api.twitter.com/1/statuses/oembed.json',
https: 'https://api.twitter.com/1/statuses/oembed.json'
'https://twitter.com/*':
http: 'https://api.twitter.com/1/statuses/oembed.json',
https: 'https://api.twitter.com/1/statuses/oembed.json'
autodiscover:
true
enabled:
true
true

View File

@ -15,8 +15,9 @@ class CMSForm extends Form {
* @return boolean
*/
public function validate() {
$buttonClicked = $this->buttonClicked();
return (
in_array($this->buttonClicked()->actionName(), $this->getValidationExemptActions())
($buttonClicked && in_array($buttonClicked->actionName(), $this->getValidationExemptActions()))
|| parent::validate()
);
}

View File

@ -513,6 +513,7 @@ body.cms { overflow: hidden; }
.cms-content-tools .field { /* Fields are more compressed in the sidebar compared to the main content editing window so the below alters the internal spacing of the fields so we can move that spacing to between the form fields rather than padding */ }
.cms-content-tools .field label { float: none; width: auto; font-size: 11px; padding: 0 8px 4px 0; }
.cms-content-tools .field .middleColumn { margin: 0; }
.cms-content-tools .field .description { margin-left: 0; }
.cms-content-tools .field input.text, .cms-content-tools .field select, .cms-content-tools .field textarea { padding: 5px; font-size: 11px; }
.cms-content-tools .field.checkbox { padding: 0 0 8px; }
.cms-content-tools .field.checkbox input { margin: 2px 0; }
@ -846,6 +847,7 @@ li.class-ErrorPage > a .jstree-pageicon { background-position: 0 -112px; }
.cms-tree.multiple li > a > .jstree-icon { display: none; }
.cms-tree.multiple li > a > .jstree-icon.jstree-checkbox { display: inline-block; }
.cms-tree.multiple li#record-0 > a .jstree-checkbox { display: none; }
.cms-tree.jstree-loading li#record-0 > .jstree-icon { background: url(../images/throbber.gif) top left no-repeat; }
.cms-tree a.jstree-loading .jstree-icon { background-image: none !important; }
.cms-tree a.jstree-loading .jstree-pageicon { background: url(../images/throbber.gif) top left no-repeat; }

View File

@ -60,6 +60,9 @@
var url = $(node).find('a:first').attr('href');
if(url && url != '#') {
// strip possible querystrings from the url to avoid duplicateing document.location.search
url = url.split('?')[0];
// Deselect all nodes (will be reselected after load according to form state)
self.jstree('deselect_all');
self.jstree('uncheck_all');

View File

@ -46,8 +46,7 @@
.jstree(this.getTreeConfig())
.bind('loaded.jstree', function(e, data) {
self.setIsLoaded(true);
self.updateFromEditForm();
self.css('visibility', 'visible');
// Add ajax settings after init period to avoid unnecessary initial ajax load
// of existing tree in DOM - see load_node_html()
data.inst._set_settings({'html_data': {'ajax': {
@ -61,6 +60,9 @@
return params;
}
}}});
self.updateFromEditForm();
self.css('visibility', 'visible');
// Only show checkboxes with .multiple class
data.inst.hide_checkboxes();
@ -239,8 +241,8 @@
* Creates a new node from the given HTML.
* Wrapping around jstree API because we want the flexibility to define
* the node's <li> ourselves. Places the node in the tree
* according to data.ParentID
*
* according to data.ParentID.
*
* Parameters:
* (String) HTML New node content (<li>)
* (Object) Map of additional data, e.g. ParentID
@ -248,7 +250,7 @@
*/
createNode: function(html, data, callback) {
var self = this,
parentNode = data.ParentID ? self.find('li[data-id='+data.ParentID+']') : false,
parentNode = data.ParentID ? self.getNodeByID(data.ParentID) : false,
newNode = $(html);
this.jstree(
@ -281,9 +283,9 @@
updateNode: function(node, html, data) {
var self = this, newNode = $(html), origClasses = node.attr('class');
var nextNode = data.NextID ? this.find('li[data-id='+data.NextID+']') : false;
var prevNode = data.PrevID ? this.find('li[data-id='+data.PrevID+']') : false;
var parentNode = data.ParentID ? this.find('li[data-id='+data.ParentID+']') : false;
var nextNode = data.NextID ? this.getNodeByID(data.NextID) : false;
var prevNode = data.PrevID ? this.getNodeByID(data.PrevID) : false;
var parentNode = data.ParentID ? this.getNodeByID(data.ParentID) : false;
// Copy attributes. We can't replace the node completely
// without removing or detaching its children nodes.
@ -346,8 +348,22 @@
updateNodesFromServer: function(ids) {
if(this.getIsUpdatingTree() || !this.getIsLoaded()) return;
var self = this, includesNewNode = false;
var self = this, i, includesNewNode = false;
this.setIsUpdatingTree(true);
self.jstree('save_selected');
var correctStateFn = function(node) {
// Duplicates can be caused by the subtree reloading through
// a tree "open"/"select" event, while at the same time creating a new node
self.getNodeByID(node.data('id')).not(node).remove();
self.jstree('deselect_all');
self.jstree('select_node', node);
// Similar to jstree's correct_state, but doesn't remove children
var hasChildren = (node.children('ul').length > 0);
node.toggleClass('jstree-leaf', !hasChildren);
if(!hasChildren) node.removeClass('jstree-closed jstree-open');
};
// TODO 'initially_opened' config doesn't apply here
self.jstree('open_node', this.getNodeByID(0));
@ -367,15 +383,6 @@
return;
}
var correctStateFn = function(node) {
self.jstree('deselect_all');
self.jstree('select_node', node);
// Similar to jstree's correct_state, but doesn't remove children
var hasChildren = (node.children('ul').length > 0);
node.toggleClass('jstree-leaf', !hasChildren);
if(!hasChildren) node.removeClass('jstree-closed jstree-open');
};
// Check if node exists, create if necessary
if(node.length) {
self.updateNode(node, nodeData.html, nodeData);
@ -384,9 +391,20 @@
}, 500);
} else {
includesNewNode = true;
self.createNode(nodeData.html, nodeData, function(newNode) {
correctStateFn(newNode);
});
// If the parent node can't be found, it might have not been loaded yet.
// This can happen for deep trees which require ajax loading.
// Assumes that the new node has been submitted to the server already.
if(nodeData.ParentID && !self.find('li[data-id='+nodeData.ParentID+']').length) {
self.jstree('load_node', -1, function() {
newNode = self.find('li[data-id='+nodeId+']');
correctStateFn(newNode);
});
} else {
self.createNode(nodeData.html, nodeData, function(newNode) {
correctStateFn(newNode);
});
}
}
});
@ -399,7 +417,7 @@
complete: function() {
self.setIsUpdatingTree(false);
}
});
});
}
});

View File

@ -738,7 +738,7 @@ jQuery.noConflict();
sessionData = hasSessionStorage ? window.sessionStorage.getItem('tabs-' + url) : null,
sessionStates = sessionData ? JSON.parse(sessionData) : false;
this.find('.cms-tabset').each(function() {
this.find('.cms-tabset, .ss-tabset').each(function() {
var index, tabset = $(this), tabsetId = tabset.attr('id'), tab,
forcedTab = tabset.find('.ss-tabs-force-active');

View File

@ -798,6 +798,10 @@ body.cms {
margin: 2px 0;
}
}
.description {
margin-left: 0;
}
}
.fieldgroup {

View File

@ -596,6 +596,12 @@ a .jstree-pageicon {
}
}
&.jstree-loading {
li#record-0 > .jstree-icon {
background: url(../images/throbber.gif) top left no-repeat;
}
}
// Show the loading indicator on the page icon rather than the default
// jstree icon (which is only used for its dragging handles)
a.jstree-loading{

View File

@ -15,7 +15,7 @@ abstract class DataFormatter extends Object {
*
* @var int
*/
public static $priority = 50;
private static $priority = 50;
/**
* Follow relations for the {@link DataObject} instances

View File

@ -97,7 +97,7 @@ class JSONDataFormatter extends DataFormatter {
$innerParts[] = ArrayData::array_to_object(array(
"className" => $relClass,
"href" => "$href.json",
"id" => $item->$fieldName
"id" => $item->ID
));
}
$serobj->$relName = $innerParts;
@ -118,7 +118,7 @@ class JSONDataFormatter extends DataFormatter {
$innerParts[] = ArrayData::array_to_object(array(
"className" => $relClass,
"href" => "$href.json",
"id" => $item->$fieldName
"id" => $item->ID
));
}
$serobj->$relName = $innerParts;

View File

@ -42,7 +42,16 @@ class Director implements TemplateGlobalProvider {
* @var array
*/
private static $test_servers = array();
/**
* Setting this explicitly specifies the protocol (http or https) used, overriding
* the normal behaviour of Director::is_https introspecting it from the request
*
* @config
* @var string - "http" or "https" to force the protocol, or false-ish to use default introspection from request
*/
private static $alternate_protocol;
/**
* @config
* @var string
@ -258,8 +267,18 @@ class Director implements TemplateGlobalProvider {
$request = new SS_HTTPRequest($httpMethod, $url, $getVars, $postVars, $body);
if($headers) foreach($headers as $k => $v) $request->addHeader($k, $v);
// Pre-request filtering
// @see issue #2517
$model = DataModel::inst();
$output = Injector::inst()->get('RequestProcessor')->preRequest($request, $session, $model);
if ($output === false) {
// @TODO Need to NOT proceed with the request in an elegant manner
throw new SS_HTTPResponse_Exception(_t('Director.INVALID_REQUEST', 'Invalid request'), 400);
}
// TODO: Pass in the DataModel
$result = Director::handleRequest($request, $session, DataModel::inst());
$result = Director::handleRequest($request, $session, $model);
// Ensure that the result is an SS_HTTPResponse object
if(is_string($result)) {
@ -272,6 +291,11 @@ class Director implements TemplateGlobalProvider {
}
}
$output = Injector::inst()->get('RequestProcessor')->postRequest($request, $result, $model);
if ($output === false) {
throw new SS_HTTPResponse_Exception("Invalid response");
}
// Restore the superglobals
$_REQUEST = $existingRequestVars;
$_GET = $existingGetVars;
@ -443,6 +467,10 @@ class Director implements TemplateGlobalProvider {
* @return boolean
*/
public static function is_https() {
if ($protocol = Config::inst()->get('Director', 'alternate_protocol')) {
return $protocol == 'https';
}
if(isset($_SERVER['HTTP_X_FORWARDED_PROTOCOL'])) {
if(strtolower($_SERVER['HTTP_X_FORWARDED_PROTOCOL']) == 'https') {
return true;

View File

@ -683,23 +683,24 @@ class Injector {
* class name of the object to register)
*
*/
public function registerService($service, $replace=null) {
public function registerService($service, $replace = null) {
$registerAt = get_class($service);
if ($replace != null) {
$registerAt = $replace;
}
$this->specs[$registerAt] = array('class' => get_class($service));
$this->serviceCache[$registerAt] = $service;
$this->inject($service);
}
/**
* Register a service with an explicit name
*
* @deprecated since 3.1.1
*/
public function registerNamedService($name, $service) {
$this->specs[$name] = array('class' => get_class($service));
$this->serviceCache[$name] = $service;
$this->inject($service);
return $this->registerService($service, $name);
}
/**

View File

@ -90,7 +90,7 @@ class SS_Backtrace {
// Filter out arguments
foreach($bt as $i => $frame) {
$match = false;
if(@$bt[$i]['class']) {
if(!empty($bt[$i]['class'])) {
foreach($ignoredArgs as $fnSpec) {
if(is_array($fnSpec) && $bt[$i]['class'] == $fnSpec[0] && $bt[$i]['function'] == $fnSpec[1]) {
$match = true;

View File

@ -94,7 +94,7 @@ class SS_Log {
// Add default context (shouldn't change until the actual log event happens)
foreach(static::$log_globals as $globalName => $keys) {
foreach($keys as $key) {
$val = @$GLOBALS[$globalName][$key];
$val = isset($GLOBALS[$globalName][$key]) ? $GLOBALS[$globalName][$key] : null;
static::$logger->setEventItem(sprintf('$%s[\'%s\']', $globalName, $key), $val);
}
}

436
dev/install/install.php5 Normal file → Executable file
View File

@ -27,7 +27,7 @@ ini_set('display_errors', '1');
error_reporting(E_ALL | E_STRICT);
// Attempt to start a session so that the username and password can be sent back to the user.
if (function_exists('session_start') && !session_id()) {
if(function_exists('session_start') && !session_id()) {
session_start();
}
@ -45,36 +45,35 @@ $dirsToCheck = array(
dirname($_SERVER['SCRIPT_FILENAME'])
);
//if they are the same, remove one of them
if ($dirsToCheck[0] == $dirsToCheck[1]) {
if($dirsToCheck[0] == $dirsToCheck[1]) {
unset($dirsToCheck[1]);
}
foreach ($dirsToCheck as $dir) {
foreach($dirsToCheck as $dir) {
//check this dir and every parent dir (until we hit the base of the drive)
// or until we hit a dir we can't read
do {
do {
//add the trailing slash we need to concatenate properly
$dir .= DIRECTORY_SEPARATOR;
//if it's readable, go ahead
if (@is_readable($dir)) {
//if the file exists, then we include it, set relevant vars and break out
if (file_exists($dir . $envFile)) {
include_once($dir . $envFile);
$envFileExists = true;
//legacy variable assignment
$usingEnv = true;
if(@is_readable($dir)) {
//if the file exists, then we include it, set relevant vars and break out
if(file_exists($dir . $envFile)) {
include_once($dir . $envFile);
$envFileExists = true;
//legacy variable assignment
$usingEnv = true;
//break out of BOTH loops because we found the $envFile
break(2);
}
}
else {
} else {
//break out of the while loop, we can't read the dir
break;
}
break;
}
//go up a directory
$dir = dirname($dir);
//here we need to check that the path of the last dir and the next one are
//here we need to check that the path of the last dir and the next one are
// not the same, if they are, we have hit the root of the drive
} while (dirname($dir) != $dir);
} while(dirname($dir) != $dir);
}
if($envFileExists) {
@ -91,62 +90,62 @@ require_once FRAMEWORK_NAME . '/dev/install/DatabaseAdapterRegistry.php';
// Set default locale, but try and sniff from the user agent
$defaultLocale = 'en_US';
$locales = array(
'af_ZA' => 'Afrikaans (South Africa)',
'ar_EG' => 'Arabic (Egypt)',
'hy_AM' => 'Armenian (Armenia)',
'ast_ES' => 'Asturian (Spain)',
'az_AZ' => 'Azerbaijani (Azerbaijan)',
'bs_BA' => 'Bosnian (Bosnia and Herzegovina)',
'bg_BG' => 'Bulgarian (Bulgaria)',
'ca_ES' => 'Catalan (Spain)',
'zh_CN' => 'Chinese (China)',
'zh_TW' => 'Chinese (Taiwan)',
'hr_HR' => 'Croatian (Croatia)',
'cs_CZ' => 'Czech (Czech Republic)',
'da_DK' => 'Danish (Denmark)',
'nl_NL' => 'Dutch (Netherlands)',
'en_GB' => 'English (United Kingdom)',
'en_US' => 'English (United States)',
'eo_XX' => 'Esperanto',
'et_EE' => 'Estonian (Estonia)',
'fo_FO' => 'Faroese (Faroe Islands)',
'fi_FI' => 'Finnish (Finland)',
'fr_FR' => 'French (France)',
'de_DE' => 'German (Germany)',
'el_GR' => 'Greek (Greece)',
'he_IL' => 'Hebrew (Israel)',
'hu_HU' => 'Hungarian (Hungary)',
'is_IS' => 'Icelandic (Iceland)',
'id_ID' => 'Indonesian (Indonesia)',
'it_IT' => 'Italian (Italy)',
'ja_JP' => 'Japanese (Japan)',
'km_KH' => 'Khmer (Cambodia)',
'lc_XX' => 'LOLCAT',
'lv_LV' => 'Latvian (Latvia)',
'lt_LT' => 'Lithuanian (Lithuania)',
'ms_MY' => 'Malay (Malaysia)',
'mi_NZ' => 'Maori (New Zealand)',
'ne_NP' => 'Nepali (Nepal)',
'nb_NO' => 'Norwegian',
'fa_IR' => 'Persian (Iran)',
'pl_PL' => 'Polish (Poland)',
'pt_BR' => 'Portuguese (Brazil)',
'pa_IN' => 'Punjabi (India)',
'ro_RO' => 'Romanian (Romania)',
'ru_RU' => 'Russian (Russia)',
'sr_RS' => 'Serbian (Serbia)',
'si_LK' => 'Sinhalese (Sri Lanka)',
'sk_SK' => 'Slovak (Slovakia)',
'sl_SI' => 'Slovenian (Slovenia)',
'es_AR' => 'Spanish (Argentina)',
'es_MX' => 'Spanish (Mexico)',
'es_ES' => 'Spanish (Spain)',
'sv_SE' => 'Swedish (Sweden)',
'th_TH' => 'Thai (Thailand)',
'tr_TR' => 'Turkish (Turkey)',
'uk_UA' => 'Ukrainian (Ukraine)',
'uz_UZ' => 'Uzbek (Uzbekistan)',
'vi_VN' => 'Vietnamese (Vietnam)',
'af_ZA' => 'Afrikaans (South Africa)',
'ar_EG' => 'Arabic (Egypt)',
'hy_AM' => 'Armenian (Armenia)',
'ast_ES' => 'Asturian (Spain)',
'az_AZ' => 'Azerbaijani (Azerbaijan)',
'bs_BA' => 'Bosnian (Bosnia and Herzegovina)',
'bg_BG' => 'Bulgarian (Bulgaria)',
'ca_ES' => 'Catalan (Spain)',
'zh_CN' => 'Chinese (China)',
'zh_TW' => 'Chinese (Taiwan)',
'hr_HR' => 'Croatian (Croatia)',
'cs_CZ' => 'Czech (Czech Republic)',
'da_DK' => 'Danish (Denmark)',
'nl_NL' => 'Dutch (Netherlands)',
'en_GB' => 'English (United Kingdom)',
'en_US' => 'English (United States)',
'eo_XX' => 'Esperanto',
'et_EE' => 'Estonian (Estonia)',
'fo_FO' => 'Faroese (Faroe Islands)',
'fi_FI' => 'Finnish (Finland)',
'fr_FR' => 'French (France)',
'de_DE' => 'German (Germany)',
'el_GR' => 'Greek (Greece)',
'he_IL' => 'Hebrew (Israel)',
'hu_HU' => 'Hungarian (Hungary)',
'is_IS' => 'Icelandic (Iceland)',
'id_ID' => 'Indonesian (Indonesia)',
'it_IT' => 'Italian (Italy)',
'ja_JP' => 'Japanese (Japan)',
'km_KH' => 'Khmer (Cambodia)',
'lc_XX' => 'LOLCAT',
'lv_LV' => 'Latvian (Latvia)',
'lt_LT' => 'Lithuanian (Lithuania)',
'ms_MY' => 'Malay (Malaysia)',
'mi_NZ' => 'Maori (New Zealand)',
'ne_NP' => 'Nepali (Nepal)',
'nb_NO' => 'Norwegian',
'fa_IR' => 'Persian (Iran)',
'pl_PL' => 'Polish (Poland)',
'pt_BR' => 'Portuguese (Brazil)',
'pa_IN' => 'Punjabi (India)',
'ro_RO' => 'Romanian (Romania)',
'ru_RU' => 'Russian (Russia)',
'sr_RS' => 'Serbian (Serbia)',
'si_LK' => 'Sinhalese (Sri Lanka)',
'sk_SK' => 'Slovak (Slovakia)',
'sl_SI' => 'Slovenian (Slovenia)',
'es_AR' => 'Spanish (Argentina)',
'es_MX' => 'Spanish (Mexico)',
'es_ES' => 'Spanish (Spain)',
'sv_SE' => 'Swedish (Sweden)',
'th_TH' => 'Thai (Thailand)',
'tr_TR' => 'Turkish (Turkey)',
'uk_UA' => 'Ukrainian (Ukraine)',
'uz_UZ' => 'Uzbek (Uzbekistan)',
'vi_VN' => 'Vietnamese (Vietnam)',
);
// Discover which databases are available
@ -217,8 +216,8 @@ if(file_exists('mysite/_config.php')) {
if(preg_match("/\\\$database\s*=\s*[^\n\r]+[\n\r]/", file_get_contents("mysite/_config.php"), $parts)) {
eval($parts[0]);
if($database) $alreadyInstalled = true;
// Assume that if $databaseConfig is defined in mysite/_config.php, then a non-environment-based installation has
// already gone ahead
// Assume that if $databaseConfig is defined in mysite/_config.php, then a non-environment-based installation has
// already gone ahead
} else if(preg_match("/\\\$databaseConfig\s*=\s*[^\n\r]+[\n\r]/", file_get_contents("mysite/_config.php"), $parts)) {
$alreadyInstalled = true;
}
@ -412,7 +411,12 @@ class InstallRequirements {
$isIIS = $this->isIIS(7);
$webserver = $this->findWebserver();
$this->requirePHPVersion('5.3.4', '5.3.2', array("PHP Configuration", "PHP5 installed", null, "PHP version " . phpversion()));
$this->requirePHPVersion('5.3.4', '5.3.2', array(
"PHP Configuration",
"PHP5 installed",
null,
"PHP version " . phpversion()
));
// Check that we can identify the root folder successfully
$this->requireFile(FRAMEWORK_NAME . '/dev/install/config-form.html', array("File permissions",
@ -425,15 +429,27 @@ class InstallRequirements {
$this->requireModule(FRAMEWORK_NAME, array("File permissions", FRAMEWORK_NAME . "/ directory exists?"));
if($isApache) {
$this->checkApacheVersion(array("Webserver Configuration", "Webserver is not Apache 1.x", "SilverStripe requires Apache version 2 or greater", $webserver));
$this->checkApacheVersion(array(
"Webserver Configuration",
"Webserver is not Apache 1.x", "SilverStripe requires Apache version 2 or greater",
$webserver
));
$this->requireWriteable('.htaccess', array("File permissions", "Is the .htaccess file writeable?", null));
} elseif($isIIS) {
$this->requireWriteable('web.config', array("File permissions", "Is the web.config file writeable?", null));
}
$this->requireWriteable('mysite/_config.php', array("File permissions", "Is the mysite/_config.php file writeable?", null));
if (!$this->checkModuleExists('cms')) {
$this->requireWriteable('mysite/code/RootURLController.php', array("File permissions", "Is the mysite/code/RootURLController.php file writeable?", null));
$this->requireWriteable('mysite/_config.php', array(
"File permissions",
"Is the mysite/_config.php file writeable?",
null
));
if(!$this->checkModuleExists('cms')) {
$this->requireWriteable('mysite/code/RootURLController.php', array(
"File permissions",
"Is the mysite/code/RootURLController.php file writeable?",
null
));
}
$this->requireWriteable('assets', array("File permissions", "Is the assets/ directory writeable?", null));
@ -441,72 +457,193 @@ class InstallRequirements {
$this->requireTempFolder(array('File permissions', 'Is a temporary directory available?', null, $tempFolder));
if($tempFolder) {
// in addition to the temp folder being available, check it is writable
$this->requireWriteable($tempFolder, array("File permissions", sprintf("Is the temporary directory writeable?", $tempFolder), null), true);
$this->requireWriteable($tempFolder, array(
"File permissions",
sprintf("Is the temporary directory writeable?", $tempFolder),
null
), true);
}
// Check for web server, unless we're calling the installer from the command-line
$this->isRunningWebServer(array("Webserver Configuration", "Server software", "Unknown", $webserver));
if($isApache) {
$this->requireApacheRewriteModule('mod_rewrite', array("Webserver Configuration", "URL rewriting support", "You need mod_rewrite to use friendly URLs with SilverStripe, but it is not enabled."));
$this->requireApacheRewriteModule('mod_rewrite', array(
"Webserver Configuration",
"URL rewriting support",
"You need mod_rewrite to use friendly URLs with SilverStripe, but it is not enabled."
));
} elseif($isIIS) {
$this->requireIISRewriteModule('IIS_UrlRewriteModule', array("Webserver Configuration", "URL rewriting support", "You need to enable the IIS URL Rewrite Module to use friendly URLs with SilverStripe, but it is not installed or enabled. Download it for IIS 7 from http://www.iis.net/expand/URLRewrite"));
$this->requireIISRewriteModule('IIS_UrlRewriteModule', array(
"Webserver Configuration",
"URL rewriting support",
"You need to enable the IIS URL Rewrite Module to use friendly URLs with SilverStripe, "
. "but it is not installed or enabled. Download it for IIS 7 from http://www.iis.net/expand/URLRewrite"
));
} else {
$this->warning(array("Webserver Configuration", "URL rewriting support", "I can't tell whether any rewriting module is running. You may need to configure a rewriting rule yourself."));
$this->warning(array(
"Webserver Configuration",
"URL rewriting support",
"I can't tell whether any rewriting module is running. You may need to configure a rewriting rule yourself."));
}
$this->requireServerVariables(array('SCRIPT_NAME','HTTP_HOST','SCRIPT_FILENAME'), array("Webserver Configuration", "Recognised webserver", "You seem to be using an unsupported webserver. The server variables SCRIPT_NAME, HTTP_HOST, SCRIPT_FILENAME need to be set."));
$this->requireServerVariables(array('SCRIPT_NAME', 'HTTP_HOST', 'SCRIPT_FILENAME'), array(
"Webserver Configuration",
"Recognised webserver",
"You seem to be using an unsupported webserver. "
. "The server variables SCRIPT_NAME, HTTP_HOST, SCRIPT_FILENAME need to be set."
));
$this->requirePostSupport(array("Webserver Configuration", "POST Support", 'I can\'t find $_POST, make sure POST is enabled.'));
$this->requirePostSupport(array(
"Webserver Configuration",
"POST Support",
'I can\'t find $_POST, make sure POST is enabled.'
));
// Check for GD support
if(!$this->requireFunction("imagecreatetruecolor", array("PHP Configuration", "GD2 support", "PHP must have GD version 2."))) {
$this->requireFunction("imagecreate", array("PHP Configuration", "GD2 support", "GD support for PHP not included."));
if(!$this->requireFunction("imagecreatetruecolor", array(
"PHP Configuration",
"GD2 support",
"PHP must have GD version 2."
))) {
$this->requireFunction("imagecreate", array(
"PHP Configuration",
"GD2 support",
"GD support for PHP not included."
));
}
// Check for XML support
$this->requireFunction('xml_set_object', array("PHP Configuration", "XML support", "XML support not included in PHP."));
$this->requireClass('DOMDocument', array("PHP Configuration", "DOM/XML support", "DOM/XML support not included in PHP."));
$this->requireFunction('simplexml_load_file', array('PHP Configuration', 'SimpleXML support', 'SimpleXML support not included in PHP.'));
$this->requireFunction('xml_set_object', array(
"PHP Configuration",
"XML support",
"XML support not included in PHP."
));
$this->requireClass('DOMDocument', array(
"PHP Configuration",
"DOM/XML support",
"DOM/XML support not included in PHP."
));
$this->requireFunction('simplexml_load_file', array(
'PHP Configuration',
'SimpleXML support',
'SimpleXML support not included in PHP.'
));
// Check for token_get_all
$this->requireFunction('token_get_all', array("PHP Configuration", "Tokenizer support", "Tokenizer support not included in PHP."));
$this->requireFunction('token_get_all', array(
"PHP Configuration",
"Tokenizer support",
"Tokenizer support not included in PHP."
));
// Check for CType support
$this->requireFunction('ctype_digit', array('PHP Configuration', 'CType support', 'CType support not included in PHP.'));
$this->requireFunction('ctype_digit', array(
'PHP Configuration',
'CType support',
'CType support not included in PHP.'
));
// Check for session support
$this->requireFunction('session_start', array('PHP Configuration', 'Session support', 'Session support not included in PHP.'));
$this->requireFunction('session_start', array(
'PHP Configuration',
'Session support',
'Session support not included in PHP.'
));
// Check for iconv support
$this->requireFunction('iconv', array('PHP Configuration', 'iconv support', 'iconv support not included in PHP.'));
$this->requireFunction('iconv', array(
'PHP Configuration',
'iconv support',
'iconv support not included in PHP.'
));
// Check for hash support
$this->requireFunction('hash', array('PHP Configuration', 'hash support', 'hash support not included in PHP.'));
// Check for mbstring support
$this->requireFunction('mb_internal_encoding', array('PHP Configuration', 'mbstring support', 'mbstring support not included in PHP.'));
$this->requireFunction('mb_internal_encoding', array(
'PHP Configuration',
'mbstring support',
'mbstring support not included in PHP.'
));
// Check for Reflection support
$this->requireClass('ReflectionClass', array('PHP Configuration', 'Reflection support', 'Reflection support not included in PHP.'));
$this->requireClass('ReflectionClass', array(
'PHP Configuration',
'Reflection support',
'Reflection support not included in PHP.'
));
// Check for Standard PHP Library (SPL) support
$this->requireFunction('spl_classes', array('PHP Configuration', 'SPL support', 'Standard PHP Library (SPL) not included in PHP.'));
$this->requireFunction('spl_classes', array(
'PHP Configuration',
'SPL support',
'Standard PHP Library (SPL) not included in PHP.'
));
$this->requireDateTimezone(array('PHP Configuration', 'date.timezone setting and validity', 'date.timezone option in php.ini must be set correctly.', ini_get('date.timezone')));
$this->requireDateTimezone(array(
'PHP Configuration',
'date.timezone setting and validity',
'date.timezone option in php.ini must be set correctly.',
ini_get('date.timezone')
));
$this->suggestClass('finfo', array('PHP Configuration', 'fileinfo support', 'fileinfo should be enabled in PHP. SilverStripe uses it for MIME type detection of files. SilverStripe will still operate, but email attachments and sending files to browser (e.g. export data to CSV) may not work correctly without finfo.'));
$this->suggestClass('finfo', array(
'PHP Configuration',
'fileinfo support',
'fileinfo should be enabled in PHP. SilverStripe uses it for MIME type detection of files. '
. 'SilverStripe will still operate, but email attachments and sending files to browser '
. '(e.g. export data to CSV) may not work correctly without finfo.'
));
$this->suggestFunction('curl_init', array('PHP Configuration', 'curl support', 'curl should be enabled in PHP. SilverStripe uses it for consuming web services via the RestfulService class and many modules rely on it.'));
$this->suggestFunction('curl_init', array(
'PHP Configuration',
'curl support',
'curl should be enabled in PHP. SilverStripe uses it for consuming web services'
. ' via the RestfulService class and many modules rely on it.'
));
$this->suggestClass('tidy', array('PHP Configuration', 'tidy support', 'Tidy provides a library of code to clean up your html. SilverStripe will operate fine without tidy but HTMLCleaner will not be effective.'));
$this->suggestClass('tidy', array(
'PHP Configuration',
'tidy support',
'Tidy provides a library of code to clean up your html. '
. 'SilverStripe will operate fine without tidy but HTMLCleaner will not be effective.'
));
$this->suggestPHPSetting('asp_tags', array(false), array('PHP Configuration', 'asp_tags option', 'This should be turned off as it can cause issues with SilverStripe'));
$this->requirePHPSetting('magic_quotes_gpc', array(false), array('PHP Configuration', 'magic_quotes_gpc option', 'This should be turned off, as it can cause issues with cookies. More specifically, unserializing data stored in cookies.'));
$this->suggestPHPSetting('display_errors', array(false), array('PHP Configuration', 'display_errors option', 'Unless you\'re in a development environment, this should be turned off, as it can expose sensitive data to website users.'));
$this->suggestPHPSetting('asp_tags', array(false), array(
'PHP Configuration',
'asp_tags option',
'This should be turned off as it can cause issues with SilverStripe'
));
$this->requirePHPSetting('magic_quotes_gpc', array(false), array(
'PHP Configuration',
'magic_quotes_gpc option',
'This should be turned off, as it can cause issues with cookies. '
. 'More specifically, unserializing data stored in cookies.'
));
$this->suggestPHPSetting('display_errors', array(false), array(
'PHP Configuration',
'display_errors option',
'Unless you\'re in a development environment, this should be turned off, '
. 'as it can expose sensitive data to website users.'
));
// on some weirdly configured webservers arg_separator.output is set to &amp;
// which will results in links like ?param=value&amp;foo=bar which will not be i
$this->suggestPHPSetting('arg_separator.output', array('&', ''), array(
'PHP Configuration',
'arg_separator.output option',
'This option defines how URL parameters are concatenated. '
. 'If not set to \'&\' this may cause issues with URL GET parameters'
));
// Check memory allocation
$this->requireMemory(32*1024*1024, 64*1024*1024, array("PHP Configuration", "Memory allocation (PHP config option 'memory_limit')", "SilverStripe needs a minimum of 32M allocated to PHP, but recommends 64M.", ini_get("memory_limit")));
$this->requireMemory(32 * 1024 * 1024, 64 * 1024 * 1024, array(
"PHP Configuration",
"Memory allocation (PHP config option 'memory_limit')",
"SilverStripe needs a minimum of 32M allocated to PHP, but recommends 64M.",
ini_get("memory_limit")
));
return $this->errors;
}
@ -517,6 +654,7 @@ class InstallRequirements {
// special case for display_errors, check the original value before
// it was changed at the start of this script.
if($settingName = 'display_errors') {
global $originalDisplayErrorsValue;
$val = $originalDisplayErrorsValue;
} else {
$val = ini_get($settingName);
@ -582,7 +720,8 @@ class InstallRequirements {
$testDetails[2] .= " You only have " . ini_get("memory_limit") . " allocated";
$this->warning($testDetails);
} elseif($mem == 0) {
$testDetails[2] .= " We can't determine how much memory you have allocated. Install only if you're sure you've allocated at least 20 MB.";
$testDetails[2] .= " We can't determine how much memory you have allocated. "
. "Install only if you're sure you've allocated at least 20 MB.";
$this->warning($testDetails);
}
}
@ -590,15 +729,15 @@ class InstallRequirements {
function getPHPMemory() {
$memString = ini_get("memory_limit");
switch(strtolower(substr($memString,-1))) {
switch(strtolower(substr($memString, -1))) {
case "k":
return round(substr($memString,0,-1)*1024);
return round(substr($memString, 0, -1) * 1024);
case "m":
return round(substr($memString,0,-1)*1024*1024);
return round(substr($memString, 0, -1) * 1024 * 1024);
case "g":
return round(substr($memString,0,-1)*1024*1024*1024);
return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
default:
return round($memString);
@ -620,7 +759,8 @@ class InstallRequirements {
$id = strtolower(str_replace(' ', '_', $section));
echo "<table id=\"{$id}_results\" class=\"testResults\" width=\"100%\">";
foreach($tests as $test => $result) {
echo "<tr class=\"$result[0]\"><td>$test</td><td>" . nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . "</td></tr>";
echo "<tr class=\"$result[0]\"><td>$test</td><td>"
. nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . "</td></tr>";
}
echo "</table>";
@ -642,7 +782,8 @@ class InstallRequirements {
break;
}
}
$output .= "<tr class=\"$result[0]\"><td>$test</td><td>" . nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . "</td></tr>";
$output .= "<tr class=\"$result[0]\"><td>$test</td><td>"
. nl2br(htmlentities($result[1], ENT_COMPAT, 'UTF-8')) . "</td></tr>";
}
$className = "good";
$text = "All Requirements Pass";
@ -652,11 +793,10 @@ class InstallRequirements {
$className = "error";
$pluralWarnings = ($warningRequirements == 1) ? 'Warning' : 'Warnings';
$text = $failedRequirements . ' Failed and '. $warningRequirements . ' '. $pluralWarnings;
}
else if($warningRequirements > 0) {
$text = $failedRequirements . ' Failed and ' . $warningRequirements . ' ' . $pluralWarnings;
} else if($warningRequirements > 0) {
$className = "warning";
$text = "All Requirements Pass but ". $warningRequirements . ' '. $pluralWarnings;
$text = "All Requirements Pass but " . $warningRequirements . ' ' . $pluralWarnings;
}
echo "<h5 class='requirement $className'>$section <a href='#'>Show All Requirements</a> <span>$text</span></h5>";
@ -669,17 +809,21 @@ class InstallRequirements {
function requireFunction($funcName, $testDetails) {
$this->testing($testDetails);
if(!function_exists($funcName)) {
$this->error($testDetails);
} else {
return true;
}
else return true;
}
function requireClass($className, $testDetails) {
$this->testing($testDetails);
if(!class_exists($className)) $this->error($testDetails);
else return false;
if(!class_exists($className)) {
$this->error($testDetails);
} else {
return false;
}
}
/**
@ -694,8 +838,9 @@ class InstallRequirements {
if($badClasses) {
$testDetails[2] .= ". The following classes are at fault: " . implode(', ', $badClasses);
$this->error($testDetails);
} else {
return true;
}
else return true;
}
function checkApacheVersion($testDetails) {
@ -753,7 +898,8 @@ class InstallRequirements {
$testDetails[2] .= " Directory '$path' not found. Please make sure you have uploaded the SilverStripe files to your webserver correctly.";
$this->error($testDetails);
} elseif(!file_exists($path . '/_config.php') && $dirname != 'mysite') {
$testDetails[2] .= " Directory '$path' exists, but is missing files. Please make sure you have uploaded the SilverStripe files to your webserver correctly.";
$testDetails[2] .= " Directory '$path' exists, but is missing files. Please make sure you have uploaded "
. "the SilverStripe files to your webserver correctly.";
$this->error($testDetails);
}
}
@ -784,10 +930,11 @@ class InstallRequirements {
$userID = posix_geteuid();
$user = posix_getpwuid($userID);
$currentOwnerID = fileowner(file_exists($filename) ? $filename : dirname($filename) );
$currentOwnerID = fileowner(file_exists($filename) ? $filename : dirname($filename));
$currentOwner = posix_getpwuid($currentOwnerID);
$testDetails[2] .= "User '$user[name]' needs to be able to write to this file:\n$filename\n\nThe file is currently owned by '$currentOwner[name]'. ";
$testDetails[2] .= "User '$user[name]' needs to be able to write to this file:\n$filename\n\nThe "
. "file is currently owned by '$currentOwner[name]'. ";
if($user['name'] == $currentOwner['name']) {
$testDetails[2] .= "We recommend that you make the file writeable.";
@ -800,10 +947,13 @@ class InstallRequirements {
if(in_array($currentOwner['name'], $groupInfo['members'])) $groupList[] = $groupInfo['name'];
}
if($groupList) {
$testDetails[2] .= " We recommend that you make the file group-writeable and change the group to one of these groups:\n - ". implode("\n - ", $groupList)
$testDetails[2] .= " We recommend that you make the file group-writeable "
. "and change the group to one of these groups:\n - " . implode("\n - ", $groupList)
. "\n\nFor example:\nchmod g+w $filename\nchgrp " . $groupList[0] . " $filename";
} else {
$testDetails[2] .= " There is no user-group that contains both the web-server user and the owner of this file. Change the ownership of the file, create a new group, or temporarily make the file writeable by everyone during the install process.";
$testDetails[2] .= " There is no user-group that contains both the web-server user and the "
. "owner of this file. Change the ownership of the file, create a new group, or "
. "temporarily make the file writeable by everyone during the install process.";
}
}
@ -1015,11 +1165,11 @@ class InstallRequirements {
function requireServerVariables($varNames, $testDetails) {
$this->testing($testDetails);
$missing = array();
foreach($varNames as $varName) {
if(!isset($_SERVER[$varName]) || !$_SERVER[$varName]) {
if(!isset($_SERVER[$varName]) || !$_SERVER[$varName]) {
$missing[] = '$_SERVER[' . $varName . ']';
}
}
}
if(!$missing) {
@ -1055,6 +1205,7 @@ class InstallRequirements {
// Must be PHP4 compatible
var $baseDir;
function getBaseDir() {
// Cache the value so that when the installer mucks with SCRIPT_FILENAME half way through, this method
// still returns the correct value.
@ -1107,22 +1258,23 @@ class Installer extends InstallRequirements {
}
function install($config) {
?>
<html>
<head>
<meta charset="utf-8" />
<title>Installing SilverStripe...</title>
<link rel="stylesheet" type="text/css" href="<?php echo FRAMEWORK_NAME; ?>/dev/install/css/install.css" />
<script src="<?php echo FRAMEWORK_NAME; ?>/thirdparty/jquery/jquery.js"></script>
</head>
<body>
?>
<html>
<head>
<meta charset="utf-8"/>
<title>Installing SilverStripe...</title>
<link rel="stylesheet" type="text/css" href="<?php echo FRAMEWORK_NAME; ?>/dev/install/css/install.css"/>
<script src="<?php echo FRAMEWORK_NAME; ?>/thirdparty/jquery/jquery.js"></script>
</head>
<body>
<div class="install-header">
<div class="inner">
<div class="brand">
<span class="logo"></span>
<h1>SilverStripe</h1>
</div>
</div>
</div>
</div>
<div id="Navigation">&nbsp;</div>
@ -1131,7 +1283,9 @@ class Installer extends InstallRequirements {
<div class="main">
<div class="inner">
<h2>Installing SilverStripe...</h2>
<p>I am now running through the installation steps (this should take about 30 seconds)</p>
<p>If you receive a fatal error, refresh this page to continue the installation</p>
<ul>
<?php
@ -1236,7 +1390,7 @@ PHP
);
}
if (!$this->checkModuleExists('cms')) {
if(!$this->checkModuleExists('cms')) {
$this->writeToFile("mysite/code/RootURLController.php", <<<PHP
<?php

View File

@ -64,14 +64,14 @@ see [Using development versions](#using-development-versions).
Composer isn't only used to download SilverStripe CMS, it can also be used to manage all SilverStripe modules. Installing a module can be done with the following command:
composer require silverstripe/forum:*
composer require "silverstripe/forum:*"
This will install the forum module in the latest compatible version.
By default, Composer updates other existing modules (like `framework` and `cms`),
and installs "dev" dependencies like PHPUnit. In case you don't need those dependencies,
use the following command instead:
composer require --no-update silverstripe/forum:*
composer require --no-update "silverstripe/forum:*"
composer update --no-dev
The `require` command has two parts. First is `silverstripe/forum`. This is the name of the package.

View File

@ -249,6 +249,32 @@ Here is a list of components for generic use:
- `[api:GridFieldPaginator]`
- `[api:GridFieldDetailForm]`
## Flexible Area Assignment through Fragments
GridField layouts can contain many components other than the table itself,
for example a search bar to find existing relations, a button to add those,
and buttons to export and print the current data. The GridField has certain
defined areas called "fragments" where these components can be placed.
The goal is for multiple components to share the same space, for example a header row.
Built-in components:
- `header`/`footer`: Renders in a `<thead>`/`<tfoot>`, should contain table markup
- `before`/`after`: Renders before/after the actual `<table>`
- `buttons-before-left`/`buttons-before-right`/`buttons-after-left`/`buttons-after-right`:
Renders in a shared row before the table. Requires [api:GridFieldButtonRow].
These built-ins can be used by passing the fragment names into the constructor
of various components. Note that some [api:GridFieldConfig] classes
will already have rows added to them. The following example will add a print button
at the bottom right of the table.
:::php
$config->addComponent(new GridFieldButtonRow('after'));
$config->addComponent(new GridFieldPrintButton('buttons-after-right'));
Further down we'll explain how to write your own components using fragments.
## Creating a custom GridFieldComponent
A single component often uses a number of interfaces.
@ -329,6 +355,42 @@ If you need more granular control, e.g. to consistently deny non-admins from del
records, use the `DataObject->can...()` methods
(see [DataObject permissions](/reference/dataobject#permissions)).
## Creating your own Fragments
Fragments are designated areas within a GridField which can be shared between component templates.
You can define your own fragments by using a `\$DefineFragment' placeholder in your components' template.
This example will simply create an area rendered before the table wrapped in a simple `<div>`.
:::php
class MyAreaComponent implements GridField_HTMLProvider {
public function getHTMLFragments( $gridField) {
return array(
'before' => '<div class="my-area">$DefineFragment(my-area)</div>'
);
}
}
We're returning raw HTML from the component, usually this would be handled by a SilverStripe template.
Please note that in templates, you'll need to escape the dollar sign on `$DefineFragment`:
These are specially processed placeholders as opposed to native template syntax.
Now you can add other components into this area by returning them as an array from
your [api:GridFieldComponent->getHTMLFragments()] implementation:
:::php
class MyShareLinkComponent implements GridField_HTMLProvider {
public function getHTMLFragments( $gridField) {
return array(
'my-area' => '<a href>...</a>'
);
}
}
Your new area can also be used by existing components, e.g. the [api:GridFieldPrintButton]
:::php
new GridFieldPrintButton('my-component-area')
## Related
* [ModelAdmin: A UI driven by GridField](/reference/modeladmin)

View File

@ -43,6 +43,7 @@ SilverStripe what values to include in the feed.
:::php
class Page_Controller extends ContentController {
private static $allowed_actions = array('rss');
public function init() {
// linkToFeed will add an appropriate HTML link tag to the website
// <head> tag to notify web browsers that an RSS feed is available
@ -80,6 +81,8 @@ updates. Update mysite/code/Page.php to something like this:
<?php
class Page extends SiteTree {}
class Page_Controller extends ContentController {
private static $allowed_actions = array('rss');
public function init() {
RSSFeed::linkToFeed($this->Link() . "rss", "10 Most Recently Updated Pages");
@ -123,6 +126,7 @@ for all the students as we've seen before.
:::php
class Page_Controller extends ContentController {
private static $allowed_actions = array('students');
public function init() {
RSSFeed::linkToFeed($this->Link("students"), "Students feed");
parent::init();
@ -172,4 +176,4 @@ accessing feeds from external sources.
## API Documentation
* `[api:RSSFeed]`
* `[api:RSSFeed]`

View File

@ -375,7 +375,7 @@ Data is defined in the static variable $db on each class, in the format:
"FirstName" => "Varchar",
"Surname" => "Varchar",
"Description" => "Text",
"Status" => "Enum('Active, Injured, Retired')",
"Status" => "Enum(array('Active', 'Injured', 'Retired'))",
"Birthday" => "Date"
);
}
@ -393,7 +393,7 @@ the default behavior by making a function called "get`<fieldname>`" or
:::php
class Player extends DataObject {
private static $db = array(
"Status" => "Enum('Active, Injured, Retired')"
"Status" => "Enum(array('Active', 'Injured', 'Retired'))"
);
// access through $myPlayer->Status

View File

@ -61,6 +61,13 @@ To let browsers know which language they're displaying a document in, you can de
Setting the '<html>' attribute is the most commonly used technique. There are other ways to specify content languages
(meta tags, HTTP headers), explained in this [w3.org article](http://www.w3.org/International/tutorials/language-decl/).
You can also set the [script direction](http://www.w3.org/International/questions/qa-scripts),
which is determined by the current locale, in order to indicate the preferred flow of characters
and default alignment of paragraphs and tables to browsers.
:::html
<html lang="$ContentLocale" dir="$i18nScriptDirection">
### Date and time formats
Formats can be set globally in the i18n class. These settings are currently only picked up by the CMS, you'll need

View File

@ -432,7 +432,7 @@ a [api:PasswordValidator]:
$validator = new PasswordValidator();
$validator->minLength(7);
$validator->checkHistoricalPasswords(6);
$validator->characterStrength('lowercase','uppercase','digits','punctuation');
$validator->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation"));
Member::set_password_validator($validator);
In addition, you can tighten password security with the following configuration settings:

View File

@ -130,7 +130,7 @@ class FieldList extends ArrayList {
// Check if a field by the same name exists in this tab
if($insertBefore) {
$tab->insertBefore($field, $insertBefore);
} elseif($tab->fieldByName($field->getName())) {
} elseif(($name = $field->getName()) && $tab->fieldByName($name)) {
// It exists, so we need to replace the old one
$this->replaceField($field->getName(), $field);
} else {

View File

@ -147,17 +147,17 @@ class FormField extends RequestHandler {
if($content || $tag != 'input') {
return "<$tag$preparedAttributes>$content</$tag>";
}
}
else {
return "<$tag$preparedAttributes />";
}
}
/**
* Create a new field.
* Creates a new field.
*
* @param string $name The internal field name, passed to forms.
* @param string $title The field label.
* @param string $title The human-readable field label.
* @param mixed $value The value of the field.
*/
public function __construct($name, $title = null, $value = null) {
@ -194,7 +194,7 @@ class FormField extends RequestHandler {
/**
* Returns the HTML ID for the form field holder element.
*
*
* @return string
*/
public function HolderID() {
@ -240,7 +240,7 @@ class FormField extends RequestHandler {
/**
* Returns the field message type, used by form validation.
*
*
* Arbitrary value which is mostly used for CSS classes in the rendered HTML,
* e.g. "required". Use {@link setError()} to set this property.
*
@ -495,7 +495,7 @@ class FormField extends RequestHandler {
/**
* Set the field value.
*
*
* @param mixed $value
*
* @return FormField.
@ -552,7 +552,7 @@ class FormField extends RequestHandler {
*/
public function securityTokenEnabled() {
$form = $this->getForm();
if(!$form) {
return false;
}
@ -605,7 +605,7 @@ class FormField extends RequestHandler {
/**
* Set name of template (without path or extension).
*
*
* Caution: Not consistently implemented in all subclasses, please check
* the {@link Field()} method on the subclass for support.
*
@ -838,8 +838,8 @@ class FormField extends RequestHandler {
} else {
$clone = $this->castedCopy('ReadonlyField');
$clone->setReadonly(true);
}
}
return $clone;
}

View File

@ -45,8 +45,8 @@ class MoneyField extends FormField {
*/
public function Field($properties = array()) {
return "<div class=\"fieldgroup\">" .
"<div class=\"fieldgroupField\">" . $this->fieldCurrency->SmallFieldHolder() . "</div>" .
"<div class=\"fieldgroupField\">" . $this->fieldAmount->SmallFieldHolder() . "</div>" .
"<div class=\"fieldgroup-field\">" . $this->fieldCurrency->SmallFieldHolder() . "</div>" .
"<div class=\"fieldgroup-field\">" . $this->fieldAmount->SmallFieldHolder() . "</div>" .
"</div>";
}

View File

@ -2468,6 +2468,25 @@ class i18n extends Object implements TemplateGlobalProvider {
public static function set_default_locale($locale) {
self::$default_locale = $locale;
}
/**
* Returns the script direction in format compatible with the HTML "dir" attribute.
*
* @see http://www.w3.org/International/tutorials/bidi-xhtml/
* @param String $locale Optional locale incl. region (underscored)
* @return String "rtl" or "ltr"
*/
public static function get_script_direction($locale = null) {
require_once 'Zend/Locale/Data.php';
if(!$locale) $locale = i18n::get_locale();
try {
$dir = Zend_Locale_Data::getList($locale, 'layout');
} catch(Zend_Locale_Exception $e) {
$dir = Zend_Locale_Data::getList(i18n::get_lang_from_locale($locale), 'layout');
}
return ($dir && $dir['characters'] == 'right-to-left') ? 'rtl' : 'ltr';
}
/**
* Includes all available language files for a certain defined locale.
@ -2506,7 +2525,8 @@ class i18n extends Object implements TemplateGlobalProvider {
$sortedModules = array();
foreach ($order as $module) {
if (isset($modules[$module])) $sortedModules[$module] = $modules[$module];
}
}
$sortedModules = array_reverse($sortedModules, true);
// Loop in reverse order, meaning the translator with the highest priority goes first
$translators = array_reverse(self::get_translators(), true);
@ -2587,6 +2607,7 @@ class i18n extends Object implements TemplateGlobalProvider {
return array(
'i18nLocale' => 'get_locale',
'get_locale',
'i18nScriptDirection' => 'get_script_direction',
);
}

View File

@ -348,7 +348,7 @@ en:
ERRORLOCKEDOUT2: 'Your account has been temporarily disabled because of too many failed attempts at logging in. Please try again in {count} minutes.'
ERRORNEWPASSWORD: 'You have entered your new password differently, try again'
ERRORPASSWORDNOTMATCH: 'Your current password does not match, please try again'
ERRORWRONGCRED: 'That doesn''t seem to be the right e-mail address or password. Please try again.'
ERRORWRONGCRED: 'The provided details don''t seem to be correct. Please try again.'
FIRSTNAME: 'First Name'
INTERFACELANG: 'Interface Language'
INVALIDNEWPASSWORD: 'We couldn''t accept that password: {password}'

View File

@ -2801,7 +2801,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @param string|array $limit A limit expression to be inserted into the LIMIT clause.
* @param string $containerClass The container class to return the results in.
*
* @return mixed The objects matching the filter, in the class specified by $containerClass
* @return DataList
*/
public static function get($callerClass = null, $filter = "", $sort = "", $join = "", $limit = null,
$containerClass = 'DataList') {

View File

@ -50,6 +50,13 @@ class Oembed {
protected static function find_endpoint($url) {
foreach(self::get_providers() as $scheme=>$endpoint) {
if(self::matches_scheme($url, $scheme)) {
$protocol = Director::is_https() ? 'https' : 'http';
if (is_array($endpoint)) {
if (array_key_exists($protocol, $endpoint)) $endpoint = $endpoint[$protocol];
else $endpoint = reset($endpoint);
}
return $endpoint;
}
}
@ -66,6 +73,7 @@ class Oembed {
protected static function matches_scheme($url, $scheme) {
$urlInfo = parse_url($url);
$schemeInfo = parse_url($scheme);
foreach($schemeInfo as $k=>$v) {
if(!array_key_exists($k, $urlInfo)) {
return false;

View File

@ -227,9 +227,10 @@ class Member extends DataObject implements TemplateGlobalProvider {
$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
if(!$e->check($this->Password, $password, $this->Salt, $this)) {
$iidentifierField =
$result->error(_t (
'Member.ERRORWRONGCRED',
'That doesn\'t seem to be the right e-mail address or password. Please try again.'
'Member.ERRORWRONGCREDS',
'The provided details don\'t seem to be correct. Please try again.'
));
}

View File

@ -7,7 +7,7 @@
* $pwdVal = new PasswordValidator();
* $pwdValidator->minLength(7);
* $pwdValidator->checkHistoricalPasswords(6);
* $pwdValidator->characterStrength('lowercase','uppercase','digits','punctuation');
* $pwdValidator->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation"));
*
* Member::set_password_validator($pwdValidator);
* </code>

View File

@ -344,6 +344,72 @@ class DirectorTest extends SapphireTest {
$_SERVER = $origServer;
}
public function testRequestFilterInDirectorTest() {
$filter = new TestRequestFilter;
$processor = new RequestProcessor(array($filter));
$currentProcessor = Injector::inst()->get('RequestProcessor');
Injector::inst()->registerService($processor, 'RequestProcessor');
$response = Director::test('some-dummy-url');
$this->assertEquals(1, $filter->preCalls);
$this->assertEquals(1, $filter->postCalls);
$filter->failPost = true;
$this->setExpectedException('SS_HTTPResponse_Exception');
$response = Director::test('some-dummy-url');
$this->assertEquals(2, $filter->preCalls);
$this->assertEquals(2, $filter->postCalls);
$filter->failPre = true;
$response = Director::test('some-dummy-url');
$this->assertEquals(3, $filter->preCalls);
// preCall 'false' will trigger an exception and prevent post call execution
$this->assertEquals(2, $filter->postCalls);
// swap back otherwise our wrapping test execution request may fail in the post processing later
Injector::inst()->registerService($currentProcessor, 'RequestProcessor');
}
}
class TestRequestFilter implements RequestFilter, TestOnly {
public $preCalls = 0;
public $postCalls = 0;
public $failPre = false;
public $failPost = false;
public function preRequest(\SS_HTTPRequest $request, \Session $session, \DataModel $model) {
++$this->preCalls;
if ($this->failPre) {
return false;
}
}
public function postRequest(\SS_HTTPRequest $request, \SS_HTTPResponse $response, \DataModel $model) {
++$this->postCalls;
if ($this->failPost) {
return false;
}
}
public function reset() {
$this->preCalls = 0;
$this->postCalls = 0;
}
}
class DirectorTestRequest_Controller extends Controller implements TestOnly {

235
tests/email/EmailTest.php Normal file
View File

@ -0,0 +1,235 @@
<?php
/**
* @package framework
* @subpackage tests
*/
class EmailTest extends SapphireTest {
public function testAttachFiles() {
$email = new Email();
$email->attachFileFromString('foo bar', 'foo.txt', 'text/plain');
$email->attachFile(__DIR__ . '/fixtures/attachment.txt', null, 'text/plain');
$this->assertEquals(
array('contents'=>'foo bar', 'filename'=>'foo.txt', 'mimetype'=>'text/plain'),
$email->attachments[0],
'File is attached correctly from string'
);
$this->assertEquals(
array('contents'=>'Hello, I\'m a text document.', 'filename'=>'attachment.txt', 'mimetype'=>'text/plain'),
$email->attachments[1],
'File is attached correctly from file'
);
}
public function testCustomHeaders() {
$email = new Email();
$email->addCustomHeader('Cc', 'test1@example.com');
$email->addCustomHeader('Bcc', 'test2@example.com');
$this->assertEmpty(
$email->customHeaders,
'addCustomHeader() doesn\'t add Cc and Bcc headers'
);
$email->addCustomHeader('Reply-To', 'test1@example.com');
$this->assertEquals(
array('Reply-To' => 'test1@example.com'),
$email->customHeaders,
'addCustomHeader() adds headers'
);
$email->addCustomHeader('Reply-To', 'test2@example.com');
$this->assertEquals(
array('Reply-To' => 'test1@example.com, test2@example.com'),
$email->customHeaders,
'addCustomHeader() appends data to existing headers'
);
}
public function testValidEmailAddress() {
$validEmails = array('test@example.com', 'test-123@example.sub.com');
$invalidEmails = array('foo.bar@', '@example.com', 'foo@');
foreach ($validEmails as $email) {
$this->assertEquals(
$email,
Email::validEmailAddress($email),
'validEmailAddress() returns a valid email address'
);
$this->assertEquals(
1,
Email::is_valid_address($email),
'is_valid_address() returns 1 for a valid email address'
);
}
foreach ($invalidEmails as $email) {
$this->assertFalse(
Email::validEmailAddress($email),
'validEmailAddress() returns false for an invalid email address'
);
$this->assertEquals(
0,
Email::is_valid_address($email),
'is_valid_address() returns 0 for an invalid email address'
);
}
}
public function testObfuscate() {
$emailAddress = 'test-1@example.com';
$direction = Email::obfuscate($emailAddress, 'direction');
$visible = Email::obfuscate($emailAddress, 'visible');
$hex = Email::obfuscate($emailAddress, 'hex');
$this->assertEquals(
'<span class="codedirection">moc.elpmaxe@1-tset</span>',
$direction,
'obfuscate() correctly reverses the email direction'
);
$this->assertEquals(
'test [dash] 1 [at] example [dot] com',
$visible,
'obfuscate() correctly obfuscates email characters'
);
$this->assertEquals(
'&#x74;&#x65;&#x73;&#x74;&#x2d;&#x31;&#x40;&#x65;&#x78;&#x61;&#x6d;&#x70;'
. '&#x6c;&#x65;&#x2e;&#x63;&#x6f;&#x6d;',
$hex,
'obfuscate() correctly returns hex representation of email'
);
}
public function testSendPlain() {
// Set custom $project - used in email headers
global $project;
$oldProject = $project;
$project = 'emailtest';
Email::set_mailer(new EmailTest_Mailer());
$email = new Email(
'from@example.com',
'to@example.com',
'Test send plain',
'Testing Email->sendPlain()',
null,
'cc@example.com',
'bcc@example.com'
);
$email->attachFile(__DIR__ . '/fixtures/attachment.txt', null, 'text/plain');
$email->addCustomHeader('foo', 'bar');
$sent = $email->sendPlain(123);
// Restore old project name after sending
$project = $oldProject;
$this->assertEquals('to@example.com', $sent['to']);
$this->assertEquals('from@example.com', $sent['from']);
$this->assertEquals('Test send plain', $sent['subject']);
$this->assertEquals('Testing Email->sendPlain()', $sent['content']);
$this->assertEquals(
array(
0 => array(
'contents'=>'Hello, I\'m a text document.',
'filename'=>'attachment.txt',
'mimetype'=>'text/plain'
)
),
$sent['files']
);
$this->assertEquals(
array(
'foo' => 'bar',
'X-SilverStripeMessageID' => 'emailtest.123',
'X-SilverStripeSite' => 'emailtest',
'Cc' => 'cc@example.com',
'Bcc' => 'bcc@example.com'
),
$sent['customheaders']
);
}
public function testSendHTML() {
// Set custom $project - used in email headers
global $project;
$oldProject = $project;
$project = 'emailtest';
Email::set_mailer(new EmailTest_Mailer());
$email = new Email(
'from@example.com',
'to@example.com',
'Test send plain',
'Testing Email->sendPlain()',
null,
'cc@example.com',
'bcc@example.com'
);
$email->attachFile(__DIR__ . '/fixtures/attachment.txt', null, 'text/plain');
$email->addCustomHeader('foo', 'bar');
$sent = $email->send(123);
// Restore old project name after sending
$project = $oldProject;
$this->assertEquals('to@example.com', $sent['to']);
$this->assertEquals('from@example.com', $sent['from']);
$this->assertEquals('Test send plain', $sent['subject']);
$this->assertContains('Testing Email->sendPlain()', $sent['content']);
$this->assertNull($sent['plaincontent']);
$this->assertEquals(
array(
0 => array(
'contents'=>'Hello, I\'m a text document.',
'filename'=>'attachment.txt',
'mimetype'=>'text/plain'
)
),
$sent['files']
);
$this->assertEquals(
array(
'foo' => 'bar',
'X-SilverStripeMessageID' => 'emailtest.123',
'X-SilverStripeSite' => 'emailtest',
'Cc' => 'cc@example.com',
'Bcc' => 'bcc@example.com'
),
$sent['customheaders']
);
}
}
class EmailTest_Mailer extends Mailer {
public function sendHTML($to, $from, $subject, $htmlContent, $attachedFiles = false, $customheaders = false,
$plainContent = false) {
return array(
'to' => $to,
'from' => $from,
'subject' => $subject,
'content' => $htmlContent,
'files' => $attachedFiles,
'customheaders' => $customheaders,
'plaincontent' => $plainContent
);
}
public function sendPlain($to, $from, $subject, $plainContent, $attachedFiles = false, $customheaders = false) {
return array(
'to' => $to,
'from' => $from,
'subject' => $subject,
'content' => $plainContent,
'files' => $attachedFiles,
'customheaders' => $customheaders
);
}
}

View File

@ -0,0 +1 @@
Hello, I'm a text document.

View File

@ -48,6 +48,43 @@ class FieldListTest extends SapphireTest {
/* We'll have 3 fields inside the tab */
$this->assertEquals(3, $tab->Fields()->Count());
}
/**
* Test that groups can be added to a fieldlist
*/
public function testFieldgroup() {
$fields = new FieldList();
$tab = new Tab('Root');
$fields->push($tab);
$fields->addFieldsToTab('Root', array(
$group1 = new FieldGroup(
new TextField('Name'),
new EmailField('Email')
),
$group2 = new FieldGroup(
new TextField('Company'),
new TextareaField('Address')
)
));
/* Check that the field objects were created */
$this->assertNotNull($fields->dataFieldByName('Name'));
$this->assertNotNull($fields->dataFieldByName('Email'));
$this->assertNotNull($fields->dataFieldByName('Company'));
$this->assertNotNull($fields->dataFieldByName('Address'));
/* The field objects in the set should be the same as the ones we created */
$this->assertSame($fields->dataFieldByName('Name'), $group1->fieldByName('Name'));
$this->assertSame($fields->dataFieldByName('Email'), $group1->fieldByName('Email'));
$this->assertSame($fields->dataFieldByName('Company'), $group2->fieldByName('Company'));
$this->assertSame($fields->dataFieldByName('Address'), $group2->fieldByName('Address'));
/* We'll have 2 fields directly inside the tab */
$this->assertEquals(2, $tab->Fields()->Count());
}
/**
* Test removing a single field from a tab in a set.

View File

@ -1,3 +1,4 @@
de:
i18nTestModule:
OTHERENTITY: Other Entity (de)
PRIORITYNOTICE: High Module Priority (de)
OTHERENTITY: Other Entity (de)

View File

@ -11,5 +11,6 @@ de:
WITHNAMESPACE: Include Entity with Namespace (de)
LAYOUTTEMPLATE: Layout Template (de)
SPRINTFNAMESPACE: My replacement: %s (de)
PRIORITYNOTICE: Low Module Priority (de)
i18nTestModuleInclude.ss:
SPRINTFINCLUDENAMESPACE: My include replacement: %s (de)
SPRINTFINCLUDENAMESPACE: My include replacement: %s (de)

View File

@ -435,6 +435,9 @@ class i18nTest extends SapphireTest {
$this->assertFalse($adapter->isTranslated('i18nTestModule.ENTITY', 'af'),
'Non-existing unloaded entity not available before call'
);
// set _fakewebroot module priority
Config::inst()->update('i18n', 'module_priority', array('subfolder','i18ntestmodule'));
i18n::include_by_locale('de');
@ -444,6 +447,11 @@ class i18nTest extends SapphireTest {
$this->assertTrue($adapter->isTranslated('i18nTestTheme1.LAYOUTTEMPLATE', null, 'de'), 'Includes theme files');
$this->assertTrue($adapter->isTranslated('i18nTestModule.OTHERENTITY', null, 'de'), 'Includes submodule files');
// check module priority
$this->assertEquals($adapter->translate('i18nTestModule.PRIORITYNOTICE', 'de'),
'High Module Priority (de)'
);
SS_ClassLoader::instance()->popManifest();
}

View File

@ -1,6 +1,16 @@
<?php
class OembedTest extends SapphireTest {
public function setUp() {
parent::setUp();
Config::nest();
}
public function tearDown() {
Config::unnest();
parent::tearDown();
}
public function testGetOembedFromUrl() {
Config::inst()->update('Oembed', 'providers', array(
'http://*.silverstripe.com/watch*'=>'http://www.silverstripe.com/oembed/'
@ -37,4 +47,53 @@ class OembedTest extends SapphireTest {
$this->assertEquals($query['maxheight'], 'foo', 'Magically creates maxheight option');
$this->assertEquals($query['maxwidth'], 'bar', 'Magically creates maxwidth option');
}
public function testRequestProtocolReflectedInGetOembedFromUrl() {
Config::inst()->update('Oembed', 'providers', array(
'http://*.silverstripe.com/watch*'=> array(
'http' => 'http://www.silverstripe.com/oembed/',
'https' => 'https://www.silverstripe.com/oembed/?scheme=https',
),
'https://*.silverstripe.com/watch*'=> array(
'http' => 'http://www.silverstripe.com/oembed/',
'https' => 'https://www.silverstripe.com/oembed/?scheme=https',
)
));
Config::inst()->update('Director', 'alternate_protocol', 'http');
foreach(array('http', 'https') as $protocol) {
$url = $protocol.'://www.silverstripe.com/watch12345';
$result = Oembed::get_oembed_from_url($url);
$this->assertInstanceOf('Oembed_Result', $result);
$this->assertEquals($result->getOembedURL(),
'http://www.silverstripe.com/oembed/?format=json&url='.urlencode($url),
'Returns http based URLs when request is over http, regardless of source URL');
}
Config::inst()->update('Director', 'alternate_protocol', 'https');
foreach(array('http', 'https') as $protocol) {
$url = $protocol.'://www.silverstripe.com/watch12345';
$result = Oembed::get_oembed_from_url($url);
$this->assertInstanceOf('Oembed_Result', $result);
$this->assertEquals($result->getOembedURL(),
'https://www.silverstripe.com/oembed/?scheme=https&format=json&url='.urlencode($url),
'Returns https based URLs when request is over https, regardless of source URL');
}
Config::inst()->update('Director', 'alternate_protocol', 'foo');
foreach(array('http', 'https') as $protocol) {
$url = $protocol.'://www.silverstripe.com/watch12345';
$result = Oembed::get_oembed_from_url($url);
$this->assertInstanceOf('Oembed_Result', $result);
$this->assertEquals($result->getOembedURL(),
'http://www.silverstripe.com/oembed/?format=json&url='.urlencode($url),
'When request protocol doesn\'t have specific handler, fall back to first option');
}
}
}