Merge 3 into master

# Conflicts:
#	CONTRIBUTING.md
#	admin/css/screen.css
#	admin/css/screen.css.map
#	admin/javascript/LeftAndMain.EditForm.js
#	admin/javascript/LeftAndMain.js
#	admin/scss/_forms.scss
#	dev/Debug.php
#	docs/en/05_Contributing/01_Code.md
#	forms/DropdownField.php
#	model/DataObject.php
#	model/Versioned.php
#	model/fieldtypes/DBLocale.php
#	tests/forms/gridfield/GridFieldExportButtonTest.yml
#	tests/model/MoneyTest.php
#	tests/model/MoneyTest.yml
#	tests/model/SQLQueryTest.php
This commit is contained in:
Damian Mooyman 2016-05-18 18:36:10 +12:00
commit 574bc6038b
21 changed files with 657 additions and 260 deletions

View File

@ -4,24 +4,6 @@ Any open source product is only as good as the community behind it. You can part
See our [high level overview](http://silverstripe.org/contributing-to-silverstripe) on silverstripe.org on how you can help out. See our [high level overview](http://silverstripe.org/contributing-to-silverstripe) on silverstripe.org on how you can help out.
## Contributing to the correct version ## Contributing code
SilverStripe core and module releases (since the 3.1.8 release) follow the [Semantic Versioning](http://semver.org) See [contributing code](docs/en/05_Contributing/01_Code.md)
(SemVar) specification for releases. Using this specification declares to the entire development community the severity
and intention of each release. It gives developers the ability to safely declare their dependencies and understand the
scope involved in each upgrade.
Each release is labeled in the format `$MAJOR`.`$MINOR`.`$PATCH`. For example, 3.1.8 or 3.2.0.
* `$MAJOR` version is incremented if any backwards incompatible changes are introduced to the public API.
* `$MINOR` version is incremented if new, backwards compatible **functionality** is introduced to the public API or
improvements are introduced within the private code.
* `$PATCH` version is incremented if only backwards compatible **bug fixes** are introduced. A bug fix is defined as
an internal change that fixes incorrect behavior.
Git Branches are setup for each `$MINOR` version (e.g. 3.1, 3.2). Each `$PATCH` release is a git tag off the `$MINOR`
branch. For example, 3.1.8 will be a git tag of 3.1.8.
When contributing code, be aware of the scope of your changes. If your change is backwards incompatible, raise your
change against the `master` branch. The master branch contains the next `$MAJOR` release. If the change is backwards
compatible raise it against the correct `$MINOR` branch.

View File

@ -77,13 +77,35 @@ class ErrorControlChain {
*/ */
public function setSuppression($suppression) { public function setSuppression($suppression) {
$this->suppression = (bool)$suppression; $this->suppression = (bool)$suppression;
// Don't modify errors unless handling fatal errors, and if errors were // If handling fatal errors, conditionally disable, or restore error display
// originally allowed to be displayed. // Note: original value of display_errors could also evaluate to "off"
if ($this->handleFatalErrors && $this->originalDisplayErrors) { if ($this->handleFatalErrors) {
ini_set('display_errors', !$suppression); if($suppression) {
$this->setDisplayErrors(0);
} else {
$this->setDisplayErrors($this->originalDisplayErrors);
}
} }
} }
/**
* Set display_errors
*
* @param mixed $errors
*/
protected function setDisplayErrors($errors) {
ini_set('display_errors', $errors);
}
/**
* Get value of display_errors ini value
*
* @return mixed
*/
protected function getDisplayErrors() {
return ini_get('display_errors');
}
/** /**
* Add this callback to the chain of callbacks to call along with the state * Add this callback to the chain of callbacks to call along with the state
* that $error must be in this point in the chain for the callback to be called * that $error must be in this point in the chain for the callback to be called
@ -178,7 +200,7 @@ class ErrorControlChain {
register_shutdown_function(array($this, 'handleFatalError')); register_shutdown_function(array($this, 'handleFatalError'));
$this->handleFatalErrors = true; $this->handleFatalErrors = true;
$this->originalDisplayErrors = ini_get('display_errors'); $this->originalDisplayErrors = $this->getDisplayErrors();
$this->setSuppression($this->suppression); $this->setSuppression($this->suppression);
$this->step(); $this->step();
@ -202,7 +224,7 @@ class ErrorControlChain {
else { else {
// Now clean up // Now clean up
$this->handleFatalErrors = false; $this->handleFatalErrors = false;
ini_set('display_errors', $this->originalDisplayErrors); $this->setDisplayErrors($this->originalDisplayErrors);
} }
} }
} }

View File

@ -14,3 +14,20 @@ was affected by these:
* When FormFields are rendered, leading & trailing whitespace is now stripped. The resulting HTML for form fields is * When FormFields are rendered, leading & trailing whitespace is now stripped. The resulting HTML for form fields is
the same for the default fields, but if you have a custom form field that is relying on trailing whitespace being the same for the default fields, but if you have a custom form field that is relying on trailing whitespace being
outputted. outputted.
* DataObject::isChanged() now defaults to only checking database fields. If you rely on this method
for checking changes to non-db field properties, use getChangedFields() instead.
### Error handling
Up until 3.4.0 error responses handled by SilverStripe have normally returned HTTP 200. The correct http response
code can be turned on by setting `Debug.friendly_error_httpcode` config to true. This option will be removed in
4.0 and fixed to always on.
:::yaml
---
Name: mydebug
---
Debug:
friendly_error_httpcode: true

View File

@ -69,6 +69,39 @@ We ask for this so that the ownership in the license is clear and unambiguous, a
The core team is then responsible for reviewing patches and deciding if they will make it into core. If The core team is then responsible for reviewing patches and deciding if they will make it into core. If
there are any problems they will follow up with you, so please ensure they have a way to contact you! there are any problems they will follow up with you, so please ensure they have a way to contact you!
### Picking the right version
SilverStripe core and module releases (since the 3.1.8 release) follow the [Semantic Versioning](http://semver.org)
(SemVer) specification for releases. Using this specification declares to the entire development community the severity
and intention of each release. It gives developers the ability to safely declare their dependencies and understand the
scope involved in each upgrade.
Each release is labeled in the format `$MAJOR`.`$MINOR`.`$PATCH`. For example, 3.1.8 or 3.2.0.
* `$MAJOR` version is incremented if any backwards incompatible changes are introduced to the public API.
* `$MINOR` version is incremented if new, backwards compatible **functionality** is introduced to the public API or
improvements are introduced within the private code.
* `$PATCH` version is incremented if only backwards compatible **bug fixes** are introduced. A bug fix is defined as
an internal change that fixes incorrect behavior.
**Public API** refers to any aspect of the system that has been designed to be used by SilverStripe modules & site developers. In SilverStripe 3, because we haven't been clear, in principle we have to treat every public or protected method as *potentially* part of the public API, but sometimes it comes to a judgement call about how likely it is that a given method will have been used in a particular way. If we were strict about never changing publicly exposed behaviour, it would be difficult to fix any bug whatsoever, which isn't in the interests of our user community.
In future major releases of SilverStripe, we will endeavour to be more explicit about documenting the public API.
**Contributing bug fixes**
Bug fixes should be raised against the most recent MINOR release branch. For example, If your project is on 3.3.1 and 3.4.0 is released, please raise your bugfix against the `3.4` branch. Older MINOR release branches are primarily intended for critical bugfixes and security issues.
**Contributing features**
When contributing a backwards compatible change, raise it against the same MAJOR branch as your project. For example, if your project is on 3.3.1, raise it against the `3` branch. It will be included in the next MINOR release, e.g. 3.4.0. And then when it is released, you should upgrade your project to use it. As it is a MINOR change, it shouldn't break anything, and be a relatively painless upgrade.
**Contributing backwards-incompatible public API changes, and removing or radically changing existing feautres**
When contributing a backwards incompatible change, you must raise it against the `master` branch.
### The Pull Request Process ### The Pull Request Process
Once your pull request is issued, it's not the end of the road. A [core committer](/contributing/core_committers/) will most likely have some questions for you and may ask you to make some changes depending on discussions you have. Once your pull request is issued, it's not the end of the road. A [core committer](/contributing/core_committers/) will most likely have some questions for you and may ask you to make some changes depending on discussions you have.
@ -123,10 +156,7 @@ If you're familiar with it, here's the short version of what you need to know. O
* **Squash your commits, so that each commit addresses a single issue.** After you rebase your work on top of the upstream master, you can squash multiple commits into one. Say, for instance, you've got three commits in related to Issue #100. Squash all three into one with the message "Description of the issue here (fixes #100)" We won't accept pull requests for multiple commits related to a single issue; it's up to you to squash and clean your commit tree. (Remember, if you squash commits you've already pushed to GitHub, you won't be able to push that same branch again. Create a new local branch, squash, and push the new squashed branch.) * **Squash your commits, so that each commit addresses a single issue.** After you rebase your work on top of the upstream master, you can squash multiple commits into one. Say, for instance, you've got three commits in related to Issue #100. Squash all three into one with the message "Description of the issue here (fixes #100)" We won't accept pull requests for multiple commits related to a single issue; it's up to you to squash and clean your commit tree. (Remember, if you squash commits you've already pushed to GitHub, you won't be able to push that same branch again. Create a new local branch, squash, and push the new squashed branch.)
* **Choose the correct branch**: Assume the current release is 3.0.3, and 3.1.0 is in beta state. * **Choose the correct branch**: see [Picking the right version](#picking-the-right-version).
Most pull requests should go against the `3.1` *pre-release branch*, only critical bugfixes
against the `3.0` *release branch*. If you're changing an API or introducing a major feature,
the pull request should go against `master` (read more about our [release process](03_Release_Process.md)). Branches are periodically merged "upwards" (3.0 into 3.1, 3.1 into master).
### Editing files directly on GitHub.com ### Editing files directly on GitHub.com

View File

@ -54,13 +54,28 @@ class ConfirmedPasswordField extends FormField {
*/ */
protected $showOnClick = false; protected $showOnClick = false;
/**
* Check if the existing password should be entered first
*
* @var bool
*/
protected $requireExistingPassword = false;
/** /**
* A place to temporarly store the confirm password value * A place to temporarily store the confirm password value
*
* @var string * @var string
*/ */
protected $confirmValue; protected $confirmValue;
/**
* Store value of "Current Password" field
*
* @var string
*/
protected $currentPasswordValue;
/** /**
* Title for the link that triggers the visibility of password fields. * Title for the link that triggers the visibility of password fields.
* *
@ -107,6 +122,7 @@ class ConfirmedPasswordField extends FormField {
// disable auto complete // disable auto complete
foreach($this->children as $child) { foreach($this->children as $child) {
/** @var FormField $child */
$child->setAttribute('autocomplete', 'off'); $child->setAttribute('autocomplete', 'off');
} }
@ -115,7 +131,7 @@ class ConfirmedPasswordField extends FormField {
// we have labels for the subfields // we have labels for the subfields
$title = false; $title = false;
parent::__construct($name, $title, null, $form); parent::__construct($name, $title);
$this->setValue($value); $this->setValue($value);
} }
@ -149,6 +165,7 @@ class ConfirmedPasswordField extends FormField {
} }
foreach($this->children as $field) { foreach($this->children as $field) {
/** @var FormField $field */
$field->setDisabled($this->isDisabled()); $field->setDisabled($this->isDisabled());
$field->setReadonly($this->isReadonly()); $field->setReadonly($this->isReadonly());
@ -222,6 +239,7 @@ class ConfirmedPasswordField extends FormField {
*/ */
public function setRightTitle($title) { public function setRightTitle($title) {
foreach($this->children as $field) { foreach($this->children as $field) {
/** @var FormField $field */
$field->setRightTitle($title); $field->setRightTitle($title);
} }
@ -229,15 +247,20 @@ class ConfirmedPasswordField extends FormField {
} }
/** /**
* @param array $titles 2 entry array with the customized title for each * Set child field titles. Titles in order should be:
* of the 2 children. * - "Current Password" (if getRequireExistingPassword() is set)
* - "Password"
* - "Confirm Password"
* *
* @return ConfirmedPasswordField * @param array $titles List of child titles
* @return $this
*/ */
public function setChildrenTitles($titles) { public function setChildrenTitles($titles) {
if(is_array($titles) && count($titles) == 2) { $expectedChildren = $this->getRequireExistingPassword() ? 3 : 2;
if(is_array($titles) && count($titles) == $expectedChildren) {
foreach($this->children as $field) { foreach($this->children as $field) {
if(isset($titles[0])) { if(isset($titles[0])) {
/** @var FormField $field */
$field->setTitle($titles[0]); $field->setTitle($titles[0]);
array_shift($titles); array_shift($titles);
@ -253,8 +276,8 @@ class ConfirmedPasswordField extends FormField {
* to handle both cases. * to handle both cases.
* *
* @param mixed $value * @param mixed $value
* * @param mixed $data
* @return ConfirmedPasswordField * @return $this
*/ */
public function setValue($value, $data = null) { public function setValue($value, $data = null) {
// If $data is a DataObject, don't use the value, since it's a hashed value // If $data is a DataObject, don't use the value, since it's a hashed value
@ -266,6 +289,9 @@ class ConfirmedPasswordField extends FormField {
if(is_array($value)) { if(is_array($value)) {
$this->value = $value['_Password']; $this->value = $value['_Password'];
$this->confirmValue = $value['_ConfirmPassword']; $this->confirmValue = $value['_ConfirmPassword'];
$this->currentPasswordValue = ($this->getRequireExistingPassword() && isset($value['_CurrentPassword']))
? $value['_CurrentPassword']
: null;
if($this->showOnClick && isset($value['_PasswordFieldVisible'])) { if($this->showOnClick && isset($value['_PasswordFieldVisible'])) {
$this->children->fieldByName($this->getName() . '[_PasswordFieldVisible]') $this->children->fieldByName($this->getName() . '[_PasswordFieldVisible]')
@ -294,6 +320,7 @@ class ConfirmedPasswordField extends FormField {
* Update the names of the child fields when updating name of field. * Update the names of the child fields when updating name of field.
* *
* @param string $name new name to give to the field. * @param string $name new name to give to the field.
* @return $this
*/ */
public function setName($name) { public function setName($name) {
$this->children->fieldByName($this->getName() . '[_Password]') $this->children->fieldByName($this->getName() . '[_Password]')
@ -342,8 +369,7 @@ class ConfirmedPasswordField extends FormField {
$validator->validationError( $validator->validationError(
$name, $name,
_t('Form.VALIDATIONPASSWORDSDONTMATCH',"Passwords don't match"), _t('Form.VALIDATIONPASSWORDSDONTMATCH',"Passwords don't match"),
"validation", "validation"
false
); );
return false; return false;
@ -355,8 +381,7 @@ class ConfirmedPasswordField extends FormField {
$validator->validationError( $validator->validationError(
$name, $name,
_t('Form.VALIDATIONPASSWORDSNOTEMPTY', "Passwords can't be empty"), _t('Form.VALIDATIONPASSWORDSNOTEMPTY', "Passwords can't be empty"),
"validation", "validation"
false
); );
return false; return false;
@ -365,6 +390,8 @@ class ConfirmedPasswordField extends FormField {
// lengths // lengths
if(($this->minLength || $this->maxLength)) { if(($this->minLength || $this->maxLength)) {
$errorMsg = null;
$limit = null;
if($this->minLength && $this->maxLength) { if($this->minLength && $this->maxLength) {
$limit = "{{$this->minLength},{$this->maxLength}}"; $limit = "{{$this->minLength},{$this->maxLength}}";
$errorMsg = _t( $errorMsg = _t(
@ -392,8 +419,7 @@ class ConfirmedPasswordField extends FormField {
$validator->validationError( $validator->validationError(
$name, $name,
$errorMsg, $errorMsg,
"validation", "validation"
false
); );
} }
} }
@ -404,14 +430,56 @@ class ConfirmedPasswordField extends FormField {
$name, $name,
_t('Form.VALIDATIONSTRONGPASSWORD', _t('Form.VALIDATIONSTRONGPASSWORD',
"Passwords must have at least one digit and one alphanumeric character"), "Passwords must have at least one digit and one alphanumeric character"),
"validation", "validation"
false
); );
return false; return false;
} }
} }
// Check if current password is valid
if(!empty($value) && $this->getRequireExistingPassword()) {
if(!$this->currentPasswordValue) {
$validator->validationError(
$name,
_t(
'ConfirmedPasswordField.CURRENT_PASSWORD_MISSING',
"You must enter your current password."
),
"validation"
);
return false;
}
// Check this password is valid for the current user
$member = Member::currentUser();
if(!$member) {
$validator->validationError(
$name,
_t(
'ConfirmedPasswordField.LOGGED_IN_ERROR',
"You must be logged in to change your password."
),
"validation"
);
return false;
}
// With a valid user and password, check the password is correct
$checkResult = $member->checkPassword($this->currentPasswordValue);
if(!$checkResult->valid()) {
$validator->validationError(
$name,
_t(
'ConfirmedPasswordField.CURRENT_PASSWORD_ERROR',
"The current password you have entered is not correct."
),
"validation"
);
return false;
}
}
return true; return true;
} }
@ -444,4 +512,36 @@ class ConfirmedPasswordField extends FormField {
return $field; return $field;
} }
/**
* Check if existing password is required
*
* @return bool
*/
public function getRequireExistingPassword() {
return $this->requireExistingPassword;
}
/**
* Set if the existing password should be required
*
* @param bool $show Flag to show or hide this field
* @return $this
*/
public function setRequireExistingPassword($show) {
// Don't modify if already added / removed
if((bool)$show === $this->requireExistingPassword) {
return $this;
}
$this->requireExistingPassword = $show;
$name = $this->getName();
$currentName = "{$name}[_CurrentPassword]";
if ($show) {
$confirmField = PasswordField::create($currentName, _t('Member.CURRENT_PASSWORD', 'Current Password'));
$this->children->unshift($confirmField);
} else {
$this->children->removeByName($currentName, true);
}
return $this;
}
} }

View File

@ -184,6 +184,11 @@ abstract class SelectField extends FormField {
return true; return true;
} }
// Safety check against casting arrays as strings in PHP>5.4
if(is_array($dataValue) || is_array($userValue)) {
return false;
}
// For non-falsey values do loose comparison // For non-falsey values do loose comparison
if($dataValue) { if($dataValue) {
return $dataValue == $userValue; return $dataValue == $userValue;

View File

@ -149,7 +149,7 @@ class GridFieldExportButton implements GridField_HTMLProvider, GridField_ActionP
} else { } else {
$value = $gridField->getDataFieldValue($item, $columnSource); $value = $gridField->getDataFieldValue($item, $columnSource);
if(!$value) { if($value === null) {
$value = $gridField->getDataFieldValue($item, $columnHeader); $value = $gridField->getDataFieldValue($item, $columnHeader);
} }
} }

View File

@ -1038,7 +1038,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Doesn't write to the database. Only sets fields as changed * Doesn't write to the database. Only sets fields as changed
* if they are not already marked as changed. * if they are not already marked as changed.
* *
* @return DataObject $this * @return $this
*/ */
public function forceChange() { public function forceChange() {
// Ensure lazy fields loaded // Ensure lazy fields loaded
@ -1233,22 +1233,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @param bool $forceChanges If set to true, force all fields to be treated as changed * @param bool $forceChanges If set to true, force all fields to be treated as changed
* @return bool True if any changes are detected * @return bool True if any changes are detected
*/ */
protected function updateChanges($forceChanges = false) { protected function updateChanges($forceChanges = false)
// Update the changed array with references to changed obj-fields {
foreach($this->record as $field => $value) { if($forceChanges) {
// Only mark ID as changed if $forceChanges // Force changes, but only for loaded fields
if($field === 'ID' && !$forceChanges) continue; foreach($this->record as $field => $value) {
// Determine if this field should be forced, or can mark itself, changed $this->changed[$field] = static::CHANGE_VALUE;
if($forceChanges
|| !$this->isInDB()
|| (is_object($value) && method_exists($value, 'isChanged') && $value->isChanged())
) {
$this->changed[$field] = self::CHANGE_VALUE;
} }
return true;
} }
return $this->isChanged();
// Check changes exist, abort if there are no changes
return $this->changed && (bool)array_filter($this->changed);
} }
/** /**
@ -1383,7 +1377,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$isNewRecord = !$this->isInDB() || $forceInsert; $isNewRecord = !$this->isInDB() || $forceInsert;
// Check changes exist, abort if there are none // Check changes exist, abort if there are none
$hasChanges = $this->updateChanges($forceInsert); $hasChanges = $this->updateChanges($isNewRecord);
if($hasChanges || $forceWrite || $isNewRecord) { if($hasChanges || $forceWrite || $isNewRecord) {
// New records have their insert into the base data table done first, so that they can pass the // New records have their insert into the base data table done first, so that they can pass the
// generated primary key on to the rest of the manipulation // generated primary key on to the rest of the manipulation
@ -2476,9 +2470,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Loads all the stub fields that an initial lazy load didn't load fully. * Loads all the stub fields that an initial lazy load didn't load fully.
* *
* @param string $tableClass Base table to load the values from. Others are joined as required. * @param string $tableClass Base table to load the values from. Others are joined as required.
* Not specifying a tableClass will load all lazy fields from all tables. * Not specifying a tableClass will load all lazy fields from all tables.
* @return bool Flag if lazy loading succeeded
*/ */
protected function loadLazyFields($tableClass = null) { protected function loadLazyFields($tableClass = null) {
if(!$this->isInDB() || !is_numeric($this->ID)) {
return false;
}
if (!$tableClass) { if (!$tableClass) {
$loaded = array(); $loaded = array();
@ -2489,7 +2488,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
} }
return; return false;
} }
$dataQuery = new DataQuery($tableClass); $dataQuery = new DataQuery($tableClass);
@ -2501,11 +2500,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
} }
// TableField sets the record ID to "new" on new row data, so don't try doing anything in that case
if(!is_numeric($this->record['ID'])) {
return;
}
// Limit query to the current record, unless it has the Versioned extension, // Limit query to the current record, unless it has the Versioned extension,
// in which case it requires special handling through augmentLoadLazyFields() // in which case it requires special handling through augmentLoadLazyFields()
$baseTable = ClassInfo::baseDataClass($this); $baseTable = ClassInfo::baseDataClass($this);
@ -2551,6 +2545,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
} }
} }
return true;
} }
/** /**
@ -2624,7 +2619,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return boolean * @return boolean
*/ */
public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) { public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) {
$fields = $fieldName ? array($fieldName) : false; $fields = $fieldName ? array($fieldName) : true;
$changed = $this->getChangedFields($fields, $changeLevel); $changed = $this->getChangedFields($fields, $changeLevel);
if(!isset($fieldName)) { if(!isset($fieldName)) {
return !empty($changed); return !empty($changed);

View File

@ -14,7 +14,7 @@ use i18n;
*/ */
class DBLocale extends DBVarchar { class DBLocale extends DBVarchar {
public function __construct($name, $size = 16) { public function __construct($name = null, $size = 16) {
parent::__construct($name, $size); parent::__construct($name, $size);
} }

View File

@ -1,9 +1,14 @@
<?php <?php
/** /**
* DataObjects that use the Hierarchy extension can be be organised as a hierarchy, with children and parents. * DataObjects that use the Hierarchy extension can be be organised as a hierarchy, with children and parents. The most
* The most obvious example of this is SiteTree. * obvious example of this is SiteTree.
*
* @package framework * @package framework
* @subpackage model * @subpackage model
*
* @property int ParentID
* @property DataObject owner
* @method DataObject Parent
*/ */
class Hierarchy extends DataExtension { class Hierarchy extends DataExtension {
@ -11,30 +16,28 @@ class Hierarchy extends DataExtension {
protected $markingFilter; protected $markingFilter;
/** /** @var int */
* @var Int
*/
protected $_cache_numChildren; protected $_cache_numChildren;
/** /**
* The lower bounds for the amount of nodes to mark. If set, the logic will expand nodes until it reaches at least
* this number, and then stops. Root nodes will always show regardless of this settting. Further nodes can be
* lazy-loaded via ajax. This isn't a hard limit. Example: On a value of 10, with 20 root nodes, each having 30
* children, the actual node count will be 50 (all root nodes plus first expanded child).
*
* @config * @config
* @var integer The lower bounds for the amount of nodes to mark. If set, the logic will expand * @var int
* nodes until it reaches at least this number, and then stops. Root nodes will always
* show regardless of this settting. Further nodes can be lazy-loaded via ajax.
* This isn't a hard limit. Example: On a value of 10, with 20 root nodes, each having
* 30 children, the actual node count will be 50 (all root nodes plus first expanded child).
*/ */
private static $node_threshold_total = 50; private static $node_threshold_total = 50;
/** /**
* Limit on the maximum children a specific node can display. Serves as a hard limit to avoid exceeding available
* server resources in generating the tree, and browser resources in rendering it. Nodes with children exceeding
* this value typically won't display any children, although this is configurable through the $nodeCountCallback
* parameter in {@link getChildrenAsUL()}. "Root" nodes will always show all children, regardless of this setting.
*
* @config * @config
* @var integer Limit on the maximum children a specific node can display. * @var int
* Serves as a hard limit to avoid exceeding available server resources
* in generating the tree, and browser resources in rendering it.
* Nodes with children exceeding this value typically won't display
* any children, although this is configurable through the $nodeCountCallback
* parameter in {@link getChildrenAsUL()}. "Root" nodes will always show
* all children, regardless of this setting.
*/ */
private static $node_threshold_leaf = 250; private static $node_threshold_leaf = 250;
@ -46,6 +49,8 @@ class Hierarchy extends DataExtension {
/** /**
* Validate the owner object - check for existence of infinite loops. * Validate the owner object - check for existence of infinite loops.
*
* @param ValidationResult $validationResult
*/ */
public function validate(ValidationResult $validationResult) { public function validate(ValidationResult $validationResult) {
// The object is new, won't be looping. // The object is new, won't be looping.
@ -78,19 +83,21 @@ class Hierarchy extends DataExtension {
} }
/** /**
* Returns the children of this DataObject as an XHTML UL. This will be called recursively on each child, * Returns the children of this DataObject as an XHTML UL. This will be called recursively on each child, so if they
* so if they have children they will be displayed as a UL inside a LI. * have children they will be displayed as a UL inside a LI.
* @param string $attributes Attributes to add to the UL. *
* @param string|callable $titleEval PHP code to evaluate to start each child - this should include '<li>' * @param string $attributes Attributes to add to the UL
* @param string $extraArg Extra arguments that will be passed on to children, for if they overload this function. * @param string|callable $titleEval PHP code to evaluate to start each child - this should include '<li>'
* @param boolean $limitToMarked Display only marked children. * @param string $extraArg Extra arguments that will be passed on to children, for if they
* @param string $childrenMethod The name of the method used to get children from each object * overload this function
* @param boolean $rootCall Set to true for this first call, and then to false for calls inside the recursion. You * @param bool $limitToMarked Display only marked children
* should not change this. * @param string $childrenMethod The name of the method used to get children from each object
* @param int $nodeCountThreshold See {@link self::$node_threshold_total} * @param bool $rootCall Set to true for this first call, and then to false for calls inside
* @param callable $nodeCountCallback Called with the node count, which gives the callback an opportunity * the recursion. You should not change this.
* to intercept the query. Useful e.g. to avoid excessive children listings * @param int $nodeCountThreshold See {@link self::$node_threshold_total}
* (Arguments: $parent, $numChildren) * @param callable $nodeCountCallback Called with the node count, which gives the callback an opportunity to
* intercept the query. Useful e.g. to avoid excessive children listings
* (Arguments: $parent, $numChildren)
* *
* @return string * @return string
*/ */
@ -175,11 +182,12 @@ class Hierarchy extends DataExtension {
/** /**
* Mark a segment of the tree, by calling mark(). * Mark a segment of the tree, by calling mark().
* The method performs a breadth-first traversal until the number of nodes is more than minCount.
* This is used to get a limited number of tree nodes to show in the CMS initially.
* *
* This method returns the number of nodes marked. After this method is called other methods * The method performs a breadth-first traversal until the number of nodes is more than minCount. This is used to
* can check isExpanded() and isMarked() on individual nodes. * get a limited number of tree nodes to show in the CMS initially.
*
* This method returns the number of nodes marked. After this method is called other methods can check
* {@link isExpanded()} and {@link isMarked()} on individual nodes.
* *
* @param int $nodeCountThreshold See {@link getChildrenAsUL()} * @param int $nodeCountThreshold See {@link getChildrenAsUL()}
* @return int The actual number of nodes marked. * @return int The actual number of nodes marked.
@ -205,9 +213,10 @@ class Hierarchy extends DataExtension {
} }
/** /**
* Filter the marking to only those object with $node->$parameterName = $parameterValue * Filter the marking to only those object with $node->$parameterName == $parameterValue
* @param string $parameterName The parameter on each node to check when marking. *
* @param mixed $parameterValue The value the parameter must be to be marked. * @param string $parameterName The parameter on each node to check when marking.
* @param mixed $parameterValue The value the parameter must be to be marked.
*/ */
public function setMarkingFilter($parameterName, $parameterValue) { public function setMarkingFilter($parameterName, $parameterValue) {
$this->markingFilter = array( $this->markingFilter = array(
@ -217,9 +226,10 @@ class Hierarchy extends DataExtension {
} }
/** /**
* Filter the marking to only those where the function returns true. * Filter the marking to only those where the function returns true. The node in question will be passed to the
* The node in question will be passed to the function. * function.
* @param string $funcName The function name. *
* @param string $funcName The name of the function to call
*/ */
public function setMarkingFilterFunction($funcName) { public function setMarkingFilterFunction($funcName) {
$this->markingFilter = array( $this->markingFilter = array(
@ -229,8 +239,9 @@ class Hierarchy extends DataExtension {
/** /**
* Returns true if the marking filter matches on the given node. * Returns true if the marking filter matches on the given node.
* @param DataObject $node Node to check. *
* @return boolean * @param DataObject $node Node to check
* @return bool
*/ */
public function markingFilterMatches($node) { public function markingFilterMatches($node) {
if(!$this->markingFilter) { if(!$this->markingFilter) {
@ -257,7 +268,11 @@ class Hierarchy extends DataExtension {
/** /**
* Mark all children of the given node that match the marking filter. * Mark all children of the given node that match the marking filter.
* @param DataObject $node Parent node. *
* @param DataObject $node Parent node
* @param mixed $context
* @param string $childrenMethod The name of the instance method to call to get the object's list of children
* @param string $numChildrenMethod The name of the instance method to call to count the object's children
* @return DataList * @return DataList
*/ */
public function markChildren($node, $context = null, $childrenMethod = "AllChildrenIncludingDeleted", public function markChildren($node, $context = null, $childrenMethod = "AllChildrenIncludingDeleted",
@ -288,8 +303,10 @@ class Hierarchy extends DataExtension {
} }
/** /**
* Ensure marked nodes that have children are also marked expanded. * Ensure marked nodes that have children are also marked expanded. Call this after marking but before iterating
* Call this after marking but before iterating over the tree. * over the tree.
*
* @param string $numChildrenMethod The name of the instance method to call to count the object's children
*/ */
protected function markingFinished($numChildrenMethod = "numChildren") { protected function markingFinished($numChildrenMethod = "numChildren") {
// Mark childless nodes as expanded. // Mark childless nodes as expanded.
@ -303,9 +320,10 @@ class Hierarchy extends DataExtension {
} }
/** /**
* Return CSS classes of 'unexpanded', 'closed', both, or neither, as well as a * Return CSS classes of 'unexpanded', 'closed', both, or neither, as well as a 'jstree-*' state depending on the
* 'jstree-*' state depending on the marking of this DataObject. * marking of this DataObject.
* *
* @param string $numChildrenMethod The name of the instance method to call to count the object's children
* @return string * @return string
*/ */
public function markingClasses($numChildrenMethod="numChildren") { public function markingClasses($numChildrenMethod="numChildren") {
@ -327,8 +345,10 @@ class Hierarchy extends DataExtension {
/** /**
* Mark the children of the DataObject with the given ID. * Mark the children of the DataObject with the given ID.
* @param int $id ID of parent node. *
* @param boolean $open If this is true, mark the parent node as opened. * @param int $id ID of parent node
* @param bool $open If this is true, mark the parent node as opened
* @return bool
*/ */
public function markById($id, $open = false) { public function markById($id, $open = false) {
if(isset($this->markedNodes[$id])) { if(isset($this->markedNodes[$id])) {
@ -344,6 +364,7 @@ class Hierarchy extends DataExtension {
/** /**
* Expose the given object in the tree, by marking this page and all it ancestors. * Expose the given object in the tree, by marking this page and all it ancestors.
*
* @param DataObject $childObj * @param DataObject $childObj
*/ */
public function markToExpose($childObj) { public function markToExpose($childObj) {
@ -356,7 +377,9 @@ class Hierarchy extends DataExtension {
} }
/** /**
* Return the IDs of all the marked nodes * Return the IDs of all the marked nodes.
*
* @return array
*/ */
public function markedNodeIDs() { public function markedNodeIDs() {
return array_keys($this->markedNodes); return array_keys($this->markedNodes);
@ -364,7 +387,8 @@ class Hierarchy extends DataExtension {
/** /**
* Return an array of this page and its ancestors, ordered item -> root. * Return an array of this page and its ancestors, ordered item -> root.
* @return array *
* @return SiteTree[]
*/ */
public function parentStack() { public function parentStack() {
$p = $this->owner; $p = $this->owner;
@ -378,20 +402,20 @@ class Hierarchy extends DataExtension {
} }
/** /**
* True if this DataObject is marked. * Cache of DataObjects' marked statuses: [ClassName][ID] = bool
* @var boolean * @var array
*/ */
protected static $marked = array(); protected static $marked = array();
/** /**
* True if this DataObject is expanded. * Cache of DataObjects' expanded statuses: [ClassName][ID] = bool
* @var boolean * @var array
*/ */
protected static $expanded = array(); protected static $expanded = array();
/** /**
* True if this DataObject is opened. * Cache of DataObjects' opened statuses: [ClassName][ID] = bool
* @var boolean * @var array
*/ */
protected static $treeOpened = array(); protected static $treeOpened = array();
@ -430,7 +454,8 @@ class Hierarchy extends DataExtension {
/** /**
* Check if this DataObject is marked. * Check if this DataObject is marked.
* @return boolean *
* @return bool
*/ */
public function isMarked() { public function isMarked() {
$baseClass = ClassInfo::baseDataClass($this->owner->class); $baseClass = ClassInfo::baseDataClass($this->owner->class);
@ -440,7 +465,8 @@ class Hierarchy extends DataExtension {
/** /**
* Check if this DataObject is expanded. * Check if this DataObject is expanded.
* @return boolean *
* @return bool
*/ */
public function isExpanded() { public function isExpanded() {
$baseClass = ClassInfo::baseDataClass($this->owner->class); $baseClass = ClassInfo::baseDataClass($this->owner->class);
@ -450,6 +476,8 @@ class Hierarchy extends DataExtension {
/** /**
* Check if this DataObject's tree is opened. * Check if this DataObject's tree is opened.
*
* @return bool
*/ */
public function isTreeOpened() { public function isTreeOpened() {
$baseClass = ClassInfo::baseDataClass($this->owner->class); $baseClass = ClassInfo::baseDataClass($this->owner->class);
@ -459,7 +487,8 @@ class Hierarchy extends DataExtension {
/** /**
* Get a list of this DataObject's and all it's descendants IDs. * Get a list of this DataObject's and all it's descendants IDs.
* @return int *
* @return int[]
*/ */
public function getDescendantIDList() { public function getDescendantIDList() {
$idList = array(); $idList = array();
@ -468,8 +497,9 @@ class Hierarchy extends DataExtension {
} }
/** /**
* Get a list of this DataObject's and all it's descendants ID, and put it in $idList. * Get a list of this DataObject's and all it's descendants ID, and put them in $idList.
* @var array $idList Array to put results in. *
* @param array $idList Array to put results in.
*/ */
public function loadDescendantIDListInto(&$idList) { public function loadDescendantIDListInto(&$idList) {
if($children = $this->AllChildren()) { if($children = $this->AllChildren()) {
@ -488,7 +518,8 @@ class Hierarchy extends DataExtension {
/** /**
* Get the children for this DataObject. * Get the children for this DataObject.
* @return ArrayList *
* @return DataList
*/ */
public function Children() { public function Children() {
if(!(isset($this->_cache_children) && $this->_cache_children)) { if(!(isset($this->_cache_children) && $this->_cache_children)) {
@ -506,7 +537,8 @@ class Hierarchy extends DataExtension {
/** /**
* Return all children, including those 'not in menus'. * Return all children, including those 'not in menus'.
* @return SS_List *
* @return DataList
*/ */
public function AllChildren() { public function AllChildren() {
return $this->owner->stageChildren(true); return $this->owner->stageChildren(true);
@ -514,11 +546,13 @@ class Hierarchy extends DataExtension {
/** /**
* Return all children, including those that have been deleted but are still in live. * Return all children, including those that have been deleted but are still in live.
* Deleted children will be marked as "DeletedFromStage" * - Deleted children will be marked as "DeletedFromStage"
* Added children will be marked as "AddedToStage" * - Added children will be marked as "AddedToStage"
* Modified children will be marked as "ModifiedOnStage" * - Modified children will be marked as "ModifiedOnStage"
* Everything else has "SameOnStage" set, as an indicator that this information has been looked up. * - Everything else has "SameOnStage" set, as an indicator that this information has been looked up.
* @return SS_List *
* @param mixed $context
* @return ArrayList
*/ */
public function AllChildrenIncludingDeleted($context = null) { public function AllChildrenIncludingDeleted($context = null) {
return $this->doAllChildrenIncludingDeleted($context); return $this->doAllChildrenIncludingDeleted($context);
@ -527,8 +561,8 @@ class Hierarchy extends DataExtension {
/** /**
* @see AllChildrenIncludingDeleted * @see AllChildrenIncludingDeleted
* *
* @param unknown_type $context * @param mixed $context
* @return SS_List * @return ArrayList
*/ */
public function doAllChildrenIncludingDeleted($context = null) { public function doAllChildrenIncludingDeleted($context = null) {
if(!$this->owner) user_error('Hierarchy::doAllChildrenIncludingDeleted() called without $this->owner'); if(!$this->owner) user_error('Hierarchy::doAllChildrenIncludingDeleted() called without $this->owner');
@ -560,8 +594,10 @@ class Hierarchy extends DataExtension {
} }
/** /**
* Return all the children that this page had, including pages that were deleted * Return all the children that this page had, including pages that were deleted from both stage & live.
* from both stage & live. *
* @return DataList
* @throws Exception
*/ */
public function AllHistoricalChildren() { public function AllHistoricalChildren() {
if(!$this->owner->hasExtension('Versioned')) { if(!$this->owner->hasExtension('Versioned')) {
@ -574,7 +610,10 @@ class Hierarchy extends DataExtension {
} }
/** /**
* Return the number of children that this page ever had, including pages that were deleted * Return the number of children that this page ever had, including pages that were deleted.
*
* @return int
* @throws Exception
*/ */
public function numHistoricalChildren() { public function numHistoricalChildren() {
if(!$this->owner->hasExtension('Versioned')) { if(!$this->owner->hasExtension('Versioned')) {
@ -586,11 +625,10 @@ class Hierarchy extends DataExtension {
} }
/** /**
* Return the number of direct children. * Return the number of direct children. By default, values are cached after the first invocation. Can be
* By default, values are cached after the first invocation. * augumented by {@link augmentNumChildrenCountQuery()}.
* Can be augumented by {@link augmentNumChildrenCountQuery()}.
* *
* @param Boolean $cache * @param bool $cache Whether to retrieve values from cache
* @return int * @return int
*/ */
public function numChildren($cache = true) { public function numChildren($cache = true) {
@ -606,10 +644,10 @@ class Hierarchy extends DataExtension {
} }
/** /**
* Return children from the stage site * Return children in the stage site.
* *
* @param showAll Inlcude all of the elements, even those not shown in the menus. * @param bool $showAll Include all of the elements, even those not shown in the menus. Only applicable when
* (only applicable when extension is applied to {@link SiteTree}). * extension is applied to {@link SiteTree}.
* @return DataList * @return DataList
*/ */
public function stageChildren($showAll = false) { public function stageChildren($showAll = false) {
@ -625,12 +663,13 @@ class Hierarchy extends DataExtension {
} }
/** /**
* Return children from the live site, if it exists. * Return children in the live site, if it exists.
* *
* @param boolean $showAll Include all of the elements, even those not shown in the menus. * @param bool $showAll Include all of the elements, even those not shown in the menus. Only
* (only applicable when extension is applied to {@link SiteTree}). * applicable when extension is applied to {@link SiteTree}.
* @param boolean $onlyDeletedFromStage Only return items that have been deleted from stage * @param bool $onlyDeletedFromStage Only return items that have been deleted from stage
* @return SS_List * @return DataList
* @throws Exception
*/ */
public function liveChildren($showAll = false, $onlyDeletedFromStage = false) { public function liveChildren($showAll = false, $onlyDeletedFromStage = false) {
if(!$this->owner->hasExtension('Versioned')) { if(!$this->owner->hasExtension('Versioned')) {
@ -652,7 +691,10 @@ class Hierarchy extends DataExtension {
} }
/** /**
* Get the parent of this class. * Get this object's parent, optionally filtered by an SQL clause. If the clause doesn't match the parent, nothing
* is returned.
*
* @param string $filter
* @return DataObject * @return DataObject
*/ */
public function getParent($filter = null) { public function getParent($filter = null) {
@ -669,7 +711,7 @@ class Hierarchy extends DataExtension {
/** /**
* Return all the parents of this class in a set ordered from the lowest to highest parent. * Return all the parents of this class in a set ordered from the lowest to highest parent.
* *
* @return SS_List * @return ArrayList
*/ */
public function getAncestors() { public function getAncestors() {
$ancestors = new ArrayList(); $ancestors = new ArrayList();
@ -683,11 +725,10 @@ class Hierarchy extends DataExtension {
} }
/** /**
* Returns a human-readable, flattened representation of the path to the object, * Returns a human-readable, flattened representation of the path to the object, using its {@link Title} attribute.
* using its {@link Title()} attribute.
* *
* @param String * @param string $separator
* @return String * @return string
*/ */
public function getBreadcrumbs($separator = ' &raquo; ') { public function getBreadcrumbs($separator = ' &raquo; ') {
$crumbs = array(); $crumbs = array();
@ -702,22 +743,25 @@ class Hierarchy extends DataExtension {
* then search the parents. * then search the parents.
* *
* @todo Write! * @todo Write!
*
* @param string $className Class name of the node to find
* @param DataObject $afterNode Used for recursive calls to this function
* @return DataObject
*/ */
public function naturalPrev( $className, $afterNode = null ) { public function naturalPrev($className, $afterNode = null ) {
return null; return null;
} }
/** /**
* Get the next node in the tree of the type. If there is no instance of the className descended from this node, * Get the next node in the tree of the type. If there is no instance of the className descended from this node,
* then search the parents. * then search the parents.
* @param string $className Class name of the node to find. * @param string $className Class name of the node to find.
* @param string|int $root ID/ClassName of the node to limit the search to * @param string|int $root ID/ClassName of the node to limit the search to
* @param DataObject afterNode Used for recursive calls to this function * @param DataObject $afterNode Used for recursive calls to this function
* @return DataObject * @return DataObject
*/ */
public function naturalNext( $className = null, $root = 0, $afterNode = null ) { public function naturalNext($className = null, $root = 0, $afterNode = null ) {
// If this node is not the node we are searching from, then we can possibly return this // If this node is not the node we are searching from, then we can possibly return this node as a solution
// node as a solution
if($afterNode && $afterNode->ID != $this->owner->ID) { if($afterNode && $afterNode->ID != $this->owner->ID) {
if(!$className || ($className && $this->owner->class == $className)) { if(!$className || ($className && $this->owner->class == $className)) {
return $this->owner; return $this->owner;
@ -761,6 +805,14 @@ class Hierarchy extends DataExtension {
return null; return null;
} }
/**
* Flush all Hierarchy caches:
* - Children (instance)
* - NumChildren (instance)
* - Marked (global)
* - Expanded (global)
* - TreeOpened (global)
*/
public function flushCache() { public function flushCache() {
$this->_cache_children = null; $this->_cache_children = null;
$this->_cache_numChildren = null; $this->_cache_numChildren = null;
@ -769,6 +821,12 @@ class Hierarchy extends DataExtension {
self::$treeOpened = array(); self::$treeOpened = array();
} }
/**
* Reset global Hierarchy caches:
* - Marked
* - Expanded
* - TreeOpened
*/
public static function reset() { public static function reset() {
self::$marked = array(); self::$marked = array();
self::$expanded = array(); self::$expanded = array();

View File

@ -650,9 +650,8 @@ abstract class SQLConditionalExpression extends SQLExpression {
* @return boolean * @return boolean
*/ */
public function filtersOnID() { public function filtersOnID() {
$regexp = '/^(.*\.)?("|`)?ID("|`)?\s?=/'; $regexp = '/^(.*\.)?("|`)?ID("|`)?\s?(=|IN)/';
// @todo - Test this works with paramaterised queries
foreach($this->getWhereParameterised($parameters) as $predicate) { foreach($this->getWhereParameterised($parameters) as $predicate) {
if(preg_match($regexp, $predicate)) return true; if(preg_match($regexp, $predicate)) return true;
} }
@ -668,7 +667,7 @@ abstract class SQLConditionalExpression extends SQLExpression {
* @return boolean * @return boolean
*/ */
public function filtersOnFK() { public function filtersOnFK() {
$regexp = '/^(.*\.)?("|`)?[a-zA-Z]+ID("|`)?\s?=/'; $regexp = '/^(.*\.)?("|`)?[a-zA-Z]+ID("|`)?\s?(=|IN)/';
// @todo - Test this works with paramaterised queries // @todo - Test this works with paramaterised queries
foreach($this->getWhereParameterised($parameters) as $predicate) { foreach($this->getWhereParameterised($parameters) as $predicate) {

View File

@ -290,15 +290,23 @@ class Member extends DataObject implements TemplateGlobalProvider {
/** /**
* Check if the passed password matches the stored one (if the member is not locked out). * Check if the passed password matches the stored one (if the member is not locked out).
* *
* @param string $password * @param string $password
* @return ValidationResult * @return ValidationResult
*/ */
public function checkPassword($password) { public function checkPassword($password) {
$result = $this->canLogIn(); $result = $this->canLogIn();
// Short-circuit the result upon failure, no further checks needed. // Short-circuit the result upon failure, no further checks needed.
if (!$result->valid()) return $result; if (!$result->valid()) {
return $result;
}
// Allow default admin to login as self
if($this->isDefaultAdmin() && Security::check_default_admin($this->Email, $password)) {
return $result;
}
// Check a password is set on this member
if(empty($this->Password) && $this->exists()) { if(empty($this->Password) && $this->exists()) {
$result->error(_t('Member.NoPassword','There is no password on this member.')); $result->error(_t('Member.NoPassword','There is no password on this member.'));
return $result; return $result;
@ -315,6 +323,16 @@ class Member extends DataObject implements TemplateGlobalProvider {
return $result; return $result;
} }
/**
* Check if this user is the currently configured default admin
*
* @return bool
*/
public function isDefaultAdmin() {
return Security::has_default_admin()
&& $this->Email === Security::default_admin_username();
}
/** /**
* Returns a valid {@link ValidationResult} if this member can currently log in, or an invalid * Returns a valid {@link ValidationResult} if this member can currently log in, or an invalid
* one with error messages to display if the member is locked out. * one with error messages to display if the member is locked out.
@ -725,14 +743,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
public function getMemberFormFields() { public function getMemberFormFields() {
$fields = parent::getFrontendFields(); $fields = parent::getFrontendFields();
$fields->replaceField('Password', $password = new ConfirmedPasswordField ( $fields->replaceField('Password', $this->getMemberPasswordField());
'Password',
$this->fieldLabel('Password'),
null,
null,
(bool) $this->ID
));
$password->setCanBeEmpty(true);
$fields->replaceField('Locale', new DropdownField ( $fields->replaceField('Locale', new DropdownField (
'Locale', 'Locale',
@ -748,6 +759,36 @@ class Member extends DataObject implements TemplateGlobalProvider {
return $fields; return $fields;
} }
/**
* Builds "Change / Create Password" field for this member
*
* @return ConfirmedPasswordField
*/
public function getMemberPasswordField() {
$editingPassword = $this->isInDB();
$label = $editingPassword
? _t('Member.EDIT_PASSWORD', 'New Password')
: $this->fieldLabel('Password');
/** @var ConfirmedPasswordField $password */
$password = ConfirmedPasswordField::create(
'Password',
$label,
null,
null,
$editingPassword
);
// If editing own password, require confirmation of existing
if($editingPassword && $this->ID == Member::currentUserID()) {
$password->setRequireExistingPassword(true);
}
$password->setCanBeEmpty(true);
$this->extend('updateMemberPasswordField', $password);
return $password;
}
/** /**
* Returns the {@link RequiredFields} instance for the Member object. This * Returns the {@link RequiredFields} instance for the Member object. This
* Validator is used when saving a {@link CMSProfileController} or added to * Validator is used when saving a {@link CMSProfileController} or added to
@ -1337,19 +1378,12 @@ class Member extends DataObject implements TemplateGlobalProvider {
require_once 'Zend/Date.php'; require_once 'Zend/Date.php';
$self = $this; $self = $this;
$this->beforeUpdateCMSFields(function($fields) use ($self) { $this->beforeUpdateCMSFields(function(FieldList $fields) use ($self) {
$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children; /** @var FieldList $mainFields */
$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->getChildren();
$password = new ConfirmedPasswordField( // Build change password field
'Password', $mainFields->replaceField('Password', $self->getMemberPasswordField());
null,
null,
null,
true // showOnClick
);
$password->setCanBeEmpty(true);
if( ! $self->ID) $password->showOnClick = false;
$mainFields->replaceField('Password', $password);
$mainFields->replaceField('Locale', new DropdownField( $mainFields->replaceField('Locale', new DropdownField(
"Locale", "Locale",

View File

@ -19,9 +19,18 @@ Feature: Manage my own settings
Then I should see "Jack" Then I should see "Jack"
And I should see "Johnson" And I should see "Johnson"
Scenario: I can't reset the password without the original
Given I follow "Change Password"
And I fill in "Current Password" with "idontknow"
And I fill in "New Password" with "newsecret"
And I fill in "Confirm Password" with "newsecret"
And I press the "Save" button
Then I should see "The current password you have entered is not correct."
Scenario: I can change my password Scenario: I can change my password
Given I follow "Change Password" Given I follow "Change Password"
And I fill in "Password" with "newsecret" And I fill in "Current Password" with "secret"
And I fill in "New Password" with "newsecret"
And I fill in "Confirm Password" with "newsecret" And I fill in "Confirm Password" with "newsecret"
And I press the "Save" button And I press the "Save" button
And I am not logged in And I am not logged in
@ -35,4 +44,4 @@ Feature: Manage my own settings
Then I should see "Sprache" Then I should see "Sprache"
# TODO Date/time format - Difficult because its not exposed anywhere in the CMS? # TODO Date/time format - Difficult because its not exposed anywhere in the CMS?
# TODO Group modification as ADMIN user # TODO Group modification as ADMIN user

View File

@ -8,6 +8,30 @@
*/ */
class ErrorControlChainTest_Chain extends ErrorControlChain { class ErrorControlChainTest_Chain extends ErrorControlChain {
protected $displayErrors = 'STDERR';
/**
* Modify method visibility to public for testing
*
* @return string
*/
public function getDisplayErrors()
{
// Protect manipulation of underlying php_ini values
return $this->displayErrors;
}
/**
* Modify method visibility to public for testing
*
* @param mixed $errors
*/
public function setDisplayErrors($errors)
{
// Protect manipulation of underlying php_ini values
$this->displayErrors = $errors;
}
// Change function visibility to be testable directly // Change function visibility to be testable directly
public function translateMemstring($memstring) { public function translateMemstring($memstring) {
return parent::translateMemstring($memstring); return parent::translateMemstring($memstring);
@ -63,10 +87,7 @@ require_once '$classpath';
class ErrorControlChainTest extends SapphireTest { class ErrorControlChainTest extends SapphireTest {
protected $displayErrors = null;
function setUp() { function setUp() {
$this->displayErrors = (bool)ini_get('display_errors');
// Check we can run PHP at all // Check we can run PHP at all
$null = is_writeable('/dev/null') ? '/dev/null' : 'NUL'; $null = is_writeable('/dev/null') ? '/dev/null' : 'NUL';
@ -79,50 +100,55 @@ class ErrorControlChainTest extends SapphireTest {
parent::setUp(); parent::setUp();
} }
public function tearDown() {
if($this->displayErrors !== null) {
ini_set('display_errors', $this->displayErrors);
$this->displayErrors = null;
}
parent::tearDown(); // TODO: Change the autogenerated stub
}
function testErrorSuppression() { function testErrorSuppression() {
// Errors disabled by default // Errors disabled by default
ini_set('display_errors', false); $chain = new ErrorControlChainTest_Chain();
$chain = new ErrorControlChain(); $chain->setDisplayErrors('Off'); // mocks display_errors: Off
$initialValue = null;
$whenNotSuppressed = null; $whenNotSuppressed = null;
$whenSuppressed = null; $whenSuppressed = null;
$chain->then(function($chain) use(&$whenNotSuppressed, &$whenSuppressed) { $chain->then(
$chain->setSuppression(true); function(ErrorControlChainTest_Chain $chain)
$whenSuppressed = ini_get('display_errors'); use(&$initialValue, &$whenNotSuppressed, &$whenSuppressed) {
$chain->setSuppression(false); $initialValue = $chain->getDisplayErrors();
$whenNotSuppressed = ini_get('display_errors'); $chain->setSuppression(false);
})->execute(); $whenNotSuppressed = $chain->getDisplayErrors();
$chain->setSuppression(true);
$whenSuppressed = $chain->getDisplayErrors();
}
)->execute();
// Disabled errors never un-disable // Disabled errors never un-disable
$this->assertFalse((bool)$whenNotSuppressed); $this->assertEquals(0, $initialValue); // Chain starts suppressed
$this->assertFalse((bool)$whenSuppressed); $this->assertEquals(0, $whenSuppressed); // false value used internally when suppressed
$this->assertEquals('Off', $whenNotSuppressed); // false value set by php ini when suppression lifted
$this->assertEquals('Off', $chain->getDisplayErrors()); // Correctly restored after run
// Errors enabled by default // Errors enabled by default
ini_set('display_errors', true); $chain = new ErrorControlChainTest_Chain();
$chain = new ErrorControlChain(); $chain->setDisplayErrors('Yes'); // non-falsey ini value
$initialValue = null;
$whenNotSuppressed = null; $whenNotSuppressed = null;
$whenSuppressed = null; $whenSuppressed = null;
$chain->then(function($chain) use(&$whenNotSuppressed, &$whenSuppressed) { $chain->then(
$chain->setSuppression(true); function(ErrorControlChainTest_Chain $chain)
$whenSuppressed = ini_get('display_errors'); use(&$initialValue, &$whenNotSuppressed, &$whenSuppressed) {
$chain->setSuppression(false); $initialValue = $chain->getDisplayErrors();
$whenNotSuppressed = ini_get('display_errors'); $chain->setSuppression(true);
})->execute(); $whenSuppressed = $chain->getDisplayErrors();
$chain->setSuppression(false);
$whenNotSuppressed = $chain->getDisplayErrors();
}
)->execute();
// Errors can be suppressed an un-suppressed when initially enabled // Errors can be suppressed an un-suppressed when initially enabled
$this->assertTrue((bool)$whenNotSuppressed); $this->assertEquals(0, $initialValue); // Chain starts suppressed
$this->assertFalse((bool)$whenSuppressed); $this->assertEquals(0, $whenSuppressed); // false value used internally when suppressed
$this->assertEquals('Yes', $whenNotSuppressed); // false value set by php ini when suppression lifted
$this->assertEquals('Yes', $chain->getDisplayErrors()); // Correctly restored after run
// Fatal error // Fatal error
$chain = new ErrorControlChainTest_Chain(); $chain = new ErrorControlChainTest_Chain();
list($out, $code) = $chain list($out, $code) = $chain

View File

@ -254,6 +254,32 @@ class DropdownFieldTest extends SapphireTest {
$this->assertEquals(count($disabledOptions), 0, 'There are no disabled options'); $this->assertEquals(count($disabledOptions), 0, 'There are no disabled options');
} }
/**
* The Field() method should be able to handle arrays as values in an edge case. If it couldn't handle it then
* this test would trigger an array to string conversion PHP notice
*
* @dataProvider arrayValueProvider
*/
public function testDropdownWithArrayValues($value) {
$field = $this->createDropdownField();
$field->setValue($value);
$this->assertInstanceOf('SilverStripe\Model\FieldType\DBHTMLText', $field->Field());
$this->assertSame($value, $field->Value());
}
/**
* @return array
*/
public function arrayValueProvider() {
return array(
array(array()),
array(array(0)),
array(array(123)),
array(array('string')),
array('Regression-ish test.')
);
}
/** /**
* Create a test dropdown field, with the option to * Create a test dropdown field, with the option to
* set what source and blank value it should contain * set what source and blank value it should contain

View File

@ -137,6 +137,18 @@ class GridFieldExportButtonTest extends SapphireTest {
$button->generateExportFileData($this->gridField) $button->generateExportFileData($this->gridField)
); );
} }
public function testZeroValue() {
$button = new GridFieldExportButton();
$button->setExportColumns(array(
'RugbyTeamNumber' => 'Rugby Team Number'
));
$this->assertEquals(
"\"Rugby Team Number\"\n2\n0\n",
$button->generateExportFileData($this->gridField)
);
}
} }
/** /**
@ -147,7 +159,8 @@ class GridFieldExportButtonTest_Team extends DataObject implements TestOnly {
private static $db = array( private static $db = array(
'Name' => 'Varchar', 'Name' => 'Varchar',
'City' => 'Varchar' 'City' => 'Varchar',
'RugbyTeamNumber' => 'Int'
); );
public function canView($member = null) { public function canView($member = null) {
@ -164,7 +177,8 @@ class GridFieldExportButtonTest_NoView extends DataObject implements TestOnly {
private static $db = array( private static $db = array(
'Name' => 'Varchar', 'Name' => 'Varchar',
'City' => 'Varchar' 'City' => 'Varchar',
'RugbyTeamNumber' => 'Int'
); );
public function canView($member = null) { public function canView($member = null) {

View File

@ -2,9 +2,11 @@ GridFieldExportButtonTest_Team:
test-team-1: test-team-1:
Name: Test Name: Test
City: City City: City
RugbyTeamNumber: 2
test-team-2: test-team-2:
Name: Test2 Name: Test2
City: 'Quoted "City" 2' City: 'Quoted "City" 2'
RugbyTeamNumber: 0
GridFieldExportButtonTest_NoView: GridFieldExportButtonTest_NoView:
item1: item1:
Name: Foo Name: Foo

View File

@ -74,6 +74,35 @@ class DBMoneyTest extends SapphireTest {
$this->assertEquals(0.0000, $moneyTest->MyMoneyAmount); $this->assertEquals(0.0000, $moneyTest->MyMoneyAmount);
} }
public function testIsChanged() {
$obj1 = $this->objFromFixture('MoneyTest_DataObject', 'test1');
$this->assertFalse($obj1->isChanged());
$this->assertFalse($obj1->isChanged('MyMoney'));
// modify non-db field
$m1 = new DBMoney();
$m1->setAmount(500);
$m1->setCurrency('NZD');
$obj1->NonDBMoneyField = $m1;
$this->assertFalse($obj1->isChanged()); // Because only detects DB fields
$this->assertTrue($obj1->isChanged('NonDBMoneyField')); // Allow change detection to non-db fields explicitly named
// Modify db field
$obj2 = $this->objFromFixture('MoneyTest_DataObject', 'test2');
$m2 = new DBMoney();
$m2->setAmount(500);
$m2->setCurrency('NZD');
$obj2->MyMoney = $m2;
$this->assertTrue($obj2->isChanged()); // Detects change to DB field
$this->assertTrue($obj2->ischanged('MyMoney'));
// Modify sub-fields
$obj3 = $this->objFromFixture('MoneyTest_DataObject', 'test3');
$obj3->MyMoneyCurrency = 'USD';
$this->assertTrue($obj3->isChanged()); // Detects change to DB field
$this->assertTrue($obj3->ischanged('MyMoneyCurrency'));
}
/** /**
* Write a Money object to the database, then re-read it to ensure it * Write a Money object to the database, then re-read it to ensure it
* is re-read properly. * is re-read properly.

View File

@ -565,29 +565,29 @@ class DataObjectTest extends SapphireTest {
$obj->IsRetired = true; $obj->IsRetired = true;
$this->assertEquals( $this->assertEquals(
$obj->getChangedFields(false, 1), $obj->getChangedFields(true, DataObject::CHANGE_STRICT),
array( array(
'FirstName' => array( 'FirstName' => array(
'before' => 'Captain', 'before' => 'Captain',
'after' => 'Captain-changed', 'after' => 'Captain-changed',
'level' => 2 'level' => DataObject::CHANGE_VALUE
), ),
'IsRetired' => array( 'IsRetired' => array(
'before' => 1, 'before' => 1,
'after' => true, 'after' => true,
'level' => 1 'level' => DataObject::CHANGE_STRICT
) )
), ),
'Changed fields are correctly detected with strict type changes (level=1)' 'Changed fields are correctly detected with strict type changes (level=1)'
); );
$this->assertEquals( $this->assertEquals(
$obj->getChangedFields(false, 2), $obj->getChangedFields(true, DataObject::CHANGE_VALUE),
array( array(
'FirstName' => array( 'FirstName' => array(
'before'=>'Captain', 'before'=>'Captain',
'after'=>'Captain-changed', 'after'=>'Captain-changed',
'level' => 2 'level' => DataObject::CHANGE_VALUE
) )
), ),
'Changed fields are correctly detected while ignoring type changes (level=2)' 'Changed fields are correctly detected while ignoring type changes (level=2)'
@ -596,50 +596,58 @@ class DataObjectTest extends SapphireTest {
$newObj = new DataObjectTest_Player(); $newObj = new DataObjectTest_Player();
$newObj->FirstName = "New Player"; $newObj->FirstName = "New Player";
$this->assertEquals( $this->assertEquals(
$newObj->getChangedFields(false, 2),
array( array(
'FirstName' => array( 'FirstName' => array(
'before' => null, 'before' => null,
'after' => 'New Player', 'after' => 'New Player',
'level' => 2 'level' => DataObject::CHANGE_VALUE
) )
), ),
$newObj->getChangedFields(true, DataObject::CHANGE_VALUE),
'Initialised fields are correctly detected as full changes' 'Initialised fields are correctly detected as full changes'
); );
} }
public function testIsChanged() { public function testIsChanged() {
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1'); $obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
$obj->NonDBField = 'bob';
$obj->FirstName = 'Captain-changed'; $obj->FirstName = 'Captain-changed';
$obj->IsRetired = true; // type change only, database stores "1" $obj->IsRetired = true; // type change only, database stores "1"
$this->assertTrue($obj->isChanged('FirstName', 1)); // Now that DB fields are changed, isChanged is true
$this->assertTrue($obj->isChanged('FirstName', 2)); $this->assertTrue($obj->isChanged('NonDBField'));
$this->assertTrue($obj->isChanged('IsRetired', 1)); $this->assertFalse($obj->isChanged('NonField'));
$this->assertFalse($obj->isChanged('IsRetired', 2)); $this->assertTrue($obj->isChanged('FirstName', DataObject::CHANGE_STRICT));
$this->assertTrue($obj->isChanged('FirstName', DataObject::CHANGE_VALUE));
$this->assertTrue($obj->isChanged('IsRetired', DataObject::CHANGE_STRICT));
$this->assertFalse($obj->isChanged('IsRetired', DataObject::CHANGE_VALUE));
$this->assertFalse($obj->isChanged('Email', 1), 'Doesnt change mark unchanged property'); $this->assertFalse($obj->isChanged('Email', 1), 'Doesnt change mark unchanged property');
$this->assertFalse($obj->isChanged('Email', 2), 'Doesnt change mark unchanged property'); $this->assertFalse($obj->isChanged('Email', 2), 'Doesnt change mark unchanged property');
$newObj = new DataObjectTest_Player(); $newObj = new DataObjectTest_Player();
$newObj->FirstName = "New Player"; $newObj->FirstName = "New Player";
$this->assertTrue($newObj->isChanged('FirstName', 1)); $this->assertTrue($newObj->isChanged('FirstName', DataObject::CHANGE_STRICT));
$this->assertTrue($newObj->isChanged('FirstName', 2)); $this->assertTrue($newObj->isChanged('FirstName', DataObject::CHANGE_VALUE));
$this->assertFalse($newObj->isChanged('Email', 1)); $this->assertFalse($newObj->isChanged('Email', DataObject::CHANGE_STRICT));
$this->assertFalse($newObj->isChanged('Email', 2)); $this->assertFalse($newObj->isChanged('Email', DataObject::CHANGE_VALUE));
$newObj->write(); $newObj->write();
$this->assertFalse($newObj->isChanged('FirstName', 1)); $this->assertFalse($newObj->ischanged());
$this->assertFalse($newObj->isChanged('FirstName', 2)); $this->assertFalse($newObj->isChanged('FirstName', DataObject::CHANGE_STRICT));
$this->assertFalse($newObj->isChanged('Email', 1)); $this->assertFalse($newObj->isChanged('FirstName', DataObject::CHANGE_VALUE));
$this->assertFalse($newObj->isChanged('Email', 2)); $this->assertFalse($newObj->isChanged('Email', DataObject::CHANGE_STRICT));
$this->assertFalse($newObj->isChanged('Email', DataObject::CHANGE_VALUE));
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1'); $obj = $this->objFromFixture('DataObjectTest_Player', 'captain1');
$obj->FirstName = null; $obj->FirstName = null;
$this->assertTrue($obj->isChanged('FirstName', 1)); $this->assertTrue($obj->isChanged('FirstName', DataObject::CHANGE_STRICT));
$this->assertTrue($obj->isChanged('FirstName', 2)); $this->assertTrue($obj->isChanged('FirstName', DataObject::CHANGE_VALUE));
/* Test when there's not field provided */ /* Test when there's not field provided */
$obj = $this->objFromFixture('DataObjectTest_Player', 'captain1'); $obj = $this->objFromFixture('DataObjectTest_Player', 'captain2');
$this->assertFalse($obj->isChanged());
$obj->NonDBField = 'new value';
$this->assertFalse($obj->isChanged());
$obj->FirstName = "New Player"; $obj->FirstName = "New Player";
$this->assertTrue($obj->isChanged()); $this->assertTrue($obj->isChanged());

View File

@ -1,8 +1,14 @@
MoneyTest_DataObject: MoneyTest_DataObject:
test1: test1:
MyMoneyCurrency: EUR MyMoneyCurrency: EUR
MyMoneyAmount: 1.23 MyMoneyAmount: 1.23
test2:
MyMoneyCurrency: USD
MyMoneyAmount: 4.45
test3:
MyMoneyCurrency: NZD
MyMoneyAmount: 7.66
MoneyTest_SubClass: MoneyTest_SubClass:
test2: test2:
MyOtherMoneyCurrency: GBP MyOtherMoneyCurrency: GBP
MyOtherMoneyAmount: 2.46 MyOtherMoneyAmount: 2.46

View File

@ -315,6 +315,41 @@ class SQLSelectTest extends SapphireTest {
"filtersOnID() is true with simple unquoted column name" "filtersOnID() is true with simple unquoted column name"
); );
$query = new SQLSelect();
$query->setWhere('"ID" = 5');
$this->assertTrue(
$query->filtersOnID(),
"filtersOnID() is true with simple quoted column name"
);
$query = new SQLSelect();
$query->setWhere(array('"ID"' => 4));
$this->assertTrue(
$query->filtersOnID(),
"filtersOnID() is true with parameterised quoted column name"
);
$query = new SQLSelect();
$query->setWhere(array('"ID" = ?' => 4));
$this->assertTrue(
$query->filtersOnID(),
"filtersOnID() is true with parameterised quoted column name"
);
$query = new SQLSelect();
$query->setWhere('"ID" IN (5,4)');
$this->assertTrue(
$query->filtersOnID(),
"filtersOnID() is true with WHERE ID IN"
);
$query = new SQLSelect();
$query->setWhere(array('"ID" IN ?' => array(1,2)));
$this->assertTrue(
$query->filtersOnID(),
"filtersOnID() is true with parameterised WHERE ID IN"
);
$query = new SQLSelect(); $query = new SQLSelect();
$query->setWhere("ID=5"); $query->setWhere("ID=5");
$this->assertTrue( $this->assertTrue(