<?php
/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *	   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @package log4php
 */

/**
 * The internal representation of throwables.
 *
 * @package log4php
 * @since 2.1
 */
class LoggerThrowableInformation {

	/** @var Exception Throwable to log */
	private $throwable;

	/** @var array Array of throwable messages */
	private $throwableArray;

	/**
	 * Create a new instance
	 *
	 * @param $throwable - a throwable as a exception
	 * @param $logger - Logger reference
	 */
	public function __construct(Exception $throwable) {
		$this->throwable = $throwable;
	}

	/**
	* Return source exception
	*
	* @return Exception
	*/
	public function getThrowable() {
		return $this->throwable;
	}

	/**
	 * @desc Returns string representation of throwable
	 *
	 * @return array
	 */
	public function getStringRepresentation() {
		if (!is_array($this->throwableArray)) {
			$renderer = new LoggerRendererException();

			$this->throwableArray = explode("\n", $renderer->render($this->throwable));
		}

		return $this->throwableArray;
	}
}


<?php
namespace FACTFinder\Core;

/**
 * Handles the conversion of parameters between requests to the client and
 * requests to the FACT-Finder server (and vice-versa).
 */
class ParametersConverter
{
    /**
     * @var FACTFinder\Util\LoggerInterface
     */
    private $log;

    /**
     * @var ConfigurationInterface
     */
    protected $configuration;

    /**
     * @param string $loggerClass Class name of logger to use. The class should
     *                            implement FACTFinder\Util\LoggerInterface.
     * @param ConfigurationInterface $configuration Configuration object to use.
     */
    public function __construct(
        $loggerClass,
        ConfigurationInterface $configuration
    ) {
        $this->log = $loggerClass::getLogger(__CLASS__);
        $this->configuration = $configuration;
    }

    /**
     * @param Parameters $clientParameters Parameters obtained from a request to
     *        the client.
     * @return Parameters Parameters ready for use with FACT-Finder.
     */
    public function convertClientToServerParameters($clientParameters)
    {
        $result = clone $clientParameters;
        $this->applyParameterMappings($result, $this->configuration->getServerMappings());
        $this->removeIgnoredParameters($result, $this->configuration->getIgnoredServerParameters());
        $this->applyWhitelist($result, $this->configuration->getWhitelistServerParameters());
        $this->ensureChannelParameter($result);
        $this->addRequiredParameters($result, $this->configuration->getRequiredServerParameters());

        return $result;
    }

    /**
     * @param Parameters $clientParameters Parameters obtained from FACT-Finder.
     * @return Parameters Parameters ready for use in requests to the client.
     */
    public function convertServerToClientParameters($serverParameters)
    {
        $result = clone $serverParameters;
        $this->applyParameterMappings($result, $this->configuration->getClientMappings());
        $this->removeIgnoredParameters($result, $this->configuration->getIgnoredClientParameters());
        $this->applyWhitelist($result, $this->configuration->getWhitelistClientParameters());
        $this->addRequiredParameters($result, $this->configuration->getRequiredClientParameters());

        return $result;
    }

    /**
     * Changes the keys in a Parameters object according to the given mapping
     * rules.
     * @param Parameters $parameters Parameters to be modified.
     * @param string[] $mappingRules Associative array of mapping rules.
     *        Parameter names will be mapped from keys to values of this array.
     */
    protected function applyParameterMappings($parameters, $mappingRules)
    {
        foreach ($mappingRules as $k => $v)
        {
            if ($k != $v && isset($parameters[$k]))
            {
                $parameters[$v] = $parameters[$k];
                unset($parameters[$k]);
            }
        }
    }

    /**
     * Removes keys from a Parameters object according to the given ignore
     * rules. It basically turns the parameters into the set difference of the
     * parameters and the ignore rules based on keys.
     * @param Parameters $parameters Parameters to be modified.
     * @param bool[] $ignoreRules Array of parameters to be ignored. The keys
     *        are the parameter names, the values are simply "true", but could
     *        technically have any value.
     */
    protected function removeIgnoredParameters($parameters, $ignoreRules)
    {
        foreach ($ignoreRules as $k => $v)
            unset($parameters[$k]);
    }

    /**
     * Removes keys from a Parameters object according to the given whitelist rules.
     * An empty whitelist means do NOT apply any whitelist (anything is allowed).
     * It removes any keys that are not keys in the given whitelist array aswell.
     * If the key of any rule starts with '/' the key is interpreted as a regular expression to be matched against.
     * @param type $parameters
     * @param bool[] $whitelistRules Array of parameters to be allowed. The keys
     *        are the parameter names, the values are simply "true", but could
     *        technically have any value.
     */
    protected function applyWhitelist($parameters, $whitelistRules)
    {
        //do not apply empty whitelist as this means no whitelist desired
        if(empty($whitelistRules)) {
            return;
        }
        //collect all keys of parameters that pass any whitelist rule
        $allowedKeys = array();
        $keys = array_keys($parameters->getArray());
        foreach ($whitelistRules as $rule => $v) {
            if(strpos($rule, '/') === 0) {
                $allowedKeys = array_merge($allowedKeys, preg_grep($rule, $keys));
            } else {
                $allowedKeys[] = $rule;
            }
        }
        $allowedKeys = array_flip($allowedKeys);
        //unset any parameters that did not pass any whitelist rule
        foreach($parameters->getArray() as $k => $v) {
            if(!isset($allowedKeys[$k])) {
                unset($parameters[$k]);
            }
        }
    }

    /**
     * Ensures that the passed parameters object has a "channel" parameter by
     * adding one if necessary.
     * @param Parameters $parameters Parameters to be modifier.
     */
    protected function ensureChannelParameter($parameters)
    {
        if (!isset($parameters['channel']) || strlen($parameters['channel']) == 0)
            $parameters['channel'] = $this->configuration->getChannel();
    }

    /**
     * Adds keys to an array of parameters according to the given require rules.
     * @param Parameters $parameters Parameters to be modified.
     * @param string[] $ignoreRules Array of required parameters. The keys are
     *        the names of the required parameter, the values are default values
     *        to be used if the parameter is not present.
     */
    protected function addRequiredParameters($parameters, $requireRules)
    {
        foreach ($requireRules as $k => $v)
            if (!isset($parameters[$k]))
                $parameters[$k] = $v;
    }
}


<?php
/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *	   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @package log4php
 */

/**
 * Pool implmentation for LoggerAppender instances.
 *
 * The pool is used when configuring log4php. First all appender instances
 * are created in the pool. Afterward, they are linked to loggers, each
 * appender can be linked to multiple loggers. This makes sure duplicate
 * appenders are not created.
 *
 * @version $Revision: 1350602 $
 * @package log4php
 */
class LoggerAppenderPool {

	/** Holds appenders indexed by their name */
	public static $appenders =  array();

	/**
	 * Adds an appender to the pool.
	 * The appender must be named for this operation.
	 * @param LoggerAppender $appender
	 */
	public static function add(LoggerAppender $appender) {
		$name = $appender->getName();

		if(empty($name)) {
			trigger_error('log4php: Cannot add unnamed appender to pool.', E_USER_WARNING);
			return;
		}

		if (isset(self::$appenders[$name])) {
			trigger_error("log4php: Appender [$name] already exists in pool. Overwriting existing appender.", E_USER_WARNING);
		}

		self::$appenders[$name] = $appender;
	}

	/**
	 * Retrieves an appender from the pool by name.
	 * @param string $name Name of the appender to retrieve.
	 * @return LoggerAppender The named appender or NULL if no such appender
	 *  exists in the pool.
	 */
	public static function get($name) {
		return isset(self::$appenders[$name]) ? self::$appenders[$name] : null;
	}

	/**
	* Removes an appender from the pool by name.
	* @param string $name Name of the appender to remove.
	*/
	public static function delete($name) {
		unset(self::$appenders[$name]);
	}

	/**
	 * Returns all appenders from the pool.
	 * @return array Array of LoggerAppender objects.
	 */
	public static function getAppenders() {
		return self::$appenders;
	}

	/**
	 * Checks whether an appender exists in the pool.
	 * @param string $name Name of the appender to look for.
	 * @return boolean TRUE if the appender with the given name exists.
	 */
	public static function exists($name) {
		return isset(self::$appenders[$name]);
	}

	/**
	 * Clears all appenders from the pool.
	 */
	public static function clear() {
		 self::$appenders =  array();
	}
}


<?php
/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *		http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @package log4php
 */

require dirname(__FILE__) . '/LoggerAutoloader.php';

/**
 * This is the central class in the log4php package. All logging operations
 * are done through this class.
 *
 * The main logging methods are:
 *

 *
{@link trace()}

 *
{@link debug()}

 *
{@link info()}

 *
{@link warn()}

 *
{@link error()}

 *
{@link fatal()}

 *

 *
 * @package    log4php
 * @license	   http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 * @version	   SVN: $Id: Logger.php 1395241 2012-10-07 08:28:53Z ihabunek $
 * @link	   http://logging.apache.org/log4php
 */
class Logger {

	/**
	 * Logger additivity. If set to true then child loggers will inherit
	 * the appenders of their ancestors by default.
	 * @var boolean
	 */
	private $additive = true;

	/**
	 * The Logger's fully qualified class name.
	 * TODO: Determine if this is useful.
	 */
	private $fqcn = 'Logger';

	/** The assigned Logger level. */
	private $level;

	/** The name of this Logger instance. */
	private $name;

	/** The parent logger. Set to null if this is the root logger. */
	private $parent;

	/** A collection of appenders linked to this logger. */
	private $appenders = array();

	/**
	 * Constructor.
	 * @param string $name Name of the logger.
	 */
	public function __construct($name) {
		$this->name = $name;
	}

	/**
	 * Returns the logger name.
	 * @return string
	 */
	public function getName() {
		return $this->name;
	}

	/**
	 * Returns the parent Logger. Can be null if this is the root logger.
	 * @return Logger
	 */
	public function getParent() {
		return $this->parent;
	}

	// ******************************************
	// *** Logging methods                    ***
	// ******************************************

