diff --git a/admin/javascript/LeftAndMain.js b/admin/javascript/LeftAndMain.js index cbaed7a67..52cf99e62 100644 --- a/admin/javascript/LeftAndMain.js +++ b/admin/javascript/LeftAndMain.js @@ -176,7 +176,7 @@ jQuery.noConflict(); var msg = (xhr.getResponseHeader('X-Status')) ? xhr.getResponseHeader('X-Status') : xhr.statusText, reathenticate = xhr.getResponseHeader('X-Reauthenticate'), msgType = (xhr.status < 200 || xhr.status > 399) ? 'bad' : 'good', - ignoredMessages = ['OK']; + ignoredMessages = ['OK', 'success']; // Enable reauthenticate dialog if requested if(reathenticate) { @@ -911,8 +911,11 @@ jQuery.noConflict(); sessionStates = sessionData ? JSON.parse(sessionData) : false; this.find('.cms-tabset, .ss-tabset').each(function() { - var index, tabset = $(this), tabsetId = tabset.attr('id'), tab, - forcedTab = tabset.find('.ss-tabs-force-active'); + var index, + tabset = $(this), + tabsetId = tabset.attr('id'), + tab, + forcedTab = tabset.children('ul').children('li.ss-tabs-force-active'); if(!tabset.data('tabs')){ return; // don't act on uninit'ed controls @@ -921,18 +924,18 @@ jQuery.noConflict(); // The tabs may have changed, notify the widget that it should update its internal state. tabset.tabs('refresh'); - // Make sure the intended tab is selected. + // Make sure the intended tab is selected. Only force the tab on the correct tabset though if(forcedTab.length) { - index = forcedTab.index(); + index = forcedTab.first().index(); } else if(overrideStates && overrideStates[tabsetId]) { tab = tabset.find(overrideStates[tabsetId].tabSelector); if(tab.length){ index = tab.index(); } } else if(sessionStates) { - $.each(sessionStates, function(i, sessionState) { - if(tabset.is('#' + sessionState.id)){ - index = sessionState.selected; + $.each(sessionStates, function(i, state) { + if(tabsetId == state.id){ + index = state.selected; } }); } diff --git a/control/HTTPRequest.php b/control/HTTPRequest.php index a75131371..3de2a3668 100644 --- a/control/HTTPRequest.php +++ b/control/HTTPRequest.php @@ -671,7 +671,7 @@ class SS_HTTPRequest implements ArrayAccess { } if ($headerOverrideIP) { - return $headerOverrideIP; + return $this->getIPFromHeaderValue($headerOverrideIP); } elseif(isset($_SERVER['REMOTE_ADDR'])) { return $_SERVER['REMOTE_ADDR']; } else { @@ -679,6 +679,28 @@ class SS_HTTPRequest implements ArrayAccess { } } + /** + * Extract an IP address from a header value that has been obtained. Accepts single IP or comma separated string of + * IPs + * + * @param string $headerValue The value from a trusted header + * @return string The IP address + */ + protected function getIPFromHeaderValue($headerValue) { + if (strpos($headerValue, ',') !== false) { + //sometimes the IP from a load balancer could be "x.x.x.x, y.y.y.y, z.z.z.z" so we need to find the most + // likely candidate + $ips = explode(',', $headerValue); + foreach ($ips as $ip) { + $ip = trim($ip); + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) { + return $ip; + } + } + } + return $headerValue; + } + /** * Returns all mimetypes from the HTTP "Accept" header * as an array. diff --git a/core/startup/ErrorControlChain.php b/core/startup/ErrorControlChain.php index 496de8615..86a149e45 100644 --- a/core/startup/ErrorControlChain.php +++ b/core/startup/ErrorControlChain.php @@ -67,9 +67,21 @@ class ErrorControlChain { $this->error = (bool)$error; } + /** + * Sets whether errors are suppressed or not + * Notes: + * - Errors cannot be suppressed if not handling errors. + * - Errors cannot be un-suppressed if original mode dis-allowed visible errors + * + * @param bool $suppression + */ public function setSuppression($suppression) { $this->suppression = (bool)$suppression; - if ($this->handleFatalErrors) ini_set('display_errors', !$suppression); + // Don't modify errors unless handling fatal errors, and if errors were + // originally allowed to be displayed. + if ($this->handleFatalErrors && $this->originalDisplayErrors) { + ini_set('display_errors', !$suppression); + } } /** @@ -167,7 +179,7 @@ class ErrorControlChain { $this->handleFatalErrors = true; $this->originalDisplayErrors = ini_get('display_errors'); - ini_set('display_errors', !$this->suppression); + $this->setSuppression($this->suppression); $this->step(); } diff --git a/core/startup/ParameterConfirmationToken.php b/core/startup/ParameterConfirmationToken.php index b141fa037..addcf2416 100644 --- a/core/startup/ParameterConfirmationToken.php +++ b/core/startup/ParameterConfirmationToken.php @@ -194,8 +194,8 @@ class ParameterConfirmationToken { $_SERVER['HTTP_HOST'], // SilverStripe base self::$alternateBaseURL !== null ? self::$alternateBaseURL : BASE_URL, - // And URL - $url + // And URL including base script (eg: if it's index.php/page/url/) + (defined('BASE_SCRIPT_URL') ? '/' . BASE_SCRIPT_URL : '') . $url, )); // Join together with protocol into our current absolute URL, avoiding duplicated "/" characters diff --git a/dev/CSVParser.php b/dev/CSVParser.php index 53b04fbdc..158d9d863 100644 --- a/dev/CSVParser.php +++ b/dev/CSVParser.php @@ -10,10 +10,10 @@ * * * $parser = new CSVParser('myfile.csv'); - * $parser->mapColumns( + * $parser->mapColumns(array( * 'first name' => 'FirstName' * 'lastname' => 'Surname', - * 'last name' => 'Surname' + * 'last name' => 'Surname', * )); * foreach($parser as $row) { * // $row is a map of column name => column value diff --git a/docs/en/00_Getting_Started/00_Server_Requirements.md b/docs/en/00_Getting_Started/00_Server_Requirements.md index aa7ddeedc..24d162b22 100644 --- a/docs/en/00_Getting_Started/00_Server_Requirements.md +++ b/docs/en/00_Getting_Started/00_Server_Requirements.md @@ -8,7 +8,7 @@ Our web-based [PHP installer](installation/) can check if you meet the requireme ## Web server software requirements - * PHP 5.3.3+ + * PHP 5.3.3+, <7 * We recommend using a PHP accelerator or opcode cache, such as [xcache](http://xcache.lighttpd.net/) or [WinCache](http://www.iis.net/download/wincacheforphp). * Allocate at least 48MB of memory to each PHP process. (SilverStripe can be resource hungry for some intensive operations.) * Required modules: dom, gd2, fileinfo, hash, iconv, mbstring, mysqli (or other database driver), session, simplexml, tokenizer, xml. @@ -23,7 +23,7 @@ Our web-based [PHP installer](installation/) can check if you meet the requireme * MySQL 5.0+ * PostgreSQL 8.3+ (requires ["postgresql" module](http://silverstripe.org/postgresql-module)) * SQL Server 2008+ (requires ["mssql" module](http://silverstripe.org/microsoft-sql-server-database/)) - * Support for `[Oracle](http://www.silverstripe.org/oracle-database-module/)` and [SQLite](http://silverstripe.org/sqlite-database/) is not commercially supported, but is under development by our open source community. + * Support for [Oracle](http://www.silverstripe.org/oracle-database-module/) and [SQLite](http://silverstripe.org/sqlite-database/) is not commercially supported, but is under development by our open source community. * One of the following web server products: * Apache 2.0+ with mod_rewrite and "AllowOverride All" set * IIS 7+ @@ -34,6 +34,11 @@ Our web-based [PHP installer](installation/) can check if you meet the requireme * Microsoft Windows XP SP3, Vista, Windows 7, Server 2008, Server 2008 R2 * Mac OS X 10.4+ +### Why doesn't SilverStripe 3 work with PHP 7? +Unfortunately, SilverStripe has classes named the same as PHP reserved words, such as "Int" and "Float". This means that +we are unable to make SilverStripe 3 support PHP 7 without breaking backward compatibility. SilverStripe 4 will work +with PHP 7 and will be released in 2016. Until then, we recommend that you use PHP 5.6. + ## Web server hardware requirements Hardware requirements vary widely depending on the traffic to your website, the complexity of its logic (i.e., PHP), and diff --git a/docs/en/02_Developer_Guides/02_Controllers/03_Access_Control.md b/docs/en/02_Developer_Guides/02_Controllers/03_Access_Control.md index e8bb04e6a..f5ce5fc61 100644 --- a/docs/en/02_Developer_Guides/02_Controllers/03_Access_Control.md +++ b/docs/en/02_Developer_Guides/02_Controllers/03_Access_Control.md @@ -30,7 +30,7 @@ directly calling methods that they shouldn't. 'cmsrestrictedaction' => 'CMS_ACCESS_CMSMain', // complexaction can only be accessed if $this->canComplexAction() returns true. - 'complexaction' '->canComplexAction' + 'complexaction' => '->canComplexAction', // complexactioncheck can only be accessed if $this->canComplexAction("MyRestrictedAction", false, 42) is true. 'complexactioncheck' => '->canComplexAction("MyRestrictedAction", false, 42)', @@ -200,4 +200,4 @@ execution. This behavior can be used to implement permission checks. ## API Documentation -* [api:Controller] \ No newline at end of file +* [api:Controller] diff --git a/tests/control/HTTPRequestTest.php b/tests/control/HTTPRequestTest.php index 3016f0c8b..d4ff9c8f6 100644 --- a/tests/control/HTTPRequestTest.php +++ b/tests/control/HTTPRequestTest.php @@ -254,4 +254,23 @@ class HTTPRequestTest extends SapphireTest { $this->assertEquals('home?test=1', $req->getURL(true)); $this->assertEquals('home', $req->getURL()); } + + public function testGetIPFromHeaderValue() { + $req = new SS_HTTPRequest('GET', '/'); + $reflectionMethod = new ReflectionMethod($req, 'getIPFromHeaderValue'); + $reflectionMethod->setAccessible(true); + + $headers = array( + '80.79.208.21, 149.126.76.1, 10.51.0.68' => '80.79.208.21', + '52.19.19.103, 10.51.0.49' => '52.19.19.103', + '10.51.0.49, 52.19.19.103' => '52.19.19.103', + '10.51.0.49' => '10.51.0.49', + '127.0.0.1, 10.51.0.49' => '127.0.0.1', + ); + + foreach ($headers as $header => $ip) { + $this->assertEquals($ip, $reflectionMethod->invoke($req, $header)); + } + + } } diff --git a/tests/core/startup/ErrorControlChainTest.php b/tests/core/startup/ErrorControlChainTest.php index b5d9031fc..a6c2c2496 100644 --- a/tests/core/startup/ErrorControlChainTest.php +++ b/tests/core/startup/ErrorControlChainTest.php @@ -63,7 +63,11 @@ require_once '$classpath'; class ErrorControlChainTest extends SapphireTest { + protected $displayErrors = null; + function setUp() { + $this->displayErrors = (bool)ini_get('display_errors'); + // Check we can run PHP at all $null = is_writeable('/dev/null') ? '/dev/null' : 'NUL'; exec("php -v 2> $null", $out, $rv); @@ -76,8 +80,48 @@ class ErrorControlChainTest extends SapphireTest { 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() { + // Errors disabled by default + ini_set('display_errors', false); + $chain = new ErrorControlChain(); + $whenNotSuppressed = null; + $whenSuppressed = null; + $chain->then(function($chain) use(&$whenNotSuppressed, &$whenSuppressed) { + $chain->setSuppression(true); + $whenSuppressed = ini_get('display_errors'); + $chain->setSuppression(false); + $whenNotSuppressed = ini_get('display_errors'); + })->execute(); + + // Disabled errors never un-disable + $this->assertFalse((bool)$whenNotSuppressed); + $this->assertFalse((bool)$whenSuppressed); + + // Errors enabled by default + ini_set('display_errors', true); + $chain = new ErrorControlChain(); + $whenNotSuppressed = null; + $whenSuppressed = null; + $chain->then(function($chain) use(&$whenNotSuppressed, &$whenSuppressed) { + $chain->setSuppression(true); + $whenSuppressed = ini_get('display_errors'); + $chain->setSuppression(false); + $whenNotSuppressed = ini_get('display_errors'); + })->execute(); + + // Errors can be suppressed an un-suppressed when initially enabled + $this->assertTrue((bool)$whenNotSuppressed); + $this->assertFalse((bool)$whenSuppressed); + // Fatal error $chain = new ErrorControlChainTest_Chain();