diff --git a/control/injector/InjectionCreator.php b/control/injector/InjectionCreator.php index c521a68d1..e6021d8a9 100644 --- a/control/injector/InjectionCreator.php +++ b/control/injector/InjectionCreator.php @@ -1,20 +1,16 @@ newInstance(); } -} \ No newline at end of file + +} diff --git a/control/injector/Injector.php b/control/injector/Injector.php index 82d9cae6f..4dca8204f 100644 --- a/control/injector/Injector.php +++ b/control/injector/Injector.php @@ -1,9 +1,13 @@ 'FactoryService' // A factory service to use to create instances. * 'construct' => array( // properties to set at construction * 'scalar', * '%$BeanId', @@ -94,25 +99,25 @@ require_once dirname(__FILE__) . '/SilverStripeServiceConfigurationLocator.php'; * * In addition to specifying the bindings directly in the configuration, * you can simply create a publicly accessible property on the target - * class which will automatically be injected if the autoScanProperties + * class which will automatically be injected if the autoScanProperties * option is set to true. This means a class defined as - * + * * * class MyController extends Controller { - * + * * private $permissionService; - * + * * public setPermissionService($p) { * $this->permissionService = $p; - * } + * } * } * - * + * * will have setPermissionService called if - * + * * * Injector::inst()->setAutoScanProperties(true) is called and - * * A service named 'PermissionService' has been configured - * + * * A service named 'PermissionService' has been configured + * * @author marcus@silverstripe.com.au * @package framework * @subpackage injector @@ -161,16 +166,18 @@ class Injector { * @var boolean */ private $autoScanProperties = false; - + /** - * The object used to create new class instances - * - * Use a custom class here to change the way classes are created to use - * a custom creation method. By default the InjectionCreator class is used, - * which simply creates a new class via 'new', however this could be overridden - * to use, for example, SilverStripe's Object::create() method. + * The default factory used to create new instances. * - * @var InjectionCreator + * The {@link InjectionCreator} is used by default, which simply directly + * creates objects. This can be changed to use a different default creation + * method if desired. + * + * Each individual component can also specify a custom factory to use by + * using the `factory` parameter. + * + * @var Factory */ protected $objectCreator; @@ -190,7 +197,7 @@ class Injector { ); $this->autoProperties = array(); - + $creatorClass = isset($config['creator']) ? $config['creator'] : 'InjectionCreator'; $locatorClass = isset($config['locator']) ? $config['locator'] : 'ServiceConfigurationLocator'; @@ -201,8 +208,6 @@ class Injector { if ($config) { $this->load($config); } - - self::$instance = $this; } /** @@ -217,7 +222,16 @@ class Injector { } return self::$instance; } - + + /** + * Sets the default global injector instance. + * + * @param Injector $instance + */ + public static function set_inst(Injector $instance) { + self::$instance = $instance; + } + /** * Indicate whether we auto scan injected objects for properties to set. * @@ -228,17 +242,16 @@ class Injector { } /** - * Sets the object to use for creating new objects + * Sets the default factory to use for creating new objects. * - * @param InjectionCreator $obj + * @param Factory $obj */ - public function setObjectCreator($obj) { + public function setObjectCreator(Factory $obj) { $this->objectCreator = $obj; } /** - * Accessor (for testing purposes) - * @return InjectionCreator + * @return Factory */ public function getObjectCreator() { return $this->objectCreator; @@ -488,8 +501,9 @@ class Injector { $constructorParams = $spec['constructor']; } - $object = $this->objectCreator->create($class, $constructorParams); - + $factory = isset($spec['factory']) ? $this->get($spec['factory']) : $this->getObjectCreator(); + $object = $factory->create($class, $constructorParams); + // figure out if we have a specific id set or not. In some cases, we might be instantiating objects // that we don't manage directly; we don't want to store these in the service cache below if (!$id) { diff --git a/control/injector/SilverStripeInjectionCreator.php b/control/injector/SilverStripeInjectionCreator.php index 809d61142..f4e454960 100644 --- a/control/injector/SilverStripeInjectionCreator.php +++ b/control/injector/SilverStripeInjectionCreator.php @@ -1,22 +1,18 @@ newInstanceArgs($params); + + return $params ? $reflector->newInstanceArgs($params) : $reflector->newInstance(); } -} \ No newline at end of file + +} diff --git a/core/Core.php b/core/Core.php index 39a726b45..18f0b1891 100644 --- a/core/Core.php +++ b/core/Core.php @@ -84,8 +84,10 @@ require_once 'control/injector/Injector.php'; // Initialise the dependency injector as soon as possible, as it is // subsequently used by some of the following code -$default_options = array('locator' => 'SilverStripeServiceConfigurationLocator'); -Injector::inst($default_options); +$injector = new Injector(array('locator' => 'SilverStripeServiceConfigurationLocator')); +$injector->registerService(Config::inst()); + +Injector::set_inst($injector); /////////////////////////////////////////////////////////////////////////////// // MANIFEST diff --git a/core/Object.php b/core/Object.php index 209f63bd6..d2d5c085e 100755 --- a/core/Object.php +++ b/core/Object.php @@ -127,7 +127,7 @@ abstract class Object { * * @param string $class the class name * @param mixed $arguments,... arguments to pass to the constructor - * @return Object + * @return static */ public static function create() { $args = func_get_args(); @@ -279,7 +279,7 @@ abstract class Object { * * @param string $class the class name * @param mixed $arguments,... arguments to pass to the constructor - * @return Object + * @return static */ public static function strong_create() { $args = func_get_args(); @@ -550,7 +550,7 @@ abstract class Object { Config::inst()->update($class, 'extensions', array($extension)); Config::inst()->extraConfigSourcesChanged($class); - Injector::inst()->unregisterAllObjects(); + Injector::inst()->unregisterNamedObject($class); // load statics now for DataObject classes if(is_subclass_of($class, 'DataObject')) { diff --git a/dev/FunctionalTest.php b/dev/FunctionalTest.php index 5de580dcc..675e767a6 100644 --- a/dev/FunctionalTest.php +++ b/dev/FunctionalTest.php @@ -48,6 +48,11 @@ class FunctionalTest extends SapphireTest { * However, this will let you inspect the intermediary headers */ protected $autoFollowRedirection = true; + + /** + * @var String + */ + protected $originalTheme = null; /** * Returns the {@link Session} object for this test @@ -64,7 +69,10 @@ class FunctionalTest extends SapphireTest { $this->mainSession = new TestSession(); // Disable theme, if necessary - if(static::get_disable_themes()) Config::inst()->update('SSViewer', 'theme', null); + if(static::get_disable_themes()) { + $this->originalTheme = Config::inst()->get('SSViewer', 'theme'); + Config::inst()->update('SSViewer', 'theme', null); + } // Switch to draft site, if necessary if(static::get_use_draft_site()) { @@ -83,6 +91,10 @@ class FunctionalTest extends SapphireTest { parent::tearDown(); unset($this->mainSession); + + if(static::get_disable_themes()) { + Config::inst()->update('SSViewer', 'theme', $this->originalTheme); + } } /** diff --git a/dev/JSTestRunner.php b/dev/JSTestRunner.php index 18e96e862..c97edc58f 100644 --- a/dev/JSTestRunner.php +++ b/dev/JSTestRunner.php @@ -6,27 +6,33 @@ * * To create your own tests, please use this template: * - * - * - * - * - * - * - * - * - * - *