	/**
	 * Log a message object with the TRACE level.
	 *
	 * @param mixed $message message
 	 * @param Exception $throwable Optional throwable information to include
	 *   in the logging event.
	 */
	public function trace($message, $throwable = null) {
		$this->log(LoggerLevel::getLevelTrace(), $message, $throwable);
	}

	/**
	 * Log a message object with the DEBUG level.
	 *
	 * @param mixed $message message
 	 * @param Exception $throwable Optional throwable information to include
	 *   in the logging event.
	 */
	public function debug($message, $throwable = null) {
		$this->log(LoggerLevel::getLevelDebug(), $message, $throwable);
	}

	/**
	 * Log a message object with the INFO Level.
	 *
	 * @param mixed $message message
 	 * @param Exception $throwable Optional throwable information to include
	 *   in the logging event.
	 */
	public function info($message, $throwable = null) {
		$this->log(LoggerLevel::getLevelInfo(), $message, $throwable);
	}

	/**
	 * Log a message with the WARN level.
	 *
	 * @param mixed $message message
  	 * @param Exception $throwable Optional throwable information to include
	 *   in the logging event.
	 */
	public function warn($message, $throwable = null) {
		$this->log(LoggerLevel::getLevelWarn(), $message, $throwable);
	}

	/**
	 * Log a message object with the ERROR level.
	 *
	 * @param mixed $message message
	 * @param Exception $throwable Optional throwable information to include
	 *   in the logging event.
	 */
	public function error($message, $throwable = null) {
		$this->log(LoggerLevel::getLevelError(), $message, $throwable);
	}

	/**
	 * Log a message object with the FATAL level.
	 *
	 * @param mixed $message message
	 * @param Exception $throwable Optional throwable information to include
	 *   in the logging event.
	 */
	public function fatal($message, $throwable = null) {
		$this->log(LoggerLevel::getLevelFatal(), $message, $throwable);
	}

	/**
	 * Log a message using the provided logging level.
	 *
	 * @param LoggerLevel $level The logging level.
	 * @param mixed $message Message to log.
 	 * @param Exception $throwable Optional throwable information to include
	 *   in the logging event.
	 */
	public function log(LoggerLevel $level, $message, $throwable = null) {
		if($this->isEnabledFor($level)) {
			$event = new LoggerLoggingEvent($this->fqcn, $this, $level, $message, null, $throwable);
			$this->callAppenders($event);
		}

		// Forward the event upstream if additivity is turned on
		if(isset($this->parent) && $this->getAdditivity()) {

			// Use the event if already created
			if (isset($event)) {
				$this->parent->logEvent($event);
			} else {
				$this->parent->log($level, $message, $throwable);
			}
		}
	}

	/**
	 * Logs an already prepared logging event object.
	 * @param LoggerLoggingEvent $event
	 */
	public function logEvent(LoggerLoggingEvent $event) {
		if($this->isEnabledFor($event->getLevel())) {
			$this->callAppenders($event);
		}

		// Forward the event upstream if additivity is turned on
		if(isset($this->parent) && $this->getAdditivity()) {
			$this->parent->logEvent($event);
		}
	}

	/**
	 * If assertion parameter evaluates as false, then logs the message
	 * using the ERROR level.
	 *
	 * @param bool $assertion
	 * @param string $msg message to log
	 */
	public function assertLog($assertion = true, $msg = '') {
		if($assertion == false) {
			$this->error($msg);
		}
	}

	/**
	 * This method creates a new logging event and logs the event without
	 * further checks.
	 *
	 * It should not be called directly. Use {@link trace()}, {@link debug()},
	 * {@link info()}, {@link warn()}, {@link error()} and {@link fatal()}
	 * wrappers.
	 *
	 * @param string $fqcn Fully qualified class name of the Logger
	 * @param Exception $throwable Optional throwable information to include
	 *   in the logging event.
	 * @param LoggerLevel $level log level
	 * @param mixed $message message to log
	 */
	public function forcedLog($fqcn, $throwable, LoggerLevel $level, $message) {
		$event = new LoggerLoggingEvent($fqcn, $this, $level, $message, null, $throwable);
		$this->callAppenders($event);

		// Forward the event upstream if additivity is turned on
		if(isset($this->parent) && $this->getAdditivity()) {
			$this->parent->logEvent($event);
		}
	}

	/**
	 * Forwards the given logging event to all linked appenders.
	 * @param LoggerLoggingEvent $event
	 */
	public function callAppenders($event) {
		foreach($this->appenders as $appender) {
			$appender->doAppend($event);
		}
	}

	// ******************************************
	// *** Checker methods                    ***
	// ******************************************

	/**
	 * Check whether this Logger is enabled for a given Level passed as parameter.
	 *
	 * @param LoggerLevel level
	 * @return boolean
	 */
	public function isEnabledFor(LoggerLevel $level) {
		return $level->isGreaterOrEqual($this->getEffectiveLevel());
	}

	/**
	 * Check whether this Logger is enabled for the TRACE Level.
	 * @return boolean
	 */
	public function isTraceEnabled() {
		return $this->isEnabledFor(LoggerLevel::getLevelTrace());
	}

	/**
	 * Check whether this Logger is enabled for the DEBUG Level.
	 * @return boolean
	 */
	public function isDebugEnabled() {
		return $this->isEnabledFor(LoggerLevel::getLevelDebug());
	}

	/**
	 * Check whether this Logger is enabled for the INFO Level.
	 * @return boolean
	 */
	public function isInfoEnabled() {
		return $this->isEnabledFor(LoggerLevel::getLevelInfo());
	}

	/**
	 * Check whether this Logger is enabled for the WARN Level.
	 * @return boolean
	 */
	public function isWarnEnabled() {
		return $this->isEnabledFor(LoggerLevel::getLevelWarn());
	}

	/**
	 * Check whether this Logger is enabled for the ERROR Level.
	 * @return boolean
	 */
	public function isErrorEnabled() {
		return $this->isEnabledFor(LoggerLevel::getLevelError());
	}

	/**
	 * Check whether this Logger is enabled for the FATAL Level.
	 * @return boolean
	 */
	public function isFatalEnabled() {
		return $this->isEnabledFor(LoggerLevel::getLevelFatal());
	}

	// ******************************************
	// *** Configuration methods              ***
	// ******************************************

	/**
	 * Adds a new appender to the Logger.
	 * @param LoggerAppender $appender The appender to add.
	 */
	public function addAppender($appender) {
		$appenderName = $appender->getName();
		$this->appenders[$appenderName] = $appender;
	}

	/** Removes all appenders from the Logger. */
	public function removeAllAppenders() {
		foreach($this->appenders as $name => $appender) {
			$this->removeAppender($name);
		}
	}

	/**
	 * Remove the appender passed as parameter form the Logger.
	 * @param mixed $appender an appender name or a {@link LoggerAppender} instance.
	 */
	public function removeAppender($appender) {
		if($appender instanceof LoggerAppender) {
			$appender->close();
			unset($this->appenders[$appender->getName()]);
		} else if (is_string($appender) and isset($this->appenders[$appender])) {
			$this->appenders[$appender]->close();
			unset($this->appenders[$appender]);
		}
	}

	/**
	 * Returns the appenders linked to this logger as an array.
	 * @return array collection of appender names
	 */
	public function getAllAppenders() {
		return $this->appenders;
	}

	/**
	 * Returns a linked appender by name.
	 * @return LoggerAppender
	 */
	public function getAppender($name) {
		return $this->appenders[$name];
	}

	/**
	 * Sets the additivity flag.
	 * @param boolean $additive
	 */
	public function setAdditivity($additive) {
		$this->additive = (bool)$additive;
	}

	/**
	 * Returns the additivity flag.
	 * @return boolean
	 */
	public function getAdditivity() {
		return $this->additive;
	}

	/**
	 * Starting from this Logger, search the Logger hierarchy for a non-null level and return it.
	 * @see LoggerLevel
	 * @return LoggerLevel or null
	 */
	public function getEffectiveLevel() {
		for($logger = $this; $logger !== null; $logger = $logger->getParent()) {
			if($logger->getLevel() !== null) {
				return $logger->getLevel();
			}
		}
	}

	/**
	 * Get the assigned Logger level.
	 * @return LoggerLevel The assigned level or null if none is assigned.
	 */
	public function getLevel() {
		return $this->level;
	}

	/**
	 * Set the Logger level.
	 *
	 * Use LoggerLevel::getLevelXXX() methods to get a LoggerLevel object, e.g.
	 * $logger->setLevel(LoggerLevel::getLevelInfo());
	 *
	 * @param LoggerLevel $level The level to set, or NULL to clear the logger level.
	 */
	public function setLevel(LoggerLevel $level = null) {
		$this->level = $level;
	}

	/**
	 * Checks whether an appender is attached to this logger instance.
	 *
	 * @param LoggerAppender $appender
	 * @return boolean
	 */
	public function isAttached(LoggerAppender $appender) {
		return isset($this->appenders[$appender->getName()]);
	}

	/**
	 * Sets the parent logger.
	 * @param Logger $logger
	 */
	public function setParent(Logger $logger) {
		$this->parent = $logger;
	}

	// ******************************************
	// *** Static methods and properties      ***
	// ******************************************

	/** The logger hierarchy used by log4php. */
	private static $hierarchy;

	/** Inidicates if log4php has been initialized */
	private static $initialized = false;

	/**
	 * Returns the hierarchy used by this Logger.
	 *
	 * Caution: do not use this hierarchy unless you have called initialize().
	 * To get Loggers, use the Logger::getLogger and Logger::getRootLogger
	 * methods instead of operating on on the hierarchy directly.
	 *
	 * @return LoggerHierarchy
	 */
	public static function getHierarchy() {
		if(!isset(self::$hierarchy)) {
			self::$hierarchy = new LoggerHierarchy(new LoggerRoot());
		}
		return self::$hierarchy;
	}

	/**
	 * Returns a Logger by name. If it does not exist, it will be created.
	 *
	 * @param string $name The logger name
	 * @return Logger
	 */
	public static function getLogger($name) {
		if(!self::isInitialized()) {
			self::configure();
		}
		return self::getHierarchy()->getLogger($name);
	}

