From a9d032329cf2bb357a12c06fb43d3cce6d50311c Mon Sep 17 00:00:00 2001
From: Demian Katz <demian.katz@villanova.edu>
Date: Fri, 6 Mar 2015 13:21:31 -0500
Subject: [PATCH] Centralized cookie handling, with new config options. -
 Thanks to Ere Maijala for initial inspiration and subsequent feedback.

---
 config/vufind/config.ini                      |  18 +-
 module/VuFind/config/module.config.php        |   5 +-
 module/VuFind/src/VuFind/Auth/Factory.php     |   4 +-
 module/VuFind/src/VuFind/Auth/Manager.php     |  21 +-
 module/VuFind/src/VuFind/Bootstrapper.php     |   5 +-
 module/VuFind/src/VuFind/Cart.php             |  48 ++---
 .../VuFind/src/VuFind/Controller/Factory.php  |  14 ++
 .../VuFind/Controller/UpgradeController.php   |   6 +-
 module/VuFind/src/VuFind/Cookie/Container.php |  49 ++---
 .../src/VuFind/Cookie/CookieManager.php       | 185 ++++++++++++++++++
 module/VuFind/src/VuFind/Service/Factory.php  |  55 +++++-
 .../src/VuFindTest/Auth/ManagerTest.php       |   3 +-
 .../unit-tests/src/VuFindTest/CartTest.php    |  52 +++--
 .../src/VuFindTheme/Initializer.php           |  15 +-
 14 files changed, 379 insertions(+), 101 deletions(-)
 create mode 100644 module/VuFind/src/VuFind/Cookie/CookieManager.php

diff --git a/config/vufind/config.ini b/config/vufind/config.ini
index d642050dc8b..5bb59850880 100644
--- a/config/vufind/config.ini
+++ b/config/vufind/config.ini
@@ -89,9 +89,6 @@ showBookBag = false
 bookBagMaxSize = 100
 ; Display bulk items (export, save, etc.) and checkboxes on search result screens?
 showBulkOptions = false
-; Set the domain used for cart-related cookies (sometimes useful for sharing the
-; cookies across subdomains)
-;bookBagCookieDomain = ".example.edu"
 ; Generator value to display in an HTML header <meta> tag:
 generator = "VuFind 2.3.1"
 
@@ -113,6 +110,21 @@ lifetime                    = 3600 ; Session lasts for 1 hour
 ;memcache_port               = 11211
 ;memcache_connection_timeout = 1
 