My Test Name

- * - *

- *
    - *
    - * + * + * + * + * jQuery - Validation Test Suite + * + * + * + * + * + * + *

    + * + * jQuery Validation Plugin Test Suite

    + *

    + *
    + *

    + *
      + * * *
      * diff --git a/dev/SapphireTestReporter.php b/dev/SapphireTestReporter.php index 84da76c72..602520d0e 100644 --- a/dev/SapphireTestReporter.php +++ b/dev/SapphireTestReporter.php @@ -300,6 +300,18 @@ class SapphireTestReporter implements PHPUnit_Framework_TestListener { } } + /** + * Risky test. + * + * @param PHPUnit_Framework_Test $test + * @param Exception $e + * @param float $time + * @since Method available since Release 3.8.0 + */ + public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) { + // Stub out to support PHPUnit 3.8 + } + /** * Trys to get the original exception thrown by the test on failure/error * to enable us to give a bit more detail about the failure/error diff --git a/dev/SilverStripeListener.php b/dev/SilverStripeListener.php index d5c0a4437..6c5c95fe9 100644 --- a/dev/SilverStripeListener.php +++ b/dev/SilverStripeListener.php @@ -46,4 +46,16 @@ class SilverStripeListener implements PHPUnit_Framework_TestListener { public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) { } + + /** + * Risky test. + * + * @param PHPUnit_Framework_Test $test + * @param Exception $e + * @param float $time + * @since Method available since Release 3.8.0 + */ + public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) { + // Stub out to support PHPUnit 3.8 + } } diff --git a/dev/TeamCityListener.php b/dev/TeamCityListener.php index fb94c49ec..0439742d6 100644 --- a/dev/TeamCityListener.php +++ b/dev/TeamCityListener.php @@ -61,4 +61,16 @@ class TeamCityListener implements PHPUnit_Framework_TestListener { $message = $this->escape($e->getMessage()); echo "##teamcity[testIgnored name='{$class}.{$test->getName()}' message='$message']\n"; } + + /** + * Risky test. + * + * @param PHPUnit_Framework_Test $test + * @param Exception $e + * @param float $time + * @since Method available since Release 3.8.0 + */ + public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) { + // Stub out to support PHPUnit 3.8 + } } diff --git a/dev/TestListener.php b/dev/TestListener.php index 5494dfd89..4c0115004 100644 --- a/dev/TestListener.php +++ b/dev/TestListener.php @@ -38,6 +38,18 @@ class SS_TestListener implements PHPUnit_Framework_TestListener { $this->class->tearDownOnce(); } + /** + * Risky test. + * + * @param PHPUnit_Framework_Test $test + * @param Exception $e + * @param float $time + * @since Method available since Release 3.8.0 + */ + public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) { + // Stub out to support PHPUnit 3.8 + } + /** * @param String Classname * @return boolean diff --git a/docs/en/installation/common-problems.md b/docs/en/installation/common-problems.md index f824e232d..2a5270594 100644 --- a/docs/en/installation/common-problems.md +++ b/docs/en/installation/common-problems.md @@ -74,3 +74,38 @@ needs to create/have write-access to: * The mysite folder (to create _config.php) * After the install, the assets directory is the only directory that needs write access. * Image thumbnails will not show in the CMS if permission is not given + +## I have whitespace before my HTML output, triggering quirks mode or preventing cookies from being set + +SilverStripe only uses class declarations in PHP files, and doesn't output any content +directly outside of these declarations. It's easy to accidentally add whitespace +or any other characters before the `` closing braket at the end of the document. + +Since we're dealing with hundreds of included files, identifying these mistakes manually can be tiresome. +The best way to detect whitespace is to look through your version control system for any uncommitted changes. +If that doesn't work out, here's a little script to run checks in all relevant PHP files. +Save it as `check.php` into your webroot, and run it as `php check.php` (or open it in your browser). +After using the script (and fixing errors afterwards), please remember to remove it again. + +```php + $file){ + if($file->getExtension() != 'php') continue; + if(preg_match('/thirdparty|vendor/',$file->getPathname())) continue; + $content = file_get_contents($file->getPathname()); + if(preg_match('/^[[:blank:]]+<\?' . 'php/', $content)) { + echo sprintf("%s: Space before opening bracket\n", $file->getPathname()); + $matched = true; + } + if(preg_match('/^\?' . '>\n?[[:blank:]]+/m', $content)) { + echo sprintf("%s: Space after closing bracket\n", $file->getPathname()); + $matched = true; + } +} +``` \ No newline at end of file diff --git a/docs/en/installation/windows-pi.md b/docs/en/installation/windows-pi.md index 83a1ca916..f645e451c 100644 --- a/docs/en/installation/windows-pi.md +++ b/docs/en/installation/windows-pi.md @@ -5,7 +5,7 @@ ## Installing SilverStripe * Download and run Microsoft Web Platform Installer (WebPI): -[![](http://www.silverstripe.org/assets/downloads/webpi/wpiBadgeGreen.jpg)](http://www.microsoft.com/web/gallery/install.aspx?appsxml=www.microsoft.com%2fweb%2fwebpi%2f2.0%2fWebApplicationList.xml&appid=105) +[![](http://www.silverstripe.org/assets/downloads/webpi/wpiBadgeGreen.png)](http://www.microsoft.com/web/gallery/install.aspx?appsxml=www.microsoft.com%2fweb%2fwebpi%2f2.0%2fWebApplicationList.xml&appid=105) * In WebPI, select 'SilverStripe' from the 'Content Management System' link * Select install. It will install dependancies like MySQL and PHP if you don't have these installed already. diff --git a/docs/en/misc/contributing/code.md b/docs/en/misc/contributing/code.md index 01d87c224..35193b02f 100644 --- a/docs/en/misc/contributing/code.md +++ b/docs/en/misc/contributing/code.md @@ -21,7 +21,7 @@ We also found the [free online git book](http://progit.org/book/) and the [git c If you're familiar with it, here's the short version of what you need to know. Once you fork and download the code: * **Don't develop on the master branch.** Always create a development branch specific to "the issue" you're working on (mostly on our [bugtracker](/misc/contributing/issues)). Name it by issue number and description. For example, if you're working on Issue #100, a `DataObject::get_one()` bugfix, your development branch should be called 100-dataobject-get-one. If you decide to work on another issue mid-stream, create a new branch for that issue--don't work on both in one branch. - +x * **Do not merge the upstream master** with your development branch; *rebase* your branch on top of the upstream branch you branched off. * **A single development branch should represent changes related to a single issue.** If you decide to work on another issue, create another branch. @@ -89,6 +89,25 @@ there are any problems they will follow up with you, so please ensure they have ![Workflow diagram](http://www.silverstripe.org/assets/doc-silverstripe-org/collaboration-on-github.png) +### Quickfire Do's and Don't's + +If you aren't familiar with git and GitHub, try reading the ["GitHub bootcamp documentation"](http://help.github.com/). +We also found the [free online git book](http://git-scm.com/book/) and the [git crash course](http://gitref.org/) useful. +If you're familiar with it, here's the short version of what you need to know. Once you fork and download the code: + + * **Don't develop on the master branch.** Always create a development branch specific to "the issue" you're working on (mostly on our [bugtracker](/misc/contributing/issues)). Name it by issue number and description. For example, if you're working on Issue #100, a `DataObject::get_one()` bugfix, your development branch should be called 100-dataobject-get-one. If you decide to work on another issue mid-stream, create a new branch for that issue--don't work on both in one branch. + + * **Do not merge the upstream master** with your development branch; *rebase* your branch on top of the upstream master. + + * **A single development branch should represent changes related to a single issue.** If you decide to work on another issue, create another 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 "Issue #100 Description of the issue here." 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. + Most pull requests should go against the `3.1.x-dev` *pre-release branch*, only critical bugfixes + against the `3.0.x-dev` *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](/misc/release-process)). Branches are periodically merged "upwards" (3.0 into 3.1, 3.1 into master). + ### Editing files directly on GitHub.com If you see a typo or another small fix that needs to be made, and you don't have an installation set up for contributions, you can edit files directly in the github.com web interface. Every file view has an "edit this file" link. @@ -109,7 +128,7 @@ step. * It's better to submit multiple patches with separate bits of functionality than a big patch containing lots of changes * Document your code inline through [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) syntax. See our -[API documentation](http://api.silverstripe.org/trunk) for good examples. +[API documentation](http://api.silverstripe.org/3.1/) for good examples. * Also check and update documentation on [doc.silverstripe.org](http://doc.silverstripe.org). Check for any references to functionality deprecated or extended through your patch. Documentation changes should be included in the patch. * We will attribute the change to you whereever possible (git does this automatically for pull requests) * If you get stuck, please post to the [forum](http://silverstripe.org/forum) or for deeper core problems, to the [core mailinglist](https://groups.google.com/forum/#!forum/silverstripe-dev) @@ -272,7 +291,7 @@ It's as if you had just started your branch. One immediate advantage you get is So when you're ready to send the new plugin upstream, you do one last rebase, test, and then merge (which is really no merge at all) and send out your pull request. Then in most cases, we have a simple fast-forward on our end (or at worst a very small rebase or merge) and over time that adds up to a simpler tree. -More info on the ["Rebasing" chapter on progit.org](http://progit.org/book/ch3-6.html) and the [git rebase man page](http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html). +More info on the ["Rebasing" chapter on git-scm.com](http://git-scm.com/book/ch3-6.html) and the [git rebase man page](http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html). ## License diff --git a/docs/en/reference/cms-architecture.md b/docs/en/reference/cms-architecture.md index 778b0ca0f..226d57da2 100644 --- a/docs/en/reference/cms-architecture.md +++ b/docs/en/reference/cms-architecture.md @@ -30,7 +30,7 @@ and the [Compass framework](http://compass-style.org/), which helps us maintain expressive and concise style declarations. The files are located in `framework/admin/scss` (and if you have the `cms` module installed, in `cms/scss`), and are compiled to a `css` folder on the same directory path. Changes to the SCSS files can be automatically converted by installing -the ["compass" module](http://www.silverstripe.org/compass-module/) for SilverStripe, +the ["compass" module](https://github.com/silverstripe-labs/silverstripe-compass) for SilverStripe, although [installing the compass framework](http://compass-style.org/install/) directly works as well. Each file describes its purpose at the top of the declarations. Note that you can write plain CSS without SCSS for your custom CMS interfaces as well, we just mandate SCSS for core usage. diff --git a/docs/en/reference/injector.md b/docs/en/reference/injector.md index 98ce9c4b2..20b8b792b 100644 --- a/docs/en/reference/injector.md +++ b/docs/en/reference/injector.md @@ -76,8 +76,7 @@ The subsequent call returns the SAME object as the first call. In this case, on creation of the MyController object, the injector will automatically instantiate the PermissionService object and set it as -the **permissions** property. - +the **permissions** property. ## Configuring objects managed by the dependency injector @@ -90,6 +89,31 @@ Configuration can be specified for two areas of dependency management * Defining dependency overrides for individual classes * Injector managed 'services' +### Factories + +Some services require non-trivial construction which means they must be created by a factory class. To do this, create +a factory class which implements the `[api:SilverStripe\Framework\Injector\Factory]` interface. You can then specify +the `factory` key in the service definition, and the factory service will be used. + +An example using the `MyFactory` service to create instances of the `MyService` service is shown below: + + :::yml + Injector: + MyService: + factory: MyFactory + MyFactory: + class: MyFactoryImplementation + + :::php + class MyFactoryImplementation implements SilverStripe\Framework\Injector\Factory { + public function create($service, array $params = array()) { + return new MyServiceImplementation(); + } + } + + // Will use MyFactoryImplementation::create() to create the service instance. + $instance = Injector::inst()->get('MyService'); + ### Dependency overrides To override the **static $dependency;** declaration for a class, you could diff --git a/docs/en/tutorials/2-extending-a-basic-site.md b/docs/en/tutorials/2-extending-a-basic-site.md index ffaf84f1a..857dcab0e 100644 --- a/docs/en/tutorials/2-extending-a-basic-site.md +++ b/docs/en/tutorials/2-extending-a-basic-site.md @@ -387,7 +387,7 @@ The controller for a page is only created when page is actually visited, while t An RSS feed is something that no news section should be without. SilverStripe makes it easy to create RSS feeds by providing an `[api:RSSFeed]` class to do all the hard work for us. Add the following in the *ArticleHolder_Controller* class: -**mysite/code/ArticlePage.php** +**mysite/code/ArticleHolder.php** :::php private static $allowed_actions = array( @@ -408,7 +408,7 @@ Depending on your browser, you should see something like the picture below. If y Now all we need is to let the user know that our RSS feed exists. Add this function to *ArticleHolder_Controller*: -**mysite/code/ArticlePage.php** +**mysite/code/ArticleHolder.php** :::php public function init() { diff --git a/i18n/i18n.php b/i18n/i18n.php index 3ede6ee56..e0b121eda 100644 --- a/i18n/i18n.php +++ b/i18n/i18n.php @@ -2208,6 +2208,24 @@ class i18n extends Object implements TemplateGlobalProvider { return (array)Config::inst()->get('i18n', 'all_locales'); } + /** + * Matches a given locale with the closest translation available in the system + * + * @param string $locale locale code + * @return string Locale of closest available translation, if available + */ + public static function get_closest_translation($locale) { + + // Check if exact match + $pool = self::get_existing_translations(); + if(isset($pool[$locale])) return $locale; + + // Fallback to best locale for common language + $lang = self::get_lang_from_locale($locale); + $candidate = self::get_locale_from_lang($lang); + if(isset($pool[$candidate])) return $candidate; + } + /** * Searches the root-directory for module-directories * (identified by having a _config.php on their first directory-level). diff --git a/main.php b/main.php index 3646323d6..46da60746 100644 --- a/main.php +++ b/main.php @@ -134,7 +134,7 @@ $chain // Then if a flush was requested, redirect to it if ($token->parameterProvided() && !$token->tokenProvided()) { // First, check if we're in dev mode, or the database doesn't have any security data - $canFlush = Director::isDev() || !Security::database_is_ready(); + $canFlush = Director::isDev(true) || !Security::database_is_ready(); // Otherwise, we start up the session if needed, then check for admin if (!$canFlush) { diff --git a/search/FulltextSearchable.php b/search/FulltextSearchable.php index b5cbd5812..3b450a131 100644 --- a/search/FulltextSearchable.php +++ b/search/FulltextSearchable.php @@ -9,7 +9,7 @@ * CAUTION: Will make all files in your /assets folder searchable by file name * unless "File" is excluded from FulltextSearchable::enable(). * - * @see http://doc.silverstripe.org/tutorial:4-site-search + * @see http://doc.silverstripe.org/framework/en/tutorials/4-site-search * * @package framework * @subpackage search diff --git a/security/Member.php b/security/Member.php index ed5a8f5af..4b58c92ad 100644 --- a/security/Member.php +++ b/security/Member.php @@ -174,7 +174,7 @@ class Member extends DataObject implements TemplateGlobalProvider { */ public function populateDefaults() { parent::populateDefaults(); - $this->Locale = i18n::get_locale(); + $this->Locale = i18n::get_closest_translation(i18n::get_locale()); } public function requireDefaultRecords() { diff --git a/security/MemberAuthenticator.php b/security/MemberAuthenticator.php index e78357df7..0aa35c996 100644 --- a/security/MemberAuthenticator.php +++ b/security/MemberAuthenticator.php @@ -11,7 +11,7 @@ class MemberAuthenticator extends Authenticator { /** * @var Array Contains encryption algorithm identifiers. * If set, will migrate to new precision-safe password hashing - * upon login. See http://open.silverstripe.org/ticket/3004. + * upon login. See http://open.silverstripe.org/ticket/3004 */ private static $migrate_legacy_hashes = array( 'md5' => 'md5_v2.4', diff --git a/src/SilverStripe/Framework/Injector/Factory.php b/src/SilverStripe/Framework/Injector/Factory.php new file mode 100644 index 000000000..ffc61ddf3 --- /dev/null +++ b/src/SilverStripe/Framework/Injector/Factory.php @@ -0,0 +1,19 @@ +getSession()->getPage(); + $tableElements = $page->findAll('css', '.ss-gridfield-table'); + assertNotNull($tableElements, 'Table elements not found'); + + // Return first found table. + foreach($tableElements as $table) { + if($table->isVisible()) return $table; + } + + assertNotNull(null, 'First visible table element not found'); + } + /** * @Given /^I should see a "([^"]*)" button in CMS Content Toolbar$/ */ @@ -233,6 +249,20 @@ class CmsUiContext extends BehatContext { $element->click(); } + /** + * Clicks on a row in the first found visible GridField table. + * Example: I click on "New Zealand" in the table + * + * @Given /^I click on "([^"]*)" in the table$/ + */ + public function iClickOnInTheFirstTable($text) { + $table_element = $this->getFirstGridFieldTable(); + + $element = $table_element->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $text)); + assertNotNull($element, sprintf('Element containing `%s` not found', $text)); + $element->click(); + } + /** * @Then /^I can see the preview panel$/ */ diff --git a/tests/behat/features/security-permissions.feature b/tests/behat/features/security-permissions.feature new file mode 100644 index 000000000..4bb8b57a2 --- /dev/null +++ b/tests/behat/features/security-permissions.feature @@ -0,0 +1,85 @@ +@javascript +Feature: Manage Security Permissions for Groups + As a site administrator + I want to control my user's security permissions in an intuitive way + So that I can easily control access to the CMS + + Background: + Given a "group" "test group" + And a "member" "ADMIN" belonging to "ADMIN Group" with "Email"="admin@test.com" + And the "group" "ADMIN group" has permissions "Full administrative rights" + And I am logged in with "ADMIN" permissions + And I go to "/admin/security" + And I click the "Groups" CMS tab + And I click on "test group" in the table + And I click the "Permissions" CMS tab + + Scenario: I can see sub-permissions being properly set and restored when using "Access to all CMS sections" + When I check "Access to all CMS sections" + Then the "Access to 'Security' section" checkbox should be checked + And the "Access to 'Security' section" field should be disabled + + When I uncheck "Access to all CMS sections" + Then the "Access to 'Security' section" checkbox should not be checked + And the "Access to 'Security' section" field should be enabled + + When I check "Access to 'Security' section" + And I check "Access to all CMS sections" + When I uncheck "Access to all CMS sections" + Then the "Access to 'Security' section" checkbox should be checked + + # Save so the driver can reset without having to deal with the popup alert. + Then I press the "Save" button + + Scenario: I can see sub-permissions being properly set and restored when using "Full administrative rights" + When I check "Access to 'Security' section" + And I check "Full administrative rights" + Then the "Access to all CMS sections" checkbox should be checked + And the "Access to all CMS sections" field should be disabled + And the "Access to 'Security' section" checkbox should be checked + And the "Access to 'Security' section" field should be disabled + + And I uncheck "Full administrative rights" + Then the "Access to all CMS sections" checkbox should not be checked + And the "Access to all CMS sections" field should be enabled + And the "Access to 'Security' section" checkbox should be checked + And the "Access to 'Security' section" field should be enabled + + # Save so the driver can reset without having to deal with the popup alert. + Then I press the "Save" button + + Scenario: I can see sub-permissions being handled properly between reloads when using "Full administrative rights" + When I check "Full administrative rights" + And I press the "Save" button + And I click the "Permissions" CMS tab + Then the "Full administrative rights" checkbox should be checked + And the "Access to 'Security' section" checkbox should be checked + And the "Access to 'Security' section" field should be disabled + + When I uncheck "Full administrative rights" + Then the "Access to 'Security' section" checkbox should not be checked + And the "Access to 'Security' section" field should be enabled + + When I press the "Save" button + And I click the "Permissions" CMS tab + Then the "Full administrative rights" checkbox should not be checked + And the "Access to 'Security' section" checkbox should not be checked + And the "Access to 'Security' section" field should be enabled + + Scenario: I can see sub-permissions being handled properly between reloads when using "Access to all CMS sections" + When I check "Access to all CMS sections" + And I press the "Save" button + And I click the "Permissions" CMS tab + Then the "Access to all CMS sections" checkbox should be checked + And the "Access to 'Security' section" checkbox should be checked + And the "Access to 'Security' section" field should be disabled + + When I uncheck "Access to all CMS sections" + Then the "Access to 'Security' section" checkbox should not be checked + And the "Access to 'Security' section" field should be enabled + + When I press the "Save" button + And I click the "Permissions" CMS tab + Then the "Access to all CMS sections" checkbox should not be checked + And the "Access to 'Security' section" checkbox should not be checked + And the "Access to 'Security' section" field should be enabled diff --git a/tests/i18n/i18nTest.php b/tests/i18n/i18nTest.php index fd98cbf82..1d2cc131c 100644 --- a/tests/i18n/i18nTest.php +++ b/tests/i18n/i18nTest.php @@ -102,6 +102,32 @@ class i18nTest extends SapphireTest { $this->assertTrue(isset($translations['de_DE']), 'Checking for de_DE translation'); } + public function testGetClosestTranslation() { + + // Validate necessary assumptions for this test + $translations = i18n::get_existing_translations(); + $this->assertTrue(isset($translations['en_US'])); + $this->assertTrue(isset($translations['en_GB'])); + $this->assertTrue(isset($translations['es_ES'])); + $this->assertTrue(isset($translations['es_AR'])); + $this->assertFalse(isset($translations['en_ZZ'])); + $this->assertFalse(isset($translations['es_ZZ'])); + $this->assertFalse(isset($translations['zz_ZZ'])); + + // Test indeterminate locales + $this->assertEmpty(i18n::get_closest_translation('zz_ZZ')); + + // Test english fallback + $this->assertEquals('en_US', i18n::get_closest_translation('en_US')); + $this->assertEquals('en_GB', i18n::get_closest_translation('en_GB')); + $this->assertEquals('en_US', i18n::get_closest_translation('en_ZZ')); + + // Test spanish fallbacks + $this->assertEquals('es_AR', i18n::get_closest_translation('es_AR')); + $this->assertEquals('es_ES', i18n::get_closest_translation('es_ES')); + $this->assertEquals('es_ES', i18n::get_closest_translation('es_XX')); + } + public function testDataObjectFieldLabels() { $oldLocale = i18n::get_locale(); i18n::set_locale('de_DE'); diff --git a/tests/injector/InjectorTest.php b/tests/injector/InjectorTest.php index 61e11dce4..06099779c 100644 --- a/tests/injector/InjectorTest.php +++ b/tests/injector/InjectorTest.php @@ -541,6 +541,28 @@ class InjectorTest extends SapphireTest { $this->assertEquals($item->property, 'othervalue'); } + /** + * Tests creating a service with a custom factory. + */ + public function testCustomFactory() { + $injector = new Injector(array( + 'service' => array('factory' => 'factory', 'constructor' => array(1, 2, 3)) + )); + + $factory = $this->getMock('SilverStripe\\Framework\\Injector\\Factory'); + $factory + ->expects($this->once()) + ->method('create') + ->with($this->equalTo('service'), $this->equalTo(array(1, 2, 3))) + ->will($this->returnCallback(function($args) { + return new TestObject(); + })); + + $injector->registerService($factory, 'factory'); + + $this->assertInstanceOf('TestObject', $injector->get('service')); + } + } class InjectorTestConfigLocator extends SilverStripeServiceConfigurationLocator implements TestOnly { @@ -667,7 +689,7 @@ class SSObjectCreator extends InjectionCreator { $this->injector = $injector; } - public function create($class, $params = array()) { + public function create($class, array $params = array()) { if (strpos($class, '(') === false) { return parent::create($class, $params); } else {