	/**
	 * Returns the Root Logger.
	 * @return LoggerRoot
	 */
	public static function getRootLogger() {
		if(!self::isInitialized()) {
			self::configure();
		}
		return self::getHierarchy()->getRootLogger();
	}

	/**
	 * Clears all Logger definitions from the logger hierarchy.
	 * @return boolean
	 */
	public static function clear() {
		return self::getHierarchy()->clear();
	}

	/**
	 * Destroy configurations for logger definitions
	 */
	public static function resetConfiguration() {
		self::getHierarchy()->resetConfiguration();
		self::getHierarchy()->clear(); // TODO: clear or not?
		self::$initialized = false;
	}

	/**
	 * Safely close all appenders.
	 * @deprecated This is no longer necessary due the appenders shutdown via
	 * destructors.
	 */
	public static function shutdown() {
		return self::getHierarchy()->shutdown();
	}

	/**
	 * check if a given logger exists.
	 *
	 * @param string $name logger name
	 * @return boolean
	 */
	public static function exists($name) {
		return self::getHierarchy()->exists($name);
	}

	/**
	 * Returns an array this whole Logger instances.
	 * @see Logger
	 * @return array
	 */
	public static function getCurrentLoggers() {
		return self::getHierarchy()->getCurrentLoggers();
	}

	/**
	 * Configures log4php.
	 *
	 * This method needs to be called before the first logging event has
	 * occured. If this method is not called before then the default
	 * configuration will be used.
	 *
	 * @param string|array $configuration Either a path to the configuration
	 *   file, or a configuration array.
	 *
	 * @param string|LoggerConfigurator $configurator A custom
	 * configurator class: either a class name (string), or an object which
	 * implements the LoggerConfigurator interface. If left empty, the default
	 * configurator implementation will be used.
	 */
	public static function configure($configuration = null, $configurator = null) {
		self::resetConfiguration();
		$configurator = self::getConfigurator($configurator);
		$configurator->configure(self::getHierarchy(), $configuration);
		self::$initialized = true;
	}

	/**
	 * Creates a logger configurator instance based on the provided
	 * configurator class. If no class is given, returns an instance of
	 * the default configurator.
	 *
	 * @param string|LoggerConfigurator $configurator The configurator class
	 * or LoggerConfigurator instance.
	 */
	private static function getConfigurator($configurator = null) {
		if ($configurator === null) {
			return new LoggerConfiguratorDefault();
		}

		if (is_object($configurator)) {
			if ($configurator instanceof LoggerConfigurator) {
				return $configurator;
			} else {
				trigger_error("log4php: Given configurator object [$configurator] does not implement the LoggerConfigurator interface. Reverting to default configurator.", E_USER_WARNING);
				return new LoggerConfiguratorDefault();
			}
		}

		if (is_string($configurator)) {
			if (!class_exists($configurator)) {
				trigger_error("log4php: Specified configurator class [$configurator] does not exist. Reverting to default configurator.", E_USER_WARNING);
				return new LoggerConfiguratorDefault();
			}

			$instance = new $configurator();

			if (!($instance instanceof LoggerConfigurator)) {
				trigger_error("log4php: Specified configurator class [$configurator] does not implement the LoggerConfigurator interface. Reverting to default configurator.", E_USER_WARNING);
				return new LoggerConfiguratorDefault();
			}

			return $instance;
		}

		trigger_error("log4php: Invalid configurator specified. Expected either a string or a LoggerConfigurator instance. Reverting to default configurator.", E_USER_WARNING);
		return new LoggerConfiguratorDefault();
	}

	/**
	 * Returns true if the log4php framework has been initialized.
	 * @return boolean
	 */
	private static function isInitialized() {
		return self::$initialized;
	}
}


<?php
namespace FACTFinder\Adapter;

use FACTFinder\Loader as FF;

class Recommendation extends PersonalisedResponse
{
    /**
     * @var FACTFinder\Util\LoggerInterface
     */
    private $log;

    /**
     * @var FACTFinder\Data\Result
     */
    private $recommendations;

    public function __construct(
        $loggerClass,
        \FACTFinder\Core\ConfigurationInterface $configuration,
        \FACTFinder\Core\Server\Request $request,
        \FACTFinder\Core\Client\UrlBuilder $urlBuilder,
        \FACTFinder\Core\AbstractEncodingConverter $encodingConverter = null
    ) {
        parent::__construct($loggerClass, $configuration, $request,
                            $urlBuilder, $encodingConverter);

        $this->log = $loggerClass::getLogger(__CLASS__);

        $this->request->setAction('Recommender.ff');
        $this->parameters['do'] = 'getRecommendation';
        $this->parameters['format'] = 'json';

        $this->useJsonResponseContentProcessor();
    }

    /**
     * Set the maximum amount of recommendations to be fetched.
     *
     * @param int $recordCount The number of records to be fetched. Something
     *        else than a positive integer is passed, the record count will be
     *        unlimited (or determined by FACT-Finder).
     */
    public function setRecordCount($recordCount)
    {
        $parameters = $this->request->getParameters();
        if (is_numeric($recordCount)
            && (int)$recordCount == (float)$recordCount // Is integer?
            && $recordCount > 0
        ) {
            $parameters['maxResults'] = $recordCount;
        }
        else
        {
            unset($parameters['maxResults']);
        }
        // Make sure that the recommendations are fetched again. In theory,
        // we only have to do this when recordCount increases.
        $this->upToDate = false;
    }

    /**
     * Set one or multiple product IDs to base recommendation on, overwriting
     * any IDs previously set.
     *
     * @param string|string[] $productIDs One or more product IDs.
     */
    public function setProductIDs($productIDs)
    {
        $parameters = $this->request->getParameters();
        $parameters['id'] = $productIDs;
        $this->upToDate = false;
    }

    /**
     * Add one or multiple product IDs to base recommendation on, in addition to
     * any IDs previously set.
     *
     * @param string|string[] $productIDs One or more product IDs.
     */
    public function addProductIDs($productIDs)
    {
        $parameters = $this->request->getParameters();
        $parameters->add('id', $productIDs);
        $this->upToDate = false;
    }

    /**
     * Returns recommendations for IDs previously specified. If no IDs have been
     * set, there will be a warning raised and an empty result will be returned.
     *
     * @return \FACTFinder\Data\Result
     */
    public function getRecommendations()
    {
        if (is_null($this->recommendations)
            || !$this->upToDate
        ) {
            $this->request->resetLoaded();
            $this->recommendations = $this->createRecommendations();
            $this->upToDate = true;
        }

        return $this->recommendations;
    }

    private function createRecommendations()
    {
        $records = array();

        $parameters = $this->request->getParameters();
        if (!isset($parameters['id']))
        {
            $this->log->warn('Recommendations cannot be loaded without a product ID. '
                           . 'Use setProductIDs() or addProductIDs() first.');
        }
        else
        {
            $recommenderData = $this->getResponseContent();
            if (parent::isValidResponse($recommenderData))
            {
                if (isset($recommenderData['resultRecords']))
                {
                    $recommenderData = $recommenderData['resultRecords'];
                }
                $position = 1;
                foreach($recommenderData as $recordData)
                {
                    $records[] = $this->createRecord($recordData, $position++);
                }
            }
        }

        return FF::getInstance(
            'Data\Result',
            $records,
            null,
            count($records)
        );
    }

    private function createRecord($recordData, $position)
    {
        return FF::getInstance(
            'Data\Record',
            (string)$recordData['id'],
            $recordData['record'],
            100.0,
            $position
        );
    }

    /**
     * Get the recommendations from FACT-Finder as the string returned by the
     * server.
     *
     * @param string $format Optional. Either 'json' or 'jsonp'. Use to
     *                       overwrite the 'format' parameter.
     * @param string $callback Optional name to overwrite the 'callback'
     *                         parameter, which determines the name of the
     *                         callback the response is wrapped in.
     *
     * @return string
     */
    public function getRawRecommendations($format = null, $callback = null)
    {
        $this->usePassthroughResponseContentProcessor();

        if (!is_null($format))
            $this->parameters['format'] = $format;
        if (!is_null($callback))
            $this->parameters['callback'] = $callback;

        return $this->getResponseContent();
    }
}


<?php
/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *	   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @package log4php
 */

/**
 * Contains various helper methods.
 *
 * @package log4php
 * @subpackage helpers
 * @since 2.3
 */
class LoggerUtils {

	/**
 	 * Splits a fully qualified class name into fragments delimited by the
 	 * namespace separator (\).
 	 *
 	 * For backward compatibility, a dot (.) can be used as a delimiter as
 	 * well.
	 *
	 * @param string $name
	 *
	 * @return array Class name split into fragments.
	 */
	public static function tokenizeClassName($name) {
		$name = str_replace('.', '\\', $name);
		$name = trim($name, ' \\');
		$fragments = explode('\\', $name);

		foreach($fragments as $key => $fragment) {
			if (trim($fragment) === '') {
				unset($fragments[$key]);
			}
		}

		return $fragments;
	}

	/**
	 * Attempts to shorten the given class name to the desired length.
	 *
	 * This is done by separating the class name into fragments (delimited
	 * by \ or .) and trimming individual fragments, starting with the left,
	 * until desired length has been reached.
	 *
	 * The final fragment (i.e. class name) will never be shortened so the
	 * result may still be longer than given length.
	 *
	 * @param string $name The (qualified) class name.
	 * @param integer $length The length to shorten to. If null or 0 is given,
	 * the name will be returned without shortening.
	 */
	public static function shortenClassName($name, $length) {
		if ($length === null || $length < 0) {
			return $name;
		}

		$name = str_replace('.', '\\', $name);
		$name = trim($name, ' \\');

		// Check if any shortening is required
		$currentLength = strlen($name);
		if ($currentLength <= $length) {
			return $name;
		}

		// Split name into fragments
		$fragments = explode('\\', $name);

		// If zero length is specified, return only last fragment
		if ($length == 0) {
			return array_pop($fragments);
		}

		// If the name splits to only one fragment, then it cannot be shortened
		$count = count($fragments);
		if ($count == 1) {
			return $name;
		}

		foreach($fragments as $key => &$fragment) {

			// Never shorten last fragment
			if ($key == $count - 1) {
				break;
			}

			// Check for empty fragments (shouldn't happen but it's possible)
			$fragLen = strlen($fragment);
			if ($fragLen <= 1) {
				continue;
			}

			// Shorten fragment to one character and check if total length satisfactory
			$fragment = substr($fragment, 0, 1);
			$currentLength = $currentLength - $fragLen + 1;

			if ($currentLength <= $length) {
				break;
			}
		}
		unset($fragment);

		return implode('\\', $fragments);
	}
}


