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 - 5.3
env: 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: matrix:
include: include:
@ -16,7 +22,7 @@ matrix:
env: DB=MYSQL CORE_RELEASE=master env: DB=MYSQL CORE_RELEASE=master
- php: 5.5 - php: 5.5
env: DB=MYSQL CORE_RELEASE=master env: DB=MYSQL CORE_RELEASE=master
- php: 5.3 - php: 5.4
env: DB=MYSQL CORE_RELEASE=master BEHAT_TEST=1 env: DB=MYSQL CORE_RELEASE=master BEHAT_TEST=1
before_script: 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\" = \"\" ]; 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" - "if [ \"$BEHAT_TEST\" = \"1\" ]; then php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss --require silverstripe/behat-extension; fi"
- cd ~/builds/ss - cd ~/builds/ss
- php ~/travis-support/travis_setup_selenium.php --base-url http://localhost --if-env BEHAT_TEST - php ~/travis-support/travis_setup_selenium.php --if-env BEHAT_TEST
- php ~/travis-support/travis_setup_apache.php --if-env BEHAT_TEST - php ~/travis-support/travis_setup_php54_webserver.php --if-env BEHAT_TEST
script: script:
- "if [ \"$BEHAT_TEST\" = \"\" ]; then phpunit framework/tests; fi" - "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: after_failure:
- "if [ \"$BEHAT_TEST\" = \"1\" ]; then sudo cat /var/log/apache2/error.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/
- "if [ \"$BEHAT_TEST\" = \"1\" ]; then sudo cat /var/log/apache2/access.log; fi"
branches: branches:
except: except:

View File

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

View File

