From 1186d42f09bf2c721e6cac6132bd8419262e7134 Mon Sep 17 00:00:00 2001
From: Andreas Fernandez <a.fernandez@scripting-base.de>
Date: Mon, 14 Sep 2015 16:44:35 +0200
Subject: [PATCH] [FEATURE] Support pecl-memcached in MemcachedBackend

The MemcachedBackend now also supports the pecl-memcached
module. The caching backend detects which modules are installed
and uses "memcache" over "memcached" to avoid being a breaking
change. If both modules are installed, an integrator can choose the
PECL module by setting the new ``peclModule`` option.

This feature was originally introduced with TYPO3 v8.0. Due to
compatibility reasons with PHP 7 this feature gets backported as the
memcache module is not available anymore.

Resolves: #83569
Related: #69794
Releases: 7.6
Change-Id: Idd4e85cf8ec71d47217b63dfe55b1231107c7b82
Reviewed-on: https://review.typo3.org/55384
Reviewed-by: Stefan Neufeind <typo3.neufeind@speedpartner.de>
Tested-by: Stefan Neufeind <typo3.neufeind@speedpartner.de>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
---
 .../Cache/Backend/MemcachedBackend.php        | 133 ++++++++++++++----
 ...upportPecl-memcachedInMemcachedBackend.rst |  39 +++++
 .../Cache/Backend/MemcachedBackendTest.php    |   4 +-
 3 files changed, 143 insertions(+), 33 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/7.6.x/Feature-69794-SupportPecl-memcachedInMemcachedBackend.rst

diff --git a/typo3/sysext/core/Classes/Cache/Backend/MemcachedBackend.php b/typo3/sysext/core/Classes/Cache/Backend/MemcachedBackend.php
index 2e1196082701..c35b1d7014f3 100644
--- a/typo3/sysext/core/Classes/Cache/Backend/MemcachedBackend.php
+++ b/typo3/sysext/core/Classes/Cache/Backend/MemcachedBackend.php
@@ -14,6 +14,10 @@ namespace TYPO3\CMS\Core\Cache\Backend;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Cache\Exception;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
 /**
  * A caching backend which stores cache entries by using Memcached.
  *
@@ -40,7 +44,7 @@ namespace TYPO3\CMS\Core\Cache\Backend;
  * This file is a backport from FLOW3 by Ingo Renner.
  * @api
  */