<?php
namespace FACTFinder\Core;

use FACTFinder\Loader as FF;

/**
 * Takes care of differences in encoding between different participants of the
 * communication. Internal to the library all strings are encoded as UTF-8, so
 * the methods of this class are only for converting to and from UTF-8. The
 * source and target encodings are determined by the configuration.
 * This abstract class does not specify how the actual conversion of a single
 * string is done. Create a subclass to implement the conversion method.
 * Also note that none of these methods handle URL en- or decoding but only deal
 * with plain character encodings.
 */
abstract class AbstractEncodingConverter
{
    const LIBRARY_ENCODING = 'UTF-8';

    /**
     * @var FACTFinder\Util\LoggerInterface
     */
    private $log;

    /**
     * @var string
     */
    protected $pageContentEncoding;
    /**
     * @var string
     */
    protected $clientUrlEncoding;

    /**
     * @param string $loggerClass Class name of logger to use. The class should
     *                            implement FACTFinder\Util\LoggerInterface.
     * @param ConfigurationInterface $configuration Configuration object to use.
     */
    function __construct(
        $loggerClass,
        ConfigurationInterface $configuration
    ) {
        $this->log = $loggerClass::getLogger(__CLASS__);

        $this->pageContentEncoding = $configuration->getPageContentEncoding();
        $this->clientUrlEncoding = $configuration->getClientUrlEncoding();
    }

    abstract protected function convertString($inCharset, $outCharset, $string);

    /**
     * Converts data from $inCharset to $outCharset.
     * @param mixed $data If a string is given, it's encoding will be converted.
     *        If an associative array is given, keys and values will be
     *        converted recursively. All other data types will be returned
     *        unchanged.
     * @return mixed
     */
    protected function convert($inCharset, $outCharset, $data)
    {
        if($inCharset == $outCharset) {
            return $data;
        }
        return $this->convert_recursive($inCharset, $outCharset, $data);
    }

    /**
     * Converts data from $inCharset to $outCharset.
     * @param mixed $data If a string is given, it's encoding will be converted.
     *        If an associative array is given, keys and values will be
     *        converted recursively. All other data types will be returned
     *        unchanged.
     * @return mixed
     */
    protected function convert_recursive($inCharset, $outCharset, $data)
    {
        if (FF::isInstanceOf($data, 'Util\Parameters'))
        {
            $result = FF::getInstance(
                'Util\Parameters',
                $this->convert($inCharset, $outCharset, $data->getArray())
            );
        }
        else if (is_array($data))
        {
            $result = array();
            foreach ($data as $k => $v)
            {
                $k = $this->convert_recursive($inCharset, $outCharset, $k);
                $result[$k] = $this->convert_recursive($inCharset, $outCharset, $v);
            }
        }
        else if (is_string($data))
        {
            $result = $this->convertString($inCharset, $outCharset, $data);
        }
        else
        {
            $result = $data;
        }

        return $result;
    }

    /**
     * Converts data held by the library for use on the rendered page.
     * Hence, it converts from the library's encoding (UTF-8) to the configured
     * page content encoding.
     * @param mixed $data Could either be a string or an associative array.
     * @return mixed
     */
    public function encodeContentForPage($data)
    {
        return $this->convert(
            self::LIBRARY_ENCODING,
            $this->pageContentEncoding,
            $data
        );
    }

    /**
     * Converts data obtained from the client URL for use within the library.
     * Hence, it converts from the configured client URL encoding to the
     * library's encoding (UTF-8).
     * @param mixed $data Data obtained from the client URL. Note that this
     *        data should already be URL decoded. Could either be a string or an
     *        associative array.
     * @return mixed
     */
    public function decodeClientUrlData($data)
    {
        return $this->convert(
            $this->clientUrlEncoding,
            self::LIBRARY_ENCODING,
            $data
        );
    }

    /**
     * Converts data held by the library for use in a client URL.
     * Hence, it converts from the configured client URL encoding to the
     * library's encoding (UTF-8).
     * @param mixed $data Data to be used in the client URL. Note that this
     *        data should not yet be URL encoded. Could either be a string or an
     *        associative array.
     * @return mixed
     */
    public function encodeClientUrlData($data)
    {
        return $this->convert(
            self::LIBRARY_ENCODING,
            $this->clientUrlEncoding,
            $data
        );
    }
}


<?php
namespace FACTFinder\Adapter;

use FACTFinder\Loader as FF;

/**
 * This tracking adapter uses the new Tracking Interface for 6.11
 */
class Tracking extends AbstractAdapter
{
    /**
     * @var FACTFinder\Util\LoggerInterface
     */
    private $log;

    public function __construct(
        $loggerClass,
        \FACTFinder\Core\ConfigurationInterface $configuration,
        \FACTFinder\Core\Server\Request $request,
        \FACTFinder\Core\Client\UrlBuilder $urlBuilder,
        \FACTFinder\Core\AbstractEncodingConverter $encodingConverter = null
    ) {
        parent::__construct($loggerClass, $configuration, $request,
                            $urlBuilder, $encodingConverter);

        $this->log = $loggerClass::getLogger(__CLASS__);

        $this->request->setAction('Tracking.ff');

        $this->request->setConnectTimeout($configuration->getTrackingConnectTimeout());
        $this->request->setTimeout($configuration->getTrackingTimeout());

        // $this->usePassthroughResponseContentProcessor(); (default)
    }

    /**
     * If all needed parameters are available at the request like described in
     * the documentation, just use this method to fetch the needed parameters
     * and track them. Make sure to set a session id if there is no parameter
     * "sid". If this argument is not set or empty and the parameter "sid" is
     * not available, it will try to use session_id() to fetch one.
     *
     * @param string $sid session id
     * @param string $userId id of user
     * @return bool Success?
     */
    public function doTrackingFromRequest($sid = null, $userId = null)
    {
        $this->setupTrackingFromRequest($sid, $userId);
        return $this->applyTracking();
    }

    /**
     * Use this method directly if you want to separate the setup from sending
     * the request. This is particularly useful when using the
     * MultiCurlRequestFactory.
     *
     * @param string $sid session id
     * @param string $userId id of user
     */
    public function setupTrackingFromRequest($sid = null, $userId = null)
    {
        if (strlen($sid) > 0)
            $this->parameters['sid'] = $sid;
        else if (!isset($this->parameters['sid'])
                 || strlen($this->parameters['sid']) == 0
        ) {
            $this->parameters['sid'] = session_id();
        }

        if (strlen($userId) > 0)
            $this->parameters['userId'] = $userId;
    }

    /**
     * Track a detail click on a product.
     *
     * @param string $id tracking id of product (see field with the role "Product number for tracking")
     * @param string $sid session id (if empty, then try to set using the function session_id() )
     * @param string $query query which led to the product
     * @param int $pos position of product in the search result
     * @param string $masterId master id of the product (see field with the role "Master article number")
     * @param string $cookieId cookie id (optional)
     * @param int $origPos original position of product in the search result. this data is delivered by FACT-Finder (optional - is set equals to $position by default)
     * @param int $page page number where the product was clicked (optional - is 1 by default)
     * @param float $simi similiarity of the product (optional - is 100.00 by default)
     * @param string $title title of product (optional - is empty by default)
     * @param int $pageSize size of the page where the product was found (optional - is 12 by default)
     * @param int $origPageSize original size of the page before the user could have changed it (optional - is set equals to $page by default)
     * @param string $userId id of user (optional if modul personalisation is not used)
     * @param string $campaign campaign name (optional)
     * @param boolean $instoreAds determines whether it's a sponsered product (optional)
     * @return boolean $success
     */
    public function trackClick(
        $id,
        $query,
        $pos,
        $masterId = null,
        $sid = null,
        $cookieId = null,
        $origPos = -1,
        $page = 1,
        $simi = 100.0,
        $title = '',
        $pageSize = 12,
        $origPageSize = -1,
        $userId = null,
        $campaign = null,
        $instoreAds = false
    ) {
        $this->setupClickTracking($id, $query, $pos, $masterId, $sid, $cookieId, $origPos, $page,
                                  $simi, $title, $pageSize, $origPageSize, $userId, $campaign, $instoreAds);
        return $this->applyTracking();
    }

    /**
     * Use this method directly if you want to separate the setup from sending
     * the request. This is particularly useful when using the
     * MultiCurlRequestFactory.
     */
    public function setupClickTracking(
        $id,
        $query,
        $pos,
        $masterId = null,
        $sid = null,
        $cookieId = null,
        $origPos = -1,
        $page = 1,
        $simi = 100.0,
        $title = '',
        $pageSize = 12,
        $origPageSize = -1,
        $userId = null,
        $campaign = null,
        $instoreAds = false
    ) {
        if (strlen($sid) == 0) $sid = session_id();
        if ($origPos == -1) $origPos = $pos;
        if ($origPageSize == -1) $origPageSize = $pageSize;
        $params = array(
            'id'            => $id,
            'query'         => $query,
            'pos'           => $pos,
            'sid'           => $sid,
            'origPos'       => $origPos,
            'page'          => $page,
            'simi'          => $simi,
            'title'         => $title,
            'event'         => 'click',
            'pageSize'      => $pageSize,
            'origPageSize'  => $origPageSize,
        );

        if (strlen($userId) > 0) $params['userId'] = $userId;
        if (strlen($cookieId) > 0) $params['cookieId'] = $cookieId;
        if (strlen($masterId) > 0) $params['masterId'] = $masterId;
        if (strlen($campaign) > 0) $params['campaign'] = $campaign;
        if ($instoreAds) $params['instoreAds'] = 'true';

        $this->parameters->clear();
        $this->parameters->setAll($params);
    }

