<?php
/**
 * Memory caching.
 *
 * This file is part of ADOdb, a Database Abstraction Layer library for PHP.
 *
 * @package ADOdb
 * @link https://adodb.org Project's web site and documentation
 * @link https://github.com/ADOdb/ADOdb Source code and issue tracker
 *
 * The ADOdb Library is dual-licensed, released under both the BSD 3-Clause
 * and the GNU Lesser General Public Licence (LGPL) v2.1 or, at your option,
 * any later version. This means you can use it in proprietary products.
 * See the LICENSE.md file distributed with this source code for details.
 * @license BSD-3-Clause
 * @license LGPL-2.1-or-later
 *
 * @copyright 2000-2013 John Lim
 * @copyright 2014 Damien Regad, Mark Newnham and the ADOdb community
 *
 * @noinspection PhpUnused
 */

// security - hide paths
if (!defined('ADODB_DIR')) die();

global $ADODB_INCLUDED_MEMCACHE;
$ADODB_INCLUDED_MEMCACHE = 1;

global $ADODB_INCLUDED_CSV;
if (empty($ADODB_INCLUDED_CSV)) {
	include_once(ADODB_DIR . '/adodb-csvlib.inc.php');
}

class ADODB_Cache_MemCache
{
	/**
	 * @var bool Prevents parent class calling non-existent function
	 */
	public $createdir = false;

	/**
	 * @var array of hosts
	 */
	private $hosts;

	/**
	 * @var int Connection Port, uses default
	 */
	private $port;

	/**
	 * @var bool memcache compression with zlib
	 */
	private $compress;

	/**
	 * @var array of options for memcached only
	 */
	private $options;

	/**
	 * @var bool Internal flag indicating successful connection
	 */
	private $isConnected = false;

	/**
	 * @var Memcache|Memcached Handle for the Memcache library
	 *
	 * Populated with the proper library on connect, used later when
	 * there are differences in specific calls between memcache and memcached
	 */
	private $memcacheLibrary = false;

	/**
	 * @var array New server feature controller lists available servers
	 */
	private $serverControllers = array();

	/**
	 * @var array New server feature template uses granular server controller
	 */
	private $serverControllerTemplate = array(
		'host' => '',
		'port' => 11211,
		'weight' => 0,
	);

	/**
	 * An integer index into the libraries
	 * @see $libraries
	 */
	const MCLIB = 1;
	const MCLIBD = 2;

	/**
	 * @var array Xrefs the library flag to the actual class name
	 */
	private $libraries = array(
		self::MCLIB => 'Memcache',
		self::MCLIBD => 'Memcached'
	);

	/**
	 * @var int An indicator of which library we are using
	 */
	private $libraryFlag;

	/**
	 * Class Constructor.
	 *
	 * @param ADOConnection $db
	 */
	public function __construct($db)
	{
		$this->hosts = $db->memCacheHost;
		$this->port = $this->serverControllerTemplate['port'] = $db->memCachePort;
		$this->compress = $db->memCacheCompress;
		$this->options = $db->memCacheOptions;
	}

	/**
	 * Return true if the current library is Memcached.
	 * @return bool
	 */
	public function isLibMemcached(): bool
	{
		return $this->libraryFlag == self::MCLIBD;
	}

	/**
	 * Lazy connection.
	 *
	 * The connection only occurs on CacheExecute call.
	 *
	 * @param string $err
	 *
	 * @return bool success of connecting to a server
	 */
	public function connect(&$err)
	{
		// do we have memcache or memcached? see the note at adodb.org on memcache
		if (class_exists('Memcache')) {
			$this->libraryFlag = self::MCLIB;
		} elseif (class_exists('Memcached')) {
			$this->libraryFlag = self::MCLIBD;
		} else {
			$err = 'Neither the Memcache nor Memcached PECL extensions were found!';
			return false;
		}

		$usedLibrary = $this->libraries[$this->libraryFlag];

		/** @var Memcache|Memcached $memCache */
		$memCache = new $usedLibrary;
		if (!$memCache) {
			$err = 'Memcache library failed to initialize';
			return false;
		}

		// Convert simple compression flag for memcached
		if ($this->isLibMemcached()) {
			$this->options[Memcached::OPT_COMPRESSION] = $this->compress;
		}

		// Are there any options available for memcached
		if ($this->isLibMemcached() && count($this->options) > 0) {
			$optionSuccess = $memCache->setOptions($this->options);
			if (!$optionSuccess) {
				$err = 'Invalid option parameters passed to Memcached';
				return false;
			}
		}

		// Have we passed a controller array
		if (!is_array($this->hosts)) {
			$this->hosts = array($this->hosts);
		}

		if (!is_array($this->hosts[0])) {
			// Old way, convert to controller
			foreach ($this->hosts as $ipAddress) {
				$connector = $this->serverControllerTemplate;
				$connector['host'] = $ipAddress;
				$connector['port'] = $this->port;

				$this->serverControllers[] = $connector;
			}
		} else {
			// New way, must validate port, etc
			foreach ($this->hosts as $controller) {
				$connector = array_merge($this->serverControllerTemplate, $controller);
				if ($this->isLibMemcached()) {
					$connector['weight'] = (int)$connector['weight'];
				} else {
					// Cannot use weight in memcache, simply discard
					$connector['weight'] = 0;
				}

				$this->serverControllers[] = $connector;
			}
		}

		// Checks for existing connections ( but only for memcached )
		if ($this->isLibMemcached() && !empty($memCache->getServerList())) {
			// Use the existing configuration
			$this->isConnected = true;
			$this->memcacheLibrary = $memCache;
			return true;
		}

		$failcnt = 0;
		foreach ($this->serverControllers as $controller) {
			if ($this->isLibMemcached()) {
				if (!@$memCache->addServer($controller['host'], $controller['port'], $controller['weight'])) {
					$failcnt++;
				}
			} else {
				if (!@$memCache->addServer($controller['host'], $controller['port'])) {
					$failcnt++;
				}
			}
		}
		if ($failcnt == sizeof($this->serverControllers)) {
			$err = 'Can\'t connect to any memcache server';
			return false;
		}

		$this->memcacheLibrary = $memCache;

		// A valid memcache connection is available
		$this->isConnected = true;
		return true;
	}