-class MemcachedBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend implements \TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface
+class MemcachedBackend extends AbstractBackend implements TaggableBackendInterface
 {
     /**
      * Max bucket size, (1024*1024)-42 bytes
@@ -48,13 +52,21 @@ class MemcachedBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend imp
      * @var int
      */
     const MAX_BUCKET_SIZE = 1048534;
+
     /**
      * Instance of the PHP Memcache class
      *
-     * @var \Memcache
+     * @var \Memcache|\Memcached
      */
     protected $memcache;
 
+    /**
+     * Used PECL module for memcached
+     *
+     * @var string
+     */
+    protected $usedPeclModule = '';
+
     /**
      * Array of Memcache server configurations
      *
@@ -64,7 +76,7 @@ class MemcachedBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend imp
 
     /**
      * Indicates whether the memcache uses compression or not (requires zlib),
-     * either 0 or MEMCACHE_COMPRESSED
+     * either 0 or \Memcached::OPT_COMPRESSION / MEMCACHE_COMPRESSED
      *
      * @var int
      */
@@ -82,14 +94,23 @@ class MemcachedBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend imp
      *
      * @param string $context FLOW3's application context
      * @param array $options Configuration options - depends on the actual backend
-     * @throws \TYPO3\CMS\Core\Cache\Exception if memcache is not installed
+     * @throws Exception if memcache is not installed
      */
     public function __construct($context, array $options = [])
     {
-        if (!extension_loaded('memcache')) {
-            throw new \TYPO3\CMS\Core\Cache\Exception('The PHP extension "memcache" must be installed and loaded in ' . 'order to use the Memcached backend.', 1213987706);
+        if (!extension_loaded('memcache') && !extension_loaded('memcached')) {
+            throw new Exception('The PHP extension "memcache" or "memcached" must be installed and loaded in ' . 'order to use the Memcached backend.', 1213987706);
         }
+
         parent::__construct($context, $options);
+
+        if ($this->usedPeclModule === '') {
+            if (extension_loaded('memcache')) {
+                $this->usedPeclModule = 'memcache';
+            } elseif (extension_loaded('memcached')) {
+                $this->usedPeclModule = 'memcached';
+            }
+        }
     }
 
     /**
@@ -114,28 +135,41 @@ class MemcachedBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend imp
      */
     protected function setCompression($useCompression)
     {
+        $compressionFlag = $this->usedPeclModule === 'memcache' ? MEMCACHE_COMPRESSED : \Memcached::OPT_COMPRESSION;
         if ($useCompression === true) {
-            $this->flags ^= MEMCACHE_COMPRESSED;
+            $this->flags ^= $compressionFlag;
         } else {
-            $this->flags &= ~MEMCACHE_COMPRESSED;
+            $this->flags &= ~$compressionFlag;
         }
     }
 
+    /**
+     * Getter for compression flag
+     *
+     * @return bool
+     * @api
+     */
+    protected function getCompression()
+    {
+        return $this->flags !== 0;
+    }
+
     /**
      * Initializes the identifier prefix
      *
      * @return void
-     * @throws \TYPO3\CMS\Core\Cache\Exception
+     * @throws Exception
      */
     public function initializeObject()
     {
         if (empty($this->servers)) {
-            throw new \TYPO3\CMS\Core\Cache\Exception('No servers were given to Memcache', 1213115903);
+            throw new Exception('No servers were given to Memcache', 1213115903);
         }
-        $this->memcache = new \Memcache();
-        $defaultPort = ini_get('memcache.default_port');
+        $memcachedPlugin = '\\' . ucfirst($this->usedPeclModule);
+        $this->memcache = new $memcachedPlugin;
+        $defaultPort = $this->usedPeclModule === 'memcache' ? ini_get('memcache.default_port') : 11211;
         foreach ($this->servers as $server) {
-            if (substr($server, 0, 7) == 'unix://') {
+            if (substr($server, 0, 7) === 'unix://') {
                 $host = $server;
                 $port = 0;
             } else {
@@ -151,15 +185,33 @@ class MemcachedBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend imp
             }
             $this->memcache->addserver($host, $port);
         }
+        if ($this->usedPeclModule === 'memcached') {
+            $this->memcache->setOption(\Memcached::OPT_COMPRESSION, $this->getCompression());
+        }
+    }
+
+    /**
+     * Sets the preferred PECL module
+     *
+     * @param string $peclModule
+     * @throws Exception
+     */
+    public function setPeclModule($peclModule)
+    {
+        if ($peclModule !== 'memcache' && $peclModule !== 'memcached') {
+            throw new Exception('PECL module must be either "memcache" or "memcached".', 1442239768);
+        }
+
+        $this->usedPeclModule = $peclModule;
     }
 
     /**
      * Initializes the identifier prefix when setting the cache.
      *
-     * @param \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache The frontend for this backend
+     * @param FrontendInterface $cache The frontend for this backend
      * @return void
      */
-    public function setCache(\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache)
+    public function setCache(FrontendInterface $cache)
     {
         parent::setCache($cache);
         $identifierHash = substr(md5(PATH_site . $this->context . $this->cacheIdentifier), 0, 12);
@@ -174,9 +226,9 @@ class MemcachedBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend imp
      * @param array $tags Tags to associate with this cache entry
      * @param int $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
      * @return void
-     * @throws \TYPO3\CMS\Core\Cache\Exception if no cache frontend has been set.
+     * @throws Exception if no cache frontend has been set.
      * @throws \InvalidArgumentException if the identifier is not valid or the final memcached key is longer than 250 characters
-     * @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException if $data is not a string
+     * @throws Exception\InvalidDataException if $data is not a string
      * @api
      */
     public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
@@ -184,14 +236,15 @@ class MemcachedBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend imp
         if (strlen($this->identifierPrefix . $entryIdentifier) > 250) {
             throw new \InvalidArgumentException('Could not set value. Key more than 250 characters (' . $this->identifierPrefix . $entryIdentifier . ').', 1232969508);
         }
-        if (!$this->cache instanceof \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface) {
-            throw new \TYPO3\CMS\Core\Cache\Exception('No cache frontend has been set yet via setCache().', 1207149215);
+        if (!$this->cache instanceof FrontendInterface) {
+            throw new Exception('No cache frontend has been set yet via setCache().', 1207149215);
         }
         if (!is_string($data)) {
-            throw new \TYPO3\CMS\Core\Cache\Exception\InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1207149231);
+            throw new Exception\InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1207149231);
         }
         $tags[] = '%MEMCACHEBE%' . $this->cacheIdentifier;
         $expiration = $lifetime !== null ? $lifetime : $this->defaultLifetime;
+        $memcacheIsUsed = $this->usedPeclModule === 'memcache';
         // Memcached consideres values over 2592000 sec (30 days) as UNIX timestamp
         // thus $expiration should be converted from lifetime to UNIX timestamp
         if ($expiration > 2592000) {
@@ -203,21 +256,34 @@ class MemcachedBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend imp
                 $success = true;
                 $chunkNumber = 1;
                 foreach ($data as $chunk) {
-                    $success = $success && $this->memcache->set($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber, $chunk, $this->flags, $expiration);
+                    if ($memcacheIsUsed) {
+                        $success = $success && $this->memcache->set($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber, $chunk, $this->flags, $expiration);
+                    } else {
+                        $success = $success && $this->memcache->set($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber, $chunk, $expiration);
+                    }
+
                     $chunkNumber++;
                 }
-                $success = $success && $this->memcache->set($this->identifierPrefix . $entryIdentifier, 'TYPO3*chunked:' . $chunkNumber, $this->flags, $expiration);
+                if ($memcacheIsUsed) {
+                    $success = $success && $this->memcache->set($this->identifierPrefix . $entryIdentifier, 'TYPO3*chunked:' . $chunkNumber, $this->flags, $expiration);
+                } else {
+                    $success = $success && $this->memcache->set($this->identifierPrefix . $entryIdentifier, 'TYPO3*chunked:' . $chunkNumber, $expiration);
+                }
             } else {
-                $success = $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $this->flags, $expiration);
+                if ($memcacheIsUsed) {
+                    $success = $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $this->flags, $expiration);
+                } else {
+                    $success = $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $expiration);
+                }
             }
             if ($success === true) {
                 $this->removeIdentifierFromAllTags($entryIdentifier);
                 $this->addIdentifierToTags($entryIdentifier, $tags);
             } else {
-                throw new \TYPO3\CMS\Core\Cache\Exception('Could not set data to memcache server.', 1275830266);
+                throw new Exception('Could not set data to memcache server.', 1275830266);
             }
         } catch (\Exception $exception) {
-            \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog('Memcache: could not set value. Reason: ' . $exception->getMessage(), 'core', \TYPO3\CMS\Core\Utility\GeneralUtility::SYSLOG_SEVERITY_WARNING);
+            GeneralUtility::sysLog('Memcache: could not set value. Reason: ' . $exception->getMessage(), 'core', GeneralUtility::SYSLOG_SEVERITY_WARNING);
         }
     }
 
