From d89a08b8b27ef9a7293e9f8cf351bedbd838e2cb Mon Sep 17 00:00:00 2001
From: mattab <matthieu.aubry@gmail.com>
Date: Sun, 12 May 2013 18:09:22 +1200
Subject: [PATCH] Fixes #3932  * you can now write
 browserCode==ff;referrerKeyword!= to select all visitors using firefox and
 that have a keyword set  * or you can write referrerKeyword==;browserCode==ff
 to select all visitors using firefox and that did not have any keyword set
 Also fixes #3933

Refs #2135
 * fixing last bugs with segment selector encoding (working on chrome + FF + opera) - I 'hope' it will work on iE...
---
 core/API/Proxy.php                            |  2 +
 core/API/Request.php                          | 12 ++++-
 core/Segment.php                              |  8 ++--
 core/SegmentExpression.php                    | 44 ++++++++++++++++---
 core/ViewDataTable.php                        | 24 ++++++++--
 .../GenerateGraphData/ChartEvolution.php      |  2 +-
 plugins/API/API.php                           |  2 +-
 .../DataTableRowAction/RowEvolution.php       |  2 +-
 plugins/CoreHome/templates/broadcast.js       |  5 ++-
 plugins/CoreHome/templates/datatable.js       |  2 +-
 .../templates/datatable_rowactions.js         |  1 +
 plugins/CoreHome/templates/menu.js            |  4 +-
 plugins/CoreHome/templates/menu.tpl           |  2 +-
 plugins/Live/Controller.php                   |  2 +-
 .../SegmentEditor/templates/Segmentation.js   |  7 +--
 plugins/UserCountryMap/Controller.php         |  6 +--
 plugins/VisitsSummary/Controller.php          |  2 +-
 tests/PHPUnit/Core/SegmentExpressionTest.php  | 13 +++---
 tests/PHPUnit/Core/SegmentTest.php            | 25 ++++++++++-
 themes/default/ajaxHelper.js                  | 13 +++++-
 20 files changed, 132 insertions(+), 46 deletions(-)

diff --git a/core/API/Proxy.php b/core/API/Proxy.php
index 2a024afbf5..e7ed698f6e 100644
--- a/core/API/Proxy.php
+++ b/core/API/Proxy.php
@@ -161,6 +161,7 @@ class Piwik_API_Proxy
 
         // Temporarily sets the Request array to this API call context
         $saveGET = $_GET;
+        $saveQUERY_STRING = @$_SERVER['QUERY_STRING'];
         foreach ($parametersRequest as $param => $value) {
             $_GET[$param] = $value;
         }
@@ -199,6 +200,7 @@ class Piwik_API_Proxy
 
             // Restore the request
             $_GET = $saveGET;