	/**
	 * Writes a cached query to the server
	 *
	 * @param string $filename The MD5 of the query to cache
	 * @param string $contents The query results
	 * @param bool $debug
	 * @param int $secs2cache
	 *
	 * @return bool true or false. true if successful save
	 */
	public function writeCache($filename, $contents, $debug, $secs2cache)
	{
		$err = '';
		if (!$this->isConnected && $debug) {
			// Call to writeCache() before connect(), try to connect
			if (!$this->connect($err)) {
				ADOConnection::outp($err);
			}
		} else {
			if (!$this->isConnected) {
				$this->connect($err);
			}
		}

		if (!$this->memcacheLibrary) {
			return false;
		}

		$failed = false;
		switch ($this->libraryFlag) {
			case self::MCLIB:
				if (!$this->memcacheLibrary->set($filename, $contents, $this->compress ? MEMCACHE_COMPRESSED : 0,
					$secs2cache)) {
					$failed = true;
				}
				break;
			case self::MCLIBD:
				if (!$this->memcacheLibrary->set($filename, $contents, $secs2cache)) {
					$failed = true;
				}
				break;
			default:
				$failed = true;
				break;
		}

		if ($failed) {
			if ($debug) {
				ADOConnection::outp(" Failed to save data at the memcache server!<br>\n");
			}
			return false;
		}

		return true;
	}

	/**
	 * Reads a cached query from the server.
	 *
	 * @param string $filename The MD5 of the query to read
	 * @param string $err The query results
	 * @param int $secs2cache
	 * @param object $rsClass **UNUSED**
	 *
	 * @return object|bool record or false.
	 *
	 * @noinspection PhpUnusedParameterInspection
	 */
	public function readCache($filename, &$err, $secs2cache, $rsClass)
	{
		if (!$this->isConnected) {
			$this->connect($err);
		}
		if (!$this->memcacheLibrary) {
			return false;
		}

		$rs = $this->memcacheLibrary->get($filename);
		if (!$rs) {
			$err = 'Item with such key doesn\'t exist on the memcache server.';
			return false;
		}

		// hack, should actually use _csv2rs
		$rs = explode("\n", $rs);
		unset($rs[0]);
		$rs = join("\n", $rs);
		$rs = unserialize($rs);
		if (!is_object($rs)) {
			$err = 'Unable to unserialize $rs';
			return false;
		}
		if ($rs->timeCreated == 0) {
			return $rs;
		} // apparently have been reports that timeCreated was set to 0 somewhere

		$tdiff = intval($rs->timeCreated + $secs2cache - time());
		if ($tdiff <= 2) {
			switch ($tdiff) {
				case 2:
					if ((rand() & 15) == 0) {
						$err = "Timeout 2";
						return false;
					}
					break;
				case 1:
					if ((rand() & 3) == 0) {
						$err = "Timeout 1";
						return false;
					}
					break;
				default:
					$err = "Timeout 0";
					return false;
			}
		}
		return $rs;
	}

	/**
	 * Flushes all of the stored memcache data
	 *
	 * @param bool $debug
	 *
	 * @return bool The response from the memcache server
	 */
	public function flushAll($debug = false)
	{
		if (!$this->isConnected) {
			$err = '';
			if (!$this->connect($err) && $debug) {
				ADOConnection::outp($err);
			}
		}
		if (!$this->memcacheLibrary) {
			return false;
		}

		$del = $this->memcacheLibrary->flush();

		if ($debug) {
			if (!$del) {
				ADOConnection::outp("flushall: failed!<br>\n");
			} else {
				ADOConnection::outp("flushall: succeeded!<br>\n");
			}
		}

		return $del;
	}

	/**
	 * Flushes the contents of a specified query
	 *
	 * @param string $filename The MD5 of the query to flush
	 * @param bool $debug
	 *
	 * @return bool The response from the memcache server
	 */
	public function flushCache($filename, $debug = false)
	{
		if (!$this->isConnected) {
			$err = '';
			if (!$this->connect($err) && $debug) {
				ADOConnection::outp($err);
			}
		}
		if (!$this->memcacheLibrary) {
			return false;
		}

		$del = $this->memcacheLibrary->delete($filename);

		if ($debug) {
			if (!$del) {
				ADOConnection::outp("flushcache: $filename entry doesn't exist on memcache server!<br>\n");
			} else {
				ADOConnection::outp("flushcache: $filename entry flushed from memcache server!<br>\n");
			}
		}

		return $del;
	}

}