@@ -250,7 +316,13 @@ class MemcachedBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend imp
      */
     public function has($entryIdentifier)
     {
-        return $this->memcache->get($this->identifierPrefix . $entryIdentifier) !== false;
+        if ($this->usedPeclModule === 'memcache') {
+            return $this->memcache->get($this->identifierPrefix . $entryIdentifier) !== false;
+        }
+
+        // pecl-memcached supports storing literal FALSE
+        $this->memcache->get($this->identifierPrefix . $entryIdentifier);
+        return $this->memcache->getResultCode() !== \Memcached::RES_NOTFOUND;
     }
 
     /**
@@ -290,13 +362,13 @@ class MemcachedBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend imp
      * Removes all cache entries of this cache.
      *
      * @return void
-     * @throws \TYPO3\CMS\Core\Cache\Exception
+     * @throws Exception
      * @api
      */
     public function flush()
     {
-        if (!$this->cache instanceof \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface) {
-            throw new \TYPO3\CMS\Core\Cache\Exception('No cache frontend has been set via setCache() yet.', 1204111376);
+        if (!$this->cache instanceof FrontendInterface) {
+            throw new Exception('No cache frontend has been set via setCache() yet.', 1204111376);
         }
         $this->flushByTag('%MEMCACHEBE%' . $this->cacheIdentifier);
     }
@@ -353,14 +425,13 @@ class MemcachedBackend extends \TYPO3\CMS\Core\Cache\Backend\AbstractBackend imp
      * Removes association of the identifier with the given tags
      *
      * @param string $entryIdentifier
-     * @param array Array of tags
      * @return void
      */
     protected function removeIdentifierFromAllTags($entryIdentifier)
     {
         // Get tags for this identifier
         $tags = $this->findTagsByIdentifier($entryIdentifier);
-        // Deassociate tags with this identifier
+        // De-associate tags with this identifier
         foreach ($tags as $tag) {
             $identifiers = $this->findIdentifiersByTag($tag);
             // Formally array_search() below should never return FALSE due to
diff --git a/typo3/sysext/core/Documentation/Changelog/7.6.x/Feature-69794-SupportPecl-memcachedInMemcachedBackend.rst b/typo3/sysext/core/Documentation/Changelog/7.6.x/Feature-69794-SupportPecl-memcachedInMemcachedBackend.rst
new file mode 100644
index 000000000000..8bfb58fd3bd4
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/7.6.x/Feature-69794-SupportPecl-memcachedInMemcachedBackend.rst
@@ -0,0 +1,39 @@
+
+.. include:: ../../Includes.txt
+
+============================================================
+Feature: #69794 - Support pecl-memcached in MemcachedBackend
+============================================================
+
+See :issue:`69794`
+
+Description
+===========
+
+Support for the PECL module "memcached" has been added to the MemcachedBackend of the Caching Framework.
+
+
+Impact
+======
+
+The MemcachedBackend checks if either "memcache" or "memcached" is installed. If both plugins are installed, the
+MemcachedBackend uses "memcache" over "memcached" to avoid being a breaking change. An integrator may set the option
+``peclModule` to use the preferred PECL module.
+
+Example code:
+
+.. code-block:: php
+
+	$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['my_memcached'] = [
+		'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
+		'backend' => \TYPO3\CMS\Core\Cache\Backend\MemcachedBackend::class,
+		'options' => [
+			'peclModule' => 'memcached',
+			'servers' => [
+				'localhost',
+				'server2:port'
+			]
+		]
+	];
+
+.. index:: PHP-API, LocalConfiguration
diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/MemcachedBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/MemcachedBackendTest.php
index 0cac73ac4e72..e74d167aca71 100644
--- a/typo3/sysext/core/Tests/Unit/Cache/Backend/MemcachedBackendTest.php
+++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/MemcachedBackendTest.php
@@ -30,8 +30,8 @@ class MemcachedBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
      */
     protected function setUp()
     {
-        if (!extension_loaded('memcache')) {
-            $this->markTestSkipped('memcache extension was not available');
+        if (!extension_loaded('memcache') && !extension_loaded('memcached')) {
+            $this->markTestSkipped('Neither "memcache" nor "memcached" extension is available');
         }
         try {
             if (!@fsockopen('localhost', 11211)) {
-- 
GitLab