    /**
     * Track a product which was added to the cart.
     *
     * @param string $id tracking id of product (see field with the role "Product number for tracking")
     * @param string $masterId master id of the product (see field with the role "Master article number")
     * @param string $tile title of product (optional - is empty by default)
     * @param string $query query which led to the product (only if module Semantic Enhancer is used)
     * @param string $sid session id (if empty, then try to set using the function session_id() )
     * @param string $cookieId cookie id (optional)
     * @param int $count number of items purchased for each product (optional - default 1)
     * @param float $price this is the single unit price (optional)
     * @param string $userId id of user (optional if modul personalisation is not used)
     * @param string $campaign campaign name (optional)
     * @param boolean $instoreAds determines whether it's a sponsered product (optional)
     * @return boolean $success
     */
    public function trackCart(
        $id,
        $masterId = null,
        $title = '',
        $query = null,
        $sid = null,
        $cookieId = null,
        $count = 1,
        $price = null,
        $userId = null,
        $campaign = null,
        $instoreAds = false
    ) {
        $this->setupCartTracking($id, $masterId, $title, $query, $sid, $cookieId, $count, $price, $userId, $campaign, $instoreAds);
        return $this->applyTracking();
    }

    /**
     * Use this method directly if you want to separate the setup from sending
     * the request. This is particularly useful when using the
     * MultiCurlRequestFactory.
     */
    public function setupCartTracking(
        $id,
        $masterId = null,
        $title = '',
        $query = null,
        $sid = null,
        $cookieId = null,
        $count = 1,
        $price = null,
        $userId = null,
        $campaign = null,
        $instoreAds = false
    ) {
        if (strlen($sid) == 0) $sid = session_id();
        $params = array(
            'id'        => $id,
            'title'     => $title,
            'sid'       => $sid,
            'count'     => $count,
            'event'     => 'cart'
        );

        if (strlen($price) > 0) $params['price'] = $price;
        if (strlen($userId) > 0) $params['userId'] = $userId;
        if (strlen($cookieId) > 0) $params['cookieId'] = $cookieId;
        if (strlen($masterId) > 0) $params['masterId'] = $masterId;
        if (strlen($query) > 0) $params['query'] = $query;
        if (strlen($campaign) > 0) $params['campaign'] = $campaign;
        if ($instoreAds) $params['instoreAds'] = 'true';

        $this->parameters->clear();
        $this->parameters->setAll($params);
    }

    /**
     * Track a product which was purchased.
     *
     * @param string $id tracking id of product (see field with the role "Product number for tracking")
     * @param string $masterId master id of the product (see field with the role "Master article number")
     * @param string $tile title of product (optional - is empty by default)
     * @param string $query query which led to the product (only if module Semantic Enhancer is used)
     * @param string $sid session id (if empty, then try to set using the function session_id() )
     * @param string $cookieId cookie id (optional)
     * @param int $count number of items purchased for each product (optional - default 1)
     * @param float $price this is the single unit price (optional)
     * @param string $userId id of user (optional if modul personalisation is not used)
     * @param string $campaign campaign name (optional)
     * @param boolean $instoreAds determines whether it's a sponsered product (optional)
     * @return boolean $success
     */
    public function trackCheckout(
        $id,
        $masterId = null,
        $title = '',
        $query = null,
        $sid = null,
        $cookieId = null,
        $count = 1,
        $price = null,
        $userId = null,
        $campaign = null,
        $instoreAds = false
    ) {
        $this->setupCheckoutTracking($id, $masterId, $title, $query, $sid, $cookieId, $count, $price, $userId, $campaign, $instoreAds);
        return $this->applyTracking();
    }

    /**
     * Use this method directly if you want to separate the setup from sending
     * the request. This is particularly useful when using the
     * MultiCurlRequestFactory.
     */
    public function setupCheckoutTracking(
        $id,
        $masterId = null,
        $title = '',
        $query = null,
        $sid = null,
        $cookieId = null,
        $count = 1,
        $price = null,
        $userId = null,
        $campaign = null,
        $instoreAds = false
    ) {
        if (strlen($sid) == 0) $sid = session_id();
        $params = array(
            'id'        => $id,
            'masterId'  => $masterId,
            'title'     => $title,
            'sid'       => $sid,
            'count'     => $count,
            'event'     => 'checkout'
        );

        if (strlen($price) > 0) $params['price'] = $price;
        if (strlen($userId) > 0) $params['userId'] = $userId;
        if (strlen($cookieId) > 0) $params['cookieId'] = $cookieId;
        if (strlen($query) > 0) $params['query'] = $query;
        if (strlen($masterId) > 0) $params['masterId'] = $masterId;
        if (strlen($campaign) > 0) $params['campaign'] = $campaign;
        if ($instoreAds) $params['instoreAds'] = 'true';

        $this->parameters->clear();
        $this->parameters->setAll($params);
    }

    /**
     * Track a click on a recommended product.
     *
     * @param string $id tracking id of product (see field with the role "Product number for tracking")
     * @param int $mainId ID of the product for which the clicked-upon item was recommended
     * @param string $masterId master id of the product (see field with the role "Master article number")
     * @param string $sid session id (if empty, then try to set using the function session_id() )
     * @param string $cookieId cookie id (optional)
     * @param string $userId id of user (optional if modul personalisation is not used)
     * @return boolean $success
     */
    public function trackRecommendationClick(
        $id,
        $mainId,
        $masterId = null,
        $sid = null,
        $cookieId = null,
        $userId = null
    ) {
        $this->setupRecommendationClickTracking($id, $mainId, $masterId, $sid, $cookieId, $userId);
        return $this->applyTracking();
    }

    /**
     * Use this method directly if you want to separate the setup from sending
     * the request. This is particularly useful when using the
     * MultiCurlRequestFactory.
     */
    public function setupRecommendationClickTracking(
        $id,
        $mainId,
        $masterId = null,
        $sid = null,
        $cookieId = null,
        $userId = null
    ) {
        if (strlen($sid) == 0) $sid = session_id();
        $params = array(
            'id'        => $id,
            'masterId'  => $masterId,
            'sid'       => $sid,
            'cookieId'  => $cookieId,
            'mainId'    => $mainId,
            'event'     => 'recommendationClick'
        );

        if (strlen($userId) > 0) $params['userId'] = $userId;
        if (strlen($cookieId) > 0) $params['cookieId'] = $cookieId;
        if (strlen($masterId) > 0) $params['masterId'] = $masterId;

        $this->parameters->clear();
        $this->parameters->setAll($params);
    }

    /**
     * Track login of a user
     *
     * @param string $sid session id (if empty, then try to set using the function session_id() )
     * @param string $cookieId cookie id (optional)
     * @param string $userId id of user who logged in
     * @return boolean $success
     */
    public function trackLogin(
        $sid = null,
        $cookieId = null,
        $userId = null
    ) {
        $this->setupLoginTracking($sid, $cookieId, $userId);
        return $this->applyTracking();
    }

    /**
     * Use this method directly if you want to separate the setup from sending
     * the request. This is particularly useful when using the
     * MultiCurlRequestFactory.
     */
    public function setupLoginTracking(
        $sid = null,
        $cookieId = null,
        $userId = null
    ) {
        if (strlen($sid) == 0) $sid = session_id();
        $params = array(
            'sid'       => $sid,
            'userId'    => $userId,
            'event'     => 'login'
        );

        if (strlen($cookieId) > 0) $params['cookieId'] = $cookieId;

        $this->parameters->clear();
        $this->parameters->setAll($params);
    }

    /**
     * send tracking
     *
     * @return boolean $success
     */
    public function applyTracking() {
        $success = trim($this->getResponseContent());
        return $success == 'The event was successfully tracked' || $success == 'true' || $success == '1';
    }
}


<?php
namespace FACTFinder\Adapter;

use FACTFinder\Loader as FF;

class Compare extends ConfigurableResponse
{
    /**
     * @var FACTFinder\Util\LoggerInterface
     */
    private $log;

    /**
     * @var mixed[]
     * @see getSimilarAttributes()
     */
    private $comparableAttributes;

    /**
     * @var FACTFinder\Data\Result
     */
    private $comparedRecords;

    public function __construct(
        $loggerClass,
        \FACTFinder\Core\ConfigurationInterface $configuration,
        \FACTFinder\Core\Server\Request $request,
        \FACTFinder\Core\Client\UrlBuilder $urlBuilder,
        \FACTFinder\Core\AbstractEncodingConverter $encodingConverter = null
    ) {
        parent::__construct($loggerClass, $configuration, $request,
                            $urlBuilder, $encodingConverter);

        $this->log = $loggerClass::getLogger(__CLASS__);

        $this->request->setAction('Compare.ff');
        $this->parameters['format'] = 'json';

        $this->useJsonResponseContentProcessor();
    }

    /**
     * Set a list-like array of product IDs to compare.
     *
     * @param string[] $productIDs
     */
    public function setProductIDs($productIDs)
    {
        $parameters = $this->request->getParameters();
        $parameters['ids'] = implode(';', $productIDs);
        $this->upToDate = false;
    }

    /**
     * Returns the attributes which can be compared and whether the products in
     * question differ in each of those attributes. If no IDs have been set,
     * there will be a warning raised and an empty array will be returned.
     *
     * @return bool[] Attribute names as keys, boolean value indicates whether
     *                products have different values for this attribute.
     */
    public function getComparableAttributes()
    {
        if (is_null($this->comparableAttributes)
            || !$this->upToDate
        ) {
            $this->comparableAttributes = $this->createComparableAttributes();
            $this->upToDate = true;
        }

        return $this->comparableAttributes;
    }

    private function createComparableAttributes()
    {
        $attributes = array();

        $parameters = $this->request->getParameters();
        if (!isset($parameters['ids']))
        {
            $this->log->warn('Comparable attributes cannot be loaded without product IDs. '
                           . 'Use setProductIDs() first.');
        }
        else
        {
            $jsonData = $this->getResponseContent();
            if (parent::isValidResponse($jsonData))
            {
                foreach($jsonData['attributes'] as $attributeData)
                {
                    $name = $attributeData['attributeName'];
                    $attributes[$name] = $attributeData['different'];
                }
            }
        }
        return $attributes;
    }