@ -15,8 +15,9 @@ class CMSForm extends Form {
* @return boolean * @return boolean
*/ */
public function validate() { public function validate() {
$buttonClicked = $this->buttonClicked();
return ( return (
in_array($this->buttonClicked()->actionName(), $this->getValidationExemptActions()) ($buttonClicked && in_array($buttonClicked->actionName(), $this->getValidationExemptActions()))
|| parent::validate() || 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 { /* 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 label { float: none; width: auto; font-size: 11px; padding: 0 8px 4px 0; }
.cms-content-tools .field .middleColumn { margin: 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 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 { padding: 0 0 8px; }
.cms-content-tools .field.checkbox input { margin: 2px 0; } .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 { display: none; }
.cms-tree.multiple li > a > .jstree-icon.jstree-checkbox { display: inline-block; } .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.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-icon { background-image: none !important; }
.cms-tree a.jstree-loading .jstree-pageicon { background: url(../images/throbber.gif) top left no-repeat; } .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'); var url = $(node).find('a:first').attr('href');
if(url && url != '#') { 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) // Deselect all nodes (will be reselected after load according to form state)
self.jstree('deselect_all'); self.jstree('deselect_all');
self.jstree('uncheck_all'); self.jstree('uncheck_all');

View File

@ -46,8 +46,7 @@
.jstree(this.getTreeConfig()) .jstree(this.getTreeConfig())
.bind('loaded.jstree', function(e, data) { .bind('loaded.jstree', function(e, data) {
self.setIsLoaded(true); self.setIsLoaded(true);
self.updateFromEditForm();
self.css('visibility', 'visible');
// Add ajax settings after init period to avoid unnecessary initial ajax load // Add ajax settings after init period to avoid unnecessary initial ajax load
// of existing tree in DOM - see load_node_html() // of existing tree in DOM - see load_node_html()
data.inst._set_settings({'html_data': {'ajax': { data.inst._set_settings({'html_data': {'ajax': {
@ -61,6 +60,9 @@
return params; return params;
} }
}}}); }}});
self.updateFromEditForm();
self.css('visibility', 'visible');
// Only show checkboxes with .multiple class // Only show checkboxes with .multiple class
data.inst.hide_checkboxes(); data.inst.hide_checkboxes();
@ -239,8 +241,8 @@
* Creates a new node from the given HTML. * Creates a new node from the given HTML.
* Wrapping around jstree API because we want the flexibility to define * Wrapping around jstree API because we want the flexibility to define
* the node's <li> ourselves. Places the node in the tree * the node's <li> ourselves. Places the node in the tree
* according to data.ParentID * according to data.ParentID.
* *
* Parameters: * Parameters:
* (String) HTML New node content (<li>) * (String) HTML New node content (<li>)
* (Object) Map of additional data, e.g. ParentID * (Object) Map of additional data, e.g. ParentID
@ -248,7 +250,7 @@
*/ */
createNode: function(html, data, callback) { createNode: function(html, data, callback) {
var self = this, var self = this,
parentNode = data.ParentID ? self.find('li[data-id='+data.ParentID+']') : false, parentNode = data.ParentID ? self.getNodeByID(data.ParentID) : false,
newNode = $(html); newNode = $(html);
this.jstree( this.jstree(
@ -281,9 +283,9 @@
updateNode: function(node, html, data) { updateNode: function(node, html, data) {
var self = this, newNode = $(html), origClasses = node.attr('class'); var self = this, newNode = $(html), origClasses = node.attr('class');
var nextNode = data.NextID ? this.find('li[data-id='+data.NextID+']') : false; var nextNode = data.NextID ? this.getNodeByID(data.NextID) : false;
var prevNode = data.PrevID ? this.find('li[data-id='+data.PrevID+']') : false; var prevNode = data.PrevID ? this.getNodeByID(data.PrevID) : false;
var parentNode = data.ParentID ? this.find('li[data-id='+data.ParentID+']') : false; var parentNode = data.ParentID ? this.getNodeByID(data.ParentID) : false;
// Copy attributes. We can't replace the node completely // Copy attributes. We can't replace the node completely
// without removing or detaching its children nodes. // without removing or detaching its children nodes.
@ -346,8 +348,22 @@
updateNodesFromServer: function(ids) { updateNodesFromServer: function(ids) {
if(this.getIsUpdatingTree() || !this.getIsLoaded()) return; if(this.getIsUpdatingTree() || !this.getIsLoaded()) return;
var self = this, includesNewNode = false; var self = this, i, includesNewNode = false;
this.setIsUpdatingTree(true); 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 // TODO 'initially_opened' config doesn't apply here
self.jstree('open_node', this.getNodeByID(0)); self.jstree('open_node', this.getNodeByID(0));
@ -367,15 +383,6 @@
return; 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 // Check if node exists, create if necessary
if(node.length) { if(node.length) {
self.updateNode(node, nodeData.html, nodeData); self.updateNode(node, nodeData.html, nodeData);
@ -384,9 +391,20 @@
}, 500); }, 500);
} else { } else {
includesNewNode = true; 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() { complete: function() {
self.setIsUpdatingTree(false); self.setIsUpdatingTree(false);
} }
}); });
} }
}); });

View File

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

View File

@ -798,6 +798,10 @@ body.cms {
margin: 2px 0; margin: 2px 0;
} }
} }
.description {
margin-left: 0;
}
} }
.fieldgroup { .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 // Show the loading indicator on the page icon rather than the default
// jstree icon (which is only used for its dragging handles) // jstree icon (which is only used for its dragging handles)
a.jstree-loading{ a.jstree-loading{

View File

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

View File

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

View File

@ -42,7 +42,16 @@ class Director implements TemplateGlobalProvider {
* @var array * @var array
*/ */
private static $test_servers = 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 * @config
* @var string * @var string
@ -258,8 +267,18 @@ class Director implements TemplateGlobalProvider {
$request = new SS_HTTPRequest($httpMethod, $url, $getVars, $postVars, $body); $request = new SS_HTTPRequest($httpMethod, $url, $getVars, $postVars, $body);
if($headers) foreach($headers as $k => $v) $request->addHeader($k, $v); 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 // 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 // Ensure that the result is an SS_HTTPResponse object
if(is_string($result)) { 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 // Restore the superglobals
$_REQUEST = $existingRequestVars; $_REQUEST = $existingRequestVars;
$_GET = $existingGetVars; $_GET = $existingGetVars;
@ -443,6 +467,10 @@ class Director implements TemplateGlobalProvider {
* @return boolean * @return boolean
*/ */
public static function is_https() { public static function is_https() {
if ($protocol = Config::inst()->get('Director', 'alternate_protocol')) {
return $protocol == 'https';
}
if(isset($_SERVER['HTTP_X_FORWARDED_PROTOCOL'])) { if(isset($_SERVER['HTTP_X_FORWARDED_PROTOCOL'])) {
if(strtolower($_SERVER['HTTP_X_FORWARDED_PROTOCOL']) == 'https') { if(strtolower($_SERVER['HTTP_X_FORWARDED_PROTOCOL']) == 'https') {
return true; return true;

View File

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

View File

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

View File

@ -94,7 +94,7 @@ class SS_Log {
// Add default context (shouldn't change until the actual log event happens) // Add default context (shouldn't change until the actual log event happens)
foreach(static::$log_globals as $globalName => $keys) { foreach(static::$log_globals as $globalName => $keys) {
foreach($keys as $key) { 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); 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); error_reporting(E_ALL | E_STRICT);
// Attempt to start a session so that the username and password can be sent back to the user. // 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(); session_start();
} }
@ -45,36 +45,35 @@ $dirsToCheck = array(
dirname($_SERVER['SCRIPT_FILENAME']) dirname($_SERVER['SCRIPT_FILENAME'])
); );
//if they are the same, remove one of them //if they are the same, remove one of them
if ($dirsToCheck[0] == $dirsToCheck[1]) { if($dirsToCheck[0] == $dirsToCheck[1]) {
unset($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) //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 // or until we hit a dir we can't read
do { do {
//add the trailing slash we need to concatenate properly //add the trailing slash we need to concatenate properly
$dir .= DIRECTORY_SEPARATOR; $dir .= DIRECTORY_SEPARATOR;
//if it's readable, go ahead //if it's readable, go ahead
if (@is_readable($dir)) { if(@is_readable($dir)) {
//if the file exists, then we include it, set relevant vars and break out //if the file exists, then we include it, set relevant vars and break out
if (file_exists($dir . $envFile)) { if(file_exists($dir . $envFile)) {
include_once($dir . $envFile); include_once($dir . $envFile);
$envFileExists = true; $envFileExists = true;
//legacy variable assignment //legacy variable assignment
$usingEnv = true; $usingEnv = true;
//break out of BOTH loops because we found the $envFile //break out of BOTH loops because we found the $envFile
break(2); break(2);
} }
} } else {
else {
//break out of the while loop, we can't read the dir //break out of the while loop, we can't read the dir
break; break;
} }
//go up a directory //go up a directory
$dir = dirname($dir); $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 // not the same, if they are, we have hit the root of the drive
} while (dirname($dir) != $dir); } while(dirname($dir) != $dir);
} }
if($envFileExists) { 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 // Set default locale, but try and sniff from the user agent
$defaultLocale = 'en_US'; $defaultLocale = 'en_US';
$locales = array( $locales = array(
'af_ZA' => 'Afrikaans (South Africa)', 'af_ZA' => 'Afrikaans (South Africa)',
'ar_EG' => 'Arabic (Egypt)', 'ar_EG' => 'Arabic (Egypt)',
'hy_AM' => 'Armenian (Armenia)', 'hy_AM' => 'Armenian (Armenia)',
'ast_ES' => 'Asturian (Spain)', 'ast_ES' => 'Asturian (Spain)',
'az_AZ' => 'Azerbaijani (Azerbaijan)', 'az_AZ' => 'Azerbaijani (Azerbaijan)',
'bs_BA' => 'Bosnian (Bosnia and Herzegovina)', 'bs_BA' => 'Bosnian (Bosnia and Herzegovina)',
'bg_BG' => 'Bulgarian (Bulgaria)', 'bg_BG' => 'Bulgarian (Bulgaria)',
'ca_ES' => 'Catalan (Spain)', 'ca_ES' => 'Catalan (Spain)',
'zh_CN' => 'Chinese (China)', 'zh_CN' => 'Chinese (China)',
'zh_TW' => 'Chinese (Taiwan)', 'zh_TW' => 'Chinese (Taiwan)',
'hr_HR' => 'Croatian (Croatia)', 'hr_HR' => 'Croatian (Croatia)',
'cs_CZ' => 'Czech (Czech Republic)', 'cs_CZ' => 'Czech (Czech Republic)',
'da_DK' => 'Danish (Denmark)', 'da_DK' => 'Danish (Denmark)',
'nl_NL' => 'Dutch (Netherlands)', 'nl_NL' => 'Dutch (Netherlands)',
'en_GB' => 'English (United Kingdom)', 'en_GB' => 'English (United Kingdom)',
'en_US' => 'English (United States)', 'en_US' => 'English (United States)',
'eo_XX' => 'Esperanto', 'eo_XX' => 'Esperanto',
'et_EE' => 'Estonian (Estonia)', 'et_EE' => 'Estonian (Estonia)',
'fo_FO' => 'Faroese (Faroe Islands)', 'fo_FO' => 'Faroese (Faroe Islands)',
'fi_FI' => 'Finnish (Finland)', 'fi_FI' => 'Finnish (Finland)',
'fr_FR' => 'French (France)', 'fr_FR' => 'French (France)',
'de_DE' => 'German (Germany)', 'de_DE' => 'German (Germany)',
'el_GR' => 'Greek (Greece)', 'el_GR' => 'Greek (Greece)',
'he_IL' => 'Hebrew (Israel)', 'he_IL' => 'Hebrew (Israel)',
'hu_HU' => 'Hungarian (Hungary)', 'hu_HU' => 'Hungarian (Hungary)',
'is_IS' => 'Icelandic (Iceland)', 'is_IS' => 'Icelandic (Iceland)',
'id_ID' => 'Indonesian (Indonesia)', 'id_ID' => 'Indonesian (Indonesia)',
'it_IT' => 'Italian (Italy)', 'it_IT' => 'Italian (Italy)',
'ja_JP' => 'Japanese (Japan)', 'ja_JP' => 'Japanese (Japan)',
'km_KH' => 'Khmer (Cambodia)', 'km_KH' => 'Khmer (Cambodia)',
'lc_XX' => 'LOLCAT', 'lc_XX' => 'LOLCAT',
'lv_LV' => 'Latvian (Latvia)', 'lv_LV' => 'Latvian (Latvia)',
'lt_LT' => 'Lithuanian (Lithuania)', 'lt_LT' => 'Lithuanian (Lithuania)',
'ms_MY' => 'Malay (Malaysia)', 'ms_MY' => 'Malay (Malaysia)',
'mi_NZ' => 'Maori (New Zealand)', 'mi_NZ' => 'Maori (New Zealand)',
'ne_NP' => 'Nepali (Nepal)', 'ne_NP' => 'Nepali (Nepal)',
'nb_NO' => 'Norwegian', 'nb_NO' => 'Norwegian',
'fa_IR' => 'Persian (Iran)', 'fa_IR' => 'Persian (Iran)',
'pl_PL' => 'Polish (Poland)', 'pl_PL' => 'Polish (Poland)',
'pt_BR' => 'Portuguese (Brazil)', 'pt_BR' => 'Portuguese (Brazil)',
'pa_IN' => 'Punjabi (India)', 'pa_IN' => 'Punjabi (India)',
'ro_RO' => 'Romanian (Romania)', 'ro_RO' => 'Romanian (Romania)',
'ru_RU' => 'Russian (Russia)', 'ru_RU' => 'Russian (Russia)',
'sr_RS' => 'Serbian (Serbia)', 'sr_RS' => 'Serbian (Serbia)',
'si_LK' => 'Sinhalese (Sri Lanka)', 'si_LK' => 'Sinhalese (Sri Lanka)',
'sk_SK' => 'Slovak (Slovakia)', 'sk_SK' => 'Slovak (Slovakia)',
'sl_SI' => 'Slovenian (Slovenia)', 'sl_SI' => 'Slovenian (Slovenia)',
'es_AR' => 'Spanish (Argentina)', 'es_AR' => 'Spanish (Argentina)',
'es_MX' => 'Spanish (Mexico)', 'es_MX' => 'Spanish (Mexico)',
'es_ES' => 'Spanish (Spain)', 'es_ES' => 'Spanish (Spain)',
'sv_SE' => 'Swedish (Sweden)', 'sv_SE' => 'Swedish (Sweden)',
'th_TH' => 'Thai (Thailand)', 'th_TH' => 'Thai (Thailand)',
'tr_TR' => 'Turkish (Turkey)', 'tr_TR' => 'Turkish (Turkey)',
'uk_UA' => 'Ukrainian (Ukraine)', 'uk_UA' => 'Ukrainian (Ukraine)',
'uz_UZ' => 'Uzbek (Uzbekistan)', 'uz_UZ' => 'Uzbek (Uzbekistan)',
'vi_VN' => 'Vietnamese (Vietnam)', 'vi_VN' => 'Vietnamese (Vietnam)',
); );
// Discover which databases are available // 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)) { if(preg_match("/\\\$database\s*=\s*[^\n\r]+[\n\r]/", file_get_contents("mysite/_config.php"), $parts)) {
eval($parts[0]); eval($parts[0]);
if($database) $alreadyInstalled = true; if($database) $alreadyInstalled = true;
// Assume that if $databaseConfig is defined in mysite/_config.php, then a non-environment-based installation has // Assume that if $databaseConfig is defined in mysite/_config.php, then a non-environment-based installation has
// already gone ahead // already gone ahead
} else if(preg_match("/\\\$databaseConfig\s*=\s*[^\n\r]+[\n\r]/", file_get_contents("mysite/_config.php"), $parts)) { } else if(preg_match("/\\\$databaseConfig\s*=\s*[^\n\r]+[\n\r]/", file_get_contents("mysite/_config.php"), $parts)) {
$alreadyInstalled = true; $alreadyInstalled = true;
} }
@ -412,7 +411,12 @@ class InstallRequirements {
$isIIS = $this->isIIS(7); $isIIS = $this->isIIS(7);
$webserver = $this->findWebserver(); $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 // Check that we can identify the root folder successfully
$this->requireFile(FRAMEWORK_NAME . '/dev/install/config-form.html', array("File permissions", $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?")); $this->requireModule(FRAMEWORK_NAME, array("File permissions", FRAMEWORK_NAME . "/ directory exists?"));
if($isApache) { 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)); $this->requireWriteable('.htaccess', array("File permissions", "Is the .htaccess file writeable?", null));
} elseif($isIIS) { } elseif($isIIS) {
$this->requireWriteable('web.config', array("File permissions", "Is the web.config file writeable?", null)); $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)); $this->requireWriteable('mysite/_config.php', array(
if (!$this->checkModuleExists('cms')) { "File permissions",
$this->requireWriteable('mysite/code/RootURLController.php', array("File permissions", "Is the mysite/code/RootURLController.php file writeable?", null)); "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)); $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)); $this->requireTempFolder(array('File permissions', 'Is a temporary directory available?', null, $tempFolder));
if($tempFolder) { if($tempFolder) {
// in addition to the temp folder being available, check it is writable // 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 // Check for web server, unless we're calling the installer from the command-line
$this->isRunningWebServer(array("Webserver Configuration", "Server software", "Unknown", $webserver)); $this->isRunningWebServer(array("Webserver Configuration", "Server software", "Unknown", $webserver));
if($isApache) { 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) { } 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 { } 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 // Check for GD support
if(!$this->requireFunction("imagecreatetruecolor", array("PHP Configuration", "GD2 support", "PHP must have GD version 2."))) { if(!$this->requireFunction("imagecreatetruecolor", array(
$this->requireFunction("imagecreate", array("PHP Configuration", "GD2 support", "GD support for PHP not included.")); "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 // Check for XML support
$this->requireFunction('xml_set_object', array("PHP Configuration", "XML support", "XML support not included in PHP.")); $this->requireFunction('xml_set_object', array(
$this->requireClass('DOMDocument', array("PHP Configuration", "DOM/XML support", "DOM/XML support not included in PHP.")); "PHP Configuration",
$this->requireFunction('simplexml_load_file', array('PHP Configuration', 'SimpleXML support', 'SimpleXML support not included in PHP.')); "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 // 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 // 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 // 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 // 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 // Check for hash support
$this->requireFunction('hash', array('PHP Configuration', 'hash support', 'hash support not included in PHP.')); $this->requireFunction('hash', array('PHP Configuration', 'hash support', 'hash support not included in PHP.'));
// Check for mbstring support // 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 // 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 // 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->suggestPHPSetting('asp_tags', array(false), array(
$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.')); 'PHP Configuration',
$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.')); '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 // 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; return $this->errors;
} }
@ -517,6 +654,7 @@ class InstallRequirements {
// special case for display_errors, check the original value before // special case for display_errors, check the original value before
// it was changed at the start of this script. // it was changed at the start of this script.
if($settingName = 'display_errors') { if($settingName = 'display_errors') {
global $originalDisplayErrorsValue;
$val = $originalDisplayErrorsValue; $val = $originalDisplayErrorsValue;
} else { } else {
$val = ini_get($settingName); $val = ini_get($settingName);
@ -582,7 +720,8 @@ class InstallRequirements {
$testDetails[2] .= " You only have " . ini_get("memory_limit") . " allocated"; $testDetails[2] .= " You only have " . ini_get("memory_limit") . " allocated";
$this->warning($testDetails); $this->warning($testDetails);
} elseif($mem == 0) { } 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); $this->warning($testDetails);
} }
} }
@ -590,15 +729,15 @@ class InstallRequirements {
function getPHPMemory() { function getPHPMemory() {
$memString = ini_get("memory_limit"); $memString = ini_get("memory_limit");
switch(strtolower(substr($memString,-1))) { switch(strtolower(substr($memString, -1))) {
case "k": case "k":
return round(substr($memString,0,-1)*1024); return round(substr($memString, 0, -1) * 1024);
case "m": case "m":
return round(substr($memString,0,-1)*1024*1024); return round(substr($memString, 0, -1) * 1024 * 1024);
case "g": case "g":
return round(substr($memString,0,-1)*1024*1024*1024); return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
default: default:
return round($memString); return round($memString);
@ -620,7 +759,8 @@ class InstallRequirements {
$id = strtolower(str_replace(' ', '_', $section)); $id = strtolower(str_replace(' ', '_', $section));
echo "<table id=\"{$id}_results\" class=\"testResults\" width=\"100%\">"; echo "<table id=\"{$id}_results\" class=\"testResults\" width=\"100%\">";
foreach($tests as $test => $result) { 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>"; echo "</table>";
@ -642,7 +782,8 @@ class InstallRequirements {
break; 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"; $className = "good";
$text = "All Requirements Pass"; $text = "All Requirements Pass";
@ -652,11 +793,10 @@ class InstallRequirements {
$className = "error"; $className = "error";
$pluralWarnings = ($warningRequirements == 1) ? 'Warning' : 'Warnings'; $pluralWarnings = ($warningRequirements == 1) ? 'Warning' : 'Warnings';
$text = $failedRequirements . ' Failed and '. $warningRequirements . ' '. $pluralWarnings; $text = $failedRequirements . ' Failed and ' . $warningRequirements . ' ' . $pluralWarnings;
} } else if($warningRequirements > 0) {
else if($warningRequirements > 0) {
$className = "warning"; $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>"; 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) { function requireFunction($funcName, $testDetails) {
$this->testing($testDetails); $this->testing($testDetails);
if(!function_exists($funcName)) { if(!function_exists($funcName)) {
$this->error($testDetails); $this->error($testDetails);
} else {
return true;
} }
else return true;
} }
function requireClass($className, $testDetails) { function requireClass($className, $testDetails) {
$this->testing($testDetails); $this->testing($testDetails);
if(!class_exists($className)) $this->error($testDetails); if(!class_exists($className)) {
else return false; $this->error($testDetails);
} else {
return false;
}
} }
/** /**
@ -694,8 +838,9 @@ class InstallRequirements {
if($badClasses) { if($badClasses) {
$testDetails[2] .= ". The following classes are at fault: " . implode(', ', $badClasses); $testDetails[2] .= ". The following classes are at fault: " . implode(', ', $badClasses);
$this->error($testDetails); $this->error($testDetails);
} else {
return true;
} }
else return true;
} }
function checkApacheVersion($testDetails) { 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."; $testDetails[2] .= " Directory '$path' not found. Please make sure you have uploaded the SilverStripe files to your webserver correctly.";
$this->error($testDetails); $this->error($testDetails);
} elseif(!file_exists($path . '/_config.php') && $dirname != 'mysite') { } 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); $this->error($testDetails);
} }
} }
@ -784,10 +930,11 @@ class InstallRequirements {
$userID = posix_geteuid(); $userID = posix_geteuid();
$user = posix_getpwuid($userID); $user = posix_getpwuid($userID);
$currentOwnerID = fileowner(file_exists($filename) ? $filename : dirname($filename) ); $currentOwnerID = fileowner(file_exists($filename) ? $filename : dirname($filename));
$currentOwner = posix_getpwuid($currentOwnerID); $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']) { if($user['name'] == $currentOwner['name']) {
$testDetails[2] .= "We recommend that you make the file writeable."; $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(in_array($currentOwner['name'], $groupInfo['members'])) $groupList[] = $groupInfo['name'];
} }
if($groupList) { 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"; . "\n\nFor example:\nchmod g+w $filename\nchgrp " . $groupList[0] . " $filename";
} else { } 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) { function requireServerVariables($varNames, $testDetails) {
$this->testing($testDetails); $this->testing($testDetails);
$missing = array(); $missing = array();
foreach($varNames as $varName) { foreach($varNames as $varName) {
if(!isset($_SERVER[$varName]) || !$_SERVER[$varName]) { if(!isset($_SERVER[$varName]) || !$_SERVER[$varName]) {
$missing[] = '$_SERVER[' . $varName . ']'; $missing[] = '$_SERVER[' . $varName . ']';
} }
} }
if(!$missing) { if(!$missing) {
@ -1055,6 +1205,7 @@ class InstallRequirements {
// Must be PHP4 compatible // Must be PHP4 compatible
var $baseDir; var $baseDir;
function getBaseDir() { function getBaseDir() {
// Cache the value so that when the installer mucks with SCRIPT_FILENAME half way through, this method // Cache the value so that when the installer mucks with SCRIPT_FILENAME half way through, this method
// still returns the correct value. // still returns the correct value.
@ -1107,22 +1258,23 @@ class Installer extends InstallRequirements {
} }
function install($config) { function install($config) {
?> ?>
<html> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8"/>
<title>Installing SilverStripe...</title> <title>Installing SilverStripe...</title>
<link rel="stylesheet" type="text/css" href="<?php echo FRAMEWORK_NAME; ?>/dev/install/css/install.css" /> <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> <script src="<?php echo FRAMEWORK_NAME; ?>/thirdparty/jquery/jquery.js"></script>
</head> </head>
<body> <body>
<div class="install-header"> <div class="install-header">
<div class="inner"> <div class="inner">
<div class="brand"> <div class="brand">
<span class="logo"></span> <span class="logo"></span>
<h1>SilverStripe</h1> <h1>SilverStripe</h1>
</div> </div>
</div> </div>
</div> </div>
<div id="Navigation">&nbsp;</div> <div id="Navigation">&nbsp;</div>
@ -1131,7 +1283,9 @@ class Installer extends InstallRequirements {
<div class="main"> <div class="main">
<div class="inner"> <div class="inner">
<h2>Installing SilverStripe...</h2> <h2>Installing SilverStripe...</h2>
<p>I am now running through the installation steps (this should take about 30 seconds)</p> <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> <p>If you receive a fatal error, refresh this page to continue the installation</p>
<ul> <ul>
<?php <?php
@ -1236,7 +1390,7 @@ PHP
); );
} }
if (!$this->checkModuleExists('cms')) { if(!$this->checkModuleExists('cms')) {
$this->writeToFile("mysite/code/RootURLController.php", <<<PHP $this->writeToFile("mysite/code/RootURLController.php", <<<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 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. This will install the forum module in the latest compatible version.
By default, Composer updates other existing modules (like `framework` and `cms`), 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, and installs "dev" dependencies like PHPUnit. In case you don't need those dependencies,
use the following command instead: use the following command instead:
composer require --no-update silverstripe/forum:* composer require --no-update "silverstripe/forum:*"
composer update --no-dev composer update --no-dev
The `require` command has two parts. First is `silverstripe/forum`. This is the name of the package. 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:GridFieldPaginator]`
- `[api:GridFieldDetailForm]` - `[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 ## Creating a custom GridFieldComponent
A single component often uses a number of interfaces. 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 records, use the `DataObject->can...()` methods
(see [DataObject permissions](/reference/dataobject#permissions)). (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 ## Related
* [ModelAdmin: A UI driven by GridField](/reference/modeladmin) * [ModelAdmin: A UI driven by GridField](/reference/modeladmin)

View File

@ -43,6 +43,7 @@ SilverStripe what values to include in the feed.
:::php :::php
class Page_Controller extends ContentController { class Page_Controller extends ContentController {
private static $allowed_actions = array('rss');
public function init() { public function init() {
// linkToFeed will add an appropriate HTML link tag to the website // linkToFeed will add an appropriate HTML link tag to the website
// <head> tag to notify web browsers that an RSS feed is available // <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 <?php
class Page extends SiteTree {} class Page extends SiteTree {}
class Page_Controller extends ContentController { class Page_Controller extends ContentController {
private static $allowed_actions = array('rss');
public function init() { public function init() {
RSSFeed::linkToFeed($this->Link() . "rss", "10 Most Recently Updated Pages"); RSSFeed::linkToFeed($this->Link() . "rss", "10 Most Recently Updated Pages");
@ -123,6 +126,7 @@ for all the students as we've seen before.
:::php :::php
class Page_Controller extends ContentController { class Page_Controller extends ContentController {
private static $allowed_actions = array('students');
public function init() { public function init() {
RSSFeed::linkToFeed($this->Link("students"), "Students feed"); RSSFeed::linkToFeed($this->Link("students"), "Students feed");
parent::init(); parent::init();
@ -172,4 +176,4 @@ accessing feeds from external sources.
## API Documentation ## 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", "FirstName" => "Varchar",
"Surname" => "Varchar", "Surname" => "Varchar",
"Description" => "Text", "Description" => "Text",
"Status" => "Enum('Active, Injured, Retired')", "Status" => "Enum(array('Active', 'Injured', 'Retired'))",
"Birthday" => "Date" "Birthday" => "Date"
); );
} }
@ -393,7 +393,7 @@ the default behavior by making a function called "get`<fieldname>`" or
:::php :::php
class Player extends DataObject { class Player extends DataObject {
private static $db = array( private static $db = array(
"Status" => "Enum('Active, Injured, Retired')" "Status" => "Enum(array('Active', 'Injured', 'Retired'))"
); );
// access through $myPlayer->Status // 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 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/). (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 ### 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 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 = new PasswordValidator();
$validator->minLength(7); $validator->minLength(7);
$validator->checkHistoricalPasswords(6); $validator->checkHistoricalPasswords(6);
$validator->characterStrength('lowercase','uppercase','digits','punctuation'); $validator->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation"));
Member::set_password_validator($validator); Member::set_password_validator($validator);
In addition, you can tighten password security with the following configuration settings: 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 // Check if a field by the same name exists in this tab
if($insertBefore) { if($insertBefore) {
$tab->insertBefore($field, $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 // It exists, so we need to replace the old one
$this->replaceField($field->getName(), $field); $this->replaceField($field->getName(), $field);
} else { } else {

View File

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

View File

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

View File

@ -2468,6 +2468,25 @@ class i18n extends Object implements TemplateGlobalProvider {
public static function set_default_locale($locale) { public static function set_default_locale($locale) {
self::$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. * Includes all available language files for a certain defined locale.
@ -2506,7 +2525,8 @@ class i18n extends Object implements TemplateGlobalProvider {
$sortedModules = array(); $sortedModules = array();
foreach ($order as $module) { foreach ($order as $module) {
if (isset($modules[$module])) $sortedModules[$module] = $modules[$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 // Loop in reverse order, meaning the translator with the highest priority goes first
$translators = array_reverse(self::get_translators(), true); $translators = array_reverse(self::get_translators(), true);
@ -2587,6 +2607,7 @@ class i18n extends Object implements TemplateGlobalProvider {
return array( return array(
'i18nLocale' => 'get_locale', 'i18nLocale' => 'get_locale',
'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.' 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' ERRORNEWPASSWORD: 'You have entered your new password differently, try again'
ERRORPASSWORDNOTMATCH: 'Your current password does not match, please 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' FIRSTNAME: 'First Name'
INTERFACELANG: 'Interface Language' INTERFACELANG: 'Interface Language'
INVALIDNEWPASSWORD: 'We couldn''t accept that password: {password}' 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|array $limit A limit expression to be inserted into the LIMIT clause.
* @param string $containerClass The container class to return the results in. * @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, public static function get($callerClass = null, $filter = "", $sort = "", $join = "", $limit = null,
$containerClass = 'DataList') { $containerClass = 'DataList') {

View File

@ -50,6 +50,13 @@ class Oembed {
protected static function find_endpoint($url) { protected static function find_endpoint($url) {
foreach(self::get_providers() as $scheme=>$endpoint) { foreach(self::get_providers() as $scheme=>$endpoint) {
if(self::matches_scheme($url, $scheme)) { 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; return $endpoint;
} }
} }
@ -66,6 +73,7 @@ class Oembed {
protected static function matches_scheme($url, $scheme) { protected static function matches_scheme($url, $scheme) {
$urlInfo = parse_url($url); $urlInfo = parse_url($url);
$schemeInfo = parse_url($scheme); $schemeInfo = parse_url($scheme);
foreach($schemeInfo as $k=>$v) { foreach($schemeInfo as $k=>$v) {
if(!array_key_exists($k, $urlInfo)) { if(!array_key_exists($k, $urlInfo)) {
return false; return false;

View File

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

View File

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

View File

@ -344,6 +344,72 @@ class DirectorTest extends SapphireTest {
$_SERVER = $origServer; $_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 { 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 */ /* We'll have 3 fields inside the tab */
$this->assertEquals(3, $tab->Fields()->Count()); $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. * Test removing a single field from a tab in a set.

View File

@ -1,3 +1,4 @@
de: de:
i18nTestModule: 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) WITHNAMESPACE: Include Entity with Namespace (de)
LAYOUTTEMPLATE: Layout Template (de) LAYOUTTEMPLATE: Layout Template (de)
SPRINTFNAMESPACE: My replacement: %s (de) SPRINTFNAMESPACE: My replacement: %s (de)
PRIORITYNOTICE: Low Module Priority (de)
i18nTestModuleInclude.ss: 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'), $this->assertFalse($adapter->isTranslated('i18nTestModule.ENTITY', 'af'),
'Non-existing unloaded entity not available before call' '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'); 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('i18nTestTheme1.LAYOUTTEMPLATE', null, 'de'), 'Includes theme files');
$this->assertTrue($adapter->isTranslated('i18nTestModule.OTHERENTITY', null, 'de'), 'Includes submodule 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(); SS_ClassLoader::instance()->popManifest();
} }

View File

@ -1,6 +1,16 @@
<?php <?php
class OembedTest extends SapphireTest { class OembedTest extends SapphireTest {
public function setUp() {
parent::setUp();
Config::nest();
}
public function tearDown() {
Config::unnest();
parent::tearDown();
}
public function testGetOembedFromUrl() { public function testGetOembedFromUrl() {
Config::inst()->update('Oembed', 'providers', array( Config::inst()->update('Oembed', 'providers', array(
'http://*.silverstripe.com/watch*'=>'http://www.silverstripe.com/oembed/' '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['maxheight'], 'foo', 'Magically creates maxheight option');
$this->assertEquals($query['maxwidth'], 'bar', 'Magically creates maxwidth 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');
}
}
} }