+; This section controls how VuFind creates cookies (to store session IDs, bookbag
+; contents, theme/language settings, etc.)
+[Cookies]
+; In case there are multiple VuFind instances on the same server and they should not
+; share cookies/sessions, this option can be enabled to limit the session to the
+; current path. Default is false, which will place cookies at the root directory.
+;limit_by_path = true
+; If VuFind is only accessed via HTTPS, this setting can be enabled to disallow
+; the browser from ever sending cookies over an unencrypted connection (i.e.
+; before being redirected to HTTPS). Default is false. 
+;only_secure = true  
+; Set the domain used for cookies (sometimes useful for sharing the cookies across
+; subdomains); by default, cookies will be restricted to the current hostname.
+;domain = ".example.edu"
+
 ; Please set the ILS that VuFind will interact with.
 ;
 ; Available drivers: Aleph, Amicus, ClaviusSQL, Evergreen, Horizon (basic database
diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php
index ff53d0003f7..800c7834ba1 100644
--- a/module/VuFind/config/module.config.php
+++ b/module/VuFind/config/module.config.php
@@ -66,6 +66,7 @@ $config = [
             'collection' => 'VuFind\Controller\Factory::getCollectionController',
             'collections' => 'VuFind\Controller\Factory::getCollectionsController',
             'record' => 'VuFind\Controller\Factory::getRecordController',
+            'upgrade' => 'VuFind\Controller\Factory::getUpgradeController',
         ],
         'invokables' => [
             'ajax' => 'VuFind\Controller\AjaxController',
@@ -99,7 +100,6 @@ $config = [
             'summon' => 'VuFind\Controller\SummonController',
             'summonrecord' => 'VuFind\Controller\SummonrecordController',
             'tag' => 'VuFind\Controller\TagController',
-            'upgrade' => 'VuFind\Controller\UpgradeController',
             'web' => 'VuFind\Controller\WebController',
             'worldcat' => 'VuFind\Controller\WorldcatController',
             'worldcatrecord' => 'VuFind\Controller\WorldcatrecordController',
@@ -139,6 +139,7 @@ $config = [
             'VuFind\ContentCoversPluginManager' => 'VuFind\Service\Factory::getContentCoversPluginManager',
             'VuFind\ContentExcerptsPluginManager' => 'VuFind\Service\Factory::getContentExcerptsPluginManager',
             'VuFind\ContentReviewsPluginManager' => 'VuFind\Service\Factory::getContentReviewsPluginManager',
+            'VuFind\CookieManager' => 'VuFind\Service\Factory::getCookieManager',
             'VuFind\DateConverter' => 'VuFind\Service\Factory::getDateConverter',
             'VuFind\DbAdapter' => 'VuFind\Service\Factory::getDbAdapter',
             'VuFind\DbAdapterFactory' => 'VuFind\Service\Factory::getDbAdapterFactory',
@@ -172,6 +173,7 @@ $config = [
             'VuFind\SearchResultsPluginManager' => 'VuFind\Service\Factory::getSearchResultsPluginManager',
             'VuFind\SearchSpecsReader' => 'VuFind\Service\Factory::getSearchSpecsReader',
             'VuFind\SearchStats' => 'VuFind\Service\Factory::getSearchStats',
+            'VuFind\SessionManager' => 'VuFind\Service\Factory::getSessionManager',
             'VuFind\SessionPluginManager' => 'VuFind\Service\Factory::getSessionPluginManager',
             'VuFind\SMS' => 'VuFind\SMS\Factory',
             'VuFind\Solr\Writer' => 'VuFind\Service\Factory::getSolrWriter',
@@ -181,7 +183,6 @@ $config = [
             'VuFind\WorldCatUtils' => 'VuFind\Service\Factory::getWorldCatUtils',
         ],
         'invokables' => [
-            'VuFind\SessionManager' => 'Zend\Session\SessionManager',
             'VuFind\Search'         => 'VuFindSearch\Service',
             'VuFind\Search\Memory'  => 'VuFind\Search\Memory',
             'VuFind\HierarchicalFacetHelper' => 'VuFind\Search\Solr\HierarchicalFacetHelper'
diff --git a/module/VuFind/src/VuFind/Auth/Factory.php b/module/VuFind/src/VuFind/Auth/Factory.php
index 9358d3157b6..3c0d1df9300 100644
--- a/module/VuFind/src/VuFind/Auth/Factory.php
+++ b/module/VuFind/src/VuFind/Auth/Factory.php
@@ -108,16 +108,16 @@ class Factory
             // here may interfere with UI rendering. If we ignore it now, it will
             // still get handled appropriately later in processing.
             error_log($e->getMessage());
-            $catalog = null; // avoid unset variable notice
         }
 
         // Load remaining dependencies:
         $userTable = $sm->get('VuFind\DbTablePluginManager')->get('user');
         $sessionManager = $sm->get('VuFind\SessionManager');
         $pm = $sm->get('VuFind\AuthPluginManager');
+        $cookies = $sm->get('VuFind\CookieManager');
 
         // Build the object:
-        return new Manager($config, $userTable, $sessionManager, $pm, $catalog);
+        return new Manager($config, $userTable, $sessionManager, $pm, $cookies);
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/Auth/Manager.php b/module/VuFind/src/VuFind/Auth/Manager.php
index 8da987b076b..ac0e2cc6f1a 100644
--- a/module/VuFind/src/VuFind/Auth/Manager.php
+++ b/module/VuFind/src/VuFind/Auth/Manager.php
@@ -26,7 +26,8 @@
  * @link     http://www.vufind.org  Main Page
  */
 namespace VuFind\Auth;
-use VuFind\Db\Row\User as UserRow, VuFind\Db\Table\User as UserTable,
+use VuFind\Cookie\CookieManager,
+    VuFind\Db\Row\User as UserRow, VuFind\Db\Table\User as UserTable,
     VuFind\Exception\Auth as AuthException,
     Zend\Config\Config, Zend\Session\SessionManager;
 
@@ -97,6 +98,13 @@ class Manager implements \ZfcRbac\Identity\IdentityProviderInterface
      */
     protected $pluginManager;
 
+    /**
+     * Cookie Manager
+     *
+     * @var CookieManager
+     */
+    protected $cookieManager;
+
     /**
      * Cache for current logged in user object
      *
@@ -111,15 +119,18 @@ class Manager implements \ZfcRbac\Identity\IdentityProviderInterface
      * @param UserTable      $userTable      User table gateway
      * @param SessionManager $sessionManager Session manager
      * @param PluginManager  $pm             Authentication plugin manager
+     * @param CookieManager  $cookieManager  Cookie manager
      */
     public function __construct(Config $config, UserTable $userTable,
-        SessionManager $sessionManager, PluginManager $pm
+        SessionManager $sessionManager, PluginManager $pm,
+        CookieManager $cookieManager
     ) {
         // Store dependencies:
         $this->config = $config;
         $this->userTable = $userTable;
         $this->sessionManager = $sessionManager;
         $this->pluginManager = $pm;
+        $this->cookieManager = $cookieManager;
 
         // Set up session:
         $this->session = new \Zend\Session\Container('Account');
@@ -348,7 +359,7 @@ class Manager implements \ZfcRbac\Identity\IdentityProviderInterface
         // Clear out the cached user object and session entry.
         $this->currentUser = false;
         unset($this->session->userId);
-        setcookie('loggedOut', 1, null, '/');
+        $this->cookieManager->set('loggedOut', 1);
 
         // Destroy the session for good measure, if requested.
         if ($destroy) {
@@ -370,7 +381,7 @@ class Manager implements \ZfcRbac\Identity\IdentityProviderInterface
      */
     public function userHasLoggedOut()
     {
-        return isset($_COOKIE['loggedOut']) && $_COOKIE['loggedOut'];
+        return (bool)$this->cookieManager->get('loggedOut');
     }
 
     /**
@@ -426,7 +437,7 @@ class Manager implements \ZfcRbac\Identity\IdentityProviderInterface
     {
         $this->currentUser = $user;
         $this->session->userId = $user->id;
-        setcookie('loggedOut', '', time() - 3600, '/'); // clear logged out cookie
+        $this->cookieManager->clear('loggedOut');
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/Bootstrapper.php b/module/VuFind/src/VuFind/Bootstrapper.php
index b352f21ffb6..8f31b5d13f6 100644
--- a/module/VuFind/src/VuFind/Bootstrapper.php
+++ b/module/VuFind/src/VuFind/Bootstrapper.php
@@ -308,10 +308,12 @@ class Bootstrapper
 
             // Setup Translator
             $request = $event->getRequest();
+            $sm = $event->getApplication()->getServiceManager();
             if (($language = $request->getPost()->get('mylang', false))
                 || ($language = $request->getQuery()->get('lng', false))
             ) {
-                setcookie('language', $language, null, '/');
+                $cookieManager = $sm->get('VuFind\CookieManager');
+                $cookieManager->set('language', $language);
             } elseif (!empty($request->getCookie()->language)) {
                 $language = $request->getCookie()->language;
             } else {
@@ -324,7 +326,6 @@ class Bootstrapper
                 $language = $config->Site->language;
             }
 
-            $sm = $event->getApplication()->getServiceManager();
             try {
                 $sm->get('VuFind\Translator')
                     ->addTranslationFile('ExtendedIni', null, 'default', $language)
diff --git a/module/VuFind/src/VuFind/Cart.php b/module/VuFind/src/VuFind/Cart.php
index 3385de5a128..2e6b8d2195c 100644
--- a/module/VuFind/src/VuFind/Cart.php
+++ b/module/VuFind/src/VuFind/Cart.php
@@ -26,6 +26,7 @@
  * @link     http://vufind.org/wiki/vufind2:developer_manual Wiki
  */
 namespace VuFind;
+use VuFind\Cookie\CookieManager;
 
 /**
  * Cart Class
@@ -69,11 +70,11 @@ class Cart
     protected $recordLoader;
 
     /**
-     * Domain context for cookies (null for default)
+     * Cookie manager
      *
-     * @var string
+     * @var CookieManager
      */
-    protected $cookieDomain;
+    protected $cookieManager;
 
     const CART_COOKIE =  'vufind_cart';
     const CART_COOKIE_SOURCES = 'vufind_cart_src';
@@ -82,24 +83,22 @@ class Cart
     /**
      * Constructor
      *
-     * @param \VuFind\Record\Loader $loader       Object for loading records
-     * @param int                   $maxSize      Maximum size of cart contents
-     * @param bool                  $active       Is cart enabled?
-     * @param array                 $cookies      Current cookie values (leave null
-     * to use $_COOKIE superglobal)
-     * @param string                $cookieDomain Domain context for cookies
-     * (optional)
+     * @param \VuFind\Record\Loader $loader        Object for loading records
+     * @param CookieManager         $cookieManager Cookie manager
+     * @param int                   $maxSize       Maximum size of cart contents
+     * @param bool                  $active        Is cart enabled?
      */
     public function __construct(\VuFind\Record\Loader $loader,
-        $maxSize = 100, $active = true, $cookies = null, $cookieDomain = null
+        \VuFind\Cookie\CookieManager $cookieManager,
+        $maxSize = 100, $active = true
     ) {
         $this->recordLoader = $loader;
+        $this->cookieManager = $cookieManager;
         $this->maxSize = $maxSize;
         $this->active = $active;
-        $this->cookieDomain = $cookieDomain;
 
         // Initialize contents
-        $this->init(null === $cookies ? $_COOKIE : $cookies);
+        $this->init($this->cookieManager->getCookies());
     }
 
     /**
@@ -292,24 +291,9 @@ class Cart
 
         // Save the cookies:
         $cookie = implode(self::CART_COOKIE_DELIM, $ids);
-        $this->setCookie(self::CART_COOKIE, $cookie, 0, '/', $this->cookieDomain);
-        $cookie = implode(self::CART_COOKIE_DELIM, $sources);
-        $this->setCookie(
-            self::CART_COOKIE_SOURCES, $cookie, 0, '/', $this->cookieDomain
-        );
-    }
-
-    /**
-     * Set a cookie (wrapper in case Zend Framework offers a better abstraction
-     * of cookie handling in the future).
-     *
-     * @return bool
-     */
-    protected function setCookie()
-    {
-        // @codeCoverageIgnoreStart
-        return call_user_func_array('setcookie', func_get_args());
-        // @codeCoverageIgnoreEnd
+        $this->cookieManager->set(self::CART_COOKIE, $cookie, 0);
+        $srcCookie = implode(self::CART_COOKIE_DELIM, $sources);
+        $this->cookieManager->set(self::CART_COOKIE_SOURCES, $srcCookie, 0);
     }
 
     /**
@@ -319,7 +303,7 @@ class Cart
      */
     public function getCookieDomain()
     {
-        return $this->cookieDomain;
+        return $this->cookieManager->getDomain();
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/Controller/Factory.php b/module/VuFind/src/VuFind/Controller/Factory.php
index 79163b4087d..ba2f465c499 100644
--- a/module/VuFind/src/VuFind/Controller/Factory.php
+++ b/module/VuFind/src/VuFind/Controller/Factory.php
@@ -96,4 +96,18 @@ class Factory
             $sm->getServiceLocator()->get('VuFind\Config')->get('config')
         );
     }
+
+    /**
+     * Construct the UpgradeController.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return UpgradeController
+     */
+    public static function getUpgradeController(ServiceManager $sm)
+    {
+        return new UpgradeController(
+            $sm->getServiceLocator()->get('VuFind\CookieManager')
+        );
+    }
 }
\ No newline at end of file
diff --git a/module/VuFind/src/VuFind/Controller/UpgradeController.php b/module/VuFind/src/VuFind/Controller/UpgradeController.php
index 26117f86fae..86cab730e63 100644
--- a/module/VuFind/src/VuFind/Controller/UpgradeController.php
+++ b/module/VuFind/src/VuFind/Controller/UpgradeController.php
@@ -66,14 +66,16 @@ class UpgradeController extends AbstractBase
 
     /**
      * Constructor
+     *
+     * @param \VuFind\Cookie\CookieManager $cookieManager Cookie manager
      */
-    public function __construct()
+    public function __construct(\VuFind\Cookie\CookieManager $cookieManager)
     {
         // We want to use cookies for tracking the state of the upgrade, since the
         // session is unreliable -- if the user upgrades a configuration that uses
         // a different session handler than the default one, we'll lose track of our
         // upgrade state in the middle of the process!
-        $this->cookie = new CookieContainer('vfup');
+        $this->cookie = new CookieContainer('vfup', $cookieManager);
 
         // ...however, once the configuration piece of the upgrade is done, we can
         // safely use the session for storing some values.  We'll use this for the
diff --git a/module/VuFind/src/VuFind/Cookie/Container.php b/module/VuFind/src/VuFind/Cookie/Container.php
index b1778220063..5f6601af9bf 100644
--- a/module/VuFind/src/VuFind/Cookie/Container.php
+++ b/module/VuFind/src/VuFind/Cookie/Container.php
@@ -47,14 +47,24 @@ class Container
      */
     protected $groupName;
 
+    /**
+     * Cookie manager.
+     *
+     * @var CookieManager
+     */
+    protected $manager;
+
     /**
      * Constructor
      *
-     * @param string $groupName Prefix to use for cookie values.
+     * @param string        $groupName Prefix to use for cookie values.
+     * @param CookieManager $manager   Cookie manager.
      */
-    public function __construct($groupName)
+    public function __construct($groupName, CookieManager $manager = null)
     {
         $this->groupName = $groupName;
+        $this->manager = (null === $manager)
+            ? new CookieManager($_COOKIE) : $manager;
     }
 
     /**
@@ -65,7 +75,7 @@ class Container
     public function getAllValues()
     {
         $retVal = [];
-        foreach ($_COOKIE as $key => $value) {
+        foreach ($this->manager->getCookies() as $key => $value) {
             if (substr($key, 0, strlen($this->groupName)) == $this->groupName) {
                 $retVal[substr($key, strlen($this->groupName))] = $value;
             }
@@ -83,7 +93,8 @@ class Container
      */
     public function & __get($var)
     {
-        return $_COOKIE[$this->groupName . $var];
+        $val = $this->manager->get($this->groupName . $var);
+        return $val;
     }
 
     /**
@@ -97,18 +108,7 @@ class Container
      */
     public function __set($var, $value)
     {
-        $_COOKIE[$this->groupName . $var] = $value;
-        if (is_array($value)) {
-            $i = 0;
-            foreach ($value as $curr) {
-                setcookie(
-                    $this->groupName . $var . '[' . $i . ']', $curr, null, '/'
-                );
-                $i++;
-            }
-        } else {
-            setcookie($this->groupName . $var, $value, null, '/');
-        }
+        $this->manager->set($this->groupName . $var, $value);
     }
 
     /**
@@ -121,7 +121,7 @@ class Container
      */
     public function __isset($var)
     {
-        return isset($_COOKIE[$this->groupName . $var]);
+        return null !== $this->manager->get($this->groupName . $var);
     }
 
     /**
@@ -134,19 +134,6 @@ class Container
      */
     public function __unset($var)
     {
-        $isArray = is_array($_COOKIE[$this->groupName . $var]);
-        if ($isArray) {
-            $count = count($_COOKIE[$this->groupName . $var]);
-        }
-        unset($_COOKIE[$this->groupName . $var]);
-        if ($isArray) {
-            for ($i = 0; $i < $count; $i++) {
-                setcookie(
-                    $this->groupName . $var . '[' . $i . ']', '', time() - 3600, '/'
-                );
-            }
-        } else {
-            setcookie($this->groupName . $var, '', time() - 3600, '/');
-        }
+        $this->manager->clear($this->groupName . $var);
     }
 }
\ No newline at end of file
diff --git a/module/VuFind/src/VuFind/Cookie/CookieManager.php b/module/VuFind/src/VuFind/Cookie/CookieManager.php
new file mode 100644
index 00000000000..20a5af45e9b
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cookie/CookieManager.php
@@ -0,0 +1,185 @@
+<?php
+/**
+ * Cookie Manager
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2015.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Cookie
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/vufind2:developer_manual Wiki
+ */
+namespace VuFind\Cookie;
+
+/**
+ * Cookie Manager
+ *
+ * @category VuFind2
+ * @package  Cookie
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/vufind2:developer_manual Wiki
+ */
+class CookieManager
+{
+    /**
+     * Cookie base path
+     *
+     * @var string
+     */
+    protected $path;
+
+    /**
+     * Cookie domain
+     *
+     * @var string
+     */
+    protected $domain;
+
+    /**
+     * Are cookies secure only?
+     *
+     * @var bool
+     */
+    protected $secure;
+
+    /**
+     * Constructor
+     *
+     * @param array  $cookies Cookie array to manipulate (e.g. $_COOKIE)
+     * @param string $path    Cookie base path (default = /)
+     * @param string $domain  Cookie domain
+     * @param bool   $secure  Are cookies secure only? (default = false)
+     */
+    public function __construct($cookies, $path = '/', $domain = null,
+        $secure = false
+    ) {
+        $this->cookies = $cookies;
+        $this->path = $path;
+        $this->domain = $domain;
+        $this->secure = $secure;
+    }
+
+    /**
+     * Get all cookie values.
+     *
+     * @return array
+     */
+    public function getCookies()
+    {
+        return $this->cookies;
+    }
+
+    /**
+     * Get the cookie domain.
+     *
+     * @return string
+     */
+    public function getDomain()
+    {
+        return $this->domain;
+    }
+
+    /**
+     * Get the cookie path.
+     *
+     * @return string
+     */
+    public function getPath()
+    {
+        return $this->path;
+    }
+
+    /**
+     * Are cookies set to "secure only" mode?
+     *
+     * @return bool
+     */
+    public function isSecure()
+    {
+        return $this->secure;
+    }
+
+    /**
+     * Set a cookie.
+     *
+     * @param string $key    Name of cookie to set
+     * @param mixed  $value  Value to set
+     * @param int    $expire Cookie expiration time
+     *
+     * @return bool
+     */
+    public function set($key, $value, $expire = 0)
+    {
+        if (is_array($value)) {
+            $success = true;
+            foreach ($value as $i => $curr) {
+                $lastSuccess = setcookie(
+                    $key . '[' . $i . ']', $curr, $expire,
+                    $this->path, $this->domain, $this->secure
+                );
+                if (!$lastSuccess) {
+                    $success = false;
+                }
+            }
+        } else {
+            $success = setcookie(
+                $key, $value, $expire, $this->path, $this->domain, $this->secure
+            );
+        }
+        if ($success) {
+            $this->cookies[$key] = $value;
+        }
+        return $success;
+    }
+
+    /**
+     * Clear a cookie.
+     *
+     * @param string $key Name of cookie to unset
+     *
+     * @return bool
+     */
+    public function clear($key)
+    {
+        $value = $this->get($key);
+        if (is_array($value)) {
+            $success = true;
+            foreach (array_keys($value) as $i) {
+                if (!$this->clear($key . '[' . $i . ']')) {
+                    $success = false;
+                }
+            }
+            return $success;
+        }
+        return $this->set($key, null, time() - 3600);
+    }
+
+    /**
+     * Retrieve a cookie value (or null if unset).
+     *
+     * @param string $key Name of cookie to retrieve
+     *
+     * @return mixed
+     */
+    public function get($key)
+    {
+        return isset($this->cookies[$key]) ? $this->cookies[$key] : null;
+    }
+}
\ No newline at end of file
diff --git a/module/VuFind/src/VuFind/Service/Factory.php b/module/VuFind/src/VuFind/Service/Factory.php
index 8cf7fd106c4..bcecfa33eee 100644
--- a/module/VuFind/src/VuFind/Service/Factory.php
+++ b/module/VuFind/src/VuFind/Service/Factory.php
@@ -94,10 +94,9 @@ class Factory
             ? (bool)$config->Site->showBookBag : false;
         $size = isset($config->Site->bookBagMaxSize)
             ? $config->Site->bookBagMaxSize : 100;
-        $domain = isset($config->Site->bookBagCookieDomain)
-            ? $config->Site->bookBagCookieDomain : null;
         return new \VuFind\Cart(
-            $sm->get('VuFind\RecordLoader'), $size, $active, $_COOKIE, $domain
+            $sm->get('VuFind\RecordLoader'), $sm->get('VuFind\CookieManager'),
+            $size, $active
         );
     }
 
@@ -176,6 +175,31 @@ class Factory
         return static::getGenericPluginManager($sm, 'Content\Reviews');
     }
 
+    /**
+     * Construct the cookie manager.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return \VuFind\Cookie\CookieManager
+     */
+    public static function getCookieManager(ServiceManager $sm)
+    {
+        $config = $sm->get('VuFind\Config')->get('config');
+        $path = '/';
+        if (isset($config->Cookies->limit_by_path)
+            && $config->Cookies->limit_by_path
+        ) {
+            $path = $sm->get('Request')->getBasePath();
+        }
+        $secure = isset($config->Cookies->only_secure)
+            ? $config->Cookies->only_secure
+            : false;
+        $domain = isset($config->Cookies->domain)
+            ? $config->Cookies->domain
+            : null;
+        return new \VuFind\Cookie\CookieManager($_COOKIE, $path, $domain, $secure);
+    }
+
     /**
      * Construct the date converter.
      *
@@ -665,6 +689,31 @@ class Factory
         );
     }
 
+    /**
+     * Construct the Session Manager.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return \Zend\Session\SessionManager
+     */
+    public static function getSessionManager(ServiceManager $sm)
+    {
+        $cookieManager = $sm->get('VuFind\CookieManager');
+        $sessionConfig = new \Zend\Session\Config\SessionConfig();
+        $options = [
+            'cookie_path' => $cookieManager->getPath(),
+            'cookie_secure' => $cookieManager->isSecure()
+        ];
+        $domain = $cookieManager->getDomain();
+        if (!empty($domain)) {
+            $options['cookie_domain'] = $domain;
+        }
+
+        $sessionConfig->setOptions($options);
+
+        return new \Zend\Session\SessionManager($sessionConfig);
+    }
+
     /**
      * Construct the Session Plugin Manager.
      *
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php
index 673d69f64ea..269c956f356 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php
@@ -461,7 +461,8 @@ class ManagerTest extends \VuFindTest\Unit\TestCase
         if (null === $pm) {
             $pm = $this->getMockPluginManager();
         }
-        return new Manager($config, $userTable, $sessionManager, $pm);
+        $cookies = new \VuFind\Cookie\CookieManager([]);
+        return new Manager($config, $userTable, $sessionManager, $pm, $cookies);
     }
 
     /**
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/CartTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/CartTest.php
index f3c67576375..af1d0fab369 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/CartTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/CartTest.php
@@ -26,6 +26,7 @@
  * @link     http://vufind.org/wiki/vufind2:unit_tests Wiki
  */
 namespace VuFindTest;
+use VuFind\Cookie\CookieManager;
 
 /**
  * Cart Test Class
@@ -62,23 +63,42 @@ class CartTest extends \PHPUnit_Framework_TestCase
     }
 
     /**
-     * Build a mock cart object.
+     * Build a mock cookie manager.
      *
-     * @param int   $maxSize Maximum size of cart contents
-     * @param bool  $active  Is cart enabled?
-     * @param array $cookies Current cookie values
+     * @param array  $cookies Current cookie values
+     * @param string $path    Cookie base path (default = /)
+     * @param string $domain  Cookie domain
+     * @param bool   $secure  Are cookies secure only? (default = false)
      *
-     * @return \VuFind\Cart
+     * @return CookieManager
      */
-    protected function getCart($maxSize = 100, $active = true, $cookies = [],
-        $domain = null
+    protected function getMockCookieManager($cookies = [], $path = '/',
+        $domain = null, $secure = false
     ) {
         return $this->getMock(
-            'VuFind\Cart', ['setCookie'],
-            [$this->loader, $maxSize, $active, $cookies, $domain]
+            'VuFind\Cookie\CookieManager', ['set'],
+            [$cookies, $path, $domain, $secure]
         );
     }
 
+    /**
+     * Build a mock cart object.
+     *
+     * @param int                 $maxSize Maximum size of cart contents
+     * @param bool                $active  Is cart enabled?
+     * @param array|CookieManager $cookies Current cookie values (or ready-to-use
+     * cookie manager)
+     *
+     * @return \VuFind\Cart
+     */
+    protected function getCart($maxSize = 100, $active = true, $cookies = [])
+    {
+        if (!($cookies instanceof CookieManager)) {
+            $cookies = $this->getMockCookieManager($cookies);
+        }
+        return new \VuFind\Cart($this->loader, $cookies, $maxSize, $active);
+    }
+
     /**
      * Test cookie domain setting.
      *
@@ -86,7 +106,8 @@ class CartTest extends \PHPUnit_Framework_TestCase
      */
     public function testCookieDomain()
     {
-        $cart = $this->getCart(100, true, [], '.example.com');
+        $manager = $this->getMockCookieManager([], '/', '.example.com');
+        $cart = $this->getCart(100, true, $manager);
         $this->assertEquals('.example.com', $cart->getCookieDomain());
     }
 
@@ -160,13 +181,14 @@ class CartTest extends \PHPUnit_Framework_TestCase
      */
     public function testCookieWrite()
     {
-        $cart = $this->getCart();
-        $cart->expects($this->at(0))
-            ->method('setCookie')
+        $manager = $this->getMockCookieManager();
+        $manager->expects($this->at(0))
+            ->method('set')
             ->with($this->equalTo('vufind_cart'), $this->equalTo('Aa'));
-        $cart->expects($this->at(1))
-            ->method('setCookie')
+        $manager->expects($this->at(1))
+            ->method('set')
             ->with($this->equalTo('vufind_cart_src'), $this->equalTo('VuFind'));
+        $cart = $this->getCart(100, true, $manager);
         $cart->addItem('VuFind|a');
     }
 
diff --git a/module/VuFindTheme/src/VuFindTheme/Initializer.php b/module/VuFindTheme/src/VuFindTheme/Initializer.php
index 4ddb7985c09..90683f04e59 100644
--- a/module/VuFindTheme/src/VuFindTheme/Initializer.php
+++ b/module/VuFindTheme/src/VuFindTheme/Initializer.php
@@ -76,6 +76,13 @@ class Initializer
      */
     protected $mobile;
 
+    /**
+     * Cookie manager
+     *
+     * @var \VuFind\Cookie\CookieManager
+     */
+    protected $cookieManager;
+
     /**
      * Constructor
      *
@@ -105,6 +112,9 @@ class Initializer
         // Grab the service manager for convenience:
         $this->serviceManager = $this->event->getApplication()->getServiceManager();
 
+        // Get the cookie manager from the service manager:
+        $this->cookieManager = $this->serviceManager->get('VuFind\CookieManager');
+
         // Get base directory from tools object:
         $this->tools = $this->serviceManager->get('VuFindTheme\ThemeInfo');
 
@@ -220,8 +230,7 @@ class Initializer
         }
 
         // Save the current setting to a cookie so it persists:
-        $_COOKIE['ui'] = $selectedUI;
-        setcookie('ui', $selectedUI, null, '/');
+        $this->cookieManager->set('ui', $selectedUI);
 
         // Do we have a valid mobile selection?
         if ($mobileTheme && $selectedUI == 'mobile') {
@@ -283,7 +292,7 @@ class Initializer
                 if (!empty($name)) {
                     $options[] = [
                         'name' => $name, 'desc' => $desc,
-                        'selected' => ($_COOKIE['ui'] == $name)
+                        'selected' => ($this->cookieManager->get('ui') == $name)
                     ];
                 }
             }
-- 
GitLab