    /**
     * Returns the records corresponding to the IDs previously specified. If no
     * IDs have been set, there will be a warning raised and an empty result
     * will be returned.
     *
     * @return \FACTFinder\Data\Result
     */
    public function getComparedRecords()
    {
        if (is_null($this->comparedRecords)
            || !$this->upToDate
        ) {
            $this->comparedRecords = $this->createComparedRecords();
            $this->upToDate = true;
        }

        return $this->comparedRecords;
    }

    private function createComparedRecords()
    {
        $records = array();

        $parameters = $this->request->getParameters();
        if (!isset($parameters['ids']))
        {
            $this->log->warn('Compared records cannot be loaded without product IDs. '
                           . 'Use setProductIDs() first.');
        }
        else
        {
            $position = 1;
            $jsonData = $this->getResponseContent();
            if (parent::isValidResponse($jsonData))
            {
                foreach($jsonData['records'] as $recordData)
                {
                    $records[] = FF::getInstance(
                        'Data\Record',
                        (string)$recordData['id'],
                        $recordData['record'],
                        100.0,
                        $position++
                    );
                }
            }
        }

        return FF::getInstance(
            'Data\Result',
            $records,
            null,
            count($records)
        );
    }
}


<?php
/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *	   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @package log4php
 */

/**
 * LoggerPatternConverter is an abstract class that provides the formatting
 * functionality that derived classes need.
 *
 *
Conversion specifiers in a conversion patterns are parsed to
 * individual PatternConverters. Each of which is responsible for
 * converting a logging event in a converter specific manner.

 *
 * @version $Revision: 1326626 $
 * @package log4php
 * @subpackage helpers
 * @since 0.3
 */
abstract class LoggerPatternConverter {

	/**
	 * Next converter in the converter chain.
	 * @var LoggerPatternConverter
	 */
	public $next = null;

	/**
	 * Formatting information, parsed from pattern modifiers.
	 * @var LoggerFormattingInfo
	 */
	protected $formattingInfo;

	/**
	 * Converter-specific formatting options.
	 * @var array
	 */
	protected $option;

	/**
	 * Constructor
	 * @param LoggerFormattingInfo $formattingInfo
	 * @param array $option
	 */
	public function __construct(LoggerFormattingInfo $formattingInfo = null, $option = null) {
		$this->formattingInfo = $formattingInfo;
		$this->option = $option;
		$this->activateOptions();
	}

	/**
	 * Called in constructor. Converters which need to process the options
	 * can override this method.
	 */
	public function activateOptions() { }

	/**
	 * Converts the logging event to the desired format. Derived pattern
	 * converters must implement this method.
	 *
	 * @param LoggerLoggingEvent $event
	 */
	abstract public function convert(LoggerLoggingEvent $event);

	/**
	 * Converts the event and formats it according to setting in the
	 * Formatting information object.
	 *
	 * @param string &$sbuf string buffer to write to
	 * @param LoggerLoggingEvent $event Event to be formatted.
	 */
	public function format(&$sbuf, $event) {
		$string = $this->convert($event);

		if (!isset($this->formattingInfo)) {
			$sbuf .= $string;
			return;
		}

		$fi = $this->formattingInfo;

		// Empty string
		if($string === '' || is_null($string)) {
			if($fi->min > 0) {
				$sbuf .= str_repeat(' ', $fi->min);
			}
			return;
		}

		$len = strlen($string);

		// Trim the string if needed
		if($len > $fi->max) {
			if ($fi->trimLeft) {
				$sbuf .= substr($string, $len - $fi->max, $fi->max);
			} else {
				$sbuf .= substr($string , 0, $fi->max);
			}
		}

		// Add padding if needed
		else if($len < $fi->min) {
			if($fi->padLeft) {
				$sbuf .= str_repeat(' ', $fi->min - $len);
				$sbuf .= $string;
			} else {
				$sbuf .= $string;
				$sbuf .= str_repeat(' ', $fi->min - $len);
			}
		}

		// No action needed
		else {
			$sbuf .= $string;
		}
	}
}


<?php
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
*	   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @package log4php
*/

/**
 * A base class from which all classes which have configurable properties are
 * extended. Provides a generic setter with integrated validation.
 *
 * @package log4php
 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 * @version $Revision $
 * @since 2.2
 */
abstract class LoggerConfigurable {

	/** Setter function for boolean type. */
	protected function setBoolean($property, $value) {
		try {
			$this->$property = LoggerOptionConverter::toBooleanEx($value);
		} catch (Exception $ex) {
			$value = var_export($value, true);
			$this->warn("Invalid value given for '$property' property: [$value]. Expected a boolean value. Property not changed.");
		}
	}

	/** Setter function for integer type. */
	protected function setInteger($property, $value) {
		try {
			$this->$property = LoggerOptionConverter::toIntegerEx($value);
		} catch (Exception $ex) {
			$value = var_export($value, true);
			$this->warn("Invalid value given for '$property' property: [$value]. Expected an integer. Property not changed.");
		}
	}

	/** Setter function for LoggerLevel values. */
	protected function setLevel($property, $value) {
		try {
			$this->$property = LoggerOptionConverter::toLevelEx($value);
		} catch (Exception $ex) {
			$value = var_export($value, true);
			$this->warn("Invalid value given for '$property' property: [$value]. Expected a level value. Property not changed.");
		}
	}

	/** Setter function for integer type. */
	protected function setPositiveInteger($property, $value) {
		try {
			$this->$property = LoggerOptionConverter::toPositiveIntegerEx($value);
		} catch (Exception $ex) {
			$value = var_export($value, true);
			$this->warn("Invalid value given for '$property' property: [$value]. Expected a positive integer. Property not changed.");
		}
	}

	/** Setter for file size. */
	protected function setFileSize($property, $value) {
		try {
			$this->$property = LoggerOptionConverter::toFileSizeEx($value);
		} catch (Exception $ex) {
			$value = var_export($value, true);
			$this->warn("Invalid value given for '$property' property: [$value]. Expected a file size value.  Property not changed.");
		}
	}

	/** Setter function for numeric type. */
	protected function setNumeric($property, $value) {
		try {
			$this->$property = LoggerOptionConverter::toNumericEx($value);
		} catch (Exception $ex) {
			$value = var_export($value, true);
			$this->warn("Invalid value given for '$property' property: [$value]. Expected a number. Property not changed.");
		}
	}

	/** Setter function for string type. */
	protected function setString($property, $value, $nullable = false) {
		if ($value === null) {
			if($nullable) {
				$this->$property= null;
			} else {
				$this->warn("Null value given for '$property' property. Expected a string. Property not changed.");
			}
		} else {
			try {
				$value = LoggerOptionConverter::toStringEx($value);
				$this->$property = LoggerOptionConverter::substConstants($value);
			} catch (Exception $ex) {
				$value = var_export($value, true);
				$this->warn("Invalid value given for '$property' property: [$value]. Expected a string. Property not changed.");
			}
		}
	}

	/** Triggers a warning. */
	protected function warn($message) {
		$class = get_class($this);
		trigger_error("log4php: $class: $message", E_USER_WARNING);
	}
}


<?php
/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *	   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @package log4php
 */

/**
 * This is a very simple filter based on level matching, which can be
 * used to reject messages with priorities outside a certain range.
 *
 *
The filter admits three options <var>LevelMin</var>, <var>LevelMax</var>
 * and <var>AcceptOnMatch</var>.

 *
 *
If the level of the {@link LoggerLoggingEvent} is not between Min and Max
 * (inclusive), then {@link LoggerFilter::DENY} is returned.

 *
 *
If the Logging event level is within the specified range, then if
 * <var>AcceptOnMatch</var> is true,
 * {@link LoggerFilter::ACCEPT} is returned, and if
 * <var>AcceptOnMatch</var> is false,
 * {@link LoggerFilter::NEUTRAL} is returned.

 *
 *
If <var>LevelMin</var> is not defined, then there is no
 * minimum acceptable level (i.e. a level is never rejected for
 * being too "low"/unimportant).  If <var>LevelMax</var> is not
 * defined, then there is no maximum acceptable level (ie a
 * level is never rejected for being too "high"/important).

 *
 *
Refer to the {@link LoggerAppender::setThreshold()} method
 * available to all appenders extending {@link LoggerAppender}
 * for a more convenient way to filter out events by level.

 *
 *

 * An example for this filter:
 *
 * {@example ../../examples/php/filter_levelrange.php 19}
 *
 *

 * The corresponding XML file:
 *
 * {@example ../../examples/resources/filter_levelrange.xml 18}
 *
 * @author Simon Kitching
 * @author based on the org.apache.log4j.varia.LevelRangeFilte Java code by Ceki Gülcü
 *
 * @version $Revision: 1213283 $
 * @package log4php
 * @subpackage filters
 * @since 0.6
 */
class LoggerFilterLevelRange extends LoggerFilter {

	/**
	 * @var boolean
	 */
	protected $acceptOnMatch = true;

	/**
	 * @var LoggerLevel
	 */
	protected $levelMin;

	/**
	 * @var LoggerLevel
	 */
	protected $levelMax;

	/**
	 * @param boolean $acceptOnMatch
	 */
	public function setAcceptOnMatch($acceptOnMatch) {
		$this->setBoolean('acceptOnMatch', $acceptOnMatch);
	}

	/**
	 * @param string $l the level min to match
	 */
	public function setLevelMin($level) {
		$this->setLevel('levelMin', $level);
	}

	/**
	 * @param string $l the level max to match
	 */
	public function setLevelMax($level) {
		$this->setLevel('levelMax', $level);
	}

	/**
	 * Return the decision of this filter.
	 *
	 * @param LoggerLoggingEvent $event
	 * @return integer
	 */
	public function decide(LoggerLoggingEvent $event) {
		$level = $event->getLevel();

		if($this->levelMin !== null) {
			if($level->isGreaterOrEqual($this->levelMin) == false) {
				// level of event is less than minimum
				return LoggerFilter::DENY;
			}
		}

		if($this->levelMax !== null) {
			if($level->toInt() > $this->levelMax->toInt()) {
				// level of event is greater than maximum
				// Alas, there is no Level.isGreater method. and using
				// a combo of isGreaterOrEqual && !Equal seems worse than
				// checking the int values of the level objects..
				return LoggerFilter::DENY;
			}
		}

		if($this->acceptOnMatch) {
			// this filter set up to bypass later filters and always return
			// accept if level in range
			return LoggerFilter::ACCEPT;
		} else {
			// event is ok for this filter; allow later filters to have a look..
			return LoggerFilter::NEUTRAL;
		}
	}
}


