Merge remote-tracking branch 'origin/3.1' into 3

Conflicts:
	admin/code/ModelAdmin.php
	control/Director.php
	model/SQLQuery.php
	security/Member.php
	tests/control/HTTPTest.php
	tests/model/SQLQueryTest.php
	tests/security/SecurityTest.php
	tests/view/SSViewerTest.php
This commit is contained in:
Damian Mooyman 2015-03-31 19:54:15 +13:00
commit 43f49e8434
28 changed files with 450 additions and 106 deletions

View File

@ -322,25 +322,28 @@ abstract class ModelAdmin extends LeftAndMain {
* @return Form
*/
public function ImportForm() {
$modelName = $this->modelClass;
$modelSNG = singleton($this->modelClass);
$modelName = $modelSNG->i18n_singular_name();
// check if a import form should be generated
if(!$this->showImportForm || (is_array($this->showImportForm) && !in_array($modelName,$this->showImportForm))) {
if(!$this->showImportForm ||
(is_array($this->showImportForm) && !in_array($this->modelClass, $this->showImportForm))
) {
return false;
}
$importers = $this->getModelImporters();
if(!$importers || !isset($importers[$modelName])) return false;
if(!$importers || !isset($importers[$this->modelClass])) return false;
if(!singleton($modelName)->canCreate(Member::currentUser())) return false;
if(!$modelSNG->canCreate(Member::currentUser())) return false;
$fields = new FieldList(
new HiddenField('ClassName', _t('ModelAdmin.CLASSTYPE'), $modelName),
new HiddenField('ClassName', _t('ModelAdmin.CLASSTYPE'), $this->modelClass),
new FileField('_CsvFile', false)
);
// get HTML specification for each import (column names etc.)
$importerClass = $importers[$modelName];
$importer = new $importerClass($modelName);
$importerClass = $importers[$this->modelClass];
$importer = new $importerClass($this->modelClass);
$spec = $importer->getImportSpec();
$specFields = new ArrayList();
foreach($spec['fields'] as $name => $desc) {

View File

@ -438,12 +438,21 @@ class Director implements TemplateGlobalProvider {
public static function absoluteURL($url, $relativeToSiteBase = false) {
if(!isset($_SERVER['REQUEST_URI'])) return false;
//a url of . or ./ is the same as an empty url
if ($url == '.' || $url == './') {
$url = '';
}
if(strpos($url,'/') === false && !$relativeToSiteBase) {
$url = dirname($_SERVER['REQUEST_URI'] . 'x') . '/' . $url;
//if there's no URL we want to force a trailing slash on the link
if (!$url) {
$url = '/';
}
$url = Controller::join_links(dirname($_SERVER['REQUEST_URI'] . 'x'), $url);
}
if(substr($url,0,4) != "http") {
if($url[0] != "/") $url = Director::baseURL() . $url;
if(strpos($url, '/') !== 0) $url = Director::baseURL() . $url;
// Sometimes baseURL() can return a full URL instead of just a path
if(substr($url,0,4) != "http") $url = self::protocolAndHost() . $url;
}
@ -802,14 +811,10 @@ class Director implements TemplateGlobalProvider {
* @param string $destURL - The URL to redirect to
*/
protected static function force_redirect($destURL) {
$response = new SS_HTTPResponse(
"<h1>Your browser is not accepting header redirects</h1>".
"<p>Please <a href=\"$destURL\">click here</a>",
301
);
$response = new SS_HTTPResponse();
$response->redirect($destURL, 301);
HTTP::add_cache_headers($response);
$response->addHeader('Location', $destURL);
// TODO: Use an exception - ATM we can be called from _config.php, before Director#handleRequest's try block
$response->output();

View File

@ -361,7 +361,8 @@ class HTTP {
if(
$body &&
Director::is_https() &&
strstr($_SERVER["HTTP_USER_AGENT"], 'MSIE')==true &&
isset($_SERVER['HTTP_USER_AGENT']) &&
strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')==true &&
strstr($contentDisposition, 'attachment;')==true
) {
// IE6-IE8 have problems saving files when https and no-cache are used

View File

@ -230,15 +230,32 @@ class Convert {
/**
* Converts an XML string to a PHP array
* See http://phpsecurity.readthedocs.org/en/latest/Injection-Attacks.html#xml-external-entity-injection
*
* @uses recursiveXMLToArray()
* @param string
*
* @param string $val
* @param boolean $disableDoctypes Disables the use of DOCTYPE, and will trigger an error if encountered.
* false by default.
* @param boolean $disableExternals Disables the loading of external entities. false by default.
* @return array
*/
public static function xml2array($val) {
$xml = new SimpleXMLElement($val);
return self::recursiveXMLToArray($xml);
public static function xml2array($val, $disableDoctypes = false, $disableExternals = false) {
// Check doctype
if($disableDoctypes && preg_match('/\<\!DOCTYPE.+]\>/', $val)) {
throw new InvalidArgumentException('XML Doctype parsing disabled');
}
// Disable external entity loading
if($disableExternals) $oldVal = libxml_disable_entity_loader($disableExternals);
try {
$xml = new SimpleXMLElement($val);
$result = self::recursiveXMLToArray($xml);
} catch(Exception $ex) {
if($disableExternals) libxml_disable_entity_loader($oldVal);
throw $ex;
}
if($disableExternals) libxml_disable_entity_loader($oldVal);
return $result;
}
/**

View File

@ -12,7 +12,7 @@ information.
All data tables in SilverStripe are defined as subclasses of [api:DataObject]. The [api:DataObject] class represents a
single row in a database table, following the ["Active Record"](http://en.wikipedia.org/wiki/Active_record_pattern)
design pattern. Database Columns are is defined as [Data Types](data_types_and_casting) in the static `$db` variable
design pattern. Database Columns are defined as [Data Types](data_types_and_casting) in the static `$db` variable
along with any [relationships](relations) defined as `$has_one`, `$has_many`, `$many_many` properties on the class.
Let's look at a simple example:
@ -401,7 +401,7 @@ Remove both Sam and Sig..
'Surname' => 'Minnée',
));
And removing Sig and Sam with that are either age 17 or 74.
And removing Sig and Sam with that are either age 17 or 43.
:::php
$players = Player::get()->exclude(array(
@ -409,7 +409,7 @@ And removing Sig and Sam with that are either age 17 or 74.
'Age' => array(17, 43)
));
// SELECT * FROM Player WHERE ("FirstName" NOT IN ('Sam','Sig) OR "Age" NOT IN ('17', '74));
// SELECT * FROM Player WHERE ("FirstName" NOT IN ('Sam','Sig) OR "Age" NOT IN ('17', '43'));
You can use [SearchFilters](searchfilters) to add additional behavior to your `exclude` command.
@ -548,7 +548,7 @@ The data for the following classes would be stored across the following tables:
- LastEdited: Datetime
- Title: Varchar
- Content: Text
NewsArticle:
NewsPage:
- ID: Int
- Summary: Text
@ -558,7 +558,7 @@ Accessing the data is transparent to the developer.
$news = NewsPage::get();
foreach($news as $article) {
echo $news->Title;
echo $article->Title;
}
The way the ORM stores the data is this:
@ -575,7 +575,7 @@ example above, NewsSection didn't have its own data, so an extra table would be
* In all the tables, ID is the primary key. A matching ID number is used for all parts of a particular record:
record #2 in Page refers to the same object as record #2 in `[api:SiteTree]`.
To retrieve a news article, SilverStripe joins the [api:SiteTree], [api:Page] and NewsArticle tables by their ID fields.
To retrieve a news article, SilverStripe joins the [api:SiteTree], [api:Page] and NewsPage tables by their ID fields.
## Related Documentation

View File

@ -5,7 +5,7 @@ summary: Add Indexes to your Data Model to optimize database queries.
It is sometimes desirable to add indexes to your data model, whether to optimize queries or add a uniqueness constraint
to a field. This is done through the `DataObject::$indexes` map, which maps index names to descriptor arrays that
represent each index. There's several supported notations:
represent each index. There're several supported notations:
:::php
<?php
@ -19,7 +19,7 @@ represent each index. There's several supported notations:
);
}
The `<index-name>` can be an an arbitrary identifier in order to allow for more than one index on a specific database
The `<index-name>` can be an arbitrary identifier in order to allow for more than one index on a specific database
column. The "advanced" notation supports more `<type>` notations. These vary between database drivers, but all of them
support the following:
@ -27,8 +27,8 @@ support the following:
* `unique`: Index plus uniqueness constraint on the value
* `fulltext`: Fulltext content index
In order to use more database specific or complex index notations, we also support raw SQL for as a value in the
`$indexes` definition. Keep in mind this will likely make your code less portable between databases.
In order to use more database specific or complex index notations, we also support raw SQL as a value in the
`$indexes` definition. Keep in mind that using raw SQL is likely to make your code less portable between DBMSs.
**mysite/code/MyTestObject.php**
@ -52,4 +52,4 @@ In order to use more database specific or complex index notations, we also suppo
## API Documentation
* [api:DataObject]
* [api:DataObject]

View File

@ -1,8 +1,8 @@
# Dynamic Default Values
The [api:DataObject::$defaults] array allows you to specify simple static values to be the default value for when a
record is created, but in many situations default values needs to be dynamically calculated. In order to do this, the
`[api:DataObject->populateDefaults()]` method will need to be overloaded.
The [api:DataObject::$defaults] array allows you to specify simple static values to be the default values when a
record is created, but in many situations default values need to be dynamically calculated. In order to do this, the
[api:DataObject->populateDefaults()] method will need to be overloaded.
This method is called whenever a new record is instantiated, and you must be sure to call the method on the parent
object!
@ -33,4 +33,4 @@ methods. For example:
$this->FullTitle = $this->Title;
}
parent::populateDefaults();
}
}

View File

@ -39,7 +39,7 @@ routing.
</div>
<div class="alert" markdown="1">
Make sure that after you have modified the `routes.yml` file, that you clear your SilverStripe caches using `flush=1`.
Make sure that after you have modified the `routes.yml` file, that you clear your SilverStripe caches using `?flush=1`.
</div>
**mysite/_config/routes.yml**
@ -70,7 +70,7 @@ Action methods can return one of four main things:
* an array. In this case the values in the array are available in the templates and the controller completes as usual by returning a [api:SS_HTTPResponse] with the body set to the current template.
* `HTML`. SilverStripe will wrap the `HTML` into a `SS_HTTPResponse` and set the status code to 200.
* an [api:SS_HTTPResponse] containing a manually defined `status code` and `body`.
* an [api:SS_HTTPResponse_Exception]. A special type of response which indicates a error. By returning the exception, the execution pipeline can adapt and display any error handlers.
* an [api:SS_HTTPResponse_Exception]. A special type of response which indicates an error. By returning the exception, the execution pipeline can adapt and display any error handlers.
**mysite/code/controllers/TeamController.php**
@ -144,7 +144,7 @@ If a template of that name does not exist, then SilverStripe will fall back to t
Controller actions can use `renderWith` to override this template selection process as in the previous example with
`htmlaction`. `MyCustomTemplate.ss` would be used rather than `TeamsController`.
For more information about templates, inheritance and how to rendering into views, See the
For more information about templates, inheritance and how to render into views, See the
[Templates and Views](../templates) documentation.
## Link
@ -154,7 +154,7 @@ Each controller should define a `Link()` method. This should be used to avoid ha
**mysite/code/controllers/TeamController.php**
:::php
public function Link($action = null) {
public function Link($action = null) {
return Controller::join_links('teams', $action);
}

View File

@ -45,7 +45,7 @@ In practice, this looks like:
FormAction::create("doSayHello")->setTitle("Say hello")
);
$required = new RequiredFields('Name')
$required = new RequiredFields('Name');
$form = new Form($this, 'HelloForm', $fields, $actions, $required);

View File

@ -16,7 +16,7 @@ throughout the site. Out of the box this includes selecting the current site the
<% with $SiteConfig %>
$Title $AnotherField
<% end_loop %>
<% end_with %>
To access variables in the PHP:
@ -61,12 +61,12 @@ Then activate the extension.
<div class="notice" markdown="1">
After adding the class and the YAML change, make sure to rebuild your database by visiting http://yoursite.com/dev/build.
You may also need to reload the screen with a `flush=1` i.e http://yoursite.com/admin/settings?flush=1.
You may also need to reload the screen with a `?flush=1` i.e http://yoursite.com/admin/settings?flush=1.
</div>
You can define as many extensions for `SiteConfig` as you need. For example, if you're developing a module and what to
You can define as many extensions for `SiteConfig` as you need. For example, if you're developing a module and want to
provide the users a place to configure settings then the `SiteConfig` panel is the place to go it.
## API Documentation
* `[api:SiteConfig]`
* `[api:SiteConfig]`

View File

@ -12,7 +12,7 @@ response and modify the session within a test.
:::php
<?php
class HomePageTest extends SapphireTest {
class HomePageTest extends FunctionalTest {
/**
* Test generation of the view
@ -24,7 +24,7 @@ response and modify the session within a test.
$this->assertEquals(200, $page->getStatusCode());
// We should see a login form
$login = $this->submitForm("#LoginForm", null, array(
$login = $this->submitForm("LoginFormID", null, array(
'Email' => 'test@test.com',
'Password' => 'wrongpassword'
));

View File

@ -0,0 +1,71 @@
# 3.1.11
# Overview
This release resolves a high level security issue in the SiteTree class, as well as
the CMS controller classes which act on these objects during creation.
This release also resolves an issue affecting GridField on sites running in
an environment with Suhosin enabled.
## Upgrading
### SiteTree::canCreate Permissions
Any user code which overrides the `SiteTree::canCreate` method should be investigated to
ensure it continues to work correctly. In particular, a second parameter may now be passed
to this method in order to determine if page creation is allowed in any given context, whether
it be at the root level, or as a child of a parent page.
The creation of pages at the root level is now corrected to follow the rules specified
by the SiteConfig, which in turn has been updated to ensure only valid CMS users are
granted this permission (when applicable).
The creation of pages beneath parent pages will now inherit from the ability to edit
this parent page.
User code which is not updated, but relies on the old implementation of SiteTree::canCreate will
now assume creation at the top level.
For example see the below code as an example
E.g.
:::php
<?php
class SingletonPage extends Page {
public function canCreate($member) {
if(static::get()->count()) return false;
$context = func_num_args() > 1 ? func_get_arg(1) : array();
return parent::canCreate($member, $context);
}
}
For more information on the reason for this change please see the security announcement below.
## Security
* 2015-03-11 [3df41e1](https://github.com/silverstripe/silverstripe-cms/commit/3df41e1) Fix SiteTree / SiteConfig permissions (Damian Mooyman) - See announcement [ss-2015-008](http://www.silverstripe.org/software/download/security-releases/ss-2015-008-sitetree-creation-permission-vulnerability)
### Bugfixes
* 2015-03-09 [1770fab](https://github.com/silverstripe/sapphire/commit/1770fab) Fix gridfield generating invalid session keys (Damian Mooyman)
* 2015-03-05 [87adc44](https://github.com/silverstripe/sapphire/commit/87adc44) Fix serialised stateid exceeding request length (Damian Mooyman)
* 2015-03-04 [eb35f26](https://github.com/silverstripe/sapphire/commit/eb35f26) Corrected padding on non-sortable columns. (Sam Minnee)
* 2015-03-03 [6e0afd5](https://github.com/silverstripe/sapphire/commit/6e0afd5) Prevent unnecessary call to config system which doesn't exist yet (micmania1)
* 2015-03-03 [4709b90](https://github.com/silverstripe/sapphire/commit/4709b90) UploadField description alignment (Loz Calver)
* 2015-03-02 [f234301](https://github.com/silverstripe/sapphire/commit/f234301) DataQuery::applyRelation using incorrect foreign key (fixes #3954) (Loz Calver)
* 2015-03-02 [f9d493d](https://github.com/silverstripe/sapphire/commit/f9d493d) Fixes case insensitive search for postgres databases (Jean-Fabien Barrois)
* 2015-02-27 [4c5a07e](https://github.com/silverstripe/sapphire/commit/4c5a07e) Updated docs (Michael Strong)
* 2015-02-25 [3a7e24a](https://github.com/silverstripe/sapphire/commit/3a7e24a) Unable to access a list of all many_many_extraFields (Loz Calver)
* 2015-02-13 [998c055](https://github.com/silverstripe/sapphire/commit/998c055) Misleading error message in SSViewer (Loz Calver)
* 2015-02-10 [bbe2799](https://github.com/silverstripe/sapphire/commit/bbe2799) Use correct query when searching for items managed by a tree dropdown field #3173 (Jean-Fabien Barrois)
* 2015-01-13 [ab24ed3](https://github.com/silverstripe/sapphire/commit/ab24ed3) Use i18n_plural_name() instead of plural_name() (Elvinas L.)
* 2014-11-17 [a142ffd](https://github.com/silverstripe/silverstripe-cms/commit/a142ffd) VirtualPages use correct casting for 'virtual' database fields (Loz Calver)
## Changelog
* [framework](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.1.11)
* [cms](https://github.com/silverstripe/silverstripe-cms/releases/tag/3.1.11)
* [installer](https://github.com/silverstripe/silverstripe-installer/releases/tag/3.1.11)

View File

@ -0,0 +1,43 @@
# 3.1.12
# Overview
This security release resolves some XSS and an XML vulnerability in the Framework.
## Upgrading
If your code relies on `Convert::xml2array` there are some important things to consider with regards to
certain vulnerabilities. In this release additional options have been added to this method to assist
users in guarding against these risks, although each option has been turned off by default.
Please refer to http://phpsecurity.readthedocs.org/en/latest/Injection-Attacks.html#xml-external-entity-injection
on details of some of the specific reasons behind the need for these changes and how you can guard
against them in your code.
Specifically this method has these two new parameters:
* The `$disableDoctypes` parameter has been added to disallow parsing of XML content containing
a <!DOCTYPE > header, which may potentially contain unguarded or recursive entity definitions.
* The `$disableExternals` parameter allows XML parsing to ignore any externally referenced
dependency within the file, ensuring that injected XML is unable to invoke data from potentially
hazardous sources.
## Security
* 2015-03-20 [ee9bddb](https://github.com/silverstripe/sapphire/commit/ee9bddb) Fix SS-2015-010 (Damian Mooyman) - See announcement [ss-2015-010](http://www.silverstripe.org/software/download/security-releases/ss-2015-010-xss-in-directorforce-redirect)
* 2015-03-20 [7f983c2](https://github.com/silverstripe/sapphire/commit/7f983c2) Fix SS-2014-017 (Damian Mooyman) - See announcement [ss-2014-017](http://www.silverstripe.org/software/download/security-releases/ss-2014-017-xml-quadratic-blowup-attack)
* 2015-03-20 [604c328](https://github.com/silverstripe/sapphire/commit/604c328) Fixed XSS vulnerability relating to rewrite_hash (Christopher Pitt) - See announcements [ss-2014-015](http://www.silverstripe.org/software/download/security-releases/ss-2014-015-ie-requests-not-properly-behaving-with-rewritehashlinks), [ss-2015-009](http://www.silverstripe.org/software/download/security-releases/ss-2015-009-xss-in-rewritten-hash-links)
### Bugfixes
* 2015-03-18 [b34c236](https://github.com/silverstripe/sapphire/commit/b34c236) Fix joins on tables containing "select" being mistaken for sub-selects Fix PHPDoc on SQLQuery::addFrom and SQLQuery::setFrom Fixes #3965 (Damian Mooyman)
* 2015-03-11 [a61c08d](https://github.com/silverstripe/sapphire/commit/a61c08d) Security::$default_message_set Config value unusable (Loz Calver)
* 2015-03-10 [9651889](https://github.com/silverstripe/sapphire/commit/9651889) Fix yaml generation to conform to version 1.1, accepted by transifex (Damian Mooyman)
* 2015-02-25 [f5f41b2](https://github.com/silverstripe/sapphire/commit/f5f41b2) Ensuring custom CMS validator uses Object-&gt;hasMethod() to respect extension decorator pattern. (Patrick Nelson)
* 2015-01-13 [9da7e90](https://github.com/silverstripe/silverstripe-cms/commit/9da7e90) . Missing translation entity (Elvinas L.)
## Changelog
* [framework](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.1.12)
* [cms](https://github.com/silverstripe/silverstripe-cms/releases/tag/3.1.12)
* [installer](https://github.com/silverstripe/silverstripe-installer/releases/tag/3.1.12)

View File

@ -188,6 +188,7 @@ class GridFieldDetailForm implements GridField_URLHandler {
*/
public function setItemEditFormCallback(Closure $cb) {
$this->itemEditFormCallback = $cb;
return $this;
}
/**

View File

@ -136,6 +136,12 @@
$('.ss-gridfield .action').entwine({
onclick: function(e){
var filterState='show'; //filterstate should equal current state.
// If the button is disabled, do nothing.
if (this.button('option', 'disabled')) {
e.preventDefault();
return;
}
if(this.hasClass('ss-gridfield-button-close') || !(this.closest('.ss-gridfield').hasClass('show-filter'))){
filterState='hidden';
@ -146,6 +152,34 @@
}
});
/**
* Don't allow users to submit empty values in grid field auto complete inputs.
*/
$('.ss-gridfield .add-existing-autocompleter').entwine({
onbuttoncreate: function () {
var self = this;
this.toggleDisabled();
this.find('input[type="text"]').on('keyup', function () {
self.toggleDisabled();
});
},
onunmatch: function () {
this.find('input[type="text"]').off('keyup');
},
toggleDisabled: function () {
var $button = this.find('.ss-ui-button'),
$input = this.find('input[type="text"]'),
inputHasValue = $input.val() !== '',
buttonDisabled = $button.is(':disabled');
if ((inputHasValue && buttonDisabled) || (!inputHasValue && !buttonDisabled)) {
$button.button("option", "disabled", !buttonDisabled);
}
}
});
// Covers both tabular delete button, and the button on the detail form
$('.ss-gridfield .col-buttons .action.gridfield-button-delete, .cms-edit-form .Actions button.action.action-delete').entwine({
onclick: function(e){

View File

@ -271,8 +271,13 @@ abstract class SQLConditionalExpression extends SQLExpression {
$filter = "(" . implode(") AND (", $join['filter']) . ")";
}
$table = strpos(strtoupper($join['table']), 'SELECT') ? $join['table'] : "\"" . $join['table'] . "\"";
$aliasClause = ($alias != $join['table']) ? " AS \"$alias\"" : "";
// Ensure tables are quoted, unless the table is actually a sub-select
$table = preg_match('/\bSELECT\b/i', $join['table'])
? $join['table']
: "\"{$join['table']}\"";
$aliasClause = ($alias != $join['table'])
? " AS \"{$alias}\""
: "";
$joins[$alias] = strtoupper($join['type']) . " JOIN " . $table . "$aliasClause ON $filter";
if(!empty($join['parameters'])) {
$parameters = array_merge($parameters, $join['parameters']);

View File

@ -50,9 +50,19 @@ class BasicAuth {
$isRunningTests = (class_exists('SapphireTest', false) && SapphireTest::is_running_test());
if(!Security::database_is_ready() || (Director::is_cli() && !$isRunningTests)) return true;
/*
* Enable HTTP Basic authentication workaround for PHP running in CGI mode with Apache
* Depending on server configuration the auth header may be in HTTP_AUTHORIZATION or
* REDIRECT_HTTP_AUTHORIZATION
*
* The follow rewrite rule must be in the sites .htaccess file to enable this workaround
* RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
*/
$authHeader = (isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] :
(isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) ? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] : null));
$matches = array();
if (isset($_SERVER['HTTP_AUTHORIZATION']) &&
preg_match('/Basic\s+(.*)$/i', $_SERVER['HTTP_AUTHORIZATION'], $matches)) {
if ($authHeader &&
preg_match('/Basic\s+(.*)$/i', $authHeader, $matches)) {
list($name, $password) = explode(':', base64_decode($matches[1]));
$_SERVER['PHP_AUTH_USER'] = strip_tags($name);
$_SERVER['PHP_AUTH_PW'] = strip_tags($password);

View File

@ -514,39 +514,44 @@ class Member extends DataObject implements TemplateGlobalProvider {
// Don't bother trying this multiple times
self::$_already_tried_to_auto_log_in = true;
if(strpos(Cookie::get('alc_enc'), ':') && !Session::get("loggedInAs")) {
list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
if(strpos(Cookie::get('alc_enc'), ':') === false
|| Session::get("loggedInAs")
|| !Security::database_is_ready()
) {
return;
}
$member = DataObject::get_by_id("Member", $uid);
list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
// check if autologin token matches
if($member) {
$hash = $member->encryptWithUserSettings($token);
if(!$member->RememberLoginToken || $member->RememberLoginToken !== $hash) {
$member = null;
}
$member = DataObject::get_by_id("Member", $uid);
// check if autologin token matches
if($member) {
$hash = $member->encryptWithUserSettings($token);
if(!$member->RememberLoginToken || $member->RememberLoginToken !== $hash) {
$member = null;
}
}
if($member) {
self::session_regenerate_id();
Session::set("loggedInAs", $member->ID);
// This lets apache rules detect whether the user has logged in
if(Member::config()->login_marker_cookie) {
Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
}
if($member) {
self::session_regenerate_id();
Session::set("loggedInAs", $member->ID);
// This lets apache rules detect whether the user has logged in
if(Member::config()->login_marker_cookie) {
Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
}
$generator = new RandomGenerator();
$token = $generator->randomToken('sha1');
$hash = $member->encryptWithUserSettings($token);
$member->RememberLoginToken = $hash;
Cookie::set('alc_enc', $member->ID . ':' . $token, 90, null, null, false, true);
$generator = new RandomGenerator();
$token = $generator->randomToken('sha1');
$hash = $member->encryptWithUserSettings($token);
$member->RememberLoginToken = $hash;
Cookie::set('alc_enc', $member->ID . ':' . $token, 90, null, null, false, true);
$member->NumVisit++;
$member->write();
$member->NumVisit++;
$member->write();
// Audit logging hook
$member->extend('memberAutoLoggedIn');
}
// Audit logging hook
$member->extend('memberAutoLoggedIn');
}
}

View File

@ -97,9 +97,10 @@ class Security extends Controller implements TemplateGlobalProvider {
/**
* Default message set used in permission failures.
*
* @config
* @var array|string
*/
private static $default_message_set = '';
private static $default_message_set;
/**
* Random secure token, can be used as a crypto key internally.
@ -198,9 +199,6 @@ class Security extends Controller implements TemplateGlobalProvider {
* If you pass an array, you can use the
* following keys:
* - default: The default message
* - logInAgain: The message to show
* if the user has just
* logged out and the
* - alreadyLoggedIn: The message to
* show if the user
* is already logged
@ -231,8 +229,8 @@ class Security extends Controller implements TemplateGlobalProvider {
} else {
// Prepare the messageSet provided
if(!$messageSet) {
if(self::$default_message_set) {
$messageSet = self::$default_message_set;
if($configMessageSet = static::config()->get('default_message_set')) {
$messageSet = $configMessageSet;
} else {
$messageSet = array(
'default' => _t(
@ -246,11 +244,6 @@ class Security extends Controller implements TemplateGlobalProvider {
. "can access that page, you can log in again below.",
"%s will be replaced with a link to log in."
),
'logInAgain' => _t(
'Security.LOGGEDOUT',
"You have been logged out. If you would like to log in again, enter "
. "your credentials below."
)
);
}

View File

@ -97,6 +97,16 @@ class DirectorTest extends SapphireTest {
$_SERVER['REQUEST_URI'] = "$rootURL/mysite/sub-page/";
Config::inst()->update('Director', 'alternate_base_url', '/mysite/');
//test empty URL
$this->assertEquals("$rootURL/mysite/sub-page/", Director::absoluteURL(''));
//test absolute - /
$this->assertEquals("$rootURL/", Director::absoluteURL('/'));
//test relative
$this->assertEquals("$rootURL/mysite/sub-page/", Director::absoluteURL('./'));
$this->assertEquals("$rootURL/mysite/sub-page/", Director::absoluteURL('.'));
// Test already absolute url
$this->assertEquals($rootURL, Director::absoluteURL($rootURL));
$this->assertEquals($rootURL, Director::absoluteURL($rootURL, true));
@ -135,8 +145,10 @@ class DirectorTest extends SapphireTest {
// absolute base URLs - you should end them in a /
Config::inst()->update('Director', 'alternate_base_url', 'http://www.example.org/');
$_SERVER['REQUEST_URI'] = "http://www.example.org/";
$this->assertEquals('http://www.example.org/', Director::baseURL());
$this->assertEquals('http://www.example.org/', Director::absoluteBaseURL());
$this->assertEquals('http://www.example.org/', Director::absoluteURL(''));
$this->assertEquals('http://www.example.org/subfolder/test', Director::absoluteURL('subfolder/test'));
// Setting it to false restores functionality

View File

@ -160,6 +160,26 @@ class HTTPTest extends FunctionalTest {
*/
public function testAbsoluteURLsAttributes() {
$this->withBaseURL('http://www.silverstripe.org/', function($test){
//empty links
$test->assertEquals(
'<a href="http://www.silverstripe.org/">test</a>',
HTTP::absoluteURLs('<a href="">test</a>')
);
$test->assertEquals(
'<a href="http://www.silverstripe.org/">test</a>',
HTTP::absoluteURLs('<a href="/">test</a>')
);
//relative
$test->assertEquals(
'<a href="http://www.silverstripe.org/">test</a>',
HTTP::absoluteURLs('<a href="./">test</a>')
);
$test->assertEquals(
'<a href="http://www.silverstripe.org/">test</a>',
HTTP::absoluteURLs('<a href=".">test</a>')
);
// links
$test->assertEquals(

View File

@ -280,4 +280,55 @@ PHP
Convert::raw2json($value)
);
}
public function testXML2Array() {
// Ensure an XML file at risk of entity expansion can be avoided safely
$inputXML = <<<XML
<?xml version="1.0"?>
<!DOCTYPE results [<!ENTITY long "SOME_SUPER_LONG_STRING">]>
<results>
<result>Now include &long; lots of times to expand the in-memory size of this XML structure</result>
<result>&long;&long;&long;</result>
</results>
XML
;
try {
Convert::xml2array($inputXML, true);
} catch(Exception $ex) {}
$this->assertTrue(
isset($ex)
&& $ex instanceof InvalidArgumentException
&& $ex->getMessage() === 'XML Doctype parsing disabled'
);
// Test without doctype validation
$expected = array(
'result' => array(
"Now include SOME_SUPER_LONG_STRING lots of times to expand the in-memory size of this XML structure",
array(
'long' => array(
array(
'long' => 'SOME_SUPER_LONG_STRING'
),
array(
'long' => 'SOME_SUPER_LONG_STRING'
),
array(
'long' => 'SOME_SUPER_LONG_STRING'
)
)
)
)
);
$result = Convert::xml2array($inputXML, false, true);
$this->assertEquals(
$expected,
$result
);
$result = Convert::xml2array($inputXML, false, false);
$this->assertEquals(
$expected,
$result
);
}
}

View File

@ -357,23 +357,34 @@ class SQLQueryTest extends SapphireTest {
public function testJoinSubSelect() {
// Test sub-select works
$query = new SQLQuery();
$query->setFrom('MyTable');
$query->addInnerJoin('(SELECT * FROM MyOtherTable)',
'Mot.MyTableID = MyTable.ID', 'Mot');
$query->addLeftJoin('(SELECT MyLastTable.MyOtherTableID, COUNT(1) as MyLastTableCount FROM MyLastTable '
. 'GROUP BY MyOtherTableID)',
'Mlt.MyOtherTableID = Mot.ID', 'Mlt');
$query->setOrderBy('COALESCE(Mlt.MyLastTableCount, 0) DESC');
$query->setFrom('"MyTable"');
$query->addInnerJoin('(SELECT * FROM "MyOtherTable")',
'"Mot"."MyTableID" = "MyTable"."ID"', 'Mot');
$query->addLeftJoin('(SELECT "MyLastTable"."MyOtherTableID", COUNT(1) as "MyLastTableCount" '
. 'FROM "MyLastTable" GROUP BY "MyOtherTableID")',
'"Mlt"."MyOtherTableID" = "Mot"."ID"', 'Mlt');
$query->setOrderBy('COALESCE("Mlt"."MyLastTableCount", 0) DESC');
$this->assertSQLEquals('SELECT *, COALESCE(Mlt.MyLastTableCount, 0) AS "_SortColumn0" FROM MyTable '.
'INNER JOIN (SELECT * FROM MyOtherTable) AS "Mot" ON Mot.MyTableID = MyTable.ID ' .
'LEFT JOIN (SELECT MyLastTable.MyOtherTableID, COUNT(1) as MyLastTableCount FROM MyLastTable '
. 'GROUP BY MyOtherTableID) AS "Mlt" ON Mlt.MyOtherTableID = Mot.ID ' .
$this->assertSQLEquals('SELECT *, COALESCE("Mlt"."MyLastTableCount", 0) AS "_SortColumn0" FROM "MyTable" '.
'INNER JOIN (SELECT * FROM "MyOtherTable") AS "Mot" ON "Mot"."MyTableID" = "MyTable"."ID" ' .
'LEFT JOIN (SELECT "MyLastTable"."MyOtherTableID", COUNT(1) as "MyLastTableCount" FROM "MyLastTable" '
. 'GROUP BY "MyOtherTableID") AS "Mlt" ON "Mlt"."MyOtherTableID" = "Mot"."ID" ' .
'ORDER BY "_SortColumn0" DESC',
$query->sql($parameters)
);
// Test that table names do not get mistakenly identified as sub-selects
$query = new SQLQuery();
$query->setFrom('"MyTable"');
$query->addInnerJoin('NewsArticleSelected', '"News"."MyTableID" = "MyTable"."ID"', 'News');
$this->assertSQLEquals(
'SELECT * FROM "MyTable" INNER JOIN "NewsArticleSelected" AS "News" ON '.
'"News"."MyTableID" = "MyTable"."ID"',
$query->sql()
);
}
public function testSetWhereAny() {

View File

@ -74,6 +74,47 @@ class SecurityTest extends FunctionalTest {
$this->autoFollowRedirection = true;
}
public function testPermissionFailureSetsCorrectFormMessages() {
Config::nest();
// Controller that doesn't attempt redirections
$controller = new SecurityTest_NullController();
$controller->response = new SS_HTTPResponse();
Security::permissionFailure($controller, array('default' => 'Oops, not allowed'));
$this->assertEquals('Oops, not allowed', Session::get('Security.Message.message'));
// Test that config values are used correctly
Config::inst()->update('Security', 'default_message_set', 'stringvalue');
Security::permissionFailure($controller);
$this->assertEquals('stringvalue', Session::get('Security.Message.message'),
'Default permission failure message value was not present');
Config::inst()->remove('Security', 'default_message_set');
Config::inst()->update('Security', 'default_message_set', array('default' => 'arrayvalue'));
Security::permissionFailure($controller);
$this->assertEquals('arrayvalue', Session::get('Security.Message.message'),
'Default permission failure message value was not present');
// Test that non-default messages work.
// NOTE: we inspect the response body here as the session message has already
// been fetched and output as part of it, so has been removed from the session
$this->logInWithPermission('EDITOR');
Config::inst()->update('Security', 'default_message_set',
array('default' => 'default', 'alreadyLoggedIn' => 'You are already logged in!'));
Security::permissionFailure($controller);
$this->assertContains('You are already logged in!', $controller->response->getBody(),
'Custom permission failure message was ignored');
Security::permissionFailure($controller,
array('default' => 'default', 'alreadyLoggedIn' => 'One-off failure message'));
$this->assertContains('One-off failure message', $controller->response->getBody(),
"Message set passed to Security::permissionFailure() didn't override Config values");
Config::unnest();
}
public function testLogInAsSomeoneElse() {
$member = DataObject::get_one('Member');
@ -517,3 +558,11 @@ class SecurityTest_SecuredController extends Controller implements TestOnly {
return 'Success';
}
}
class SecurityTest_NullController extends Controller implements TestOnly {
public function redirect($url, $code = 302) {
// NOOP
}
}

View File

@ -1114,9 +1114,11 @@ after')
}
public function testRewriteHashlinks() {
$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
Config::inst()->update('SSViewer', 'rewrite_hash_links', true);
$_SERVER['REQUEST_URI'] = 'http://path/to/file?foo"onclick="alert(\'xss\')""';
// Emulate SSViewer::process()
$base = Convert::raw2att($_SERVER['REQUEST_URI']);
@ -1127,6 +1129,8 @@ after')
<html>
<head><% base_tag %></head>
<body>
<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>
$ExternalInsertedLink
<a class="inline" href="#anchor">InlineLink</a>
$InsertedLink
<svg><use xlink:href="#sprite"></use></svg>
@ -1135,15 +1139,24 @@ after')
$tmpl = new SSViewer($tmplFile);
$obj = new ViewableData();
$obj->InsertedLink = '<a class="inserted" href="#anchor">InsertedLink</a>';
$obj->ExternalInsertedLink = '<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>';
$result = $tmpl->process($obj);
$this->assertContains(
'<a class="inserted" href="' . $base . '#anchor">InsertedLink</a>',
$result
);
$this->assertContains(
'<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>',
$result
);
$this->assertContains(
'<a class="inline" href="' . $base . '#anchor">InlineLink</a>',
$result
);
$this->assertContains(
'<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>',
$result
);
$this->assertContains(
'<svg><use xlink:href="#sprite"></use></svg>',
$result,
@ -1176,7 +1189,7 @@ after')
$obj->InsertedLink = '<a class="inserted" href="#anchor">InsertedLink</a>';
$result = $tmpl->process($obj);
$this->assertContains(
'<a class="inserted" href="<?php echo strip_tags(',
'<a class="inserted" href="<?php echo Convert::raw2att(',
$result
);
// TODO Fix inline links in PHP mode

View File

@ -4675,7 +4675,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
$text = preg_replace(
'/(<a[^>]+href *= *)"#/i',
'\\1"\' . (Config::inst()->get(\'SSViewer\', \'rewrite_hash_links\') ?' .
' strip_tags( $_SERVER[\'REQUEST_URI\'] ) : "") .
' Convert::raw2att( $_SERVER[\'REQUEST_URI\'] ) : "") .
\'#',
$text
);

View File

@ -1129,7 +1129,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
$text = preg_replace(
'/(<a[^>]+href *= *)"#/i',
'\\1"\' . (Config::inst()->get(\'SSViewer\', \'rewrite_hash_links\') ?' .
' strip_tags( $_SERVER[\'REQUEST_URI\'] ) : "") .
' Convert::raw2att( $_SERVER[\'REQUEST_URI\'] ) : "") .
\'#',
$text
);

View File

@ -1115,9 +1115,9 @@ class SSViewer implements Flushable {
if($this->rewriteHashlinks && $rewrite) {
if(strpos($output, '<base') !== false) {
if($rewrite === 'php') {
$thisURLRelativeToBase = "<?php echo strip_tags(\$_SERVER['REQUEST_URI']); ?>";
$thisURLRelativeToBase = "<?php echo Convert::raw2att(\$_SERVER['REQUEST_URI']); ?>";
} else {
$thisURLRelativeToBase = strip_tags($_SERVER['REQUEST_URI']);
$thisURLRelativeToBase = Convert::raw2att($_SERVER['REQUEST_URI']);
}
$output = preg_replace('/(<a[^>]+href *= *)"#/i', '\\1"' . $thisURLRelativeToBase . '#', $output);