+            $_SERVER['QUERY_STRING'] = $saveQUERY_STRING;
 
             // log the API Call
             try {
diff --git a/core/API/Request.php b/core/API/Request.php
index 22d0436b3c..d9ec3f7cff 100644
--- a/core/API/Request.php
+++ b/core/API/Request.php
@@ -48,6 +48,12 @@ class Piwik_API_Request
     static public function getRequestArrayFromString($request)
     {
         $defaultRequest = $_GET + $_POST;
+
+        $requestRaw = self::getRequestParametersGET();
+        if(!empty($requestRaw['segment'])) {
+            $defaultRequest['segment'] = $requestRaw['segment'];
+        }
+
         $requestArray = $defaultRequest;
 
         if (!is_null($request)) {
@@ -63,9 +69,8 @@ class Piwik_API_Request
             $request = str_replace(array("\n", "\t"), '', $request);
 
             $requestParsed = Piwik_Common::getArrayFromQueryString($request);
-
-//            parse_str($request, $requestArray);
             $requestArray = $requestParsed + $defaultRequest;
+
         }
 
         foreach ($requestArray as &$element) {
@@ -209,6 +214,9 @@ class Piwik_API_Request
      */
     public static function getRequestParametersGET()
     {
+        if(empty($_SERVER['QUERY_STRING'])) {
+            return array();
+        }
         $GET = Piwik_Common::getArrayFromQueryString($_SERVER['QUERY_STRING']);
         return $GET;
     }
diff --git a/core/Segment.php b/core/Segment.php
index a02581e829..af57fe59a9 100644
--- a/core/Segment.php
+++ b/core/Segment.php
@@ -37,9 +37,9 @@ class Piwik_Segment
         // First try with url decoded value. If that fails, try with raw value.
         // If that also fails, it will throw the exception
         try {
-            $this->initializeSegment($string, $idSites);
-        } catch(Exception $e) {
             $this->initializeSegment( urldecode($string), $idSites);
+        } catch(Exception $e) {
+            $this->initializeSegment($string, $idSites);
         }
     }
 
@@ -50,7 +50,6 @@ class Piwik_Segment
      */
     protected function initializeSegment($string, $idSites)
     {
-        $string = Piwik_Common::unsanitizeInputValue($string);
         // As a preventive measure, we restrict the filter size to a safe limit
         $string = substr($string, 0, self::SEGMENT_TRUNCATE_LIMIT);
 
@@ -111,7 +110,8 @@ class Piwik_Segment
             // apply presentation filter
             if (isset($segment['sqlFilter'])
                 && !empty($segment['sqlFilter'])
-                && $matchType != Piwik_SegmentExpression::MATCH_IS_NOT_NULL
+                && $matchType != Piwik_SegmentExpression::MATCH_IS_NOT_NULL_NOR_EMPTY
+                && $matchType != Piwik_SegmentExpression::MATCH_IS_NULL_OR_EMPTY
             ) {
                 $value = call_user_func($segment['sqlFilter'], $value, $segment['sqlSegment'], $matchType, $name);
 
diff --git a/core/SegmentExpression.php b/core/SegmentExpression.php
index fe178abccf..43aa1294a1 100644
--- a/core/SegmentExpression.php
+++ b/core/SegmentExpression.php
@@ -27,8 +27,11 @@ class Piwik_SegmentExpression
     const MATCH_CONTAINS = '=@';
     const MATCH_DOES_NOT_CONTAIN = '!@';
 
-    // Note: undocumented for now, only used in API.getSuggestedValuesForSegment
-    const MATCH_IS_NOT_NULL = '::';
+    // Note: you can't write this in the API, but access this feature
+    // via field!=        <- IS NOT NULL
+    // or via field==     <- IS NULL / empty
+    const MATCH_IS_NOT_NULL_NOR_EMPTY = '::NOT_NULL';
+    const MATCH_IS_NULL_OR_EMPTY = '::NULL';
 
     // Special case, since we look up Page URLs/Page titles in a sub SQL query
     const MATCH_ACTIONS_CONTAINS = 'IN';
@@ -72,9 +75,8 @@ class Piwik_SegmentExpression
                 . self::MATCH_LESS_OR_EQUAL . '|'
                 . self::MATCH_LESS . '|'
                 . self::MATCH_CONTAINS . '|'
-                . self::MATCH_IS_NOT_NULL . '|'
                 . self::MATCH_DOES_NOT_CONTAIN
-                . '){1}(.+)/';
+                . '){1}(.*)/';
             $match = preg_match($pattern, $operand, $matches);
             if ($match == 0) {
                 throw new Exception('The segment \'' . $operand . '\' is not valid.');
@@ -83,6 +85,19 @@ class Piwik_SegmentExpression
             $leftMember = $matches[1];
             $operation = $matches[2];
             $valueRightMember = urldecode($matches[3]);
+
+            // is null / is not null
+            if ($valueRightMember === '') {
+                if($operation == self::MATCH_NOT_EQUAL) {
+                    $operation = self::MATCH_IS_NOT_NULL_NOR_EMPTY;
+                } elseif($operation == self::MATCH_EQUAL) {
+                    $operation = self::MATCH_IS_NULL_OR_EMPTY;
+                } else {
+                    throw new Exception('The segment \'' . $operand . '\' has no value specified. You can leave this value empty ' .
+                        'only when you use the operators: ' . self::MATCH_NOT_EQUAL . ' (is not) or ' . self::MATCH_EQUAL . ' (is)');
+                }
+            }
+
             $parsedSubExpressions[] = array(
                 self::INDEX_BOOL_OPERATOR => $operator,
                 self::INDEX_OPERAND       => array(
@@ -158,12 +173,14 @@ class Piwik_SegmentExpression
         $matchType = $def[1];
         $value = $def[2];
 
+        $alsoMatchNULLValues = false;
         switch ($matchType) {
             case self::MATCH_EQUAL:
                 $sqlMatch = '=';
                 break;
             case self::MATCH_NOT_EQUAL:
                 $sqlMatch = '<>';
+                $alsoMatchNULLValues = true;
                 break;
             case self::MATCH_GREATER:
                 $sqlMatch = '>';
@@ -184,13 +201,19 @@ class Piwik_SegmentExpression
             case self::MATCH_DOES_NOT_CONTAIN:
                 $sqlMatch = 'NOT LIKE';
                 $value = '%' . $this->escapeLikeString($value) . '%';
+                $alsoMatchNULLValues = true;
                 break;
 
-            case self::MATCH_IS_NOT_NULL:
+            case self::MATCH_IS_NOT_NULL_NOR_EMPTY:
                 $sqlMatch = 'IS NOT NULL AND ('.$field.' <> \'\' OR '.$field.' = 0)';
                 $value = null;
                 break;
 
+            case self::MATCH_IS_NULL_OR_EMPTY:
+                $sqlMatch = 'IS NULL OR '.$field.' = \'\' ';
+                $value = null;
+                break;
+
             case self::MATCH_ACTIONS_CONTAINS:
                 // this match type is not accessible from the outside
                 // (it won't be matched in self::parseSubExpressions())
@@ -204,11 +227,18 @@ class Piwik_SegmentExpression
                 break;
         }
 
+        // We match NULL values when rows are excluded only when we are not doing a
+        $alsoMatchNULLValues = $alsoMatchNULLValues && !empty($value);
+
         if ($matchType === self::MATCH_ACTIONS_CONTAINS
             || is_null($value)) {
-            $sqlExpression = "$field $sqlMatch";
+            $sqlExpression = "( $field $sqlMatch )";
         } else {
-            $sqlExpression = "$field $sqlMatch ?";
+            if($alsoMatchNULLValues) {
+                $sqlExpression = "( $field IS NULL OR $field $sqlMatch ? )";
+            } else {
+                $sqlExpression = "$field $sqlMatch ?";
+            }
         }
 
         $this->checkFieldIsAvailable($field, $availableTables);
diff --git a/core/ViewDataTable.php b/core/ViewDataTable.php
index 28dc4b23bf..89cc36ff3d 100644
--- a/core/ViewDataTable.php
+++ b/core/ViewDataTable.php
@@ -583,13 +583,26 @@ abstract class Piwik_ViewDataTable
             }
         }
 
+        $segment = $this->getRawSegmentFromRequest();
+        if(!empty($segment)) {
+            $requestString .= '&segment=' . $segment;
+        }
+        return $requestString;
+    }
+
+    /**
+     * @return array|bool
+     */
+    static public function getRawSegmentFromRequest()
+    {
         // we need the URL encoded segment parameter, we fetch it from _SERVER['QUERY_STRING'] instead of default URL decoded _GET
+        $segmentRaw = false;
         $segment = Piwik_Common::getRequestVar('segment', '', 'string');
         if (!empty($segment)) {
-            $requestRaw = Piwik_API_Request::getRequestParametersGET();
-            $requestString .= '&segment=' . $requestRaw['segment'];
+            $request = Piwik_API_Request::getRequestParametersGET();
+            $segmentRaw = $request['segment'];
         }
-        return $requestString;
+        return $segmentRaw;
     }
 
     /**
@@ -768,6 +781,11 @@ abstract class Piwik_ViewDataTable
                 unset($javascriptVariablesToSet[$name]);
             }
         }
+
+        $rawSegment = $this->getRawSegmentFromRequest();
+        $javascriptVariablesToSet['segment'] = $rawSegment;
+
+
         return $javascriptVariablesToSet;
     }
 
diff --git a/core/ViewDataTable/GenerateGraphData/ChartEvolution.php b/core/ViewDataTable/GenerateGraphData/ChartEvolution.php
index 65d7988216..cbcf93d7be 100644
--- a/core/ViewDataTable/GenerateGraphData/ChartEvolution.php
+++ b/core/ViewDataTable/GenerateGraphData/ChartEvolution.php
@@ -191,7 +191,7 @@ class Piwik_ViewDataTable_GenerateGraphData_ChartEvolution extends Piwik_ViewDat
                     'idSite'  => $idSite,
                     'period'  => $period->getLabel(),
                     'date'    => $dateInUrl->toString(),
-                    'segment' => Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('segment', false))
+                    'segment' => Piwik_ViewDataTable::getRawSegmentFromRequest()
                 );
                 $hash = '';
                 if (!empty($queryStringAsHash)) {
diff --git a/plugins/API/API.php b/plugins/API/API.php
index e9d3511f19..2402b515fd 100644
--- a/plugins/API/API.php
+++ b/plugins/API/API.php
@@ -1659,7 +1659,7 @@ class Piwik_API_API
 
         // Select non empty fields only
         // Note: this optimization has only a very minor impact
-        $requestLastVisits.= "&segment=$segmentName" . Piwik_SegmentExpression::MATCH_IS_NOT_NULL . "null";
+        $requestLastVisits.= "&segment=$segmentName".urlencode('!=');
 
         // By default Live fetches all actions for all visitors, but we'd rather do this only when required
         if($this->doesSegmentNeedActionsData($segmentName)) {
diff --git a/plugins/CoreHome/DataTableRowAction/RowEvolution.php b/plugins/CoreHome/DataTableRowAction/RowEvolution.php
index 5df1ce3e16..ab42ea3452 100644
--- a/plugins/CoreHome/DataTableRowAction/RowEvolution.php
+++ b/plugins/CoreHome/DataTableRowAction/RowEvolution.php
@@ -91,7 +91,7 @@ class Piwik_CoreHome_DataTableRowAction_RowEvolution
             list($this->date, $lastN) =
                 Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution::getDateRangeAndLastN($this->period, $end);
         }
-        $this->segment = Piwik_Common::getRequestVar('segment', '', 'string');
+        $this->segment = Piwik_ViewDataTable::getRawSegmentFromRequest();
 
         $this->loadEvolutionReport();
     }
diff --git a/plugins/CoreHome/templates/broadcast.js b/plugins/CoreHome/templates/broadcast.js
index 31c3fb6c29..508777c174 100644
--- a/plugins/CoreHome/templates/broadcast.js
+++ b/plugins/CoreHome/templates/broadcast.js
@@ -462,7 +462,10 @@ var broadcast = {
             hashStr = url.substring(url.indexOf("#"), url.length);
         }
         else {
-            hashStr = (location.hash);
+            locationSplit = location.href.split('#');
+            if(typeof locationSplit[1] != 'undefined') {
+                hashStr = '#' + locationSplit[1];
+            }
         }
 
         return hashStr;
diff --git a/plugins/CoreHome/templates/datatable.js b/plugins/CoreHome/templates/datatable.js
index 1369b11786..a0a9b4afd4 100644
--- a/plugins/CoreHome/templates/datatable.js
+++ b/plugins/CoreHome/templates/datatable.js
@@ -1367,7 +1367,7 @@ dataTable.prototype =
                 // doing AJAX request
                 var menuItem = null;
                 $("#root").find(">ul.nav a").each(function () {
-                    if ($(this).attr('name') == url) {
+                    if ($(this).attr('href') == url) {
                         menuItem = this;
                         return false
                     }
diff --git a/plugins/CoreHome/templates/datatable_rowactions.js b/plugins/CoreHome/templates/datatable_rowactions.js
index afe31cc61b..238c4e7309 100644
--- a/plugins/CoreHome/templates/datatable_rowactions.js
+++ b/plugins/CoreHome/templates/datatable_rowactions.js
@@ -213,6 +213,7 @@ DataTable_RowAction.prototype.getLabelFromTr = function (tr) {
     if (!value) {
         value = label.text();
     }
+    value = value.trim();
 
     return encodeURIComponent(value);
 };
diff --git a/plugins/CoreHome/templates/menu.js b/plugins/CoreHome/templates/menu.js
index 3be160b1ca..b2010152be 100644
--- a/plugins/CoreHome/templates/menu.js
+++ b/plugins/CoreHome/templates/menu.js
@@ -29,7 +29,7 @@ menu.prototype =
 
     onItemClick: function (item) {
         $('ul.nav').trigger('piwikSwitchPage', item);
-        broadcast.propagateAjax($(item).attr('name'));
+        broadcast.propagateAjax( $(item).attr('href').substr(1) );
         return false;
     },
 
@@ -45,7 +45,7 @@ menu.prototype =
         // for all sub menu we want to have a unique id based on their module and action
         // for main menu we want to add just the module as its id.
         this.menuNode.find('li').each(function () {
-            var url = $(this).find('a').attr('name');
+            var url = $(this).find('a').attr('href').substr(1);
             var module = broadcast.getValueFromUrl("module", url);
             var action = broadcast.getValueFromUrl("action", url);
             var moduleId = broadcast.getValueFromUrl("idGoal", url) || broadcast.getValueFromUrl("idDashboard", url);
diff --git a/plugins/CoreHome/templates/menu.tpl b/plugins/CoreHome/templates/menu.tpl
index e9e2fcd853..476483f745 100644
--- a/plugins/CoreHome/templates/menu.tpl
+++ b/plugins/CoreHome/templates/menu.tpl
@@ -6,7 +6,7 @@
             <ul>
                 {foreach from=$level2 key=name item=urlParameters name=level2}
                     {if strpos($name, '_') !== 0}
-                        <li><a name='{$urlParameters._url|@urlRewriteWithParameters}' href='#{$urlParameters._url|@urlRewriteWithParameters|substr:1}'
+                        <li><a href='#{$urlParameters._url|@urlRewriteWithParameters|substr:1}'
                                onclick='return piwikMenu.onItemClick(this);'>{$name|translate|escape:'html'}</a></li>
                     {/if}
                 {/foreach}
diff --git a/plugins/Live/Controller.php b/plugins/Live/Controller.php
index 40f2300678..ef703aa743 100644
--- a/plugins/Live/Controller.php
+++ b/plugins/Live/Controller.php
@@ -130,7 +130,7 @@ class Piwik_Live_Controller extends Piwik_Controller
 
     private function setCounters($view)
     {
-        $segment = Piwik_Common::getRequestVar('segment', false, 'string');
+        $segment = Piwik_ViewDataTable::getRawSegmentFromRequest();
         $last30min = Piwik_Live_API::getInstance()->getCounters($this->idSite, $lastMinutes = 30, $segment);
         $last30min = $last30min[0];
         $today = Piwik_Live_API::getInstance()->getCounters($this->idSite, $lastMinutes = 24 * 60, $segment);
diff --git a/plugins/SegmentEditor/templates/Segmentation.js b/plugins/SegmentEditor/templates/Segmentation.js
index eb8379efe9..0b5ab93147 100644
--- a/plugins/SegmentEditor/templates/Segmentation.js
+++ b/plugins/SegmentEditor/templates/Segmentation.js
@@ -948,12 +948,7 @@ $(document).ready( function(){
     var changeSegment = function(segmentDefinition){
         $('#segmentEditorPanel a.close').click();
         segmentDefinition = cleanupSegmentDefinition(segmentDefinition);
-
-//        if($.browser.mozilla ) {
-            segmentDefinition = encodeURIComponent(segmentDefinition);
-//        }
-//        alert('new segment to reload='+segmentDefinition);
-
+        segmentDefinition = encodeURIComponent(segmentDefinition);
         return broadcast.propagateNewPage('segment=' + segmentDefinition, true);
     };
 
diff --git a/plugins/UserCountryMap/Controller.php b/plugins/UserCountryMap/Controller.php
index b0ca11de21..5195ec7421 100644
--- a/plugins/UserCountryMap/Controller.php
+++ b/plugins/UserCountryMap/Controller.php
@@ -73,7 +73,7 @@ class Piwik_UserCountryMap_Controller extends Piwik_Controller
                                                               'date'                        => $date,
                                                               'token_auth'                  => $token_auth,
                                                               'format'                      => 'json',
-                                                              'segment'                     => Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('segment', '')),
+                                                              'segment'                     => Piwik_ViewDataTable::getRawSegmentFromRequest(),
                                                               'showRawMetrics'              => 1,
                                                               'enable_filter_excludelowpop' => 1,
                                                               'filter_excludelowpop_value'  => -1
@@ -144,7 +144,7 @@ class Piwik_UserCountryMap_Controller extends Piwik_Controller
                                                 'date'           => self::REAL_TIME_WINDOW,
                                                 'token_auth'     => $token_auth,
                                                 'format'         => 'json',
-                                                'segment'        => Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('segment', '')),
+                                                'segment'        => Piwik_ViewDataTable::getRawSegmentFromRequest(),
                                                 'showRawMetrics' => 1
                                            ));
 
@@ -192,7 +192,7 @@ class Piwik_UserCountryMap_Controller extends Piwik_Controller
             . "&period=" . $period
             . "&date=" . $date
             . "&token_auth=" . $token_auth
-            . "&segment=" . Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('segment', ''))
+            . "&segment=" . Piwik_ViewDataTable::getRawSegmentFromRequest()
             . "&enable_filter_excludelowpop=1"
             . "&showRawMetrics=1";
 
diff --git a/plugins/VisitsSummary/Controller.php b/plugins/VisitsSummary/Controller.php
index c93cf11679..fd62d14e38 100644
--- a/plugins/VisitsSummary/Controller.php
+++ b/plugins/VisitsSummary/Controller.php
@@ -129,7 +129,7 @@ class Piwik_VisitsSummary_Controller extends Piwik_Controller
         $dataTableVisit = self::getVisitsSummary();
         $dataRow = $dataTableVisit->getRowsCount() == 0 ? new Piwik_DataTable_Row() : $dataTableVisit->getFirstRow();
 
-        $dataTableActions = Piwik_Actions_API::getInstance()->get($idSite, Piwik_Common::getRequestVar('period'), Piwik_Common::getRequestVar('date'), Piwik_Common::getRequestVar('segment', false));
+        $dataTableActions = Piwik_Actions_API::getInstance()->get($idSite, Piwik_Common::getRequestVar('period'), Piwik_Common::getRequestVar('date'), Piwik_ViewDataTable::getRawSegmentFromRequest());
         $dataActionsRow =
             $dataTableActions->getRowsCount() == 0 ? new Piwik_DataTable_Row() : $dataTableActions->getFirstRow();
 
diff --git a/tests/PHPUnit/Core/SegmentExpressionTest.php b/tests/PHPUnit/Core/SegmentExpressionTest.php
index ea53e5d049..afbfa23854 100644
--- a/tests/PHPUnit/Core/SegmentExpressionTest.php
+++ b/tests/PHPUnit/Core/SegmentExpressionTest.php
@@ -60,10 +60,10 @@ class SegmentExpressionTest extends PHPUnit_Framework_TestCase
         return array(
             array('A==B%', array('where' => " A = ? ", 'bind' => array('B%'))),
             array('ABCDEF====B===', array('where' => " ABCDEF = ? ", 'bind' => array('==B==='))),
-            array('A===B;CDEF!=C!=', array('where' => " A = ? AND CDEF <> ? ", 'bind' => array('=B', 'C!='))),
+            array('A===B;CDEF!=C!=', array('where' => " A = ? AND ( CDEF IS NULL OR CDEF <> ? ) ", 'bind' => array('=B', 'C!='))),
             array('A==B,C==D', array('where' => " (A = ? OR C = ? )", 'bind' => array('B', 'D'))),
-            array('A!=B;C==D', array('where' => " A <> ? AND C = ? ", 'bind' => array('B', 'D'))),
-            array('A!=B;C==D,E!=Hello World!=', array('where' => " A <> ? AND (C = ? OR E <> ? )", 'bind' => array('B', 'D', 'Hello World!='))),
+            array('A!=B;C==D', array('where' => " ( A IS NULL OR A <> ? ) AND C = ? ", 'bind' => array('B', 'D'))),
+            array('A!=B;C==D,E!=Hello World!=', array('where' => " ( A IS NULL OR A <> ? ) AND (C = ? OR ( E IS NULL OR E <> ? ) )", 'bind' => array('B', 'D', 'Hello World!='))),
 
             array('A>B', array('where' => " A > ? ", 'bind' => array('B'))),
             array('A<B', array('where' => " A < ? ", 'bind' => array('B'))),
@@ -74,7 +74,7 @@ class SegmentExpressionTest extends PHPUnit_Framework_TestCase
             array('A>=B;C>=D,E<w_ow great!', array('where' => " A >= ? AND (C >= ? OR E < ? )", 'bind' => array('B', 'D', 'w_ow great!'))),
 
             array('A=@B_', array('where' => " A LIKE ? ", 'bind' => array('%B\_%'))),
-            array('A!@B%', array('where' => " A NOT LIKE ? ", 'bind' => array('%B\%%'))),
+            array('A!@B%', array('where' => " ( A IS NULL OR A NOT LIKE ? ) ", 'bind' => array('%B\%%'))),
         );
     }
 
@@ -107,8 +107,7 @@ class SegmentExpressionTest extends PHPUnit_Framework_TestCase
             array(',;,'),
             array(','),
             array(',,'),
-            array('==='),
-            array('!=')
+            array('!='),
         );
     }
 
@@ -126,6 +125,6 @@ class SegmentExpressionTest extends PHPUnit_Framework_TestCase
         } catch (Exception $e) {
             return;
         }
-        $this->fail('Expected exception not raised');
+        $this->fail('Expected exception not raised for:' . var_export($segment->getSql(), true));
     }
 }
diff --git a/tests/PHPUnit/Core/SegmentTest.php b/tests/PHPUnit/Core/SegmentTest.php
index 35da2a58a3..c007cc9d20 100644
--- a/tests/PHPUnit/Core/SegmentTest.php
+++ b/tests/PHPUnit/Core/SegmentTest.php
@@ -55,7 +55,7 @@ class SegmentTest extends PHPUnit_Framework_TestCase
 
             // AND, with 2 values rewrites
             array('countryCode==a;visitorType!=returning;visitorType==new', array(
-                'where' => ' log_visit.location_country = ? AND log_visit.visitor_returning <> ? AND log_visit.visitor_returning = ? ',
+                'where' => ' log_visit.location_country = ? AND ( log_visit.visitor_returning IS NULL OR log_visit.visitor_returning <> ? ) AND log_visit.visitor_returning = ? ',
                 'bind'  => array('a', '1', '0'))),
 
             // OR, with 2 value rewrites
@@ -63,6 +63,27 @@ class SegmentTest extends PHPUnit_Framework_TestCase
                 'where' => ' (log_visit.referer_type = ? OR log_visit.referer_type = ? )',
                 'bind'  => array(Piwik_Common::REFERER_TYPE_SEARCH_ENGINE,
                                  Piwik_Common::REFERER_TYPE_DIRECT_ENTRY))),
+
+            // IS NOT NULL
+            array('browserCode==ff;referrerKeyword!=', array(
+                'where' => ' log_visit.config_browser_name = ? AND ( log_visit.referer_keyword IS NOT NULL AND (log_visit.referer_keyword <> \'\' OR log_visit.referer_keyword = 0) ) ',
+                'bind'  => array('ff')
+            )),
+            array('referrerKeyword!=,browserCode==ff', array(
+                'where' => ' (( log_visit.referer_keyword IS NOT NULL AND (log_visit.referer_keyword <> \'\' OR log_visit.referer_keyword = 0) ) OR log_visit.config_browser_name = ? )',
+                'bind'  => array('ff')
+            )),
+
+            // IS NULL
+            array('browserCode==ff;referrerKeyword==', array(
+                'where' => ' log_visit.config_browser_name = ? AND ( log_visit.referer_keyword IS NULL OR log_visit.referer_keyword = \'\' ) ',
+                'bind'  => array('ff')
+            )),
+            array('referrerKeyword==,browserCode==ff', array(
+                'where' => ' (( log_visit.referer_keyword IS NULL OR log_visit.referer_keyword = \'\' ) OR log_visit.config_browser_name = ? )',
+                'bind'  => array('ff')
+            )),
+
         );
     }
 
@@ -259,7 +280,7 @@ class SegmentTest extends PHPUnit_Framework_TestCase
                 WHERE
                     ( log_conversion.idvisit = ? )
                     AND
-                    ( log_conversion.idgoal <> ? AND log_link_visit_action.custom_var_k1 = ? AND log_conversion.idgoal = ? )",
+                    ( ( log_conversion.idgoal IS NULL OR log_conversion.idgoal <> ? ) AND log_link_visit_action.custom_var_k1 = ? AND log_conversion.idgoal = ? )",
             "bind" => array(1, 2, 'Test', 1));
 
         $this->assertEquals($this->_filterWhitsSpaces($expected), $this->_filterWhitsSpaces($query));
diff --git a/themes/default/ajaxHelper.js b/themes/default/ajaxHelper.js
index f2e7e12a71..10b9459216 100644
--- a/themes/default/ajaxHelper.js
+++ b/themes/default/ajaxHelper.js
@@ -300,10 +300,19 @@ function ajaxHelper() {
     this._buildAjaxCall = function () {
         var that = this;
 
+        var parameters = this._mixinDefaultGetParams(this.getParams);
+
+        var url = 'index.php?';
+        // we took care of encoding &segment properly already, so we don't use $.param for it ($.param URL encodes the values)
+        if(parameters['segment']) {
+            url += 'segment=' + parameters['segment'] + '&';
+            delete parameters['segment'];
+        }
+        url += $.param(parameters);
         var ajaxCall = {
             type:     'POST',
             async:    this.async !== false,
-            url:      'index.php?' + $.param(this._mixinDefaultGetParams(this.getParams)),
+            url:      url,
             dataType: this.format || 'json',
             error:    this.errorCallback,
             success:  function (response) {
@@ -363,7 +372,7 @@ function ajaxHelper() {
         var defaultParams = {
             idSite:  piwik.idSite || broadcast.getValueFromUrl('idSite'),
             period:  piwik.period || broadcast.getValueFromUrl('period'),
-            segment: broadcast.getValueFromHash('segment', window.location.href)
+            segment: broadcast.getValueFromHash('segment', window.location.href.split('#')[1])
         };
 
         // never append token_auth to url
-- 
GitLab