<?php
namespace FACTFinder\Util;

use FACTFinder\Loader as FF;

/**
 * Stubs the cURL interface without issuing any network requests.
 */
class CurlStub implements CurlInterface
{
    // These are a replacement for the newer CURLM error codes that have been
    // added in cURL 7.15.4 and 7.32.1 until PHP adopts them.
    const M_BAD_SOCKET = 5;
    const M_UNKNOWN_OPTION = 6;
    const M_ADDED_ALREADY = 7;

    /**
     * @var int
     * Used to hand out unique indices for handles. This is used both for easy
     * and for multi handles.
     */
    private $lastHandle = -1;

    private $mapOptionCounts = array();
    private $mapOptions = array();
    private $mapResponses = array();
    private $mapErrorCodes = array();
    private $mapInfo = array();

    /**
     * @var CurlHandle[]
     */
    private $handles = array();

    /**
     * @var CurlMultiHandle[]
     */
    private $multiHandles = array();

    /**
     * Easy interface
     */

    public function init($url = null)
    {
        $ch = ++$this->lastHandle;

        $handle = FF::getInstance('Util\CurlHandle');

        $handle->options[CURLOPT_RETURNTRANSFER] = false;

        $this->handles[$ch] = $handle;

        if($url !== null)
            $this->setopt($ch, CURLOPT_URL, $url);

        return $ch;
    }

    public function copy_handle($ch)
    {
        $newCh = ++$this->lastHandle;

        $handle = clone $this->handles[$ch];

        $this->handles[$newCh] = $handle;

        return $newCh;
    }

    public function setopt($ch, $option, $value)
    {
        if(!isset($this->handles[$ch]))
            return false;

        $this->handles[$ch]->options[$option] = $value;

        return true;
    }

    public function setopt_array($ch, $options)
    {
        foreach($options as $option => $value)
        {
            if(!$this->setopt($ch, $option, $value))
                return false;
        }
        return true;
    }

    public function exec($ch)
    {
        if(!isset($this->handles[$ch]))
            return false;

        $handle = $this->handles[$ch];

        $response = $this->getResponse($handle);

        // TODO: Is this really what PHP would do?
        if(is_null($response))
            return false;

        // TODO: Use more of the behavior options like the callbacks
        // WRITEFUNCTION and READFUNCTION.
        if($handle->options[CURLOPT_RETURNTRANSFER])
            return $response;

        echo $response;
        return true;
    }

    public function errno($ch)
    {
        if(!isset($this->handles[$ch]))
            return 0;

        return $this->getErrorCode($this->handles[$ch]);
    }

    public function error($ch)
    {
        $errno = $this->errno($ch);
        return self::$errorLookup[$errno];
    }

    public function getinfo($ch, $opt = 0)
    {
        // TODO: Include logic to build CURLINFO_EFFECTIVE_URL?
        if(!isset($this->handles[$ch]))
            trigger_error(__FUNCTION__.'(): '.$ch.' is not a valid cURL handle resource', E_USER_WARNING);

        if($opt == 0)
            return $this->getInfoArray($this->handles[$ch]);

        return $this->getInformation($this->handles[$ch], $opt);
    }

    public function close($ch)
    {
        // Somehow PHP's cURL implementation ignores the first two calls to
        // curl_close after the handle has been added to a multi handle. In fact
        // it even seems to ignore them once the handle has been removed from
        // the multi handle again. However, if the multi handle is closed
        // without first removing the easy handle, curl_close works again
        // immediately.
        // We don't mimick this behavior exactly. Instead, we simply ignore
        // calls to curl_close while the handle is added to a multi handle and
        // resume listening to them, once the handle is removed or the multi
        // handle is closed (which also removes the handle).
        if (isset($this->handles[$ch])
            && $this->handles[$ch]->inMultiHandle
        ) {
            return;
        }

        unset($this->handles[$ch]);
    }

    /**
     * Multi interface
     */

    public function multi_init()
    {
        $mh = ++$this->lastHandle;

        $handle = FF::getInstance('Util\CurlMultiHandle');

        $this->multiHandles[$mh] = $handle;

        return $mh;
    }

    public function multi_add_handle($mh, $ch)
    {
        if (!isset($this->multiHandles[$mh]))
            return CURLM_BAD_HANDLE;

        if (!isset($this->handles[$ch]))
            return CURLM_BAD_EASY_HANDLE;

        if ($this->handles[$ch]->inMultiHandle)
            return $this->versionNewerThan(7,32,1)
                   ? self::M_ADDED_ALREADY
                   : CURLM_BAD_EASY_HANDLE;

        $this->multiHandles[$mh]->handles[] = $ch;
        // TODO: Figure out a reasonable way to determine these random
        // durations, so that even for a large number of easy handles it should
        // still possible for them to return in any order. This will probably
        // depend on the number of existing easy handles.
        $this->handles[$ch]->durationLeft = rand(0,5);
        $this->handles[$ch]->inMultiHandle = true;

        return 0;
    }

    public function multi_select($mh, $timeout = 1.0)
    {
        $active = 0;
        foreach ($this->multiHandles[$mh]->handles as $ch)
        {
            $handle = $this->handles[$ch];

            if ($handle->durationLeft > 0)
                --$handle->durationLeft;
            else if ($handle->durationLeft == 0)
                ++$active;
        }

        // TODO: This never returns -1. However, some version of cURL itself
        // are able to return -1 indefinitely, unless you just let your thread
        // sleep some time and then call multi_exec right away. Should this
        // (somewhat buggy) behavior be mimicked to ensure that tested
        // applications are able to deal with these problems?
        // TODO: We are also ignoring the timeout here. Can that cause problems
        // for tested applications? If not, ignoring the timeout seems perfectly
        // reasonable as it cuts down on test run time.
        return $active;
    }

    public function multi_exec($mh, &$still_running)
    {
        // PHP emits a warning if $mh is not a valid handle. Should we mimick
        // this behavior?
        if (!isset($this->multiHandles[$mh]))
            return false;

        $still_running = 0;
        $mhandle = $this->multiHandles[$mh];

        // Mimick cURL's odd CURLM_CALL_MULTI_PERFORM behavior prior to version
        // 7.20.0 by simply returning that signal on every second call to
        // multi_exec. Note that we still have to count the number of active
        // easy handles remaining.
        // TODO: This latter thing could be alleviated by keeping track of that
        // number incrementally.
        if (!$this->versionNewerThan(7, 20, 0))
        {
            if (!$mhandle->performReturned)
            {
                foreach ($mhandle->handles as $ch)
                {
                    if ($this->handles[$ch]->durationLeft == 0)
                        ++$still_running;
                }
                $mhandle->performReturned = true;
                return CURLM_CALL_MULTI_PERFORM;
            }
            else
            {
                $mhandle->performReturned = false;
            }
        }

        foreach ($mhandle->handles as $ch)
        {
            $handle = $this->handles[$ch];

            if ($handle->durationLeft > 0)
                ++$still_running;
            else if ($handle->durationLeft == 0)
            {
                // This handle is done. Add a message to the multi handle's
                // queue.
                array_push(
                    $mhandle->messageQueue,
                    array(
                        'msg' => CURLMSG_DONE,
                        'result' => $this->getErrorCode($handle),
                        'handle' => $ch
                    )
                );

                // Signify that the response is ready to be fetched if
                // CURLOPT_RETURNTRANSFER was set.
                $handle->durationLeft = -1;

                // On the other hand, if that option was not set, fetch the
                // response immediately and print it to stdout.
                if (!$handle->options[CURLOPT_RETURNTRANSFER])
                {
                    $response = $this->getResponse($handle);
                    if (!is_null($response))
                        echo $response;
                }

                // TODO: Use more of the behavior options like the callbacks
                // WRITEFUNCTION and READFUNCTION.
            }
        }

        return CURLM_OK;
    }

    public function multi_info_read($mh, &$msgs_in_queue = null)
    {
        // PHP emits a warning if $mh is not a valid handle. Should we mimick
        // this behavior?
        if (!isset($this->multiHandles[$mh]))
            return false;

        $message = array_shift($this->multiHandles[$mh]->messageQueue);

        if (is_null($message))
            $message = false;

        $msgs_in_queue = count($this->multiHandles[$mh]->messageQueue);

        return $message;
    }

    public function multi_getcontent($ch)
    {
        // PHP emits a warning if $ch is not a valid handle. Should we mimick
        // this behavior?
        if (!isset($this->handles[$ch]))
            return false;

        $handle = $this->handles[$ch];

        if (!$handle->inMultiHandle ||
            $handle->durationLeft != -1 ||
            !$handle->options[CURLOPT_RETURNTRANSFER])
            return null;

        $response = $this->getResponse($handle);

        // TODO: Is this really what PHP would do?
        if(is_null($response))
            return false;

        return $response;
    }

    public function multi_remove_handle($mh, $ch)
    {
        // PHP emits a warning if either $mh or $ch is not a valid handle.
        // Should we mimick this behavior?
        if (!isset($this->multiHandles[$mh]))
            return null;
        if (!isset($this->handles[$ch]))
            return false;

        if ($key = array_search($ch, $this->multiHandles[$mh]->handles))
        {
            unset($this->multiHandles[$mh]->handles[$key]);
            $this->handles[$ch]->inMultiHandle = false;
            return CURLM_OK;
        }
        else
        {
            // The given handle was not added to the multi handle anyway.
            return CURLM_BAD_EASY_HANDLE;
        }
    }

    public function multi_close($mh)
    {
        if (isset($this->multiHandles[$mh]))
        {
            foreach ($this->multiHandles[$mh]->handles as $ch)
                $this->handles[$ch]->inMultiHandle = false;

            unset($this->multiHandles[$mh]);
        }
    }

    /**
     *  Miscellaneous
     */

    public function version($age = CURLVERSION_NOW)
    {
        return curl_version($age);
    }

    /**
     * Stub configuration
     */

    public function setResponse($expectedResponse, $requiredOptions = array())
    {
        $hash = $this->getHashAndSetOptionMaps($requiredOptions);
        $this->mapResponses[$hash] = $expectedResponse;
    }

    public function setErrorCode($expectedErrorCode, $requiredOptions = array())
    {
        $hash = $this->getHashAndSetOptionMaps($requiredOptions);
        $this->mapErrorCodes[$hash] = $expectedErrorCode;
    }

    public function setInformation($expectedInfo, $requiredOptions = array())
    {
        $hash = $this->getHashAndSetOptionMaps($requiredOptions);
        $this->mapInfo[$hash] = $expectedInfo;
    }

    private function getHashAndSetOptionMaps($requiredOptions)
    {
        ksort($requiredOptions);
        $hash = md5(http_build_query($requiredOptions));

        $this->mapOptionCounts[$hash] = count($requiredOptions);
        arsort($this->mapOptionCounts);
        $this->mapOptions[$hash] = $requiredOptions;
        return $hash;
    }

    /**
     * Helper functions
     */

    private function versionNewerThan($major, $minor = 0, $patch = 0)
    {
        $versionInfo = $this->version();

        $versionParts = explode('.', $versionInfo['version']);

        return $major > $versionParts[0]
            || $major == $versionParts[0] && $minor > $versionParts[1]
            || $major == $versionParts[0] && $minor == $versionParts[1] && $patch >= $versionParts[2];
    }

    private function getResponse($handle)
    {
        $response = false;

        $key = $this->determineKey($handle, "mapResponses");

        if($key !== null)
            $response = $this->mapResponses[$key];

        return $response;
    }

    private function getErrorCode($handle)
    {
        $errorCode = 0;

        $key = $this->determineKey($handle, "mapErrorCodes");

        if($key !== null)
            $errorCode = $this->mapErrorCodes[$key];

        return $errorCode;
    }

    private function getInformation($handle, $opt)
    {
        $info = '';

        $key = $this->determineKey($handle, "mapInfo");

        if($key !== null && isset($this->mapInfo[$key][$opt]))
            $info = $this->mapInfo[$key][$opt];

        return $info;
    }

    private function getInfoArray($handle)
    {
        $infoArray = array();

        foreach(self::$infoLookup as $strKey)
        {
            $infoArray[$strKey] = '';
        }

        $handleKey = $this->determineKey($handle, "mapInfo");

        if($handleKey !== null)
        {
            foreach($this->mapInfo[$handleKey] as $intKey => $value)
            {
                $strKey = self::$infoLookup[$intKey];
                $infoArray[$strKey] = $value;
            }
        }

        return $infoArray;
    }

    private function determineKey($handle, $map)
    {
        // TODO: Treat URL option separately, so that order of parameters does not matter
        // TODO: Treat HEADER option as array of individual options
        $returnValue = null;
        $options = $handle->options;
        foreach($this->mapOptionCounts as $hash => $optionCount)
        {
            foreach($this->mapOptions[$hash] as $option => $value)
            {
                if(!isset($options[$option]) || $options[$option] != $value)
                    continue 2;
            }

            // We need this check, because the element in mapOptionCounts might have been created
            // for a different type of output. In this case, we need to continue searching
            if(!isset($this->{$map}[$hash]))
                continue;

            $returnValue = $hash;
            break;
        }
        return $returnValue;
    }

    /**
     * Lookup tables
     */

    private static $errorLookup = array(
        0  => 'CURLE_OK',
        1  => 'CURLE_UNSUPPORTED_PROTOCOL',
        2  => 'CURLE_FAILED_INIT',
        3  => 'CURLE_URL_MALFORMAT',
        4  => 'CURLE_URL_MALFORMAT_USER',
        5  => 'CURLE_COULDNT_RESOLVE_PROXY',
        6  => 'CURLE_COULDNT_RESOLVE_HOST',
        7  => 'CURLE_COULDNT_CONNECT',
        8  => 'CURLE_FTP_WEIRD_SERVER_REPLY',
        9  => 'CURLE_REMOTE_ACCESS_DENIED',
        11 => 'CURLE_FTP_WEIRD_PASS_REPLY',
        13 => 'CURLE_FTP_WEIRD_PASV_REPLY',
        14 => 'CURLE_FTP_WEIRD_227_FORMAT',
        15 => 'CURLE_FTP_CANT_GET_HOST',
        17 => 'CURLE_FTP_COULDNT_SET_TYPE',
        18 => 'CURLE_PARTIAL_FILE',
        19 => 'CURLE_FTP_COULDNT_RETR_FILE',
        21 => 'CURLE_QUOTE_ERROR',
        22 => 'CURLE_HTTP_RETURNED_ERROR',
        23 => 'CURLE_WRITE_ERROR',
        25 => 'CURLE_UPLOAD_FAILED',
        26 => 'CURLE_READ_ERROR',
        27 => 'CURLE_OUT_OF_MEMORY',
        28 => 'CURLE_OPERATION_TIMEOUTED', // in the original cURL lib this is called 'CURLE_OPERATION_TIMEDOUT' instead
        30 => 'CURLE_FTP_PORT_FAILED',
        31 => 'CURLE_FTP_COULDNT_USE_REST',
        33 => 'CURLE_RANGE_ERROR',
        34 => 'CURLE_HTTP_POST_ERROR',
        35 => 'CURLE_SSL_CONNECT_ERROR',
        36 => 'CURLE_BAD_DOWNLOAD_RESUME',
        37 => 'CURLE_FILE_COULDNT_READ_FILE',
        38 => 'CURLE_LDAP_CANNOT_BIND',
        39 => 'CURLE_LDAP_SEARCH_FAILED',
        41 => 'CURLE_FUNCTION_NOT_FOUND',
        42 => 'CURLE_ABORTED_BY_CALLBACK',
        43 => 'CURLE_BAD_FUNCTION_ARGUMENT',
        45 => 'CURLE_INTERFACE_FAILED',
        47 => 'CURLE_TOO_MANY_REDIRECTS',
        48 => 'CURLE_UNKNOWN_TELNET_OPTION',
        49 => 'CURLE_TELNET_OPTION_SYNTAX',
        51 => 'CURLE_PEER_FAILED_VERIFICATION',
        52 => 'CURLE_GOT_NOTHING',
        53 => 'CURLE_SSL_ENGINE_NOTFOUND',
        54 => 'CURLE_SSL_ENGINE_SETFAILED',
        55 => 'CURLE_SEND_ERROR',
        56 => 'CURLE_RECV_ERROR',
        58 => 'CURLE_SSL_CERTPROBLEM',
        59 => 'CURLE_SSL_CIPHER',
        60 => 'CURLE_SSL_CACERT',
        61 => 'CURLE_BAD_CONTENT_ENCODING',
        62 => 'CURLE_LDAP_INVALID_URL',
        63 => 'CURLE_FILESIZE_EXCEEDED',
        64 => 'CURLE_USE_SSL_FAILED',
        65 => 'CURLE_SEND_FAIL_REWIND',
        66 => 'CURLE_SSL_ENGINE_INITFAILED',
        67 => 'CURLE_LOGIN_DENIED',
        68 => 'CURLE_TFTP_NOTFOUND',
        69 => 'CURLE_TFTP_PERM',
        70 => 'CURLE_REMOTE_DISK_FULL',
        71 => 'CURLE_TFTP_ILLEGAL',
        72 => 'CURLE_TFTP_UNKNOWNID',
        73 => 'CURLE_REMOTE_FILE_EXISTS',
        74 => 'CURLE_TFTP_NOSUCHUSER',
        75 => 'CURLE_CONV_FAILED',
        76 => 'CURLE_CONV_REQD',
        77 => 'CURLE_SSL_CACERT_BADFILE',
        78 => 'CURLE_REMOTE_FILE_NOT_FOUND',
        79 => 'CURLE_SSH',
        80 => 'CURLE_SSL_SHUTDOWN_FAILED',
        81 => 'CURLE_AGAIN',
        82 => 'CURLE_SSL_CRL_BADFILE',
        83 => 'CURLE_SSL_ISSUER_ERROR',
        84 => 'CURLE_FTP_PRET_FAILED',
        85 => 'CURLE_RTSP_CSEQ_ERROR',
        86 => 'CURLE_RTSP_SESSION_ERROR',
        87 => 'CURLE_FTP_BAD_FILE_LIST',
        88 => 'CURLE_CHUNK_FAILED'
    );

    public static $infoLookup = array(
        CURLINFO_EFFECTIVE_URL => 'url',
        CURLINFO_HTTP_CODE => 'http_code',
        CURLINFO_FILETIME => 'filetime',
        CURLINFO_TOTAL_TIME => 'total_time',
        CURLINFO_NAMELOOKUP_TIME => 'namelookup_time',
        CURLINFO_CONNECT_TIME => 'connect_time',
        CURLINFO_PRETRANSFER_TIME => 'pretransfer_time',
        CURLINFO_STARTTRANSFER_TIME => 'starttransfer_time',
        CURLINFO_REDIRECT_COUNT => 'redirect_count',
        CURLINFO_REDIRECT_TIME => 'redirect_time',
        CURLINFO_SIZE_UPLOAD => 'size_upload',
        CURLINFO_SIZE_DOWNLOAD => 'size_download',
        CURLINFO_SPEED_DOWNLOAD => 'speed_download',
        CURLINFO_SPEED_UPLOAD => 'speed_upload',
        CURLINFO_HEADER_SIZE => 'header_size',
        CURLINFO_HEADER_OUT => 'request_header',
        CURLINFO_REQUEST_SIZE => 'request_size',
        CURLINFO_SSL_VERIFYRESULT => 'ssl_verify_result',
        CURLINFO_CONTENT_LENGTH_DOWNLOAD => 'download_content_length',
        CURLINFO_CONTENT_LENGTH_UPLOAD => 'upload_content_length',
        CURLINFO_CONTENT_TYPE => 'content_type'
    );
}