commit c6697cff77c1d14c915c0701cf89b521f24b1faf
Author: coderkun
Date: Fri Apr 25 13:19:46 2014 +0200
link Characters in ranking on Characters page
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 00000000..63a582df
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,8 @@
+syntax: regexp
+^media/*
+^tmp/*
+^uploads/*
+^seminarymedia/*
+^seminaryuploads/*
+^www/analytics/config/config.ini.php*
+^www/analytics/temp/*
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 00000000..cb3fb9ef
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,50 @@
+Options -Indexes -MultiViews
+
+ErrorDocument 403 /www/error403.html
+ErrorDocument 404 /www/error404.html
+ErrorDocument 500 /www/error500.html
+
+
+
+
+ Require all granted
+
+
+ Require all denied
+
+
+
+ Require all denied
+
+
+
+ Require all denied
+
+
+
+
+ Allow From All
+
+
+ Order Deny,Allow
+ Deny From All
+
+
+
+ Order Deny,Allow
+ Deny From All
+
+
+
+ Order Deny,Allow
+ Deny From All
+
+
+
+
+
+ RewriteEngine On
+
+ RewriteBase /
+ RewriteRule ^(.*)$ www/$1 [L]
+
diff --git a/agents/BottomlevelAgent.inc b/agents/BottomlevelAgent.inc
new file mode 100644
index 00000000..37816614
--- /dev/null
+++ b/agents/BottomlevelAgent.inc
@@ -0,0 +1,25 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\agents;
+
+
+ /**
+ * The BottomlevelAgent is the standard Agent and can have indefinite
+ * SubAgents.
+ *
+ * @author coderkun
+ */
+ abstract class BottomlevelAgent extends \nre\core\Agent
+ {
+ }
+
+?>
diff --git a/agents/IntermediateAgent.inc b/agents/IntermediateAgent.inc
new file mode 100644
index 00000000..7139653d
--- /dev/null
+++ b/agents/IntermediateAgent.inc
@@ -0,0 +1,48 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\agents;
+
+
+ /**
+ * The IntermediateAgent assumes the task of a module. There is only one
+ * IntermediateAgent per request.
+ *
+ * @author coderkun
+ */
+ abstract class IntermediateAgent extends \nre\core\Agent
+ {
+
+
+
+
+ /**
+ * Get the layout if it was explicitly defined.
+ *
+ * @return string Layout of the IntermediateAgent
+ */
+ public static function getLayout($agentName)
+ {
+ // Determine classname
+ $className = Autoloader::concatClassNames($agentName, 'Agent');
+
+ // Check property
+ if(isset($className::$layout)) {
+ return $className::$layout;
+ }
+
+
+ return null;
+ }
+
+ }
+
+?>
diff --git a/agents/ToplevelAgent.inc b/agents/ToplevelAgent.inc
new file mode 100644
index 00000000..9d6d6f8a
--- /dev/null
+++ b/agents/ToplevelAgent.inc
@@ -0,0 +1,395 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\agents;
+
+
+ /**
+ * The ToplevelAgent assumes the task of a FrontController. There is
+ * only one per request.
+ *
+ * @author coderkun
+ */
+ class ToplevelAgent extends \nre\core\Agent
+ {
+ /**
+ * Stage: Load
+ *
+ * @var string
+ */
+ const STAGE_LOAD = 'load';
+ /**
+ * Stage: Run
+ *
+ * @var string
+ */
+ const STAGE_RUN = 'run';
+
+ /**
+ * Current request
+ *
+ * @var Request
+ */
+ private $request;
+ /**
+ * Current response
+ *
+ * @var Response
+ */
+ private $response;
+ /**
+ * Layout instace
+ *
+ * @var Layout
+ */
+ private $layout = null;
+ /**
+ * IntermediateAgent instance
+ *
+ * @var IntermediateAgent
+ */
+ private $intermediateAgent = null;
+
+
+
+
+ /**
+ * Construct a ToplevelAgent.
+ *
+ * @throws ServiceUnavailableException
+ * @throws DatamodelException
+ * @throws DriverNotValidException
+ * @throws DriverNotFoundException
+ * @throws ViewNotFoundException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws ControllerNotValidException
+ * @throws ControllerNotFoundException
+ * @param Request $request Current request
+ * @param Response $respone Current response
+ * @param Logger $log Log-system
+ */
+ protected function __construct(\nre\core\Request $request, \nre\core\Response $response, \nre\core\Logger $log=null)
+ {
+ // Store values
+ $this->request = $request;
+ $this->response = $response;
+
+
+ // Create response
+ $response = clone $response;
+ $response->clearParams(1);
+ $response->addParams(
+ null,
+ \nre\configs\CoreConfig::$defaults['action']
+ );
+
+ // Call parent constructor
+ parent::__construct($request, $response, $log, true);
+
+
+ // Load IntermediateAgent
+ $this->loadIntermediateAgent();
+ }
+
+
+
+
+ /**
+ * Run the Controller of this Agent and its SubAgents.
+ *
+ * @throws ServiceUnavailableException
+ * @param Request $request Current request
+ * @param Response $response Current response
+ * @return Exception Last occurred exception of SubAgents
+ */
+ public function run(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ try {
+ return $this->_run($request, $response);
+ }
+ catch(\nre\exceptions\AccessDeniedException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_FORBIDDEN, self::STAGE_RUN);
+ }
+ catch(\nre\exceptions\ParamsNotValidException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_NOT_FOUND, self::STAGE_RUN);
+ }
+ catch(\nre\exceptions\IdNotFoundException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_NOT_FOUND, self::STAGE_RUN);
+ }
+ catch(\nre\exceptions\DatamodelException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE, self::STAGE_RUN);
+ }
+ catch(\nre\exceptions\ActionNotFoundException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_NOT_FOUND, self::STAGE_RUN);
+ }
+ }
+
+
+ /**
+ * Generate output of the Controller of this Agent and its
+ * SubAgents.
+ *
+ * @param array $data View data
+ * @return string Generated output
+ */
+ public function render($data=array())
+ {
+ // Render IntermediateAgent
+ $data = array();
+ $data['intermediate'] = $this->intermediateAgent->render();
+
+
+ // Render ToplevelAgent
+ return parent::render($data);
+ }
+
+
+ /**
+ * Return the IntermediateAgent.
+ *
+ * @return IntermediateAgent IntermediateAgent
+ */
+ public function getIntermediateAgent()
+ {
+ return $this->intermediateAgent;
+ }
+
+
+
+
+ /**
+ * Load a SubAgent and add it.
+ *
+ * @throws ServiceUnavailableException
+ * @throws FatalDatamodelException
+ * @throws AgentNotFoundException
+ * @throws AgentNotValidException
+ * @param string $agentName Name of the Agent to load
+ * @param mixed … Additional parameters for the agent
+ */
+ protected function addSubAgent($agentName)
+ {
+ try {
+ call_user_func_array(
+ array(
+ $this,
+ '_addSubAgent'
+ ),
+ func_get_args()
+ );
+ }
+ catch(\nre\exceptions\DatamodelException $e) {
+ throw new \nre\exceptions\FatalDatamodelException($e->getDatamodelMessage(), $e->getDatamodelErrorNumber());
+ }
+ }
+
+
+
+
+ /**
+ * Load IntermediateAgent defined by the current request.
+ *
+ * @throws ServiceUnavailableException
+ */
+ private function loadIntermediateAgent()
+ {
+ try {
+ $this->_loadIntermediateAgent();
+ }
+ catch(\nre\exceptions\ViewNotFoundException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_NOT_FOUND);
+ }
+ catch(\nre\exceptions\DatamodelException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE);
+ }
+ catch(\nre\exceptions\DriverNotValidException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE);
+ }
+ catch(\nre\exceptions\DriverNotFoundException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE);
+ }
+ catch(\nre\exceptions\ModelNotValidException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE);
+ }
+ catch(\nre\exceptions\ModelNotFoundException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE);
+ }
+ catch(\nre\exceptions\ControllerNotValidException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE);
+ }
+ catch(\nre\exceptions\ControllerNotFoundException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_NOT_FOUND);
+ }
+ catch(\nre\exceptions\AgentNotValidException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE);
+ }
+ catch(\nre\exceptions\AgentNotFoundException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_NOT_FOUND);
+ }
+ }
+
+
+ /**
+ * Load IntermediateAgent defined by the current request.
+ *
+ * @throws ServiceUnavailableException
+ */
+ private function _loadIntermediateAgent()
+ {
+ // Determine IntermediateAgent
+ $agentName = $this->response->getParam(1);
+ if(is_null($agentName)) {
+ $agentName = $this->request->getParam(1, 'intermediate');
+ $this->response->addParam($agentName);
+ }
+
+ // Load IntermediateAgent
+ IntermediateAgent::load($agentName);
+
+
+ // Determine Action
+ $action = $this->response->getParam(2);
+ if(is_null($action)) {
+ $action = $this->request->getParam(2, 'action');
+ $this->response->addParam($action);
+ }
+
+ // Construct IntermediateAgent
+ $this->intermediateAgent = \nre\agents\IntermediateAgent::factory(
+ $agentName,
+ $this->request,
+ $this->response,
+ $this->log
+ );
+ }
+
+
+ /**
+ * Run the Controller of this Agent and its SubAgents.
+ *
+ * @throws AccessDeniedException
+ * @throws IdNotFoundException
+ * @throws ServiceUnavailableException
+ * @throws DatamodelException
+ * @param Request $request Current request
+ * @param Response $response Current response
+ * @return Exception Last occurred exception of SubAgents
+ */
+ private function _run(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ // Run IntermediateAgent
+ $this->runIntermediateAgent();
+
+
+ // TODO Request instead of response?
+ $response = clone $response;
+ $response->clearParams(2);
+ $response->addParam(\nre\configs\CoreConfig::$defaults['action']);
+
+
+ // Run ToplevelAgent
+ return parent::run($request, $response);
+ }
+
+
+ /**
+ * Run IntermediateAgent.
+ *
+ * @throws AccessDeniedException
+ * @throws ParamsNotValidException
+ * @throws IdNotFoundException
+ * @throws ServiceUnavailableException
+ * @throws DatamodelException
+ */
+ private function runIntermediateAgent()
+ {
+ $this->intermediateAgent->run(
+ $this->request,
+ $this->response
+ );
+ }
+
+
+ /**
+ * Handle an error that occurred during
+ * loading/cnostructing/running of the IntermediateAgent.
+ *
+ * @throws ServiceUnavailableException
+ * @param Exception $exception Occurred exception
+ * @param int $httpStatusCode HTTP-statuscode
+ * @param string $stage Stage of execution
+ */
+ private function error($exception, $httpStatusCode, $stage=self::STAGE_LOAD)
+ {
+ // Log error
+ $this->log($exception, \nre\core\Logger::LOGMODE_AUTO);
+
+
+ try {
+ // Define ErrorAgent
+ $this->response->clearParams(1);
+ $this->response->addParams(
+ \nre\configs\AppConfig::$defaults['intermediate-error'],
+ \nre\configs\CoreConfig::$defaults['action'],
+ $httpStatusCode
+ );
+
+ // Load ErrorAgent
+ $this->_loadIntermediateAgent();
+
+ // Run ErrorAgent
+ if($stage == self::STAGE_RUN) {
+ $this->_run($this->request, $this->response);
+ }
+ }
+ catch(\nre\exceptions\ActionNotFoundException $e) {
+ throw new \nre\exceptions\ServiceUnavailableException($e);
+ }
+ catch(\nre\exceptions\DatamodelException $e) {
+ throw new \nre\exceptions\ServiceUnavailableException($e);
+ }
+ catch(\nre\exceptions\DriverNotValidException $e) {
+ throw new \nre\exceptions\ServiceUnavailableException($e);
+ }
+ catch(\nre\exceptions\DriverNotFoundException $e) {
+ throw new \nre\exceptions\ServiceUnavailableException($e);
+ }
+ catch(\nre\exceptions\ModelNotValidException $e) {
+ throw new \nre\exceptions\ServiceUnavailableException($e);
+ }
+ catch(\nre\exceptions\ModelNotFoundException $e) {
+ throw new \nre\exceptions\ServiceUnavailableException($e);
+ }
+ catch(\nre\exceptions\ViewNotFoundException $e) {
+ throw new \nre\exceptions\ServiceUnavailableException($e);
+ }
+ catch(\nre\exceptions\ControllerNotValidException $e) {
+ throw new \nre\exceptions\ServiceUnavailableException($e);
+ }
+ catch(\nre\exceptions\ControllerNotFoundException $e) {
+ throw new \nre\exceptions\ServiceUnavailableException($e);
+ }
+ catch(\nre\exceptions\AgentNotValidException $e) {
+ throw new \nre\exceptions\ServiceUnavailableException($e);
+ }
+ catch(\nre\exceptions\AgentNotFoundException $e) {
+ throw new \nre\exceptions\ServiceUnavailableException($e);
+ }
+ catch(Exception $e) {
+ throw new \nre\exceptions\ServiceUnavailableException($e);
+ }
+ }
+
+
+ }
+
+?>
diff --git a/agents/bottomlevel/MenuAgent.inc b/agents/bottomlevel/MenuAgent.inc
new file mode 100644
index 00000000..49512791
--- /dev/null
+++ b/agents/bottomlevel/MenuAgent.inc
@@ -0,0 +1,37 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\bottomlevel;
+
+
+ /**
+ * Agent to display a menu.
+ *
+ * @author Oliver Hanraths
+ */
+ class MenuAgent extends \nre\agents\BottomlevelAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ // Add Seminary menu
+ $this->addSubAgent('Seminarymenu');
+ }
+
+ }
+
+?>
diff --git a/agents/bottomlevel/QuestgroupshierarchypathAgent.inc b/agents/bottomlevel/QuestgroupshierarchypathAgent.inc
new file mode 100644
index 00000000..a2cb8086
--- /dev/null
+++ b/agents/bottomlevel/QuestgroupshierarchypathAgent.inc
@@ -0,0 +1,35 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\bottomlevel;
+
+
+ /**
+ * Agent to display the Questgroups hierarchy path.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuestgroupshierarchypathAgent extends \nre\agents\BottomlevelAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/bottomlevel/SeminarybarAgent.inc b/agents/bottomlevel/SeminarybarAgent.inc
new file mode 100644
index 00000000..10315ab5
--- /dev/null
+++ b/agents/bottomlevel/SeminarybarAgent.inc
@@ -0,0 +1,35 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\bottomlevel;
+
+
+ /**
+ * Agent to display a sidebar with Seminary related information.
+ *
+ * @author Oliver Hanraths
+ */
+ class SeminarybarAgent extends \nre\agents\BottomlevelAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/bottomlevel/SeminarymenuAgent.inc b/agents/bottomlevel/SeminarymenuAgent.inc
new file mode 100644
index 00000000..375eab1e
--- /dev/null
+++ b/agents/bottomlevel/SeminarymenuAgent.inc
@@ -0,0 +1,35 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\bottomlevel;
+
+
+ /**
+ * Agent to display a menu with Seminary related links.
+ *
+ * @author Oliver Hanraths
+ */
+ class SeminarymenuAgent extends \nre\agents\BottomlevelAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/bottomlevel/UserrolesAgent.inc b/agents/bottomlevel/UserrolesAgent.inc
new file mode 100644
index 00000000..b6d6e65b
--- /dev/null
+++ b/agents/bottomlevel/UserrolesAgent.inc
@@ -0,0 +1,35 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\bottomlevel;
+
+
+ /**
+ * Agent to display and manage userroles.
+ *
+ * @author Oliver Hanraths
+ */
+ class UserrolesAgent extends \nre\agents\BottomlevelAgent
+ {
+
+
+
+
+ /**
+ * Action: user.
+ */
+ public function user(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/intermediate/AchievementsAgent.inc b/agents/intermediate/AchievementsAgent.inc
new file mode 100644
index 00000000..e6b965f9
--- /dev/null
+++ b/agents/intermediate/AchievementsAgent.inc
@@ -0,0 +1,35 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\intermediate;
+
+
+ /**
+ * Agent to list Achievements.
+ *
+ * @author Oliver Hanraths
+ */
+ class AchievementsAgent extends \nre\agents\IntermediateAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/intermediate/CharactergroupsAgent.inc b/agents/intermediate/CharactergroupsAgent.inc
new file mode 100644
index 00000000..77ac5f5a
--- /dev/null
+++ b/agents/intermediate/CharactergroupsAgent.inc
@@ -0,0 +1,35 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\intermediate;
+
+
+ /**
+ * Agent to display Character groups.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharactergroupsAgent extends \nre\agents\IntermediateAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/intermediate/CharactergroupsquestsAgent.inc b/agents/intermediate/CharactergroupsquestsAgent.inc
new file mode 100644
index 00000000..bfc87de1
--- /dev/null
+++ b/agents/intermediate/CharactergroupsquestsAgent.inc
@@ -0,0 +1,35 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\intermediate;
+
+
+ /**
+ * Agent to display Character groups Quests.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharactergroupsquestsAgent extends \nre\agents\IntermediateAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/intermediate/CharactersAgent.inc b/agents/intermediate/CharactersAgent.inc
new file mode 100644
index 00000000..25102198
--- /dev/null
+++ b/agents/intermediate/CharactersAgent.inc
@@ -0,0 +1,43 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\intermediate;
+
+
+ /**
+ * Agent to list registered Characters and their data.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharactersAgent extends \nre\agents\IntermediateAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+
+ /**
+ * Action: character.
+ */
+ public function character(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/intermediate/ErrorAgent.inc b/agents/intermediate/ErrorAgent.inc
new file mode 100644
index 00000000..85bb6f95
--- /dev/null
+++ b/agents/intermediate/ErrorAgent.inc
@@ -0,0 +1,35 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\intermediate;
+
+
+ /**
+ * Agent to show an error page.
+ *
+ * @author Oliver Hanraths
+ */
+ class ErrorAgent extends \nre\agents\IntermediateAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/intermediate/IntroductionAgent.inc b/agents/intermediate/IntroductionAgent.inc
new file mode 100644
index 00000000..3a06fdff
--- /dev/null
+++ b/agents/intermediate/IntroductionAgent.inc
@@ -0,0 +1,35 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\intermediate;
+
+
+ /**
+ * Agent to show an introduction page.
+ *
+ * @author Oliver Hanraths
+ */
+ class IntroductionAgent extends \nre\agents\IntermediateAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/intermediate/LibraryAgent.inc b/agents/intermediate/LibraryAgent.inc
new file mode 100644
index 00000000..aedc890a
--- /dev/null
+++ b/agents/intermediate/LibraryAgent.inc
@@ -0,0 +1,35 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\intermediate;
+
+
+ /**
+ * Agent to list Quest topics.
+ *
+ * @author Oliver Hanraths
+ */
+ class LibraryAgent extends \nre\agents\IntermediateAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/intermediate/MediaAgent.inc b/agents/intermediate/MediaAgent.inc
new file mode 100644
index 00000000..c3fd35ae
--- /dev/null
+++ b/agents/intermediate/MediaAgent.inc
@@ -0,0 +1,35 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\intermediate;
+
+
+ /**
+ * Agent to process and show media.
+ *
+ * @author Oliver Hanraths
+ */
+ class MediaAgent extends \nre\agents\IntermediateAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/intermediate/QuestgroupsAgent.inc b/agents/intermediate/QuestgroupsAgent.inc
new file mode 100644
index 00000000..52ecd4e1
--- /dev/null
+++ b/agents/intermediate/QuestgroupsAgent.inc
@@ -0,0 +1,36 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\intermediate;
+
+
+ /**
+ * Agent to display Questgroups.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuestgroupsAgent extends \nre\agents\IntermediateAgent
+ {
+
+
+
+
+ /**
+ * Action: questgroup.
+ */
+ public function questgroup(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ $this->addSubAgent('Questgroupshierarchypath', 'index', $request->getParam(3), $request->getParam(4));
+ }
+
+ }
+
+?>
diff --git a/agents/intermediate/QuestsAgent.inc b/agents/intermediate/QuestsAgent.inc
new file mode 100644
index 00000000..273a47ab
--- /dev/null
+++ b/agents/intermediate/QuestsAgent.inc
@@ -0,0 +1,54 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\intermediate;
+
+
+ /**
+ * Agent to display Quests.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuestsAgent extends \nre\agents\IntermediateAgent
+ {
+
+
+
+
+ /**
+ * Action: quest.
+ */
+ public function quest(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ $this->addSubAgent('Questgroupshierarchypath', 'index', $request->getParam(3), $request->getParam(4), true);
+ }
+
+
+ /**
+ * Action: submissions.
+ */
+ public function submissions(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ $this->addSubAgent('Questgroupshierarchypath', 'index', $request->getParam(3), $request->getParam(4), true);
+ }
+
+
+ /**
+ * Action: submission.
+ */
+ public function submission(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ $this->addSubAgent('Questgroupshierarchypath', 'index', $request->getParam(3), $request->getParam(4), true);
+ }
+
+ }
+
+?>
diff --git a/agents/intermediate/SeminariesAgent.inc b/agents/intermediate/SeminariesAgent.inc
new file mode 100644
index 00000000..53f08124
--- /dev/null
+++ b/agents/intermediate/SeminariesAgent.inc
@@ -0,0 +1,35 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\intermediate;
+
+
+ /**
+ * Agent to list registered seminaries.
+ *
+ * @author Oliver Hanraths
+ */
+ class SeminariesAgent extends \nre\agents\IntermediateAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/intermediate/UploadsAgent.inc b/agents/intermediate/UploadsAgent.inc
new file mode 100644
index 00000000..457b6a49
--- /dev/null
+++ b/agents/intermediate/UploadsAgent.inc
@@ -0,0 +1,35 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\intermediate;
+
+
+ /**
+ * Agent to process and show user uploads.
+ *
+ * @author Oliver Hanraths
+ */
+ class UploadsAgent extends \nre\agents\IntermediateAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/intermediate/UsersAgent.inc b/agents/intermediate/UsersAgent.inc
new file mode 100644
index 00000000..a9094490
--- /dev/null
+++ b/agents/intermediate/UsersAgent.inc
@@ -0,0 +1,44 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\intermediate;
+
+
+ /**
+ * Agent to list registered users and their data.
+ *
+ * @author Oliver Hanraths
+ */
+ class UsersAgent extends \nre\agents\IntermediateAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+
+ /**
+ * Action: user.
+ */
+ public function user(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ $this->addSubAgent('Userroles', 'user');
+ }
+
+ }
+
+?>
diff --git a/agents/toplevel/BinaryAgent.inc b/agents/toplevel/BinaryAgent.inc
new file mode 100644
index 00000000..f2d6d33e
--- /dev/null
+++ b/agents/toplevel/BinaryAgent.inc
@@ -0,0 +1,41 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\toplevel;
+
+
+ /**
+ * Agent to display binary data (e. g. images).
+ *
+ * @author Oliver Hanraths
+ */
+ class BinaryAgent extends \hhu\z\ToplevelAgent
+ {
+
+
+
+
+ protected function __construct(\nre\core\Request $request, \nre\core\Response $response, \nre\core\Logger $log=null)
+ {
+ parent::__construct($request, $response, $log);
+ }
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/toplevel/FaultAgent.inc b/agents/toplevel/FaultAgent.inc
new file mode 100644
index 00000000..1ceb682e
--- /dev/null
+++ b/agents/toplevel/FaultAgent.inc
@@ -0,0 +1,35 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\toplevel;
+
+
+ /**
+ * Agent to display a toplevel error page.
+ *
+ * @author Oliver Hanraths
+ */
+ class FaultAgent extends \nre\agents\ToplevelAgent
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ }
+
+ }
+
+?>
diff --git a/agents/toplevel/HtmlAgent.inc b/agents/toplevel/HtmlAgent.inc
new file mode 100644
index 00000000..bedcf848
--- /dev/null
+++ b/agents/toplevel/HtmlAgent.inc
@@ -0,0 +1,70 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\agents\toplevel;
+
+
+ /**
+ * Agent to display a HTML-page.
+ *
+ * @author Oliver Hanraths
+ */
+ class HtmlAgent extends \hhu\z\ToplevelAgent
+ {
+
+
+
+
+ protected function __construct(\nre\core\Request $request, \nre\core\Response $response, \nre\core\Logger $log=null)
+ {
+ parent::__construct($request, $response, $log);
+
+
+ $this->setLanguage($request);
+ }
+
+
+ /**
+ * Action: index.
+ */
+ public function index(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ // Add menu
+ $this->addSubAgent('Menu');
+
+ // Add Seminary sidebar
+ $this->addSubAgent('Seminarybar');
+ }
+
+
+
+
+ private function setLanguage(\nre\core\Request $request)
+ {
+ // Set domain
+ $domain = \nre\configs\AppConfig::$app['genericname'];
+
+ // Get language
+ $locale = $request->getGetParam('lang', 'language');
+ if(is_null($locale)) {
+ return;
+ }
+
+ // Load translation
+ putenv("LC_ALL=$locale");
+ setlocale(LC_ALL, $locale);
+ bindtextdomain($domain, ROOT.DS.\nre\configs\AppConfig::$dirs['locale']);
+ textdomain($domain);
+ }
+
+ }
+
+?>
diff --git a/apis/WebApi.inc b/apis/WebApi.inc
new file mode 100644
index 00000000..6851ee2d
--- /dev/null
+++ b/apis/WebApi.inc
@@ -0,0 +1,250 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\apis;
+
+
+ /**
+ * WebApi-implementation.
+ *
+ * This class runs and renders an web-applictaion.
+ *
+ * @author coderkun
+ */
+ class WebApi extends \nre\core\Api
+ {
+
+
+
+
+ /**
+ * Construct a new WebApi.
+ */
+ public function __construct()
+ {
+ parent::__construct(
+ new \nre\requests\WebRequest(),
+ new \nre\responses\WebResponse()
+ );
+
+ // Add routes
+ $this->addRoutes();
+
+ // Disable screen logging for AJAX requests
+ if($this->request->getParam(0, 'toplevel') == 'ajax') {
+ $this->log->disableAutoLogToScreen();
+ }
+ }
+
+
+
+
+ /**
+ * Run application.
+ *
+ * This method runs the application and handles all errors.
+ */
+ public function run()
+ {
+ try {
+ $exception = parent::run();
+
+ if(!is_null($exception)) {
+ $this->errorService($exception);
+ }
+ }
+ catch(\nre\exceptions\ServiceUnavailableException $e) {
+ $this->errorService($e);
+ }
+ catch(\nre\exceptions\ActionNotFoundException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_NOT_FOUND);
+ }
+ catch(\nre\exceptions\FatalDatamodelException $e) {
+ $this->errorService($e);
+ }
+ catch(\nre\exceptions\DatamodelException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE);
+ }
+ catch(\nre\exceptions\DriverNotValidException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE);
+ }
+ catch(\nre\exceptions\DriverNotFoundException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE);
+ }
+ catch(\nre\exceptions\ModelNotValidException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE);
+ }
+ catch(\nre\exceptions\ModelNotFoundException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE);
+ }
+ catch(\nre\exceptions\ViewNotFoundException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_NOT_FOUND);
+ }
+ catch(\nre\exceptions\ControllerNotValidException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE);
+ }
+ catch(\nre\exceptions\ControllerNotFoundException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_NOT_FOUND);
+ }
+ catch(\nre\exceptions\AgentNoaatValidException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_SERVICE_UNAVAILABLE);
+ }
+ catch(\nre\exceptions\AgentNotFoundException $e) {
+ $this->error($e, \nre\core\WebUtils::HTTP_NOT_FOUND);
+ }
+ catch(\nre\exceptions\ClassNotValidException $e) {
+ $this->errorService($e);
+ }
+ catch(\nre\exceptions\ClassNotFoundException $e) {
+ $this->errorService($e);
+ }
+ }
+
+
+ /**
+ * Render output.
+ */
+ public function render()
+ {
+ // Generate output
+ parent::render();
+
+
+ // Set HTTP-header
+ $this->response->header();
+
+ // Show output
+ echo $this->response->getOutput();
+ }
+
+
+
+
+ /**
+ * Add routes (normal and reverse) defined in the AppConfig.
+ */
+ private function addRoutes()
+ {
+ // Normal routes
+ if(property_exists('\nre\configs\AppConfig', 'routes')) {
+ foreach(\nre\configs\AppConfig::$routes as &$route) {
+ $this->request->addRoute($route[0], $route[1], $route[2]);
+ }
+ }
+
+ // Reverse routes
+ if(property_exists('\nre\configs\AppConfig', 'reverseRoutes')) {
+ foreach(\nre\configs\AppConfig::$reverseRoutes as &$route) {
+ $this->request->addReverseRoute($route[0], $route[1], $route[2]);
+ }
+ }
+
+ // Revalidate request
+ $this->request->revalidate();
+ }
+
+
+ /**
+ * Handle an error that orrcurred during the
+ * loading/constructing/running of the ToplevelAgent.
+ *
+ * @param Exception $exception Occurred exception
+ * @param int $httpStatusCode HTTP-statuscode
+ */
+ private function error(\nre\core\Exception $exception, $httpStatusCode)
+ {
+ // Log error message
+ $this->log($exception, \nre\core\Logger::LOGMODE_AUTO);
+
+ try {
+ // Set agent for handling errors
+ $this->response->clearParams();
+ $this->response->addParams(
+ \nre\configs\AppConfig::$defaults['toplevel-error'],
+ \nre\configs\AppConfig::$defaults['intermediate-error'],
+ \nre\configs\CoreConfig::$defaults['action'],
+ $httpStatusCode
+ );
+
+ // Run this agent
+ parent::run();
+ }
+ catch(\nre\exceptions\ServiceUnavailableException $e) {
+ $this->errorService($e);
+ }
+ catch(\nre\exceptions\ActionNotFoundException $e) {
+ $this->errorService($e);
+ }
+ catch(\nre\exceptions\DatamodelException $e) {
+ $this->errorService($e);
+ }
+ catch(\nre\exceptions\DriverNotValidException $e) {
+ $this->errorService($e);
+ }
+ catch(\nre\exceptions\DriverNotFoundException $e) {
+ $this->errorService($e);
+ }
+ catch(\nre\exceptions\ModelNotValidException $e) {
+ $this->errorService($e);
+ }
+ catch(\nre\exceptions\ModelNotFoundException $e) {
+ $this->errorService($e);
+ }
+ catch(\nre\exceptions\ViewNotFoundException $e) {
+ $this->errorService($e);
+ }
+ catch(\nre\exceptions\ControllerNotValidException $e) {
+ $this->errorService($e);
+ }
+ catch(\nre\exceptions\ControllerNotFoundException $e) {
+ $this->errorService($e);
+ }
+ catch(\nre\exceptions\AgentNotValidException $e) {
+ $this->errorService($e);
+ }
+ catch(\nre\exceptions\AgentNotFoundException $e) {
+ $this->errorService($e);
+ }
+ catch(Exception $e) {
+ $this->errorService($e);
+ }
+ }
+
+
+ /**
+ * Handle a error which cannot be handles by the system (and
+ * HTTP 503).
+ *
+ * $param Exception $exception Occurred exception
+ */
+ private function errorService($exception)
+ {
+ // Log error message
+ $this->log($exception, \nre\core\Logger::LOGMODE_AUTO);
+
+ // Set HTTP-rtatuscode
+ $this->response->addHeader(\nre\core\WebUtils::getHttpHeader(503));
+
+
+ // Read and print static error file
+ $fileName = ROOT.DS.\nre\configs\CoreConfig::getClassDir('views').DS.\nre\configs\CoreConfig::$defaults['errorFile'].\nre\configs\CoreConfig::getFileExt('views');
+ ob_start();
+ include($fileName);
+ $this->response->setOutput(ob_get_clean());
+
+
+ // Prevent further execution
+ $this->response->setExit();
+ }
+
+ }
+
+?>
diff --git a/app/Controller.inc b/app/Controller.inc
new file mode 100644
index 00000000..461c9fd1
--- /dev/null
+++ b/app/Controller.inc
@@ -0,0 +1,106 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z;
+
+
+ /**
+ * Abstract class for implementing an application Controller.
+ *
+ * @author Oliver Hanraths
+ */
+ abstract class Controller extends \nre\core\Controller
+ {
+ /**
+ * Required components
+ *
+ * @var array
+ */
+ public $components = array('auth');
+ /**
+ * Linker instance
+ *
+ * @var Linker
+ */
+ protected $linker = null;
+
+
+
+
+ /**
+ * Construct a new application Controller.
+ *
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws ViewNotFoundException
+ * @param string $layoutName Name of the current Layout
+ * @param string $action Current Action
+ * @param Agent $agent Corresponding Agent
+ */
+ public function __construct($layoutName, $action, $agent)
+ {
+ parent::__construct($layoutName, $action, $agent);
+ }
+
+
+
+ /**
+ * Prefilter that is executed before running the Controller.
+ *
+ * @param Request $request Current request
+ * @param Response $response Current response
+ */
+ public function preFilter(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ parent::preFilter($request, $response);
+
+ // Create linker
+ $this->linker = new \nre\core\Linker($this->request);
+
+ // Create text formatter
+ $this->set('t', new \hhu\z\TextFormatter($this->linker));
+
+ // Create date and time and number formatter
+ $this->set('dateFormatter', new \IntlDateFormatter(
+ \nre\core\Config::getDefault('locale'),
+ \IntlDateFormatter::MEDIUM,
+ \IntlDateFormatter::NONE,
+ NULL
+ ));
+ $this->set('timeFormatter', new \IntlDateFormatter(
+ \nre\core\Config::getDefault('locale'),
+ \IntlDateFormatter::NONE,
+ \IntlDateFormatter::SHORT,
+ NULL
+ ));
+ $this->set('numberFormatter', new \NumberFormatter(
+ \nre\core\Config::getDefault('locale'),
+ \NumberFormatter::DEFAULT_STYLE
+ ));
+ }
+
+
+ /**
+ * Postfilter that is executed after running the Controller.
+ *
+ * @param Request $request Current request
+ * @param Response $response Current response
+ */
+ public function postFilter(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ parent::postFilter($request, $response);
+ }
+
+ }
+
+?>
diff --git a/app/Model.inc b/app/Model.inc
new file mode 100644
index 00000000..32835d04
--- /dev/null
+++ b/app/Model.inc
@@ -0,0 +1,42 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z;
+
+
+ /**
+ * Abstract class for implementing an application Model.
+ *
+ * @author Oliver Hanraths
+ */
+ class Model extends \nre\models\DatabaseModel
+ {
+
+
+
+
+ /**
+ * Construct a new application Model.
+ *
+ * @throws DatamodelException
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ */
+ public function __construct()
+ {
+ parent::__construct('mysqli', \nre\configs\AppConfig::$database);
+ }
+
+ }
+
+?>
diff --git a/app/QuesttypeAgent.inc b/app/QuesttypeAgent.inc
new file mode 100644
index 00000000..23c4d1b2
--- /dev/null
+++ b/app/QuesttypeAgent.inc
@@ -0,0 +1,267 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z;
+
+
+ /**
+ * Abstract class for implementing a QuesttypeAgent.
+ *
+ * @author Oliver Hanraths
+ */
+ abstract class QuesttypeAgent extends \nre\agents\BottomlevelAgent
+ {
+ /**
+ * Current request
+ *
+ * @var Request
+ */
+ private $request;
+ /**
+ * Current response
+ *
+ * @var Response
+ */
+ private $response;
+
+
+
+
+ /**
+ * Load a QuesttypeAgent.
+ *
+ * @static
+ * @throws QuesttypeAgentNotFoundException
+ * @throws QuesttypeAgentNotValidException
+ * @param string $questtypeName Name of the QuesttypeAgent to load
+ */
+ public static function load($questtypeName)
+ {
+ // Determine full classname
+ $className = self::getClassName($questtypeName);
+
+ try {
+ // Load class
+ static::loadClass($questtypeName, $className);
+
+ // Validate class
+ static::checkClass($className, get_class());
+ }
+ catch(\nre\exceptions\ClassNotValidException $e) {
+ throw new \hhu\z\exceptions\QuesttypeAgentNotValidException($e->getClassName());
+ }
+ catch(\nre\exceptions\ClassNotFoundException $e) {
+ throw new \hhu\z\exceptions\QuesttypeAgentNotFoundException($e->getClassName());
+ }
+ }
+
+
+ /**
+ * Instantiate a QuesttypeAgent (Factory Pattern).
+ *
+ * @static
+ * @throws DatamodelException
+ * @throws DriverNotValidException
+ * @throws DriverNotFoundException
+ * @throws ViewNotFoundException
+ * @throws QuesttypeModelNotValidException
+ * @throws QuesttypeModelNotFoundException
+ * @throws QuesttypeControllerNotValidException
+ * @throws QuesttypeControllerNotFoundException
+ * @param string $questtypeName Name of the QuesttypeAgent to instantiate
+ * @param Request $request Current request
+ * @param Response $respone Current respone
+ * @param Logger $log Log-system
+ */
+ public static function factory($questtypeName, \nre\core\Request $request, \nre\core\Response $response, \nre\core\Logger $log=null)
+ {
+ // Determine full classname
+ $className = self::getClassName($questtypeName);
+
+ // Construct and return Questmodule
+ return new $className($request, $response, $log);
+ }
+
+
+ /**
+ * Determine the Agent-classname for the given Questtype-name.
+ *
+ * @static
+ * @param string $questtypeName Questtype-name to get Agent-classname of
+ * @return string Classname for the Questtype-name
+ */
+ private static function getClassName($questtypeName)
+ {
+ $className = \nre\core\ClassLoader::concatClassNames($questtypeName, \nre\core\ClassLoader::stripClassType(\nre\core\ClassLoader::stripNamespace(get_class())), 'agent');
+
+
+ return \nre\configs\AppConfig::$app['namespace']."questtypes\\$className";
+ }
+
+
+ /**
+ * Load the class of a QuesttypeAgent.
+ *
+ * @static
+ * @throws ClassNotFoundException
+ * @param string $questtypeName Name of the QuesttypeAgent to load
+ * @param string $fullClassName Name of the class to load
+ */
+ private static function loadClass($questtypeName, $fullClassName)
+ {
+ // Determine folder to look in
+ $className = explode('\\', $fullClassName);
+ $className = array_pop($className);
+
+ // Determine filename
+ $fileName = ROOT.DS.\nre\configs\AppConfig::$dirs['questtypes'].DS.strtolower($questtypeName).DS.$className.\nre\configs\CoreConfig::getFileExt('includes');
+
+ // Check file
+ if(!file_exists($fileName))
+ {
+ throw new \nre\exceptions\ClassNotFoundException(
+ $fullClassName
+ );
+ }
+
+ // Include file
+ include_once($fileName);
+ }
+
+
+ /**
+ * Check inheritance of the QuesttypeAgent-class.
+ *
+ * @static
+ * @throws ClassNotValidException
+ * @param string $className Name of the class to check
+ * @param string $parentClassName Name of the parent class
+ */
+ public static function checkClass($className, $parentClassName)
+ {
+ // Check if class is subclass of parent class
+ if(!is_subclass_of($className, $parentClassName)) {
+ throw new \nre\exceptions\ClassNotValidException(
+ $className
+ );
+ }
+ }
+
+
+
+
+ /**
+ * Construct a new QuesttypeAgent.
+ *
+ * @throws DatamodelException
+ * @throws DriverNotValidException
+ * @throws DriverNotFoundException
+ * @throws ViewNotFoundException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws QuesttypeModelNotValidException
+ * @throws QuesttypeModelNotFoundException
+ * @throws QuesttypeControllerNotValidException
+ * @throws QuesttypeControllerNotFoundException
+ * @param Request $request Current request
+ * @param Response $respone Current response
+ * @param Logger $log Log-system
+ */
+ protected function __construct(\nre\core\Request $request, \nre\core\Response $response, \nre\core\Logger $log=null)
+ {
+ // Store values
+ $this->request = $request;
+ $this->response = $response;
+
+
+ // Call parent constructor
+ parent::__construct($request, $response, $log);
+ }
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ $this->controller->saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers);
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ return $this->controller->matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers);
+ }
+
+
+
+
+ /**
+ * Load the Controller of this Agent.
+ *
+ * @throws DatamodelException
+ * @throws DriverNotValidException
+ * @throws DriverNotFoundException
+ * @throws ViewNotFoundException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws QuesttypeModelNotValidException
+ * @throws QuesttypeModelNotFoundException
+ * @throws QuesttypeControllerNotValidException
+ * @throws QuesttypeControllerNotFoundException
+ */
+ protected function loadController()
+ {
+ // Determine Controller name
+ $controllerName = \nre\core\ClassLoader::stripClassType(\nre\core\ClassLoader::getClassName(get_class($this)));
+
+ // Determine ToplevelAgent
+ $toplevelAgentName = $this->response->getParam(0);
+ if(is_null($toplevelAgentName)) {
+ $toplevelAgentName = $this->request->getParam(0, 'toplevel');
+ $this->response->addParam($toplevelAgentName);
+ }
+
+ // Determine Action
+ $action = $this->response->getParam(2);
+ if(is_null($action)) {
+ $action = $this->request->getParam(2, 'action');
+ $this->response->addParam($action);
+ }
+
+
+ // Load Controller
+ \hhu\z\QuesttypeController::load($controllerName);
+
+ // Construct Controller
+ $this->controller = QuesttypeController::factory($controllerName, $toplevelAgentName, $action, $this);
+ }
+
+ }
+
+?>
diff --git a/app/QuesttypeController.inc b/app/QuesttypeController.inc
new file mode 100644
index 00000000..fadfded0
--- /dev/null
+++ b/app/QuesttypeController.inc
@@ -0,0 +1,308 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z;
+
+
+ /**
+ * Abstract class for implementing a QuesttypeController.
+ *
+ * @author Oliver Hanraths
+ */
+ abstract class QuesttypeController extends \hhu\z\Controller
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('seminaries', 'questgroups', 'quests', 'characters');
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public abstract function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers);
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public abstract function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data);
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public abstract function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers);
+
+
+ /**
+ * Action: quest.
+ *
+ * Show the task of a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public abstract function quest($seminary, $questgroup, $quest, $character, $exception);
+
+
+
+ /**
+ * Action: submission.
+ *
+ * Show the submission of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public abstract function submission($seminary, $questgroup, $quest, $character);
+
+
+
+
+ /**
+ * Load a QuesttypeController.
+ *
+ * @static
+ * @throws QuesttypeControllerNotFoundException
+ * @throws QuesttypeControllerNotValidException
+ * @param string $controllerName Name of the QuesttypeController to load
+ */
+ public static function load($controllerName)
+ {
+ // Determine full classname
+ $className = self::getClassName($controllerName);
+
+ try {
+ // Load class
+ static::loadClass($controllerName, $className);
+
+ // Validate class
+ static::checkClass($className, get_class());
+ }
+ catch(\nre\exceptions\ClassNotValidException $e) {
+ throw new \hhu\z\exceptions\QuesttypeControllerNotValidException($e->getClassName());
+ }
+ catch(\nre\exceptions\ClassNotFoundException $e) {
+ throw new \hhu\z\exceptions\QuesttypeControllerNotFoundException($e->getClassName());
+ }
+ }
+
+
+ /**
+ * Instantiate a QuesttypeController (Factory Pattern).
+ *
+ * @static
+ * @throws DatamodelException
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws QuesttypeModelNotValidException
+ * @throws QuesttypeModelNotFoundException
+ * @throws ViewNotFoundException
+ * @param string $controllerName Name of the QuesttypeController to instantiate
+ * @param string $layoutName Name of the current Layout
+ * @param string $action Current Action
+ */
+ public static function factory($controllerName, $layoutName, $action, $agent)
+ {
+ // Determine full classname
+ $className = self::getClassName($controllerName);
+
+ // Construct and return Controller
+ return new $className($layoutName, $action, $agent);
+ }
+
+
+ /**
+ * Determine the Controller-classname for the given Questtype-name.
+ *
+ * @static
+ * @param string $questtypeName Questtype-name to get Controller-classname of
+ * @return string Classname for the Questtype-name
+ */
+ private static function getClassName($questtypeName)
+ {
+ $className = \nre\core\ClassLoader::concatClassNames($questtypeName, \nre\core\ClassLoader::stripClassType(\nre\core\ClassLoader::stripNamespace(get_class())), 'controller');
+
+
+ return \nre\configs\AppConfig::$app['namespace']."questtypes\\$className";
+ }
+
+
+ /**
+ * Load the class of a QuesttypeController
+ *
+ * @static
+ * @throws ClassNotFoundException
+ * @param string $questtypeName Name of the QuesttypeController to load
+ * @param string $fullClassName Name of the class to load
+ */
+ private static function loadClass($questtypeName, $fullClassName)
+ {
+ // Determine folder to look in
+ $className = explode('\\', $fullClassName);
+ $className = array_pop($className);
+
+ // Determine filename
+ $fileName = ROOT.DS.\nre\configs\AppConfig::$dirs['questtypes'].DS.strtolower($questtypeName).DS.$className.\nre\configs\CoreConfig::getFileExt('includes');
+
+ // Check file
+ if(!file_exists($fileName))
+ {
+ throw new \nre\exceptions\ClassNotFoundException(
+ $fullClassName
+ );
+ }
+
+ // Include file
+ include_once($fileName);
+ }
+
+
+ /**
+ * Check inheritance of the QuesttypeController-class.
+ *
+ * @static
+ * @throws ClassNotValidException
+ * @param string $className Name of the class to check
+ * @param string $parentClassName Name of the parent class
+ */
+ public static function checkClass($className, $parentClassName)
+ {
+ // Check if class is subclass of parent class
+ if(!is_subclass_of($className, $parentClassName)) {
+ throw new \nre\exceptions\ClassNotValidException(
+ $className
+ );
+ }
+ }
+
+
+
+
+ /**
+ * Construct a new application Controller.
+ *
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws QuesttypeModelNotValidException
+ * @throws QuesttypeModelNotFoundException
+ * @throws ViewNotFoundException
+ * @param string $layoutName Name of the current Layout
+ * @param string $action Current Action
+ * @param Agent $agent Corresponding Agent
+ */
+ public function __construct($layoutName, $action, $agent)
+ {
+ parent::__construct($layoutName, $action, $agent);
+ }
+
+
+
+
+ /**
+ * Load the Models of this Controller.
+ *
+ * @throws DatamodelException
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws QuesttypeModelNotValidException
+ * @throws QuesttypeModelNotFoundException
+ */
+ protected function loadModels()
+ {
+ // Load default models
+ parent::loadModels();
+
+ // Load QuesttypeModel
+ $this->loadModel();
+ }
+
+
+ /**
+ * Load the Model of the Questtype.
+ *
+ * @throws QuesttypeModelNotValidException
+ * @throws QuesttypeModelNotFoundException
+ */
+ private function loadModel()
+ {
+ // Determine Model
+ $model = \nre\core\ClassLoader::stripClassType(\nre\core\ClassLoader::stripClassType(\nre\core\ClassLoader::stripNamespace(get_class($this))));
+
+ // Load class
+ QuesttypeModel::load($model);
+
+ // Construct Model
+ $modelName = ucfirst(strtolower($model));
+ $this->$modelName = QuesttypeModel::factory($model);
+ }
+
+
+ /**
+ * Load the View of this QuesttypeController.
+ *
+ * @throws ViewNotFoundException
+ * @param string $layoutName Name of the current Layout
+ * @param string $action Current Action
+ */
+ protected function loadView($layoutName, $action)
+ {
+ // Check Layout name
+ if(is_null($layoutName)) {
+ return;
+ }
+
+ // Determine controller name
+ $controllerName = \nre\core\ClassLoader::stripClassType(\nre\core\ClassLoader::getClassName(get_class($this)));
+
+
+ // Load view
+ $this->view = QuesttypeView::loadAndFactory($layoutName, $controllerName, $action);
+ }
+
+ }
+
+?>
diff --git a/app/QuesttypeModel.inc b/app/QuesttypeModel.inc
new file mode 100644
index 00000000..d30699fb
--- /dev/null
+++ b/app/QuesttypeModel.inc
@@ -0,0 +1,154 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z;
+
+
+ /**
+ * Abstract class for implementing a QuesttypeModel.
+ *
+ * @author Oliver Hanraths
+ */
+ abstract class QuesttypeModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Load a Model.
+ *
+ * @static
+ * @throws QuesttypeModelNotFoundException
+ * @throws QuesttypeModelNotValidException
+ * @param string $modelName Name of the QuesttypeModel to load
+ */
+ public static function load($modelName)
+ {
+ // Determine full classname
+ $className = self::getClassName($modelName);
+
+ try {
+ // Load class
+ static::loadClass($modelName, $className);
+
+ // Validate class
+ static::checkClass($className, get_class());
+ }
+ catch(\nre\exceptions\ClassNotValidException $e) {
+ throw new \hhu\z\exceptions\QuesttypeModelNotValidException($e->getClassName());
+ }
+ catch(\nre\exceptions\ClassNotFoundException $e) {
+ throw new \hhu\z\exceptions\QuesttypeModelNotFoundException($e->getClassName());
+ }
+ }
+
+
+ /**
+ * Instantiate a QuesttypeModel (Factory Pattern).
+ *
+ * @static
+ * @param string $questtypeName Name of the QuesttypeModel to instantiate
+ */
+ public static function factory($questtypeName)
+ {
+ // Determine full classname
+ $className = self::getClassName($questtypeName);
+
+ // Construct and return Model
+ return new $className();
+ }
+
+
+ /**
+ * Determine the Model-classname for the given Questtype-name.
+ *
+ * @static
+ * @param string $questtypeName Questtype-name to get Model-classname of
+ * @return string Classname for the Questtype-name
+ */
+ private static function getClassName($questtypeName)
+ {
+ $className = \nre\core\ClassLoader::concatClassNames($questtypeName, \nre\core\ClassLoader::stripClassType(\nre\core\ClassLoader::stripNamespace(get_class())), 'model');
+
+
+ return \nre\configs\AppConfig::$app['namespace']."questtypes\\$className";
+ }
+
+
+ /**
+ * Load the class of a QuesttypeModel.
+ *
+ * @static
+ * @throws ClassNotFoundException
+ * @param string $questtypeName Name of the QuesttypeModel to load
+ * @param string $fullClassName Name of the class to load
+ */
+ private static function loadClass($questtypeName, $fullClassName)
+ {
+ // Determine folder to look in
+ $className = explode('\\', $fullClassName);
+ $className = array_pop($className);
+
+ // Determine filename
+ $fileName = ROOT.DS.\nre\configs\AppConfig::$dirs['questtypes'].DS.strtolower($questtypeName).DS.$className.\nre\configs\CoreConfig::getFileExt('includes');
+
+ // Check file
+ if(!file_exists($fileName))
+ {
+ throw new \nre\exceptions\ClassNotFoundException(
+ $fullClassName
+ );
+ }
+
+ // Include file
+ include_once($fileName);
+ }
+
+
+ /**
+ * Check inheritance of the QuesttypeModel-class.
+ *
+ * @static
+ * @throws ClassNotValidException
+ * @param string $className Name of the class to check
+ * @param string $parentClassName Name of the parent class
+ */
+ public static function checkClass($className, $parentClassName)
+ {
+ // Check if class is subclass of parent class
+ if(!is_subclass_of($className, $parentClassName)) {
+ throw new \nre\exceptions\ClassNotValidException(
+ $className
+ );
+ }
+ }
+
+
+
+
+ /**
+ * Construct a new QuesttypeModel.
+ *
+ * @throws DatamodelException
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @throws QuesttypeModelNotValidException
+ * @throws QuesttypeModelNotFoundException
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ }
+
+?>
diff --git a/app/QuesttypeView.inc b/app/QuesttypeView.inc
new file mode 100644
index 00000000..8e77ee00
--- /dev/null
+++ b/app/QuesttypeView.inc
@@ -0,0 +1,76 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z;
+
+
+ /**
+ * Abstract class for implementing a QuesttypeView.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuesttypeView extends \nre\core\View
+ {
+
+
+
+
+ /**
+ * Load and instantiate the QuesttypeView of a QuesttypeAgent.
+ *
+ * @throws ViewNotFoundException
+ * @param string $layoutName Name of Layout in use
+ * @param string $agentName Name of the Agent
+ * @param string $action Current Action
+ * @param bool $isToplevel Agent is a ToplevelAgent
+ */
+ public static function loadAndFactory($layoutName, $agentName=null, $action=null, $isToplevel=false)
+ {
+ return new QuesttypeView($layoutName, $agentName, $action, $isToplevel);
+ }
+
+
+
+
+ /**
+ * Construct a new QuesttypeView.
+ *
+ * @throws ViewNotFoundException
+ * @param string $layoutName Name of Layout in use
+ * @param string $agentName Name of the Agent
+ * @param string $action Current Action
+ * @param bool $isToplevel Agent is a ToplevelAgent
+ */
+ protected function __construct($layoutName, $agentName=null, $action=null, $isToplevel=false)
+ {
+ // Create template filename
+ // LayoutName
+ $fileName = ROOT.DS.\nre\configs\AppConfig::$dirs['questtypes'].DS.strtolower($agentName).DS.strtolower($layoutName).DS;
+
+ // Action
+ $fileName .= strtolower($action);
+
+ // File extension
+ $fileName .= \nre\configs\CoreConfig::getFileExt('views');
+
+
+ // Check template file
+ if(!file_exists($fileName)) {
+ throw new \nre\exceptions\ViewNotFoundException($fileName);
+ }
+
+ // Save filename
+ $this->templateFilename = $fileName;
+ }
+
+ }
+
+?>
diff --git a/app/TextFormatter.inc b/app/TextFormatter.inc
new file mode 100644
index 00000000..e11a42ed
--- /dev/null
+++ b/app/TextFormatter.inc
@@ -0,0 +1,141 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z;
+
+
+ /**
+ * Class to format text with different syntax tags.
+ *
+ * @author Oliver Hanraths
+ */
+ class TextFormatter
+ {
+ /**
+ * Linker to create links.
+ *
+ * @var Linker
+ */
+ private $linker;
+ /**
+ * Media-Model to retrieve media data
+ *
+ * @static
+ * @var model
+ */
+ private static $Media = null;
+
+
+
+
+ /**
+ * Create a new text formatter.
+ *
+ * @param Linker $linker Linker to create links with
+ */
+ public function __construct(\nre\core\Linker $linker)
+ {
+ $this->linker = $linker;
+ }
+
+
+
+
+ /**
+ * Format a string.
+ *
+ * @param string $string String to format
+ * @return string Formatted string
+ */
+ public function t($string)
+ {
+ // Remove chars
+ $string = htmlspecialchars($string, ENT_NOQUOTES);
+
+ // Create tables
+ $string = str_replace('[table]', '
', $string);
+ $string = str_replace('[/table]', '
', $string);
+ $string = str_replace('[tr]', '
', $string);
+ $string = str_replace('[/tr]', ' ', $string);
+ $string = str_replace('[th]', '', $string);
+ $string = str_replace('[/th]', ' ', $string);
+ $string = str_replace('[td]', '', $string);
+ $string = str_replace('[/td]', ' ', $string);
+
+ // Create links
+ $string = preg_replace('!(^|\s)"([^"]+)":(https?://[^\s]+)(\s|$)!i', '$1$2 $4', $string);
+ $string = preg_replace('!(^|\s)(https?://[^\s]+)(\s|$)!i', '$1$2 $4', $string);
+
+ // Handle Seminarymedia
+ $seminarymedia = array();
+ preg_match_all('/\[seminarymedia:(\d+)\]/iu', $string, $matches); //, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
+ $seminarymediaIds = array_unique($matches[1]);
+ foreach($seminarymediaIds as &$seminarymediaId)
+ {
+ $replacement = null;
+ if(!is_null(\hhu\z\controllers\SeminaryController::$seminary) && $this->loadMediaModel())
+ {
+ try {
+ $medium = self::$Media->getSeminaryMediaById($seminarymediaId);
+ $replacement = sprintf(
+ ' ',
+ $this->linker->link(array('media','seminary', \hhu\z\controllers\SeminaryController::$seminary['url'],$medium['url'])),
+ $medium['description']
+ );
+ }
+ catch(\nre\exceptions\IdNotFoundException $e) {
+ }
+ }
+
+ $seminarymedia[$seminarymediaId] = $replacement;
+ }
+ foreach($seminarymedia as $seminarymediaId => $replacement) {
+ $string = str_replace("[seminarymedia:$seminarymediaId]", $replacement, $string);
+ }
+
+
+ // Return processed string
+ return nl2br($string);
+ }
+
+
+
+
+ /**
+ * Load the Media-Model if it is not loaded
+ *
+ * @return boolean Whether the Media-Model has been loaded or not
+ */
+ private function loadMediaModel()
+ {
+ // Do not load Model if it has already been loaded
+ if(!is_null(self::$Media)) {
+ return true;
+ }
+
+ try {
+ // Load class
+ Model::load('media');
+
+ // Construct Model
+ self::$Media = Model::factory('media');
+ }
+ catch(\Exception $e) {
+ }
+
+
+ // Return whether Media-Model has been loaded or not
+ return !is_null(self::$Media);
+ }
+
+ }
+
+?>
diff --git a/app/ToplevelAgent.inc b/app/ToplevelAgent.inc
new file mode 100644
index 00000000..97aea57b
--- /dev/null
+++ b/app/ToplevelAgent.inc
@@ -0,0 +1,36 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z;
+
+
+ /**
+ * Abstract class for implementing an application Controller.
+ *
+ * @author Oliver Hanraths
+ */
+ abstract class ToplevelAgent extends \nre\agents\ToplevelAgent
+ {
+
+
+
+
+ protected function __construct(\nre\core\Request $request, \nre\core\Response $response, \nre\core\Logger $log=null)
+ {
+ parent::__construct($request, $response, $log);
+
+
+ // Set timezone
+ date_default_timezone_set(\nre\configs\AppConfig::$app['timeZone']);
+ }
+ }
+
+?>
diff --git a/app/Utils.inc b/app/Utils.inc
new file mode 100644
index 00000000..6f0475df
--- /dev/null
+++ b/app/Utils.inc
@@ -0,0 +1,112 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z;
+
+
+ /**
+ * Class for implementing utility methods.
+ *
+ * @author Oliver Hanraths
+ */
+ class Utils
+ {
+
+
+ /**
+ * Mask HTML-chars for save output.
+ *
+ * @static
+ * @param string $string String to be masked
+ * @return string Masked string
+ */
+ public static function t($string)
+ {
+ return nl2br(htmlspecialchars($string));
+ }
+
+
+ /**
+ * ‚htmlspecialchars‘ with support for UTF-8.
+ *
+ * @static
+ * @param string $string String to be masked
+ * @return string Masked string
+ */
+ public static function htmlspecialchars_utf8($string)
+ {
+ return htmlspecialchars($string, ENT_COMPAT, 'UTF-8');
+ }
+
+
+ /**
+ * Cut a string to the given length but only word boundaries.
+ *
+ * @static
+ * @param string $string String to cut
+ * @param int $length Length to cut string
+ * @param int $scope Maximum length to cut string regardless word boundaries
+ * @return string Cutted string
+ */
+ public static function shortenString($string, $length, $scope)
+ {
+ // Determine length
+ $length = min($length, strlen($string));
+
+ // Look for word boundary
+ if(($pos = strpos($string, ' ', $length)) !== false)
+ {
+ // Check if boundary is outside of scope
+ if($pos > $length + $scope) {
+ $pos = strrpos(substr($string, 0, $pos), ' ');
+ }
+ }
+ else {
+ $pos = strlen($string);
+ }
+
+
+ // Cut string and return it
+ return substr($string, 0, $pos);
+ }
+
+
+ /**
+ * Send an e‑mail.
+ *
+ * @param string $from Sender of mail
+ * @param mixed $to One (string) or many (array) receivers
+ * @param string $subject Subject of mail
+ * @param string $message Message of mail
+ * @param boolean $html Whether mail should be formatted as HTML or not
+ * @return Whether mail has been send or not
+ */
+ public static function sendMail($from, $to, $subject, $message, $html=false)
+ {
+ // Set receivers
+ $to = is_array($to) ? implode(',', $to) : $to;
+
+ // Set header
+ $headers = array();
+ $headers[] = 'Content-type: text/'.($html ? 'html' : 'plain').'; charset=UTF-8';
+ if(!is_null($from)) {
+ $headers[] = "From: $from";
+ }
+ $header = implode("\r\n", $headers)."\r\n";
+
+
+ // Send mail
+ return mail($to, $subject, $message, $header);
+ }
+
+ }
+
+?>
diff --git a/app/controllers/IntermediateController.inc b/app/controllers/IntermediateController.inc
new file mode 100644
index 00000000..19647d7e
--- /dev/null
+++ b/app/controllers/IntermediateController.inc
@@ -0,0 +1,139 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Abstract class for implementing a Controller of an IntermediateAgent.
+ *
+ * @author Oliver Hanraths
+ */
+ abstract class IntermediateController extends \hhu\z\Controller
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('users', 'userroles', 'seminaries', 'characters');
+ /**
+ * Current user
+ *
+ * @var array
+ */
+ public static $user = null;
+
+
+
+
+ /**
+ * Construct a new IntermediateController.
+ *
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws ViewNotFoundException
+ * @param string $layoutName Name of the current Layout
+ * @param string $action Current Action
+ * @param Agent $agent Corresponding Agent
+ */
+ public function __construct($layoutName, $action, $agent)
+ {
+ parent::__construct($layoutName, $action, $agent);
+ }
+
+
+
+ /**
+ * Prefilter that is executed before running the Controller.
+ *
+ * @param Request $request Current request
+ * @param Response $response Current response
+ */
+ public function preFilter(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ parent::preFilter($request, $response);
+
+ // Get userdata
+ try {
+ self::$user = $this->Users->getUserById($this->Auth->getUserId());
+ self::$user['roles'] = array_map(function($r) { return $r['name']; }, $this->Userroles->getUserrolesForUserById(self::$user['id']));
+ }
+ catch(\nre\exceptions\IdNotFoundException $e) {
+ }
+
+ // Check permissions
+ $this->checkPermission($request, $response);
+
+ // Set userdata
+ $this->set('loggedUser', self::$user);
+ }
+
+
+ /**
+ * Postfilter that is executed after running the Controller.
+ *
+ * @param Request $request Current request
+ * @param Response $response Current response
+ */
+ public function postFilter(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ parent::postFilter($request, $response);
+ }
+
+
+
+
+ /**
+ * Check user permissions.
+ *
+ * @throws AccessDeniedException
+ */
+ private function checkPermission(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ // Determine user
+ $userRoles = array('guest');
+ if(!is_null(self::$user)) {
+ $userRoles = self::$user['roles'];
+ }
+
+
+ // Do not check error pages
+ if($response->getParam(0, 'toplevel') == \nre\core\Config::getDefault('toplevel-error')) {
+ return;
+ }
+ if($response->getParam(1, 'intermediate') == \nre\core\Config::getDefault('intermediate-error')) {
+ return;
+ }
+
+ // Determine permissions of Intermediate Controller for current action
+ $controller = $this->agent->controller;
+ $action = $this->request->getParam(2, 'action');
+ if(!property_exists($controller, 'permissions')) {
+ return; // Allow if nothing is specified
+ }
+ if(!array_key_exists($action, $controller->permissions)) {
+ return; // Allow if Action is not specified
+ }
+ $permissions = $controller->permissions[$action];
+
+
+ // Check permissions
+ if(count(array_intersect($userRoles, $permissions)) == 0) {
+ throw new \nre\exceptions\AccessDeniedException();
+ }
+ }
+
+ }
+
+?>
diff --git a/app/controllers/SeminaryController.inc b/app/controllers/SeminaryController.inc
new file mode 100644
index 00000000..b20f4d3c
--- /dev/null
+++ b/app/controllers/SeminaryController.inc
@@ -0,0 +1,290 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Abstract class for implementing a Controller for a Seminary and its
+ * concepts.
+ *
+ * @author Oliver Hanraths
+ */
+ abstract class SeminaryController extends \hhu\z\controllers\IntermediateController
+ {
+ /**
+ * Required components
+ *
+ * @var array
+ */
+ public $components = array('achievement', 'auth');
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('seminaries', 'characters', 'characterroles', 'achievements');
+ /**
+ * Current Seminary
+ *
+ * var array
+ */
+ public static $seminary = null;
+ /**
+ * Character of current user and Seminary
+ *
+ * @var array
+ */
+ public static $character = null;
+
+
+
+
+ /**
+ * Construct a new Seminary Controller.
+ *
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws ViewNotFoundException
+ * @param string $layoutName Name of the current Layout
+ * @param string $action Current Action
+ * @param Agent $agent Corresponding Agent
+ */
+ public function __construct($layoutName, $action, $agent)
+ {
+ parent::__construct($layoutName, $action, $agent);
+ }
+
+
+
+ /**
+ * Prefilter that is executed before running the Controller.
+ *
+ * @param Request $request Current request
+ * @param Response $response Current response
+ */
+ public function preFilter(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ parent::preFilter($request, $response);
+
+ // Get Seminary and Character data
+ try {
+ self::$seminary = $this->Seminaries->getSeminaryByUrl($this->request->getParam(3));
+ if(!is_null(self::$user))
+ {
+ self::$character = $this->Characters->getCharacterForUserAndSeminary(self::$user['id'], self::$seminary['id']);
+ self::$character['characterroles'] = array_map(function($r) { return $r['name']; }, $this->Characterroles->getCharacterrolesForCharacterById(self::$character['id']));
+ }
+ }
+ catch(\nre\exceptions\IdNotFoundException $e) {
+ }
+
+ // Check permissions
+ $this->checkPermission($request, $response);
+
+ // Check achievements
+ $this->checkAchievements($request, $response);
+
+ // Set Seminary and Character data
+ $this->set('loggedSeminary', self::$seminary);
+ $this->set('loggedCharacter', self::$character);
+ }
+
+
+ /**
+ * Postfilter that is executed after running the Controller.
+ *
+ * @param Request $request Current request
+ * @param Response $response Current response
+ */
+ public function postFilter(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ parent::postFilter($request, $response);
+ }
+
+
+
+
+ /**
+ * Check user permissions.
+ *
+ * @throws AccessDeniedException
+ */
+ private function checkPermission(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ // Do not check index page
+ if(is_null($request->getParam(3))) {
+ return;
+ }
+
+
+ // Determine permissions for current action
+ $action = $this->request->getParam(2, 'action');
+ if(!property_exists($this, 'seminaryPermissions')) {
+ return; // Allow if nothing is specified
+ }
+ if(!array_key_exists($action, $this->seminaryPermissions)) {
+ return; // Allow if Action is not specified
+ }
+ $permissions = $this->seminaryPermissions[$action];
+
+
+ // Check permissions
+ if(is_null(self::$character) || !array_key_exists('characterroles', self::$character) || count(array_intersect(self::$character['characterroles'], $permissions)) == 0) {
+ throw new \nre\exceptions\AccessDeniedException();
+ }
+ }
+
+
+ /**
+ * Check for newly achieved Achievements.
+ */
+ private function checkAchievements(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ // Check if Character is present
+ if(is_null(self::$character)) {
+ return;
+ }
+
+ // Get unachieved Achievments
+ $achievements = $this->Achievements->getUnachhievedAchievementsForCharacter(self::$seminary['id'], self::$character['id']);
+ if(in_array('user', self::$character['characterroles'])) {
+ $achievements = array_merge($achievements, $this->Achievements->getUnachievedOnlyOnceAchievementsForSeminary(self::$seminary['id']));
+ }
+
+ // Check conditions
+ foreach($achievements as &$achievement)
+ {
+ // Check deadline
+ if($achievement['deadline'] < date('Y-m-d H:i:s')) {
+ continue;
+ }
+
+ // Get conditions
+ $conditions = array();
+ $progress = 0;
+ switch($achievement['condition'])
+ {
+ // Date conditions
+ case 'date':
+ $conditionsDate = $this->Achievements->getAchievementConditionsDate($achievement['id']);
+ foreach($conditionsDate as &$condition)
+ {
+ $conditions[] = array(
+ 'func' => 'checkAchievementConditionDate',
+ 'params' => array(
+ $condition['select']
+ )
+ );
+ }
+ break;
+ // Character conditions
+ case 'character':
+ $conditionsCharacter = $this->Achievements->getAchievementConditionsCharacter($achievement['id']);
+ foreach($conditionsCharacter as &$condition)
+ {
+ $conditions[] = array(
+ 'func' => 'checkAchievementConditionCharacter',
+ 'params' => array(
+ $condition['field'],
+ $condition['value'],
+ self::$character['id']
+ )
+ );
+ }
+ break;
+ // Quest conditions
+ case 'quest':
+ $conditionsQuest = $this->Achievements->getAchievementConditionsQuest($achievement['id']);
+ foreach($conditionsQuest as &$condition)
+ {
+ $conditions[] = array(
+ 'func' => 'checkAchievementConditionQuest',
+ 'params' => array(
+ $condition['field'],
+ $condition['count'],
+ $condition['value'],
+ $condition['status'],
+ $condition['groupby'],
+ $condition['quest_id'],
+ self::$character['id']
+ )
+ );
+ }
+ break;
+ // Achievement conditions
+ case 'achievement':
+ $conditionsAchievement = $this->Achievements->getAchievementConditionsAchievement($achievement['id']);
+ foreach($conditionsAchievement as &$condition)
+ {
+ $conditions[] = array(
+ 'func' => 'checkAchievementConditionAchievement',
+ 'params' => array(
+ $condition['field'],
+ $condition['count'],
+ $condition['value'],
+ $condition['groupby'],
+ $condition['meta_achievement_id'],
+ self::$character['id']
+ )
+ );
+ }
+ break;
+ }
+
+ // Do not achieve Achievements without conditions
+ if(empty($conditions)) {
+ continue;
+ }
+
+ // Check conditions
+ $achieved = ($achievement['all_conditions'] == 1);
+ foreach($conditions as &$condition)
+ {
+ // Calculate result of condition
+ $result = call_user_func_array(
+ array(
+ $this->Achievements,
+ $condition['func']
+ ),
+ $condition['params']
+ );
+
+ // The overall result and abort if possible
+ if($achievement['all_conditions'])
+ {
+ if(!$result) {
+ $achieved = false;
+ break;
+ }
+ }
+ else
+ {
+ if($result) {
+ $achieved = true;
+ break;
+ }
+ }
+
+ }
+
+ // Set status
+ if($achieved) {
+ $this->Achievements->setAchievementAchieved($achievement['id'], self::$character['id']);
+ }
+ }
+ }
+
+ }
+
+?>
diff --git a/app/exceptions/FileUploadException.inc b/app/exceptions/FileUploadException.inc
new file mode 100644
index 00000000..3fb62e6f
--- /dev/null
+++ b/app/exceptions/FileUploadException.inc
@@ -0,0 +1,75 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\exceptions;
+
+
+ /**
+ * Exception: File upload went wrong
+ *
+ * @author Oliver Hanraths
+ */
+ class FileUploadException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 203;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'File upload went wrong';
+
+ /**
+ * Nested message
+ *
+ * @var string
+ */
+ private $nestedMessage;
+
+
+
+
+ /**
+ * Construct a new exception.
+ */
+ function __construct($nestedMessage=null, $message=self::MESSAGE, $code=self::CODE)
+ {
+ parent::__construct(
+ $message,
+ $code,
+ $nestedMessage
+ );
+
+ // Store values
+ $this->nestedMessage = $nestedMessage;
+ }
+
+
+
+
+ /**
+ * Get nested message.
+ *
+ * @return Nested message
+ */
+ public function getNestedMessage()
+ {
+ return $this->nestedMessage;
+ }
+
+ }
+
+?>
diff --git a/app/exceptions/MaxFilesizeException.inc b/app/exceptions/MaxFilesizeException.inc
new file mode 100644
index 00000000..f16f335e
--- /dev/null
+++ b/app/exceptions/MaxFilesizeException.inc
@@ -0,0 +1,51 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\exceptions;
+
+
+ /**
+ * Exception: File exceeds size maximum.
+ *
+ * @author Oliver Hanraths
+ */
+ class MaxFilesizeException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 202;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'File exceeds size maximum';
+
+
+
+
+ /**
+ * Construct a new exception.
+ */
+ function __construct($message=self::MESSAGE, $code=self::CODE)
+ {
+ parent::__construct(
+ $message,
+ $code
+ );
+ }
+
+ }
+
+?>
diff --git a/app/exceptions/QuesttypeAgentNotFoundException.inc b/app/exceptions/QuesttypeAgentNotFoundException.inc
new file mode 100644
index 00000000..5f6f4ea9
--- /dev/null
+++ b/app/exceptions/QuesttypeAgentNotFoundException.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\exceptions;
+
+
+ /**
+ * Exception: QuesttypeAgent not found.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuesttypeAgentNotFoundException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 101;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'QuesttypeAgent not found';
+
+ /**
+ * Name of the class that was not found
+ *
+ * @var string
+ */
+ private $questtypeName;
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $questtypeName Name of the QuesttypeAgent that was not found
+ */
+ function __construct($questtypeName, $message=self::MESSAGE, $code=self::CODE)
+ {
+ parent::__construct(
+ $message,
+ $code,
+ $questtypeName
+ );
+
+ // Store values
+ $this->questtypeName = $questtypeName;
+ }
+
+
+
+
+ /**
+ * Get the name of the QuesttypeAgent that was not found.
+ *
+ * @return string Name of the QuesttypeAgent that was not found
+ */
+ public function getClassName()
+ {
+ return $this->questtypeName;
+ }
+
+ }
+
+?>
diff --git a/app/exceptions/QuesttypeAgentNotValidException.inc b/app/exceptions/QuesttypeAgentNotValidException.inc
new file mode 100644
index 00000000..8fa7237f
--- /dev/null
+++ b/app/exceptions/QuesttypeAgentNotValidException.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\exceptions;
+
+
+ /**
+ * Exception: QuesttypeAgent not valid.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuesttypeAgentNotValidException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 102;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'QuesttypeAgent not valid';
+
+ /**
+ * Name of the invalid class
+ *
+ * @var string
+ */
+ private $questtypeName;
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $questtypeName Name of the invalid QuesttypeAgent
+ */
+ function __construct($questtypeName, $message=self::MESSAGE, $code=self::CODE)
+ {
+ parent::__construct(
+ $message,
+ $code,
+ $questtypeName
+ );
+
+ // Store value
+ $this->questtypeName = $questtypeName;
+ }
+
+
+
+
+ /**
+ * Get the name of the invalid QuesttypeAgent.
+ *
+ * @return string Name of the invalid QuesttypeAgent
+ */
+ public function getClassName()
+ {
+ return $this->questtypeName;
+ }
+
+ }
+
+?>
diff --git a/app/exceptions/QuesttypeControllerNotFoundException.inc b/app/exceptions/QuesttypeControllerNotFoundException.inc
new file mode 100644
index 00000000..6d3a4a36
--- /dev/null
+++ b/app/exceptions/QuesttypeControllerNotFoundException.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\exceptions;
+
+
+ /**
+ * Exception: QuesttypeController not found.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuesttypeControllerNotFoundException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 103;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'QuesttypeController not found';
+
+ /**
+ * Name of the class that was not found
+ *
+ * @var string
+ */
+ private $questtypeName;
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $questtypeName Name of the QuesttypeController that was not found
+ */
+ function __construct($questtypeName, $message=self::MESSAGE, $code=self::CODE)
+ {
+ parent::__construct(
+ $message,
+ $code,
+ $questtypeName
+ );
+
+ // Store values
+ $this->questtypeName = $questtypeName;
+ }
+
+
+
+
+ /**
+ * Get the name of the QuesttypeController that was not found.
+ *
+ * @return string Name of the QuesttypeController that was not found
+ */
+ public function getClassName()
+ {
+ return $this->questtypeName;
+ }
+
+ }
+
+?>
diff --git a/app/exceptions/QuesttypeControllerNotValidException.inc b/app/exceptions/QuesttypeControllerNotValidException.inc
new file mode 100644
index 00000000..8c6735b8
--- /dev/null
+++ b/app/exceptions/QuesttypeControllerNotValidException.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\exceptions;
+
+
+ /**
+ * Exception: QuesttypeController not valid.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuesttypeControllerNotValidException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 104;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'QuesttypeController not valid';
+
+ /**
+ * Name of the invalid class
+ *
+ * @var string
+ */
+ private $questtypeName;
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $questtypeName Name of the invalid QuesttypeController
+ */
+ function __construct($questtypeName, $message=self::MESSAGE, $code=self::CODE)
+ {
+ parent::__construct(
+ $message,
+ $code,
+ $questtypeName
+ );
+
+ // Store value
+ $this->questtypeName = $questtypeName;
+ }
+
+
+
+
+ /**
+ * Get the name of the invalid QuesttypeController.
+ *
+ * @return string Name of the invalid QuesttypeController
+ */
+ public function getClassName()
+ {
+ return $this->questtypeName;
+ }
+
+ }
+
+?>
diff --git a/app/exceptions/QuesttypeModelNotFoundException.inc b/app/exceptions/QuesttypeModelNotFoundException.inc
new file mode 100644
index 00000000..a2aa01c8
--- /dev/null
+++ b/app/exceptions/QuesttypeModelNotFoundException.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\exceptions;
+
+
+ /**
+ * Exception: QuesttypeModel not found.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuesttypeModelNotFoundException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 105;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'QuesttypeModel not found';
+
+ /**
+ * Name of the class that was not found
+ *
+ * @var string
+ */
+ private $questtypeName;
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $questtypeName Name of the QuesttypeModel that was not found
+ */
+ function __construct($questtypeName, $message=self::MESSAGE, $code=self::CODE)
+ {
+ parent::__construct(
+ $message,
+ $code,
+ $questtypeName
+ );
+
+ // Store values
+ $this->questtypeName = $questtypeName;
+ }
+
+
+
+
+ /**
+ * Get the name of the QuesttypeModel that was not found.
+ *
+ * @return string Name of the QuesttypeModel that was not found
+ */
+ public function getClassName()
+ {
+ return $this->questtypeName;
+ }
+
+ }
+
+?>
diff --git a/app/exceptions/QuesttypeModelNotValidException.inc b/app/exceptions/QuesttypeModelNotValidException.inc
new file mode 100644
index 00000000..4a78beb6
--- /dev/null
+++ b/app/exceptions/QuesttypeModelNotValidException.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\exceptions;
+
+
+ /**
+ * Exception: QuesttypeModel not valid.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuesttypeModelNotValidException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 106;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'QuesttypeModel not valid';
+
+ /**
+ * Name of the invalid class
+ *
+ * @var string
+ */
+ private $questtypeName;
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $questtypeName Name of the invalid QuesttypeModel
+ */
+ function __construct($questtypeName, $message=self::MESSAGE, $code=self::CODE)
+ {
+ parent::__construct(
+ $message,
+ $code,
+ $questtypeName
+ );
+
+ // Store value
+ $this->questtypeName = $questtypeName;
+ }
+
+
+
+
+ /**
+ * Get the name of the invalid QuesttypeModel.
+ *
+ * @return string Name of the invalid QuesttypeModel
+ */
+ public function getClassName()
+ {
+ return $this->questtypeName;
+ }
+
+ }
+
+?>
diff --git a/app/exceptions/SubmissionNotValidException.inc b/app/exceptions/SubmissionNotValidException.inc
new file mode 100644
index 00000000..e2923bdf
--- /dev/null
+++ b/app/exceptions/SubmissionNotValidException.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\exceptions;
+
+
+ /**
+ * Exception: Character submission not valid.
+ *
+ * @author Oliver Hanraths
+ */
+ class SubmissionNotValidException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 200;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'Character submission not valid';
+
+ /**
+ * Nested exception
+ *
+ * @var Exception
+ */
+ private $nestedException;
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $nestedException Nested exception
+ */
+ function __construct($nestedException, $message=self::MESSAGE, $code=self::CODE)
+ {
+ parent::__construct(
+ $message,
+ $code,
+ $nestedException
+ );
+
+ // Store value
+ $this->nestedException = $nestedException;
+ }
+
+
+
+
+ /**
+ * Get Nested exception.
+ *
+ * @return string Nested exception
+ */
+ public function getNestedException()
+ {
+ return $this->nestedException;
+ }
+
+ }
+
+?>
diff --git a/app/exceptions/WrongFiletypeException.inc b/app/exceptions/WrongFiletypeException.inc
new file mode 100644
index 00000000..131356b8
--- /dev/null
+++ b/app/exceptions/WrongFiletypeException.inc
@@ -0,0 +1,75 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\exceptions;
+
+
+ /**
+ * Exception: File has wrong filetype.
+ *
+ * @author Oliver Hanraths
+ */
+ class WrongFiletypeException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 201;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'File has wrong type “%s”';
+
+ /**
+ * Type of file
+ *
+ * @var string
+ */
+ private $type;
+
+
+
+
+ /**
+ * Construct a new exception.
+ */
+ function __construct($type, $message=self::MESSAGE, $code=self::CODE)
+ {
+ parent::__construct(
+ $message,
+ $code,
+ $type
+ );
+
+ // Store values
+ $this->type = $type;
+ }
+
+
+
+
+ /**
+ * Get type of file.
+ *
+ * @return Type of file
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ }
+
+?>
diff --git a/app/lib/Password.inc b/app/lib/Password.inc
new file mode 100644
index 00000000..f9acf7d5
--- /dev/null
+++ b/app/lib/Password.inc
@@ -0,0 +1,316 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\lib
+ {
+
+
+ /**
+ * Class to ensure that Compatibility library below is loaded.
+ *
+ * @author Oliver Hanraths
+ */
+ class Password
+ {
+
+ /**
+ * Call this function to ensure this file is loaded.
+ */
+ public static function load()
+ {
+ }
+
+ }
+
+ }
+
+
+
+
+/**
+ * A Compatibility library with PHP 5.5's simplified password hashing API.
+ *
+ * @author Anthony Ferrara
+ * @license http://www.opensource.org/licenses/mit-license.html MIT License
+ * @copyright 2012 The Authors
+ */
+
+namespace {
+
+if (!defined('PASSWORD_DEFAULT')) {
+
+ define('PASSWORD_BCRYPT', 1);
+ define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
+
+ /**
+ * Hash the password using the specified algorithm
+ *
+ * @param string $password The password to hash
+ * @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
+ * @param array $options The options for the algorithm to use
+ *
+ * @return string|false The hashed password, or false on error.
+ */
+ function password_hash($password, $algo, array $options = array()) {
+ if (!function_exists('crypt')) {
+ trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
+ return null;
+ }
+ if (!is_string($password)) {
+ trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
+ return null;
+ }
+ if (!is_int($algo)) {
+ trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
+ return null;
+ }
+ $resultLength = 0;
+ switch ($algo) {
+ case PASSWORD_BCRYPT:
+ // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
+ $cost = 10;
+ if (isset($options['cost'])) {
+ $cost = $options['cost'];
+ if ($cost < 4 || $cost > 31) {
+ trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
+ return null;
+ }
+ }
+ // The length of salt to generate
+ $raw_salt_len = 16;
+ // The length required in the final serialization
+ $required_salt_len = 22;
+ $hash_format = sprintf("$2y$%02d$", $cost);
+ // The expected length of the final crypt() output
+ $resultLength = 60;
+ break;
+ default:
+ trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
+ return null;
+ }
+ $salt_requires_encoding = false;
+ if (isset($options['salt'])) {
+ switch (gettype($options['salt'])) {
+ case 'NULL':
+ case 'boolean':
+ case 'integer':
+ case 'double':
+ case 'string':
+ $salt = (string) $options['salt'];
+ break;
+ case 'object':
+ if (method_exists($options['salt'], '__tostring')) {
+ $salt = (string) $options['salt'];
+ break;
+ }
+ case 'array':
+ case 'resource':
+ default:
+ trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
+ return null;
+ }
+ if (PasswordCompat\binary\_strlen($salt) < $required_salt_len) {
+ trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", PasswordCompat\binary\_strlen($salt), $required_salt_len), E_USER_WARNING);
+ return null;
+ } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
+ $salt_requires_encoding = true;
+ }
+ } else {
+ $buffer = '';
+ $buffer_valid = false;
+ if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
+ $buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM);
+ if ($buffer) {
+ $buffer_valid = true;
+ }
+ }
+ if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
+ $buffer = openssl_random_pseudo_bytes($raw_salt_len);
+ if ($buffer) {
+ $buffer_valid = true;
+ }
+ }
+ if (!$buffer_valid && @is_readable('/dev/urandom')) {
+ $f = fopen('/dev/urandom', 'r');
+ $read = PasswordCompat\binary\_strlen($buffer);
+ while ($read < $raw_salt_len) {
+ $buffer .= fread($f, $raw_salt_len - $read);
+ $read = PasswordCompat\binary\_strlen($buffer);
+ }
+ fclose($f);
+ if ($read >= $raw_salt_len) {
+ $buffer_valid = true;
+ }
+ }
+ if (!$buffer_valid || PasswordCompat\binary\_strlen($buffer) < $raw_salt_len) {
+ $bl = PasswordCompat\binary\_strlen($buffer);
+ for ($i = 0; $i < $raw_salt_len; $i++) {
+ if ($i < $bl) {
+ $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
+ } else {
+ $buffer .= chr(mt_rand(0, 255));
+ }
+ }
+ }
+ $salt = $buffer;
+ $salt_requires_encoding = true;
+ }
+ if ($salt_requires_encoding) {
+ // encode string with the Base64 variant used by crypt
+ $base64_digits =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+ $bcrypt64_digits =
+ './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+ $base64_string = base64_encode($salt);
+ $salt = strtr(rtrim($base64_string, '='), $base64_digits, $bcrypt64_digits);
+ }
+ $salt = PasswordCompat\binary\_substr($salt, 0, $required_salt_len);
+
+ $hash = $hash_format . $salt;
+
+ $ret = crypt($password, $hash);
+
+ if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != $resultLength) {
+ return false;
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Get information about the password hash. Returns an array of the information
+ * that was used to generate the password hash.
+ *
+ * array(
+ * 'algo' => 1,
+ * 'algoName' => 'bcrypt',
+ * 'options' => array(
+ * 'cost' => 10,
+ * ),
+ * )
+ *
+ * @param string $hash The password hash to extract info from
+ *
+ * @return array The array of information about the hash.
+ */
+ function password_get_info($hash) {
+ $return = array(
+ 'algo' => 0,
+ 'algoName' => 'unknown',
+ 'options' => array(),
+ );
+ if (PasswordCompat\binary\_substr($hash, 0, 4) == '$2y$' && PasswordCompat\binary\_strlen($hash) == 60) {
+ $return['algo'] = PASSWORD_BCRYPT;
+ $return['algoName'] = 'bcrypt';
+ list($cost) = sscanf($hash, "$2y$%d$");
+ $return['options']['cost'] = $cost;
+ }
+ return $return;
+ }
+
+ /**
+ * Determine if the password hash needs to be rehashed according to the options provided
+ *
+ * If the answer is true, after validating the password using password_verify, rehash it.
+ *
+ * @param string $hash The hash to test
+ * @param int $algo The algorithm used for new password hashes
+ * @param array $options The options array passed to password_hash
+ *
+ * @return boolean True if the password needs to be rehashed.
+ */
+ function password_needs_rehash($hash, $algo, array $options = array()) {
+ $info = password_get_info($hash);
+ if ($info['algo'] != $algo) {
+ return true;
+ }
+ switch ($algo) {
+ case PASSWORD_BCRYPT:
+ $cost = isset($options['cost']) ? $options['cost'] : 10;
+ if ($cost != $info['options']['cost']) {
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Verify a password against a hash using a timing attack resistant approach
+ *
+ * @param string $password The password to verify
+ * @param string $hash The hash to verify against
+ *
+ * @return boolean If the password matches the hash
+ */
+ function password_verify($password, $hash) {
+ if (!function_exists('crypt')) {
+ trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
+ return false;
+ }
+ $ret = crypt($password, $hash);
+ if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != PasswordCompat\binary\_strlen($hash) || PasswordCompat\binary\_strlen($ret) <= 13) {
+ return false;
+ }
+
+ $status = 0;
+ for ($i = 0; $i < PasswordCompat\binary\_strlen($ret); $i++) {
+ $status |= (ord($ret[$i]) ^ ord($hash[$i]));
+ }
+
+ return $status === 0;
+ }
+}
+
+}
+
+namespace PasswordCompat\binary {
+ /**
+ * Count the number of bytes in a string
+ *
+ * We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension.
+ * In this case, strlen() will count the number of *characters* based on the internal encoding. A
+ * sequence of bytes might be regarded as a single multibyte character.
+ *
+ * @param string $binary_string The input string
+ *
+ * @internal
+ * @return int The number of bytes
+ */
+ function _strlen($binary_string) {
+ if (function_exists('mb_strlen')) {
+ return mb_strlen($binary_string, '8bit');
+ }
+ return strlen($binary_string);
+ }
+
+ /**
+ * Get a substring based on byte limits
+ *
+ * @see _strlen()
+ *
+ * @param string $binary_string The input string
+ * @param int $start
+ * @param int $length
+ *
+ * @internal
+ * @return string The substring
+ */
+ function _substr($binary_string, $start, $length) {
+ if (function_exists('mb_substr')) {
+ return mb_substr($binary_string, $start, $length, '8bit');
+ }
+ return substr($binary_string, $start, $length);
+ }
+
+}
diff --git a/bootstrap.inc b/bootstrap.inc
new file mode 100644
index 00000000..55647ac0
--- /dev/null
+++ b/bootstrap.inc
@@ -0,0 +1,33 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ // Include required classes
+ require_once(ROOT.DS.'configs'.DS.'CoreConfig.inc');
+ require_once(ROOT.DS.\nre\configs\CoreConfig::getClassDir('core').DS.'Autoloader.inc');
+
+
+ // Set PHP-logging
+ ini_set('error_log', ROOT.DS.\nre\configs\CoreConfig::getClassDir('logs').DS.'php'.\nre\configs\CoreConfig::getFileExt('logs'));
+
+ // Register autoloader
+ \nre\core\Autoloader::register();
+
+
+ // Initialize WebApi
+ $webApi = new \nre\apis\WebApi();
+
+ // Run WebApi
+ $webApi->run();
+
+ // Render output
+ $webApi->render();
+
+?>
diff --git a/configs/AppConfig.inc b/configs/AppConfig.inc
new file mode 100644
index 00000000..65e3672c
--- /dev/null
+++ b/configs/AppConfig.inc
@@ -0,0 +1,215 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace nre\configs;
+
+
+ /**
+ * Application configuration.
+ *
+ * This class contains static variables with configuration values for
+ * the specific application.
+ *
+ * @author Oliver Hanraths
+ */
+ final class AppConfig
+ {
+
+ /**
+ * Application values
+ *
+ * @static
+ * @var array
+ */
+ public static $app = array(
+ 'name' => 'Questlab',
+ 'genericname' => 'The Legend of Z',
+ 'namespace' => 'hhu\\z\\',
+ 'timeZone' => 'Europe/Berlin',
+ 'mailsender' => 'noreply@zyren.inf-d.de'
+ );
+
+
+ /**
+ * Default values
+ *
+ * @static
+ * @var array
+ */
+ public static $defaults = array(
+ 'toplevel' => 'html',
+ 'toplevel-error' => 'fault',
+ 'intermediate' => 'introduction',
+ 'intermediate-error' => 'error',
+ 'language' => 'de_DE.utf8',
+ 'locale' => 'de-DE'
+ );
+
+
+ /**
+ * Directories
+ *
+ * @static
+ * @var array
+ */
+ public static $dirs = array(
+ 'locale' => 'locale',
+ 'media' => 'media',
+ 'seminarymedia' => 'seminarymedia',
+ 'questtypes' => 'questtypes',
+ 'temporary' => 'tmp',
+ 'uploads' => 'uploads',
+ 'seminaryuploads' => 'seminaryuploads'
+ );
+
+
+ /**
+ * Media sizes
+ *
+ * @static
+ * @var array
+ */
+ public static $media = array(
+ 'questgroup' => array(
+ 'width' => 480,
+ 'height' => 5000
+ ),
+ 'avatar' => array(
+ 'width' => 500,
+ 'height' => 500
+ )
+ );
+
+
+ /**
+ * Miscellaneous settings
+ *
+ * @static
+ * @var array
+ */
+ public static $misc = array(
+ 'ranking_range' => 2,
+ 'achievements_range' => 3
+ );
+
+
+ /**
+ * Validation settings for user input
+ *
+ * @static
+ * @var array
+ */
+ public static $validation = array(
+ 'username' => array(
+ 'minlength' => 5,
+ 'maxlength' => 12,
+ 'regex' => '/^\w*$/'
+ ),
+ 'email' => array(
+ 'regex' => '/^\S+@[\w\d.-]{2,}\.[\w]{2,6}$/iU'
+ ),
+ 'prename' => array(
+ 'minlength' => 2,
+ 'maxlength' => 32,
+ 'regex' => '/^\S*$/'
+ ),
+ 'surname' => array(
+ 'minlength' => 2,
+ 'maxlength' => 32,
+ 'regex' => '/^\S*$/'
+ ),
+ 'password' => array(
+ 'minlength' => 5,
+ 'maxlength' => 64
+ ),
+ 'charactername' => array(
+ 'minlength' => 5,
+ 'maxlength' => 12,
+ 'regex' => '/^\w*$/'
+ )
+ );
+
+
+ /**
+ * Routes
+ *
+ * @static
+ * @var array
+ */
+ public static $routes = array(
+ array('^users/([^/]+)/(edit|delete)/?$', 'users/$2/$1', true),
+ array('^users/(?!(index|login|register|logout|create|edit|delete))/?', 'users/user/$1', true),
+ array('^seminaries/([^/]+)/(edit|delete)/?$', 'seminaries/$2/$1', true),
+ array('^seminaries/(?!(index|create|edit|delete))/?', 'seminaries/seminary/$1', true),
+ array('^questgroups/([^/]+)/(create)/?$', 'questgroups/$2/$1', true),
+ array('^questgroups/([^/]+)/([^/]+)/?$', 'questgroups/questgroup/$1/$2', true),
+ array('^quests/([^/]+)/(create|createmedia)/?$', 'quests/$2/$1' , true),
+ array('^quests/([^/]+)/([^/]+)/([^/]+)/(submissions)/?$', 'quests/$4/$1/$2/$3', true),
+ array('^quests/([^/]+)/([^/]+)/([^/]+)/(submission)/([^/]+)/?$', 'quests/$4/$1/$2/$3/$5', true),
+ array('^quests/(?!(index|create|createmedia))/?', 'quests/quest/$1', true),
+ array('^characters/([^/]+)/(register|manage)/?$', 'characters/$2/$1', true),
+ array('^characters/([^/]+)/?$', 'characters/index/$1', true),
+ array('^characters/([^/]+)/(?!(index|create|register|manage))/?', 'characters/character/$1/$2', true),
+ array('^charactergroups/([^/]+)/?$', 'charactergroups/index/$1', true),
+ array('^charactergroups/([^/]+)/([^/]+)/?$', 'charactergroups/groupsgroup/$1/$2', true),
+ array('^charactergroups/([^/]+)/([^/]+)/(managegroup)/?$', 'charactergroups/$3/$1/$2', true),
+ array('^charactergroups/([^/]+)/([^/]+)/(?!(managegroup))/?', 'charactergroups/group/$1/$2/$3', true),
+ array('^charactergroupsquests/([^/]+)/([^/]+)/([^/]+)/?$', 'charactergroupsquests/quest/$1/$2/$3', true),
+ array('^achievements/([^/]+)/?$', 'achievements/index/$1', true),
+ array('^library/([^/]+)/?$', 'library/index/$1', true),
+ array('^library/([^/]+)/([^/]+)/?$', 'library/topic/$1/$2', true),
+ array('^media/(.*)$', 'media/$1?layout=binary', false),
+ array('^uploads/(.*)$', 'uploads/$1?layout=binary', false)
+ );
+
+
+ /**
+ * Reverse routes
+ *
+ * @static
+ * @var array
+ */
+ public static $reverseRoutes = array(
+ array('^users/user/(.*)$', 'users/$1', true),
+ array('^users/([^/]+)/(.*)$', 'users/$2/$1', true),
+ array('^seminaries/seminary/(.*)$', 'seminaries/$1', false),
+ array('^questgroups/create/(.*)$', 'questgroups/$2/$1', true),
+ array('^questgroups/questgroup/(.*)$', 'questgroups/$1', true),
+ array('^quests/quest/(.*)$', 'quests/$1', true),
+ array('^quests/(create|createmedia)/(.*)$', 'quests/$2/$1' , true),
+ array('^quests/(submissions|submission)/(.*)$', 'quests/$2/$1', true),
+ array('^characters/(index|character)/(.*)$', 'characters/$2', true),
+ array('^characters/(register|manage)/(.*)$', 'characters/$2/$1', true),
+ array('^charactergroups/(index|group)/(.*)$', 'charactergroups/$2', true),
+ array('^charactergroups/groupsgroup/(.*)$', 'charactergroups/$1', true),
+ array('^charactergroups/(managegroup)/(.*)$', 'charactergroups/$2/$1', true),
+ array('^charactergroupsquests/quest/(.*)$', 'charactergroupsquests/$1', true),
+ array('^achievements/index/(.*)$', 'achievements/$1', true),
+ array('^library/(index|topic)/(.*)$', 'library/$2', true)
+ );
+
+
+ /**
+ * Database connection settings
+ *
+ * @static
+ * @var array
+ */
+ public static $database = array(
+ 'user' => 'z',
+ 'host' => 'localhost',
+ 'password' => 'legendofZ',
+ 'db' => 'z'
+ );
+
+ }
+
+?>
diff --git a/configs/CoreConfig.inc b/configs/CoreConfig.inc
new file mode 100644
index 00000000..45bc7e4a
--- /dev/null
+++ b/configs/CoreConfig.inc
@@ -0,0 +1,167 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\configs;
+
+
+ /**
+ * Core configuration.
+ *
+ * This class contains static variables with configuration values.
+ *
+ * @author coderkun
+ */
+ final class CoreConfig
+ {
+
+ /**
+ * Core values
+ *
+ * @static
+ * @var array
+ */
+ public static $core = array(
+ 'namespace' => 'nre\\',
+ );
+
+
+ /**
+ * Directories
+ *
+ * @static
+ * @var array
+ */
+ public static $dirs = array(
+ 'core' => 'core',
+ 'publicDir' => 'www'
+ );
+
+
+ /**
+ * File extensions
+ *
+ * @static
+ * @var array
+ */
+ public static $fileExts = array(
+ 'default' => 'inc',
+ 'views' => 'tpl',
+ 'logs' => 'log',
+ );
+
+
+ /**
+ * Default values
+ *
+ * @static
+ * @var array
+ */
+ public static $defaults = array(
+ 'action' => 'index',
+ 'errorFile' => 'error',
+ 'inlineErrorFile' => 'inlineerror'
+ );
+
+
+ /**
+ * Miscellaneous settings
+ *
+ * @static
+ * @var array
+ */
+ public static $misc = array(
+ 'fileExtDot' => '.'
+ );
+
+
+ /**
+ * Logging settings
+ *
+ * @static
+ * @var array
+ */
+ public static $log = array(
+ 'filename' => 'errors',
+ 'format' => 'Fehler %d: %s in %s, Zeile %d'
+ );
+
+
+ /**
+ * Class-specific settings
+ *
+ * @static
+ * @var array
+ */
+ public static $classes = array(
+ 'linker' => array(
+ 'url' => array(
+ 'length' => 128,
+ 'delimiter' => '-'
+ )
+ )
+ );
+
+
+
+
+ /**
+ * Determine the directory for a specific classtype.
+ *
+ * @param string $classType Classtype to get directory of
+ * @return string Directory of given classtype
+ */
+ public static function getClassDir($classType)
+ {
+ // Default directory (for core classes)
+ $classDir = self::$dirs['core'];
+
+ // Configurable directory
+ if(array_key_exists($classType, self::$dirs)) {
+ $classDir = self::$dirs[$classType];
+ }
+ else
+ {
+ // Default directory for classtype
+ if(is_dir(ROOT.DS.$classType)) {
+ $classDir = $classType;
+ }
+ }
+
+
+ // Return directory
+ return $classDir;
+ }
+
+
+ /**
+ * Determine the file extension for a specific filetype.
+ *
+ * @param string $fileType Filetype to get file extension of
+ * @return string File extension of given filetype
+ */
+ public static function getFileExt($fileType)
+ {
+ // Default file extension
+ $fileExt = self::$fileExts['default'];
+
+ // Configurable file extension
+ if(array_key_exists($fileType, self::$fileExts)) {
+ $fileExt = self::$fileExts[$fileType];
+ }
+
+
+ // Return file extension
+ return self::$misc['fileExtDot'].$fileExt;
+ }
+
+ }
+
+?>
diff --git a/controllers/AchievementsController.inc b/controllers/AchievementsController.inc
new file mode 100644
index 00000000..89b3f703
--- /dev/null
+++ b/controllers/AchievementsController.inc
@@ -0,0 +1,172 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the Agent to list Achievements.
+ *
+ * @author Oliver Hanraths
+ */
+ class AchievementsController extends \hhu\z\controllers\SeminaryController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('achievements', 'seminaries', 'media', 'characters');
+ /**
+ * User permissions
+ *
+ * @var array
+ */
+ public $permissions = array(
+ 'index' => array('admin', 'moderator', 'user')
+ );
+ /**
+ * User seminary permissions
+ *
+ * @var array
+ */
+ public $seminaryPermissions = array(
+ 'index' => array('admin', 'moderator', 'user')
+ );
+
+
+
+
+ /**
+ * Action: index.
+ *
+ * List Achievements of a Seminary.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of Seminary
+ */
+ public function index($seminaryUrl)
+ {
+ // Get Seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Character
+ $character = SeminaryController::$character;
+
+ // Get seldom Achievements
+ $seldomAchievements = $this->Achievements->getSeldomAchievements($seminary['id'], \nre\configs\AppConfig::$misc['achievements_range'], false);
+ foreach($seldomAchievements as &$achievement) {
+ $achievement['achieved'] = $this->Achievements->hasCharacterAchievedAchievement($achievement['id'], $character['id']);
+ }
+
+ // Get Characters with the most Achievements
+ $successfulCharacters = $this->Characters->getCharactersWithMostAchievements($seminary['id'], \nre\configs\AppConfig::$misc['achievements_range'], false);
+
+ // Get achieved Achievements
+ $achievedAchievements = $this->Achievements->getAchievedAchievementsForCharacter($character['id'], false);
+
+ // Get unachieved Achievements
+ $unachievedAchievements = $this->Achievements->getUnachhievedAchievementsForCharacter($seminary['id'], $character['id'], true, false);
+ foreach($unachievedAchievements as &$achievement)
+ {
+ // Get Character progress
+ if($achievement['progress'])
+ {
+ $conditions = array();
+ switch($achievement['condition'])
+ {
+ // Character conditions
+ case 'character':
+ $conditionsCharacter = $this->Achievements->getAchievementConditionsCharacter($achievement['id']);
+ foreach($conditionsCharacter as &$condition)
+ {
+ $conditions[] = array(
+ 'func' => 'getAchievementConditionCharacterProgress',
+ 'params' => array(
+ $condition['field'],
+ $condition['value'],
+ $character['id']
+ )
+ );
+ }
+ break;
+ // Quest conditions
+ case 'quest':
+ $conditionsQuest = $this->Achievements->getAchievementConditionsQuest($achievement['id']);
+ foreach($conditionsQuest as &$condition)
+ {
+ $conditions[] = array(
+ 'func' => 'getAchievementConditionQuestProgress',
+ 'params' => array(
+ $condition['field'],
+ $condition['count'],
+ $condition['value'],
+ $condition['status'],
+ $condition['groupby'],
+ $condition['quest_id'],
+ $character['id']
+ )
+ );
+ }
+ break;
+ // Achievement conditions
+ case 'achievement':
+ $conditionsAchievement = $this->Achievements->getAchievementConditionsAchievement($achievement['id']);
+ foreach($conditionsAchievement as &$condition)
+ {
+ $conditions[] = array(
+ 'func' => 'getAchievementConditionAchievementProgress',
+ 'params' => array(
+ $condition['field'],
+ $condition['count'],
+ $condition['value'],
+ $condition['groupby'],
+ $condition['meta_achievement_id'],
+ $character['id']
+ )
+ );
+ }
+ break;
+ }
+
+ $characterProgresses = array();
+ foreach($conditions as &$condition)
+ {
+ // Calculate progress of condition
+ $characterProgresses[] = call_user_func_array(
+ array(
+ $this->Achievements,
+ $condition['func']
+ ),
+ $condition['params']
+ );
+ }
+
+ $achievement['characterProgress'] = array_sum($characterProgresses) / count($characterProgresses);
+ }
+ }
+
+ // Get ranking
+ $character['rank'] = $this->Achievements->getCountRank($seminary['id'], count($achievedAchievements));
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('character', $character);
+ $this->set('seldomAchievements', $seldomAchievements);
+ $this->set('successfulCharacters', $successfulCharacters);
+ $this->set('achievedAchievements', $achievedAchievements);
+ $this->set('unachievedAchievements', $unachievedAchievements);
+ }
+
+ }
+
+?>
diff --git a/controllers/BinaryController.inc b/controllers/BinaryController.inc
new file mode 100644
index 00000000..0d4396ef
--- /dev/null
+++ b/controllers/BinaryController.inc
@@ -0,0 +1,37 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the BinaryAgent to show binary data.
+ *
+ * @author Oliver Hanraths
+ */
+ class BinaryController extends \hhu\z\controllers\IntermediateController
+ {
+
+
+
+
+ /**
+ * Action: index.
+ *
+ * Create binary data.
+ */
+ public function index()
+ {
+ }
+
+ }
+
+?>
diff --git a/controllers/CharactergroupsController.inc b/controllers/CharactergroupsController.inc
new file mode 100644
index 00000000..7a961bf5
--- /dev/null
+++ b/controllers/CharactergroupsController.inc
@@ -0,0 +1,236 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the CharactergroupsAgent to display Character groups.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharactergroupsController extends \hhu\z\controllers\SeminaryController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('seminaries', 'charactergroups', 'charactergroupsquests', 'characters', 'avatars', 'media');
+ /**
+ * User permissions
+ *
+ * @var array
+ */
+ public $permissions = array(
+ 'quest' => array('admin', 'moderator', 'user'),
+ 'manage' => array('admin', 'moderator', 'user')
+ );
+ /**
+ * User seminary permissions
+ *
+ * @var array
+ */
+ public $seminaryPermissions = array(
+ 'quest' => array('admin', 'moderator', 'user'),
+ 'manage' => array('admin', 'moderator')
+ );
+
+
+
+
+ /**
+ * Action: index.
+ *
+ * Show Character groups-groups for a Seminary.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of a Seminary
+ */
+ public function index($seminaryUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Character groups-groups
+ $groupsgroups = $this->Charactergroups->getGroupsroupsForSeminary($seminary['id']);
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('groupsgroups', $groupsgroups);
+ }
+
+
+ /**
+ * Action: groupsgroups.
+ *
+ * Show Character groups for a Character groups-group of a
+ * Seminary.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of a Seminary
+ * @param string $groupsgroupUrl URL-Title of a Character groups-group
+ */
+ public function groupsgroup($seminaryUrl, $groupsgroupUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Character groups-group
+ $groupsgroup = $this->Charactergroups->getGroupsgroupByUrl($seminary['id'], $groupsgroupUrl);
+
+ // Get Character groups
+ $groups = $this->Charactergroups->getGroupsForGroupsgroup($groupsgroup['id']);
+
+ // Get Character groups-group Quests
+ $quests = $this->Charactergroupsquests->getQuestsForCharactergroupsgroup($groupsgroup['id']);
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('groupsgroup', $groupsgroup);
+ $this->set('groups', $groups);
+ $this->set('quests', $quests);
+ }
+
+
+ /**
+ * Action: group.
+ *
+ * Show a Character group for a Character groups-group of a
+ * Seminary.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of a Seminary
+ * @param string $groupsgroupUrl URL-Title of a Character groups-group
+ * @param string $groupUrl URL-Title of a Character group
+ */
+ public function group($seminaryUrl, $groupsgroupUrl, $groupUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Character groups-group
+ $groupsgroup = $this->Charactergroups->getGroupsgroupByUrl($seminary['id'], $groupsgroupUrl);
+
+ // Get Character group
+ $group = $this->Charactergroups->getGroupByUrl($groupsgroup['id'], $groupUrl);
+ $group['characters'] = $this->Characters->getCharactersForGroup($group['id']);
+ $group['rank'] = $this->Charactergroups->getXPRank($groupsgroup['id'], $group['xps']);
+
+ // Get Character avatars
+ foreach($group['characters'] as &$character)
+ {
+ $avatar = $this->Avatars->getAvatarById($character['avatar_id']);
+ if(!is_null($avatar['small_avatarpicture_id'])) {
+ $character['small_avatar'] = $this->Media->getSeminaryMediaById($avatar['small_avatarpicture_id']);
+ }
+ }
+
+ // Get Character groups Quests
+ $quests = $this->Charactergroupsquests->getQuestsForGroup($group['id']);
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('groupsgroup', $groupsgroup);
+ $this->set('group', $group);
+ $this->set('quests', $quests);
+ }
+
+
+ /**
+ * Action: managegroup.
+ *
+ * Manage a Character group for a Character groups-group of a
+ * Seminary.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of a Seminary
+ * @param string $groupsgroupUrl URL-Title of a Character groups-group
+ * @param string $groupUrl URL-Title of a Character group
+ */
+ public function managegroup($seminaryUrl, $groupsgroupUrl, $groupUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Character groups-group
+ $groupsgroup = $this->Charactergroups->getGroupsgroupByUrl($seminary['id'], $groupsgroupUrl);
+
+ // Get Character group
+ $group = $this->Charactergroups->getGroupByUrl($groupsgroup['id'], $groupUrl);
+
+ // Manage
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('actions')) && count($this->request->getPostParam('actions')) > 0) // && !is_null($this->request->getPostParam('characters')) && count($this->request->getPostParam('characters')) > 0)
+ {
+ $actions = $this->request->getPostParam('actions');
+ $action = array_keys($actions)[0];
+ $selectedCharacters = $this->request->getPostParam('characters');
+
+ switch($action)
+ {
+ // Add Characters to group
+ case 'addcharacters':
+ var_dump("hier");
+ foreach($selectedCharacters as &$characterId) {
+ var_dump("add ".$characterId);
+ $this->Charactergroups->addCharacterToCharactergroup($group['id'], $characterId);
+ }
+ break;
+ // Remove Characters from group
+ case 'removecharacters':
+ foreach($selectedCharacters as &$characterId) {
+ $this->Charactergroups->removeCharacterFromCharactergroup($group['id'], $characterId);
+ }
+ break;
+ }
+ }
+
+ // Get additional data for group
+ $group['characters'] = $this->Characters->getCharactersForGroup($group['id']);
+ $group['rank'] = $this->Charactergroups->getXPRank($groupsgroup['id'], $group['xps']);
+
+ // Get Character avatars
+ foreach($group['characters'] as &$character)
+ {
+ $avatar = $this->Avatars->getAvatarById($character['avatar_id']);
+ if(!is_null($avatar['small_avatarpicture_id'])) {
+ $character['small_avatar'] = $this->Media->getSeminaryMediaById($avatar['small_avatarpicture_id']);
+ }
+ }
+
+ // Get Character groups Quests
+ $quests = $this->Charactergroupsquests->getQuestsForGroup($group['id']);
+
+ // Get all Characters of Seminary
+ $groupCharacterIds = array_map(function($c) { return $c['id']; }, $group['characters']);
+ $seminaryCharacters = $this->Characters->getCharactersForSeminary($seminary['id']);
+ $characters = array();
+ foreach($seminaryCharacters as &$character) {
+ if(!in_array($character['id'], $groupCharacterIds)) {
+ $characters[] = $character;
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('groupsgroup', $groupsgroup);
+ $this->set('group', $group);
+ $this->set('quests', $quests);
+ $this->set('characters', $characters);
+ }
+
+ }
+
+?>
diff --git a/controllers/CharactergroupsquestsController.inc b/controllers/CharactergroupsquestsController.inc
new file mode 100644
index 00000000..0285db3e
--- /dev/null
+++ b/controllers/CharactergroupsquestsController.inc
@@ -0,0 +1,91 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the CharactergroupsquestsAgent to display Character
+ * groups Quests.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharactergroupsquestsController extends \hhu\z\controllers\SeminaryController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('seminaries', 'charactergroups', 'charactergroupsquests', 'media');
+ /**
+ * User permissions
+ *
+ * @var array
+ */
+ public $permissions = array(
+ 'quest' => array('admin', 'moderator', 'user')
+ );
+ /**
+ * User seminary permissions
+ *
+ * @var array
+ */
+ public $seminaryPermissions = array(
+ 'quest' => array('admin', 'moderator', 'user')
+ );
+
+
+
+
+ /**
+ * Action: quest.
+ *
+ * Show a Character groups Quest for a Character groups-group
+ * of a Seminary.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of a Seminary
+ * @param string $groupsgroupUrl URL-Title of a Character groups-group
+ * @param string $questUrl URL-Title of a Character groups Quest
+ */
+ public function quest($seminaryUrl, $groupsgroupUrl, $questUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Character groups-group
+ $groupsgroup = $this->Charactergroups->getGroupsgroupByUrl($seminary['id'], $groupsgroupUrl);
+
+ // Get Character groups-group Quests
+ $quest = $this->Charactergroupsquests->getQuestByUrl($groupsgroup['id'], $questUrl);
+
+ // Get Character groups-groups
+ $groups = $this->Charactergroupsquests->getGroupsForQuest($quest['id']);
+
+ // Media
+ $questmedia = null;
+ if(!is_null($quest['questsmedia_id'])) {
+ $questmedia = $this->Media->getSeminaryMediaById($quest['questsmedia_id']);
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('groupsgroup', $groupsgroup);
+ $this->set('quest', $quest);
+ $this->set('groups', $groups);
+ $this->set('media', $questmedia);
+ }
+
+ }
+
+?>
diff --git a/controllers/CharactersController.inc b/controllers/CharactersController.inc
new file mode 100644
index 00000000..a50a2663
--- /dev/null
+++ b/controllers/CharactersController.inc
@@ -0,0 +1,388 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the Agent to list registered users and their data.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharactersController extends \hhu\z\controllers\SeminaryController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('seminaries', 'characters', 'users', 'charactergroups', 'charactertypes', 'seminarycharacterfields', 'avatars', 'media', 'quests', 'questgroups', 'questtopics');
+ /**
+ * Required components
+ *
+ * @var array
+ */
+ public $components = array('validation');
+ /**
+ * User permissions
+ *
+ * @var array
+ */
+ public $permissions = array(
+ 'index' => array('admin', 'moderator'),
+ 'character' => array('admin', 'moderator', 'user'),
+ 'register' => array('admin', 'moderator', 'user'),
+ 'manage' => array('admin', 'moderator')
+ );
+ /**
+ * User seminary permissions
+ *
+ * @var array
+ */
+ public $seminaryPermissions = array(
+ 'index' => array('admin', 'moderator'),
+ 'character' => array('admin', 'moderator', 'user'),
+ 'manage' => array('admin', 'moderator')
+ );
+
+
+
+
+ /**
+ * Action: index.
+ *
+ * List registered Characters for a Seminary
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of a Seminary
+ */
+ public function index($seminaryUrl)
+ {
+ // Get Seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get registered Characters
+ $characters = $this->Characters->getCharactersForSeminary($seminary['id']);
+
+ // Additional Character information
+ foreach($characters as &$character)
+ {
+ // Level
+ $character['xplevel'] = $this->Characters->getXPLevelOfCharacters($character['id']);
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('characters', $characters);
+ }
+
+
+ /**
+ * Action: character.
+ *
+ * Show a Charater and its details.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of a Seminary
+ * @param string $characterUrl URL-name of a Charater
+ */
+ public function character($seminaryUrl, $characterUrl)
+ {
+ // Get Seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+ $seminary['achievable_xps'] = $this->Seminaries->getTotalXPs($seminary['id']);
+
+ // Get Character
+ $character = $this->Characters->getCharacterByUrl($seminary['id'], $characterUrl);
+ $character['quest_xps'] = $this->Characters->getQuestXPsOfCharacter($character['id']);
+ $character['xplevel'] = $this->Characters->getXPLevelOfCharacters($character['id']);
+ $character['rank'] = $this->Characters->getXPRank($seminary['id'], $character['xps']);
+
+ // Get Seminarycharacterfields
+ $characterfields = $this->Seminarycharacterfields->getFieldsForCharacter($character['id']);
+
+ // Get User
+ $user = $this->Users->getUserById($character['user_id']);
+
+ // Get Character groups
+ $groups = $this->Charactergroups->getGroupsForCharacter($character['id']);
+
+ // Get Achievements
+ $achievements = $this->Achievements->getAchievedAchievementsForCharacter($character['id']);
+
+ // Get Achievements with deadline (milestones)
+ $milestones = $this->Achievements->getDeadlineAchievements($seminary['id']);
+ foreach($milestones as &$milestone) {
+ $milestone['achieved'] = $this->Achievements->hasCharacterAchievedAchievement($milestone['id'], $character['id']);
+ }
+
+ // Get ranking
+ $ranking = array(
+ 'superior' => $this->Characters->getSuperiorCharacters($seminary['id'], $character['xps'], \nre\configs\AppConfig::$misc['ranking_range']),
+ 'inferior' => $this->Characters->getInferiorCharacters($seminary['id'], $character['xps'], \nre\configs\AppConfig::$misc['ranking_range'])
+ );
+
+ // Get Quest topics
+ $questtopics = $this->Questtopics->getQuesttopicsForSeminary($seminary['id']);
+ foreach($questtopics as &$questtopic)
+ {
+ $questtopic['questcount'] = $this->Questtopics->getQuestCountForQuesttopic($questtopic['id']);
+ $questtopic['characterQuestcount'] = $this->Questtopics->getCharacterQuestCountForQuesttopic($questtopic['id'], $character['id']);
+ }
+
+ // Get “last” Quest
+ $lastQuest = null;
+ if(count(array_intersect(array('admin', 'moderator'), \hhu\z\controllers\SeminaryController::$character['characterroles'])) > 0)
+ {
+ $lastQuest = $this->Quests->getLastQuestForCharacter($character['id']);
+ if(!is_null($lastQuest)) {
+ $lastQuest['questgroup'] = $this->Questgroups->getQuestgroupById($lastQuest['questgroup_id']);
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('character', $character);
+ $this->set('characterfields', $characterfields);
+ $this->set('user', $user);
+ $this->set('groups', $groups);
+ $this->set('achievements', $achievements);
+ $this->set('milestones', $milestones);
+ $this->set('ranking', $ranking);
+ $this->set('questtopics', $questtopics);
+ $this->set('lastQuest', $lastQuest);
+ }
+
+
+ /**
+ * Acton: register.
+ *
+ * Register a new character for a Seminary.
+ *
+ * @throws IdNotFoundException
+ * @throws ParamsNotValidException
+ * @param string $seminaryUrl URL-Title of a Seminary
+ */
+ public function register($seminaryUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Check for already existing Character
+ try {
+ $this->Characters->getCharacterForUserAndSeminary($this->Auth->getUserId(), $seminary['id']);
+ throw new \nre\exceptions\AccessDeniedException();
+ }
+ catch(\nre\exceptions\IdNotFoundException $e) {
+ // The should be the case
+ }
+
+
+ // Character types
+ $types = $this->Charactertypes->getCharacterTypesForSeminary($seminary['id']);
+
+ // Character fields
+ $fields = $this->Seminarycharacterfields->getFieldsForSeminary($seminary['id']);
+
+ // Register Character
+ $charactername = '';
+ $validation = true;
+ $fieldsValidation = true;
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('create')))
+ {
+ // Validate Character properties
+ $validation = $this->Validation->validateParams($this->request->getPostParams(), array('charactername'));
+ $charactername = $this->request->getPostParam('charactername');
+ if($this->Characters->characterNameExists($charactername)) {
+ $validation = $this->Validation->addValidationResult($validation, 'charactername', 'exist', true);
+ }
+
+ // Validate type
+ $typeIndex = null;
+ foreach($types as $index => &$type)
+ {
+ $type['selected'] = ($type['url'] == $this->request->getPostParam('type'));
+ if($type['selected']) {
+ $typeIndex = $index;
+ }
+ }
+ if(is_null($typeIndex)) {
+ throw new \nre\exceptions\ParamsNotValidException($typeIndex);
+ }
+
+ // Validate fields
+ $fieldsValues = $this->request->getPostParam('fields');
+ foreach($fields as &$field)
+ {
+ if(!array_key_exists($field['url'], $fieldsValues)) {
+ throw new \nre\exceptions\ParamsNotValidException($index);
+ }
+ $field['uservalue'] = $fieldsValues[$field['url']];
+ if($field['required'])
+ {
+ $fieldValidation = $this->Validation->validate($fieldsValues[$field['url']], array('regex'=>$field['regex']));
+ if($fieldValidation !== true)
+ {
+ if(!is_array($fieldsValidation)) {
+ $fieldsValidation = array();
+ }
+ $fieldsValidation[$field['url']] = $fieldValidation;
+ }
+ }
+ }
+
+ // Register
+ if($validation === true && $fieldsValidation === true)
+ {
+ $characterId = $this->Characters->createCharacter($this->Auth->getUserId(), $types[$typeIndex]['id'], $charactername);
+
+ // Add Seminary fields
+ foreach($fields as &$field) {
+ if(!empty($fieldsValues[$field['url']])) {
+ $this->Characters->setSeminaryFieldOfCharacter($characterId, $field['id'], $fieldsValues[$field['url']]);
+ }
+ }
+
+ // Send mail
+ $this->sendRegistrationMail($charactername);
+
+ // Redirect
+ $this->redirect($this->linker->link(array('seminaries')));
+ }
+ }
+
+ // Get XP-levels
+ $xplevels = $this->Characters->getXPLevelsForSeminary($seminary['id']);
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('types', $types);
+ $this->set('fields', $fields);
+ $this->set('charactername', $charactername);
+ $this->set('validation', $validation);
+ $this->set('fieldsValidation', $fieldsValidation);
+ $this->set('xplevels', $xplevels);
+ }
+
+
+ /**
+ * Action: manage.
+ *
+ * Manage Characters.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of a Seminary
+ */
+ public function manage($seminaryUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Do action
+ $selectedCharacters = array();
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('actions')) && count($this->request->getPostParam('actions')) > 0 && !is_null($this->request->getPostParam('characters')) && count($this->request->getPostParam('characters')) > 0)
+ {
+ $actions = $this->request->getPostParam('actions');
+ $action = array_keys($actions)[0];
+ $selectedCharacters = $this->request->getPostParam('characters');
+
+ switch($action)
+ {
+ // Add/remove role to/from Characters
+ case 'addrole':
+ case 'removerole':
+ // Determine role and check permissions
+ $role = null;
+ switch($actions[$action])
+ {
+ case _('Admin'):
+ if(count(array_intersect(array('admin', 'moderator'), \hhu\z\controllers\IntermediateController::$user['roles'])) <= 0 || !in_array('admin', \hhu\z\controllers\SeminaryController::$character['characterroles'])) {
+ throw new \nre\exceptions\AccessDeniedException();
+ }
+ $role = 'admin';
+ break;
+ case _('Moderator'):
+ if(count(array_intersect(array('admin', 'moderator'), \hhu\z\controllers\IntermediateController::$user['roles'])) <= 0 || !in_array('admin', \hhu\z\controllers\SeminaryController::$character['characterroles'])) {
+ throw new \nre\exceptions\AccessDeniedException();
+ }
+ $role = 'moderator';
+ break;
+ case _('User'):
+ if(count(array_intersect(array('admin', 'moderator'), \hhu\z\controllers\IntermediateController::$user['roles'])) <= 0 || count(array_intersect(array('admin', 'moderator'), \hhu\z\controllers\SeminaryController::$character['characterroles'])) <= 0) {
+ throw new \nre\exceptions\AccessDeniedException();
+ }
+ $role = 'user';
+ break;
+ }
+
+ // Add role
+ if($action == 'addrole') {
+ foreach($selectedCharacters as &$characterId) {
+ $this->Characterroles->addCharacterroleToCharacter($characterId, $role);
+ }
+ }
+ // Remove role
+ else {
+ foreach($selectedCharacters as &$characterId) {
+ $this->Characterroles->removeCharacterroleFromCharacter($characterId, $role);
+ }
+ }
+ break;
+ }
+ }
+
+ // Get registered Characters
+ $characters = $this->Characters->getCharactersForSeminary($seminary['id']);
+ foreach($characters as &$character)
+ {
+ $character['xplevel'] = $this->Characters->getXPLevelOfCharacters($character['id']);
+ $character['characterroles'] = array_map(function($r) { return $r['name']; }, $this->Characterroles->getCharacterrolesForCharacterById($character['id']));
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('characters', $characters);
+ $this->set('selectedCharacters', $selectedCharacters);
+ }
+
+
+
+
+ /**
+ * Send mail for new Character registration.
+ *
+ * @param string $charactername Name of newly registered Character
+ */
+ private function sendRegistrationMail($charactername)
+ {
+ $sender = \nre\configs\AppConfig::$app['mailsender'];
+ if(empty($sender)) {
+ return;
+ }
+
+ // Send notification mail to system moderators
+ $subject = sprintf('new Character registration: %s', $charactername);
+ $message = sprintf('User “%s” <%s> has registered a new Character “%s” for the Seminary “%s”', self::$user['username'], self::$user['email'], $charactername, self::$seminary['title']);
+ $characters = $this->Characters->getCharactersWithCharacterRole(self::$seminary['id'], 'moderator');
+ foreach($characters as &$character)
+ {
+ $moderator = $this->Users->getUserById($character['user_id']);
+ \hhu\z\Utils::sendMail($sender, $moderator['email'], $subject, $message);
+ }
+ }
+
+ }
+
+?>
diff --git a/controllers/ErrorController.inc b/controllers/ErrorController.inc
new file mode 100644
index 00000000..a27e3afd
--- /dev/null
+++ b/controllers/ErrorController.inc
@@ -0,0 +1,51 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the Agent to show an error page.
+ *
+ * @author Oliver Hanraths
+ */
+ class ErrorController extends \hhu\z\Controller
+ {
+
+
+
+
+
+
+ /**
+ * Action: index.
+ *
+ * Set HTTP-header and print an error message.
+ *
+ * @param int $httpStatusCode HTTP-statuscode of the error that occurred
+ */
+ public function index($httpStatusCode)
+ {
+ // Set HTTP-header
+ if(!array_key_exists($httpStatusCode, \nre\core\WebUtils::$httpStrings)) {
+ $httpStatusCode = 200;
+ }
+ $this->response->addHeader(\nre\core\WebUtils::getHttpHeader($httpStatusCode));
+
+ // Display statuscode and message
+ $this->set('code', $httpStatusCode);
+ $this->set('string', \nre\core\WebUtils::$httpStrings[$httpStatusCode]);
+ $this->set('userId', $this->Auth->getUserId());
+ }
+
+ }
+
+?>
diff --git a/controllers/FaultController.inc b/controllers/FaultController.inc
new file mode 100644
index 00000000..be01fea7
--- /dev/null
+++ b/controllers/FaultController.inc
@@ -0,0 +1,37 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the Agent to display a toplevel error page.
+ *
+ * @author Oliver Hanraths
+ */
+ class FaultController extends \nre\core\Controller
+ {
+
+
+
+
+ /**
+ * Action: index.
+ *
+ * Show the error message.
+ */
+ public function index()
+ {
+ }
+
+ }
+
+?>
diff --git a/controllers/HtmlController.inc b/controllers/HtmlController.inc
new file mode 100644
index 00000000..2d7eef25
--- /dev/null
+++ b/controllers/HtmlController.inc
@@ -0,0 +1,59 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the HtmlAgent to display a HTML-page.
+ *
+ * @author Oliver Hanraths
+ */
+ class HtmlController extends \hhu\z\Controller
+ {
+
+
+
+
+ /**
+ * Prefilter.
+ *
+ * @param Request $request Current request
+ * @param Response $response Current response
+ */
+ public function preFilter(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ parent::preFilter($request, $response);
+
+ // Set content-type
+ $this->response->addHeader("Content-type: text/html; charset=utf-8");
+ }
+
+
+ /**
+ * Action: index.
+ *
+ * Create the HTML-structure.
+ */
+ public function index()
+ {
+ // Set the name of the current IntermediateAgent as page title
+ $this->set('title', $this->request->getParam(1, 'intermediate'));
+
+ // Set userdata
+ $this->set('loggedUser', IntermediateController::$user);
+ $this->set('loggedSeminary', SeminaryController::$seminary);
+ $this->set('loggedCharacter', SeminaryController::$character);
+ }
+
+ }
+
+?>
diff --git a/controllers/IntroductionController.inc b/controllers/IntroductionController.inc
new file mode 100644
index 00000000..c1a07f41
--- /dev/null
+++ b/controllers/IntroductionController.inc
@@ -0,0 +1,37 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the Agent to show an introduction page.
+ *
+ * @author Oliver Hanraths
+ */
+ class IntroductionController extends \hhu\z\controllers\IntermediateController
+ {
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index()
+ {
+ // Pass data to view
+ $this->set('userId', $this->Auth->getUserId());
+ }
+
+ }
+
+?>
diff --git a/controllers/LibraryController.inc b/controllers/LibraryController.inc
new file mode 100644
index 00000000..1fee6e2c
--- /dev/null
+++ b/controllers/LibraryController.inc
@@ -0,0 +1,135 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the LibraryAgent to list Quest topics.
+ *
+ * @author Oliver Hanraths
+ */
+ class LibraryController extends \hhu\z\controllers\SeminaryController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('questtopics', 'seminaries', 'quests', 'questgroups');
+ /**
+ * User permissions
+ *
+ * @var array
+ */
+ public $permissions = array(
+ 'index' => array('admin', 'moderator', 'user')
+ );
+ /**
+ * User seminary permissions
+ *
+ * @var array
+ */
+ public $seminaryPermissions = array(
+ 'index' => array('admin', 'moderator', 'user')
+ );
+
+
+
+
+ /**
+ * Action: index.
+ *
+ * List Questtopics of a Seminary.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of Seminary
+ */
+ public function index($seminaryUrl)
+ {
+ // Get Seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Character
+ $character = SeminaryController::$character;
+
+ // Get Quest topics
+ $totalQuestcount = 0;
+ $totalCharacterQuestcount = 0;
+ $questtopics = $this->Questtopics->getQuesttopicsForSeminary($seminary['id']);
+ foreach($questtopics as &$questtopic)
+ {
+ // Get Quest count
+ $questtopic['questcount'] = $this->Questtopics->getQuestCountForQuesttopic($questtopic['id']);
+ $totalQuestcount += $questtopic['questcount'];
+
+ // Get Character progress
+ $questtopic['characterQuestcount'] = $this->Questtopics->getCharacterQuestCountForQuesttopic($questtopic['id'], $character['id']);
+ $totalCharacterQuestcount += $questtopic['characterQuestcount'];
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('totalQuestcount', $totalQuestcount);
+ $this->set('totalCharacterQuestcount', $totalCharacterQuestcount);
+ $this->set('questtopics', $questtopics);
+ }
+
+
+ /**
+ * Action: topic.
+ *
+ * Show a Questtopic and its Quests with Questsubtopics.
+ *
+ * @throws IdNotFoundException
+ * @param string $eminaryUrl URL-Title of Seminary
+ * @param string $questtopicUrl URL-Title of Questtopic
+ */
+ public function topic($seminaryUrl, $questtopicUrl)
+ {
+ // Get Seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Character
+ $character = SeminaryController::$character;
+
+ // Get Questtopic
+ $questtopic = $this->Questtopics->getQuesttopicByUrl($seminary['id'], $questtopicUrl);
+ $questtopic['questcount'] = $this->Questtopics->getQuestCountForQuesttopic($questtopic['id']);
+ $questtopic['characterQuestcount'] = $this->Questtopics->getCharacterQuestCountForQuesttopic($questtopic['id'], $character['id']);
+
+ // Get Quests
+ $quests = array();
+ foreach($this->Quests->getQuestsForQuesttopic($questtopic['id']) as $quest)
+ {
+ if($this->Quests->hasCharacterEnteredQuest($quest['id'], $character['id']))
+ {
+ // Get Questgroup
+ $quest['questgroup'] = $this->Questgroups->getQuestgroupById($quest['questgroup_id']);
+
+ // Get Subtopics
+ $quest['subtopics'] = $this->Questtopics->getQuestsubtopicsForQuest($quest['id']);
+
+ $quests[] = $quest;
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('questtopic', $questtopic);
+ $this->set('quests', $quests);
+ }
+
+ }
+
+?>
diff --git a/controllers/MediaController.inc b/controllers/MediaController.inc
new file mode 100644
index 00000000..c81511d4
--- /dev/null
+++ b/controllers/MediaController.inc
@@ -0,0 +1,432 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the MediaAgent to process and show Media.
+ *
+ * @author Oliver Hanraths
+ */
+ class MediaController extends \hhu\z\controllers\SeminaryController
+ {
+ /**
+ * User permissions
+ *
+ * @var array
+ */
+ public $permissions = array(
+ 'index' => array('admin', 'moderator', 'user', 'guest'),
+ 'seminaryheader' => array('admin', 'moderator', 'user'),
+ 'seminary' => array('admin', 'moderator', 'user'),
+ 'avatar' => array('admin', 'moderator', 'user'),
+ 'achievement' => array('admin', 'moderator', 'user')
+ );
+ /**
+ * User seminary permissions
+ *
+ * @var array
+ */
+ public $seminaryPermissions = array(
+ 'seminary' => array('admin', 'moderator', 'user', 'guest'),
+ 'achievement' => array('admin', 'moderator', 'user', 'guest')
+ );
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('seminaries', 'achievements', 'media', 'avatars');
+
+
+
+
+ /**
+ * Prefilter.
+ *
+ * @param Request $request Current request
+ * @param Response $response Current response
+ */
+ public function preFilter(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ parent::preFilter($request, $response);
+
+ // Set headers for caching control
+ $response->addHeader("Pragma: public");
+ $response->addHeader("Cache-control: public, max-age=".(60*60*24));
+ $response->addHeader("Expires: ".gmdate('r', time()+(60*60*24)));
+ $response->addHeader("Date: ".gmdate(\DateTime::RFC822));
+ }
+
+
+ /**
+ * Action: index
+ *
+ * Display a medium.
+ *
+ * @param string $mediaUrl URL-name of the medium
+ * @param string $action Action for processing the media
+ */
+ public function index($mediaUrl, $action=null)
+ {
+ // Get Media
+ $media = $this->Media->getMediaByUrl($mediaUrl);
+
+ // Get file
+ $file = $this->getMediaFile($media, $action);
+ if(is_null($media)) {
+ return;
+ }
+
+
+ // Pass data to view
+ $this->set('media', $media);
+ $this->set('file', $file);
+ }
+
+
+ /**
+ * Action: seminaryheader
+ *
+ * Display the header of a Seminary.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-title of the Seminary
+ * @param string $action Action for processing the media
+ */
+ public function seminaryheader($seminaryUrl, $action=null)
+ {
+ // Get Seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get media
+ $media = $this->Media->getSeminaryMediaById($seminary['seminarymedia_id']);
+
+ // Get file
+ $file = $this->getMediaFile($media, $action);
+ if(is_null($file)) {
+ return;
+ }
+
+
+ // Pass data to view
+ $this->set('media', $media);
+ $this->set('file', $file);
+ }
+
+
+ /**
+ * Action: seminary.
+ *
+ * Display a Seminary medium.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-title of the Seminary
+ * @param string $mediaUrl URL-name of the medium
+ * @param string $action Action for processing the media
+ */
+ public function seminary($seminaryUrl, $mediaUrl, $action=null)
+ {
+ // Get Seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Media
+ $media = $this->Media->getSeminaryMediaByUrl($seminary['id'], $mediaUrl);
+
+ // Get file
+ $file = $this->getMediaFile($media, $action);
+ if(is_null($file)) {
+ return;
+ }
+
+
+ // Pass data to view
+ $this->set('media', $media);
+ $this->set('file', $file);
+ }
+
+
+ /**
+ * Action: avatar.
+ *
+ * Display an Avatar as full size or portrait.
+ *
+ * @throws ParamsNotValidException
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-title of the Seminary
+ * @param string $charactertypeUrl URL-title of Character type
+ * @param int $xplevel XP-level
+ * @param string $action Size to show (avatar or portrait)
+ */
+ public function avatar($seminaryUrl, $charactertypeUrl, $xplevel, $action='avatar')
+ {
+ // Get Seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Avatar
+ $avatar = $this->Avatars->getAvatarByTypeAndLevel($seminary['id'], $charactertypeUrl, $xplevel);
+
+ // Get media
+ switch($action)
+ {
+ case null:
+ case 'avatar':
+ $media = $this->Media->getSeminaryMediaById($avatar['avatarpicture_id']);
+ $file = $this->getMediaFile($media, 'avatar');
+ break;
+ case 'portrait':
+ $media = $this->Media->getSeminaryMediaById($avatar['small_avatarpicture_id']);
+ $file = $this->getMediaFile($media);
+ break;
+ default:
+ throw new \nre\exceptions\ParamsNotValidException($action);
+ break;
+ }
+
+ // Get file
+ if(is_null($file)) {
+ return;
+ }
+
+
+ // Pass data to view
+ $this->set('media', $media);
+ $this->set('file', $file);
+ }
+
+
+ /**
+ * Action: achievement
+ *
+ * Display the achievement of a Seminary.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-title of the Seminary
+ * @param string $achievementUrl URL-title of the Achievement
+ * @param string $action Action for processing the media
+ */
+ public function achievement($seminaryUrl, $achievementUrl)
+ {
+ // Get Seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Character
+ $character = SeminaryController::$character;
+
+ // Get Achievement
+ $achievement = $this->Achievements->getAchievementByUrl($seminary['id'], $achievementUrl);
+
+ // Get media
+ $index = '';
+ if(is_null($character) || !$this->Achievements->hasCharacterAchievedAchievement($achievement['id'], $character['id'])) {
+ $index = 'unachieved_achievementsmedia_id';
+ }
+ else {
+ $index = 'achieved_achievementsmedia_id';
+ }
+ if(is_null($achievement[$index])) {
+ throw new \nre\exceptions\IdNotFoundException($achievementUrl);
+ }
+ $media = $this->Media->getSeminaryMediaById($achievement[$index]);
+
+ // Get file
+ $file = $this->getMediaFile($media, null);
+ if(is_null($file)) {
+ return;
+ }
+
+
+ // Pass data to view
+ $this->set('media', $media);
+ $this->set('file', $file);
+ }
+
+
+
+
+ /**
+ * Determine file information and set the HTTP-header for
+ * caching accordingly.
+ *
+ * @param string $fileName Filename
+ * @return boolean HTTP-status 304 was set (in cache)
+ */
+ private function setCacheHeaders($fileName)
+ {
+ // Determine last change of file
+ $fileLastModified = gmdate('r', filemtime($fileName));
+
+ // Generate E-Tag
+ $fileEtag = hash('sha256', $fileLastModified.$fileName);
+
+
+ // Set header
+ $this->response->addHeader("Last-Modified: ".$fileLastModified);
+ $this->response->addHeader("Etag: ".$fileEtag);
+ // HTTP-status
+ $headerModifiedSince = $this->request->getServerParam('HTTP_IF_MODIFIED_SINCE');
+ $headerNoneMatch = $this->request->getServerParam('HTTP_IF_NONE_MATCH');
+ if(
+ !is_null($headerModifiedSince) && $fileLastModified < strtotime($headerModifiedSince) &&
+ !is_null($headerNoneMatch) && $headerNoneMatch == $fileEtag
+ ) {
+ $this->response->setExit(true);
+ $this->response->addHeader(\nre\core\WebUtils::getHttpHeader(304));
+
+ return true;
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Determine the file for a medium and process it if necessary.
+ *
+ * @throws IdNotFoundException
+ * @throws ParamsNotValidException
+ * @param array $media Medium to get file for
+ * @param string $action Action for processing the media
+ * @return object File for the medium (or null if medium is cached)
+ */
+ private function getMediaFile($media, $action=null)
+ {
+ // Get format
+ $format = explode('/', $media['mimetype']);
+ $format = $format[1];
+
+ // Set content-type
+ $this->response->addHeader("Content-type: ".$media['mimetype']."");
+
+ // Set filename
+ $media['filename'] = ROOT.DS.\nre\configs\AppConfig::$dirs['seminarymedia'].DS.$media['id'];
+ if(!file_exists($media['filename'])) {
+ throw new \nre\exceptions\IdNotFoundException($mediaUrl);
+ }
+
+ // Cache
+ if($this->setCacheHeaders($media['filename'])) {
+ return null;
+ }
+
+ // Load and process file
+ $file = null;
+ switch($action)
+ {
+ // No action
+ case null:
+ // Do not process the file
+ $file = file_get_contents($media['filename']);
+ break;
+ case 'questgroup':
+ if(!in_array(strtoupper($format), self::getImageTypes())) {
+ $file = file_get_contents($media['filename']);
+ }
+ else
+ {
+ $file = self::resizeImage(
+ $media['filename'],
+ $format,
+ \nre\configs\AppConfig::$media['questgroup']['width'],
+ \nre\configs\AppConfig::$media['questgroup']['height']
+ );
+ }
+ break;
+ case 'avatar':
+ $file = self::resizeImage(
+ $media['filename'],
+ $format,
+ \nre\configs\AppConfig::$media['avatar']['width'],
+ \nre\configs\AppConfig::$media['avatar']['height']
+ );
+ break;
+ default:
+ throw new ParamsNotValidException($action);
+ break;
+ }
+
+
+ // Return file
+ return $file;
+ }
+
+
+ /**
+ * Get supported image types.
+ *
+ * @return array List of supported image types
+ */
+ private static function getImageTypes()
+ {
+ $im = new \Imagick();
+
+
+ return $im->queryFormats();
+ }
+
+
+ /**
+ * Resize an image.
+ *
+ * @param string $fileName Absolute pathname of image to resize
+ * @param string $mimeType Mimetype of target image
+ * @param int $width Max. width to resize to
+ * @param int $height Max. height to resize to
+ * @return mixed Resized image
+ */
+ private static function resizeImage($fileName, $mimeType, $width, $height)
+ {
+ // Read image from cache
+ $tempFileName = ROOT.DS.\nre\configs\AppConfig::$dirs['temporary'].DS.'media-'.basename($fileName).'-'.$width.'x'.$height;
+ if(file_exists($tempFileName))
+ {
+ // Check age of file
+ if(date('r', filemtime($tempFileName)+(60*60*24)) > date('r', time())) {
+ // Too old, delete
+ unlink($tempFileName);
+ }
+ else {
+ // Valid, read and return
+ return file_get_contents($tempFileName);
+ }
+ }
+
+
+ // ImageMagick
+ $im = new \Imagick($fileName);
+
+ // Calculate new size
+ $geometry = $im->getImageGeometry();
+ if($geometry['width'] < $width) {
+ $width = $geometry['width'];
+ }
+ if($geometry['height'] < $height) {
+ $height = $geometry['width'];
+ }
+
+ // Process
+ $im->thumbnailImage($width, $height, true);
+ $im->contrastImage(1);
+ $im->setImageFormat($mimeType);
+
+ // Save temporary file
+ $im->writeImage($tempFileName);
+
+
+ // Return resized image
+ return $im;
+ }
+
+ }
+
+?>
diff --git a/controllers/MenuController.inc b/controllers/MenuController.inc
new file mode 100644
index 00000000..6c6a7d58
--- /dev/null
+++ b/controllers/MenuController.inc
@@ -0,0 +1,52 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the Agent to display a menu.
+ *
+ * @author Oliver Hanraths
+ */
+ class MenuController extends \hhu\z\Controller
+ {
+
+
+
+
+ /**
+ * Prefilter.
+ *
+ * @param Request $request Current request
+ * @param Response $response Current response
+ */
+ public function preFilter(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ parent::preFilter($request, $response);
+
+ // Set userdata
+ $this->set('loggedUser', IntermediateController::$user);
+ $this->set('loggedCharacter', SeminaryController::$character);
+ $this->set('loggedSeminary', SeminaryController::$seminary);
+ }
+
+
+ /**
+ * Action: index.
+ */
+ public function index()
+ {
+ }
+
+ }
+
+?>
diff --git a/controllers/QuestgroupsController.inc b/controllers/QuestgroupsController.inc
new file mode 100644
index 00000000..8e4d01c4
--- /dev/null
+++ b/controllers/QuestgroupsController.inc
@@ -0,0 +1,238 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the QuestgroupsAgent to display Questgroups.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuestgroupsController extends \hhu\z\controllers\SeminaryController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('seminaries', 'questgroupshierarchy', 'questgroups', 'quests', 'questtexts', 'media');
+ /**
+ * User permissions
+ *
+ * @var array
+ */
+ public $permissions = array(
+ 'questgroup' => array('admin', 'moderator', 'user'),
+ 'create' => array('admin', 'moderator', 'user')
+ );
+ /**
+ * User seminary permissions
+ *
+ * @var array
+ */
+ public $seminaryPermissions = array(
+ 'questgroup' => array('admin', 'moderator', 'user'),
+ 'create' => array('admin', 'moderator')
+ );
+
+
+
+
+ /**
+ * Action: questgroup.
+ *
+ * Display a Questgroup and its data.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of a Seminary
+ * @param string $questgroupUrl URL-Title of a Questgroup
+ */
+ public function questgroup($seminaryUrl, $questgroupUrl)
+ {
+ // Get Seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Questgroup
+ $questgroup = $this->Questgroups->getQuestgroupByUrl($seminary['id'], $questgroupUrl);
+
+ // Get Questgrouphierarchy
+ $questgroup['hierarchy'] = $this->Questgroupshierarchy->getHierarchyForQuestgroup($questgroup['id']);
+
+ // Get Character
+ $character = $this->Characters->getCharacterForUserAndSeminary($this->Auth->getUserId(), $seminary['id']);
+
+ // Check permission
+ if(count(array_intersect(array('admin','moderator'), SeminaryController::$character['characterroles'])) == 0)
+ {
+ $previousQuestgroup = $this->Questgroups->getPreviousQuestgroup($questgroup['id']);
+ if(!is_null($previousQuestgroup)) {
+ if(!$this->Questgroups->hasCharacterSolvedQuestgroup($previousQuestgroup['id'], $character['id'])) {
+ throw new \nre\exceptions\AccessDeniedException();
+ }
+ }
+ }
+
+ // Set status “entered”
+ $this->Questgroups->setQuestgroupEntered($questgroup['id'], $character['id']);
+
+ // Get child Questgroupshierarchy
+ $childQuestgroupshierarchy = null;
+ if(!empty($questgroup['hierarchy']))
+ {
+ $childQuestgroupshierarchy = $this->Questgroupshierarchy->getChildQuestgroupshierarchy($questgroup['hierarchy']['id']);
+ foreach($childQuestgroupshierarchy as &$hierarchy)
+ {
+ // Get Questgroups
+ $hierarchy['questgroups'] = $this->Questgroups->getQuestgroupsForHierarchy($hierarchy['id'], $questgroup['id']);
+
+ // Get additional data
+ foreach($hierarchy['questgroups'] as $i => &$group)
+ {
+ $group['solved'] = $this->Questgroups->hasCharacterSolvedQuestgroup($group['id'], $character['id']);
+
+ // Check permission of Questgroups
+ if($i >= 1 && count(array_intersect(array('admin','moderator'), SeminaryController::$character['characterroles'])) == 0)
+ {
+ if(!$hierarchy['questgroups'][$i-1]['solved'])
+ {
+ $hierarchy['questgroups'] = array_slice($hierarchy['questgroups'], 0, $i);
+ break;
+ }
+ }
+
+ // Get cumulated data
+ $data = $this->Questgroups->getCumulatedDataForQuestgroup($group['id'], $character['id']);
+ $group['xps'] = $data['xps'];
+ $group['character_xps'] = $data['character_xps'];
+
+ // Attach related Questgroups
+ $group['relatedQuestgroups'] = array();
+ $relatedQuestgroups = $this->Questgroups->getRelatedQuestsgroupsOfQuestgroup($group['id']);
+ foreach($relatedQuestgroups as &$relatedQuestgroup) {
+ if($this->Questgroups->hasCharacterEnteredQuestgroup($relatedQuestgroup['id'], $character['id'])) {
+ $group['relatedQuestgroups'][] = $this->Questgroups->getQuestgroupById($relatedQuestgroup['id']);
+ }
+ }
+ }
+ }
+ }
+
+ // Get texts
+ $questgroupTexts = $this->Questgroups->getQuestgroupTexts($questgroup['id']);
+
+ // Get Character XPs
+ $questgroup['character_xps'] = $this->Questgroups->getAchievedXPsForQuestgroup($questgroup['id'], $character['id']);
+
+ // Media
+ $picture = null;
+ if(!is_null($questgroup['questgroupspicture_id']))
+ {
+ $picture = $this->Media->getSeminaryMediaById($questgroup['questgroupspicture_id']);
+ }
+
+
+ // Get Quests
+ $quests = array();
+ if(count($childQuestgroupshierarchy) == 0)
+ {
+ $currentQuest = null;
+ do {
+ // Get next Quest
+ if(is_null($currentQuest)) {
+ $currentQuest = $this->Quests->getFirstQuestOfQuestgroup($questgroup['id']);
+ }
+ else {
+ $nextQuests = $this->Quests->getNextQuests($currentQuest['id']);
+ $currentQuest = null;
+ foreach($nextQuests as &$nextQuest) {
+ if($this->Quests->hasCharacterEnteredQuest($nextQuest['id'], $character['id'])) {
+ $currentQuest = $nextQuest;
+ break;
+ }
+ }
+ }
+
+ // Add additional data
+ if(!is_null($currentQuest))
+ {
+ // Set status
+ $currentQuest['solved'] = $this->Quests->hasCharacterSolvedQuest($currentQuest['id'], $character['id']);
+
+ // Attach related Questgroups
+ $currentQuest['relatedQuestgroups'] = array();
+ $relatedQuestgroups = $this->Questgroups->getRelatedQuestsgroupsOfQuest($currentQuest['id']);
+ foreach($relatedQuestgroups as &$relatedQuestgroup)
+ {
+ if($this->Questgroups->hasCharacterEnteredQuestgroup($relatedQuestgroup['id'], $character['id'])) {
+ $currentQuest['relatedQuestgroups'][] = $this->Questgroups->getQuestgroupById($relatedQuestgroup['id']);
+ }
+ }
+
+ // Add Quest to Quests
+ $quests[] = $currentQuest;
+ }
+ }
+ while(!is_null($currentQuest) && ($currentQuest['solved'] || count(array_intersect(array('admin','moderator'), SeminaryController::$character['characterroles'])) > 0));
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('questgroup', $questgroup);
+ $this->set('childquestgroupshierarchy', $childQuestgroupshierarchy);
+ $this->set('texts', $questgroupTexts);
+ $this->set('picture', $picture);
+ $this->set('quests', $quests);
+ }
+
+
+ /**
+ * Action: create.
+ *
+ * Create a new Questgroup.
+ *
+ * @param string $seminaryUrl URL-Title of a Seminary
+ */
+ public function create($seminaryUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Create Questgroup
+ $validation = true;
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('create')))
+ {
+ // TODO Validation
+ $title = $this->request->getPostParam('title');
+
+ // Create new Questgroup
+ if($validation === true)
+ {
+ $questgroupId = $this->Questgroups->createQuestgroup(
+ $this->Auth->getUserId(),
+ $seminary['id'],
+ $title
+ );
+
+ // Redirect
+ $this->redirect($this->linker->link(array('seminaries', 'seminary', $seminary['url'])));
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ }
+
+ }
+
+?>
diff --git a/controllers/QuestgroupshierarchypathController.inc b/controllers/QuestgroupshierarchypathController.inc
new file mode 100644
index 00000000..fabf2bbb
--- /dev/null
+++ b/controllers/QuestgroupshierarchypathController.inc
@@ -0,0 +1,90 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of QuestgroupshierarchypathAgent to display the
+ * Questgroups hierarchy path.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuestgroupshierarchypathController extends \hhu\z\Controller
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('seminaries', 'questgroups', 'questgroupshierarchy', 'quests', 'questtexts');
+
+
+
+
+ /**
+ * Action: index.
+ *
+ * Calculate and show the hierarchy path of a Questgroup.
+ *
+ * @param string $seminaryUrl URL-Title of a Seminary
+ * @param string $questgroupUrl URL-Title of a Questgroup
+ * @param boolean $showGroup Show the current group itself
+ */
+ public function index($seminaryUrl, $questgroupUrl, $showGroup=false)
+ {
+ // Get Seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Questgroup
+ $questgroup = $this->Questgroups->getQuestgroupByUrl($seminary['id'], $questgroupUrl);
+ $questgroup['hierarchy'] = $this->Questgroupshierarchy->getHierarchyForQuestgroup($questgroup['id']);
+
+ // Get parent Questgrouphierarchy
+ $currentQuestgroup = $questgroup;
+ $parentQuestgroupshierarchy = array();
+ if($showGroup) {
+ array_unshift($parentQuestgroupshierarchy, $currentQuestgroup);
+ }
+ if(is_null($questgroup['hierarchy']))
+ {
+ // Get related Questgroup
+ $questtext = $this->Questtexts->getRelatedQuesttextForQuestgroup($currentQuestgroup['id']);
+ $quest = $this->Quests->getQuestById($questtext['quest_id']);
+ $currentQuestgroup = $this->Questgroups->getQuestgroupById($quest['questgroup_id']);
+ $currentQuestgroup['hierarchy'] = $this->Questgroupshierarchy->getHierarchyForQuestgroup($currentQuestgroup['id']);
+ $quest['questgroup'] = $currentQuestgroup;
+
+ // Use Hierarchy name for optional Questgroup
+ if(!empty($parentQuestgroupshierarchy)) {
+ $parentQuestgroupshierarchy[0]['hierarchy'] = $currentQuestgroup['hierarchy'];
+ unset($parentQuestgroupshierarchy[0]['hierarchy']['questgroup_pos']);
+ }
+
+ array_unshift($parentQuestgroupshierarchy, $quest);
+ array_unshift($parentQuestgroupshierarchy, $currentQuestgroup);
+ }
+ while(!empty($currentQuestgroup['hierarchy']) && !is_null($currentQuestgroup['hierarchy']['parent_questgroup_id']))
+ {
+ $currentQuestgroup = $this->Questgroups->GetQuestgroupById($currentQuestgroup['hierarchy']['parent_questgroup_id']);
+ $currentQuestgroup['hierarchy'] = $this->Questgroupshierarchy->getHierarchyForQuestgroup($currentQuestgroup['id']);
+ array_unshift($parentQuestgroupshierarchy, $currentQuestgroup);
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('parentquestgroupshierarchy', $parentQuestgroupshierarchy);
+ }
+
+ }
+
+?>
diff --git a/controllers/QuestsController.inc b/controllers/QuestsController.inc
new file mode 100644
index 00000000..8a60b3bc
--- /dev/null
+++ b/controllers/QuestsController.inc
@@ -0,0 +1,809 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the QuestsAgent to display Quests.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuestsController extends \hhu\z\controllers\SeminaryController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('seminaries', 'questgroups', 'quests', 'questtexts', 'media', 'questtypes', 'questgroupshierarchy');
+ /**
+ * User permissions
+ *
+ * @var array
+ */
+ public $permissions = array(
+ 'index' => array('admin', 'moderator', 'user'),
+ 'quest' => array('admin', 'moderator', 'user'),
+ 'submissions' => array('admin', 'moderator', 'user'),
+ 'submission' => array('admin', 'moderator', 'user'),
+ 'create' => array('admin', 'moderator', 'user')
+ );
+ /**
+ * User seminary permissions
+ *
+ * @var array
+ */
+ public $seminaryPermissions = array(
+ 'index' => array('admin', 'moderator', 'user'),
+ 'quest' => array('admin', 'moderator', 'user'),
+ 'submissions' => array('admin', 'moderator'),
+ 'submission' => array('admin', 'moderator'),
+ 'create' => array('admin', 'moderator')
+ );
+
+
+
+
+ /**
+ * Action: index.
+ *
+ * List all Quests for a Seminary.
+ *
+ * @param string $seminaryUrl URL-Title of Seminary
+ */
+ public function index($seminaryUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Prepare filters
+ $filters = array(
+ 'questgroups' => array(),
+ 'questtypes' => array()
+ );
+
+ // Get selected filters
+ $selectedFilters = array(
+ 'questgroup' => "0",
+ 'questtype' => ""
+ );
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('filters'))) {
+ $selectedFilters = $this->request->getPostParam('filters');
+ }
+
+ // Get Quests
+ $quests = array();
+ foreach($this->Quests->getQuestsForSeminary($seminary['id']) as $quest)
+ {
+ // Get Questgroup
+ $quest['questgroup'] = $this->Questgroups->getQuestgroupById($quest['questgroup_id']);
+ if($selectedFilters['questgroup'] != "0" && $selectedFilters['questgroup'] != $quest['questgroup']['id']) {
+ continue;
+ }
+
+ // Get Questtype
+ $quest['questtype'] = $this->Questtypes->getQuesttypeById($quest['questtype_id']);
+ if($selectedFilters['questtype'] != "" && $selectedFilters['questtype'] != $quest['questtype']['classname']) {
+ continue;
+ }
+
+ // Add filter values
+ $filters['questgroups'][$quest['questgroup']['id']] = $quest['questgroup'];
+ $filters['questtypes'][$quest['questtype']['classname']] = $quest['questtype'];
+
+ // Add open submissions count
+ $quest['opensubmissionscount'] = count($this->Characters->getCharactersSubmittedQuest($quest['id']));
+
+ $quests[] = $quest;
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('quests', $quests);
+ $this->set('filters', $filters);
+ $this->set('selectedFilters', $selectedFilters);
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Show a quest and its task.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of Seminary
+ * @param string $questgroupUrl URL-Title of Questgroup
+ * @param string $questUrl URL-Title of Quest
+ * @param string $questtexttypeUrl URL-Title of Questtexttype
+ * @param int $questtextPos Position of Questtext
+ */
+ public function quest($seminaryUrl, $questgroupUrl, $questUrl, $questtexttypeUrl=null, $questtextPos=1)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Questgroup
+ $questgroup = $this->Questgroups->getQuestgroupByUrl($seminary['id'], $questgroupUrl);
+ $questgroup['picture'] = null;
+ if(!is_null($questgroup['questgroupspicture_id'])) {
+ $questgroup['picture'] = $this->Media->getSeminaryMediaById($questgroup['questgroupspicture_id']);
+ }
+
+ // Get Quest
+ $quest = $this->Quests->getQuestByUrl($seminary['id'], $questgroup['id'], $questUrl);
+
+ // Get Character
+ $character = $this->Characters->getCharacterForUserAndSeminary($this->Auth->getUserId(), $seminary['id']);
+
+ // Check permissions
+ if(count(array_intersect(array('admin','moderator'), SeminaryController::$character['characterroles'])) == 0)
+ {
+ $previousQuests = $this->Quests->getPreviousQuests($quest['id']);
+ if(count($previousQuests) == 0)
+ {
+ // Previous Questgroup
+ $previousQuestgroup = $this->Questgroups->getPreviousQuestgroup($questgroup['id']);
+ if(!is_null($previousQuestgroup) && !$this->Questgroups->hasCharacterSolvedQuestgroup($previousQuestgroup['id'], $character['id'])) {
+ throw new \nre\exceptions\AccessDeniedException();
+ }
+ }
+ else
+ {
+ // Previous Quests
+ // One previous Quest has to be solved and no other
+ // following Quests of ones has to be tried
+ $solved = false;
+ $tried = false;
+ foreach($previousQuests as &$previousQuest)
+ {
+ // // Check previous Quest
+ if($this->Quests->hasCharacterSolvedQuest($previousQuest['id'], $character['id']))
+ {
+ $solved = true;
+
+ // Check following Quests
+ $followingQuests = $this->Quests->getNextQuests($previousQuest['id']);
+ foreach($followingQuests as $followingQuest)
+ {
+ // Check following Quest
+ if($followingQuest['id'] != $quest['id'] && $this->Quests->hasCharacterTriedQuest($followingQuest['id'], $character['id']))
+ {
+ $tried = true;
+ break;
+ }
+ }
+
+ break;
+ }
+ }
+ if(!$solved || $tried) {
+ throw new \nre\exceptions\AccessDeniedException();
+ }
+ }
+ }
+
+ // Set status “entered”
+ $this->Quests->setQuestEntered($quest['id'], $character['id']);
+
+ // Has Character solved quest?
+ $solved = $this->Quests->hasCharacterSolvedQuest($quest['id'], $character['id']);
+
+ // Get Questtexts
+ $questtexts = array();
+ $questtexts['Prolog'] = $this->Questtexts->getQuesttextsOfQuest($quest['id'], 'Prolog');
+ if($solved) {
+ $questtexts['Epilog'] = $this->Questtexts->getQuesttextsOfQuest($quest['id'], 'Epilog');
+ }
+ foreach($questtexts as &$questtextList)
+ {
+ foreach($questtextList as &$questtext)
+ {
+ // Questtext media
+ if(!is_null($questtext['questsmedia_id'])) {
+ $questtext['media'] = $this->Media->getSeminaryMediaById($questtext['questsmedia_id']);
+ }
+
+ // Related Questgroups
+ $questtext['relatedQuestsgroups'] = $this->Questgroups->getRelatedQuestsgroupsOfQuesttext($questtext['id']);
+ }
+ }
+
+ // Quest status
+ $questStatus = $this->request->getGetParam('status');
+
+ // Quest media
+ $questmedia = null;
+ if(!is_null($quest['questsmedia_id'])) {
+ $questmedia = $this->Media->getSeminaryMediaById($quest['questsmedia_id']);
+ }
+
+ // Task
+ $task = null;
+ $questtype = $this->Questtypes->getQuesttypeById($quest['questtype_id']);
+ if(!is_null($questtype['classname'])) {
+ $task = $this->renderTask($questtype['classname'], $seminary, $questgroup, $quest, $character);
+ }
+ else
+ {
+ // Mark Quest as solved
+ $this->Quests->setQuestSolved($quest['id'], $character['id']);
+ $solved = true;
+ }
+
+ // Get (related) Questtext
+ $relatedQuesttext = $this->Questtexts->getRelatedQuesttextForQuestgroup($questgroup['id']);
+ if(!empty($relatedQuesttext)) {
+ $relatedQuesttext['quest'] = $this->Quests->getQuestById($relatedQuesttext['quest_id']);
+ if(!empty($relatedQuesttext['quest'])) {
+ $relatedQuesttext['quest']['questgroup_url'] = $this->Questgroups->getQuestgroupById($relatedQuesttext['quest']['questgroup_id'])['url'];
+ }
+ }
+
+ // Next Quest/Questgroup
+ $nextQuests = null;
+ $charactedHasChoosenNextQuest = false;
+ $nextQuestgroup = null;
+ if($solved)
+ {
+ // Next Quest
+ $nextQuests = $this->Quests->getNextQuests($quest['id']);
+ foreach($nextQuests as &$nextQuest)
+ {
+ // Set entered status of Quest
+ $nextQuest['entered'] = $this->Quests->hasCharacterEnteredQuest($nextQuest['id'], $character['id']);
+ if($nextQuest['entered']) {
+ $charactedHasChoosenNextQuest = true;
+ }
+ }
+
+ // Next Questgroup
+ if(empty($nextQuests))
+ {
+ if(is_null($relatedQuesttext))
+ {
+ $nextQuestgroup = $this->Questgroups->getNextQuestgroup($questgroup['id']);
+ $nextQuestgroup['hierarchy'] = $this->Questgroupshierarchy->getHierarchyForQuestgroup($nextQuestgroup['id']);
+ }
+ else
+ {
+ // Related (Main-) Quest
+ $nextQuest = $relatedQuesttext['quest'];
+ $nextQuest['entered'] = true;
+ $nextQuests = array($nextQuest);
+ }
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('questgroup', $questgroup);
+ //$this->set('questtexttype', $questtexttype);
+ $this->set('questtexts', $questtexts);
+ //$this->set('hasEpilog', $hasEpilog);
+ $this->set('quest', $quest);
+ $this->set('queststatus', $questStatus);
+ $this->set('relatedquesttext', $relatedQuesttext);
+ $this->set('nextquests', $nextQuests);
+ $this->set('charactedHasChoosenNextQuest', $charactedHasChoosenNextQuest);
+ $this->set('nextquestgroup', $nextQuestgroup);
+ $this->set('task', $task);
+ $this->set('media', $questmedia);
+ $this->set('solved', $solved);
+ }
+
+
+ /**
+ * List Character submissions for a Quest.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of Seminary
+ * @param string $questgroupUrl URL-Title of Questgroup
+ * @param string $questUrl URL-Title of Quest
+ */
+ public function submissions($seminaryUrl, $questgroupUrl, $questUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Questgroup
+ $questgroup = $this->Questgroups->getQuestgroupByUrl($seminary['id'], $questgroupUrl);
+ $questgroup['picture'] = null;
+ if(!is_null($questgroup['questgroupspicture_id'])) {
+ $questgroup['picture'] = $this->Media->getSeminaryMediaById($questgroup['questgroupspicture_id']);
+ }
+
+ // Get Quest
+ $quest = $this->Quests->getQuestByUrl($seminary['id'], $questgroup['id'], $questUrl);
+
+ // Media
+ $questmedia = null;
+ if(!is_null($quest['questsmedia_id'])) {
+ $questmedia = $this->Media->getSeminaryMediaById($quest['questsmedia_id']);
+ }
+
+ // Get submitted Character submissions waiting for approval
+ $submittedSubmissionCharacters = $this->Characters->getCharactersSubmittedQuest($quest['id']);
+
+ // Get unsolved Character submissions
+ $unsolvedSubmissionCharacters = $this->Characters->getCharactersUnsolvedQuest($quest['id']);
+
+ // Get solved Character submissions
+ $solvedSubmissionCharacters = $this->Characters->getCharactersSolvedQuest($quest['id']);
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('questgroup', $questgroup);
+ $this->set('quest', $quest);
+ $this->set('media', $questmedia);
+ $this->set('submittedSubmissionCharacters', $submittedSubmissionCharacters);
+ $this->set('unsolvedSubmissionCharacters', $unsolvedSubmissionCharacters);
+ $this->set('solvedSubmissionCharacters', $solvedSubmissionCharacters);
+ }
+
+
+ /**
+ * Show and handle the submission of a Character for a Quest.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of Seminary
+ * @param string $questgroupUrl URL-Title of Questgroup
+ * @param string $questUrl URL-Title of Quest
+ * @param string $characterUrl URL-Title of Character
+ */
+ public function submission($seminaryUrl, $questgroupUrl, $questUrl, $characterUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Questgroup
+ $questgroup = $this->Questgroups->getQuestgroupByUrl($seminary['id'], $questgroupUrl);
+ $questgroup['picture'] = null;
+ if(!is_null($questgroup['questgroupspicture_id'])) {
+ $questgroup['picture'] = $this->Media->getSeminaryMediaById($questgroup['questgroupspicture_id']);
+ }
+
+ // Get Quest
+ $quest = $this->Quests->getQuestByUrl($seminary['id'], $questgroup['id'], $questUrl);
+
+ // Character
+ $character = $this->Characters->getCharacterByUrl($seminary['id'], $characterUrl);
+
+ // Media
+ $questmedia = null;
+ if(!is_null($quest['questsmedia_id'])) {
+ $questmedia = $this->Media->getSeminaryMediaById($quest['questsmedia_id']);
+ }
+
+ // Questtype
+ $questtype = $this->Questtypes->getQuesttypeById($quest['questtype_id']);
+
+ // Render Questtype output
+ $output = $this->renderTaskSubmission($questtype['classname'], $seminary, $questgroup, $quest, $character);
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('questgroup', $questgroup);
+ $this->set('quest', $quest);
+ $this->set('character', $character);
+ $this->set('media', $questmedia);
+ $this->set('output', $output);
+ }
+
+
+ /**
+ * Action: create.
+ *
+ * Create a new Quest.
+ *
+ * @param string $seminaryUrl URL-Title of a Seminary
+ */
+ public function create($seminaryUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Quest groups
+ $questgroups = $this->Questgroups->getQuestgroupsForSeminary($seminary['id']);
+
+ // Quest types
+ $questtypes = $this->Questtypes->getQuesttypes();
+
+ // Create Quest
+ $validation = true;
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('create')))
+ {
+ // TODO Validation
+ $name = $this->request->getPostParam('name');
+ $xps = $this->request->getPostParam('xps');
+ $prolog = $this->request->getPostParam('prolog');
+ $entrytext = $this->request->getPostParam('entrytext');
+ $wrongtext = $this->request->getPostParam('wrongtext');
+ $task = $this->request->getPostParam('task');
+
+ // Validate Questgroup
+ $questgroupIndex = null;
+ foreach($questgroups as $index => &$questgroup)
+ {
+ $questgroup['selected'] = ($questgroup['url'] == $this->request->getPostParam('questgroup'));
+ if($questgroup['selected']) {
+ $questgroupIndex = $index;
+ }
+ }
+ if(is_null($questgroupIndex)) {
+ throw new \nre\exceptions\ParamsNotValidException($questgroup);
+ }
+
+ // Validate Questtype
+ $questtypeIndex = null;
+ foreach($questtypes as $index => &$questtype)
+ {
+ $questtype['selected'] = ($questtype['url'] == $this->request->getPostParam('questtype'));
+ if($questtype['selected']) {
+ $questtypeIndex = $index;
+ }
+ }
+ if(is_null($questtypeIndex)) {
+ throw new \nre\exceptions\ParamsNotValidException($questtype);
+ }
+
+ // Questmedia
+ $questmedia = null;
+ if(array_key_exists('questmedia', $_FILES) && !empty($_FILES['questmedia']) && $_FILES['questmedia']['error'] == 0) {
+ $questmedia = $_FILES['questmedia'];
+ }
+
+ // Process Prolog
+ if(!empty($prolog)) {
+ $prolog = preg_split('/\s*(_|=){5,}\s*/', $prolog, -1, PREG_SPLIT_NO_EMPTY);
+ }
+
+ // Create new Quest
+ if($validation === true)
+ {
+ $questId = $this->Quests->createQuest(
+ $this->Auth->getUserId(),
+ $name,
+ $questgroups[$questgroupIndex]['id'],
+ $questtypes[$questtypeIndex]['id'],
+ $xps,
+ $entrytext,
+ $wrongtext,
+ $task
+ );
+
+ // Create Questmedia
+ if(!is_null($questmedia))
+ {
+ $questsmediaId = $this->Media->createQuestMedia(
+ $this->Auth->getUserId(),
+ $seminary['id'],
+ $questmedia['name'],
+ $name,
+ $questmedia['type'],
+ $questmedia['tmp_name']
+ );
+ if($questsmediaId > 0) {
+ $this->Quests->setQuestmedia($questId, $questsmediaId);
+ }
+ }
+
+ // Add Prolog-texts
+ if(!empty($prolog)) {
+ $this->Questtexts->addQuesttextsToQuest($this->Auth->getUserId(), $questId, 'Prolog', $prolog);
+ }
+
+
+ // Redirect
+ $this->redirect($this->linker->link(array('quests', 'index', $seminary['url'])));
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('questgroups', $questgroups);
+ $this->set('questtypes', $questtypes);
+ }
+
+
+ /**
+ * Action: createmedia.
+ * TODO only temporary for easier data import.
+ *
+ * Display a form for creating new Seminary media.
+ *
+ * @param string $seminaryUrl URL-title of Seminary
+ */
+ public function createmedia($seminaryUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Create media
+ $mediaId = null;
+ $error = null;
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('submit')))
+ {
+ $file = $_FILES['file'];
+ $error = $file['error'];
+ if(empty($error))
+ {
+ $mediaId = $this->Media->createQuestMedia(
+ $this->Auth->getUserId(),
+ $seminary['id'],
+ $file['name'],
+ $this->request->getPostParam('description'),
+ $file['type'],
+ $file['tmp_name']
+ );
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('mediaid', $mediaId);
+ }
+
+
+
+
+ /**
+ * Render and handle the task of a Quest.
+ *
+ * @param string $questtypeClassname Name of the class for the Questtype of a Quest
+ * @param array $seminary Seminary data
+ * @param array $questgroup Questgroup data
+ * @param array $quest Quest data
+ * @param array $character Character data
+ * @return string Rendered output
+ */
+ private function renderTask($questtypeClassname, $seminary, $questgroup, $quest, $character)
+ {
+ $task = null;
+ try {
+ // Generate request and response
+ $request = clone $this->request;
+ $response = $this->createQuesttypeResponse('quest', $seminary, $questgroup, $quest, $character);
+
+ // Load Questtype Agent
+ $questtypeAgent = $this->loadQuesttypeAgent($questtypeClassname, $request, $response);
+
+ // Solve Quest
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('submit')))
+ {
+ // Get user answers
+ $answers = $this->request->getPostParam('answers');
+
+ // Save answers in database
+ try {
+ if(!$this->Quests->hasCharacterSolvedQuest($quest['id'], $character['id'])) {
+ $questtypeAgent->saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers);
+ }
+
+ // Match answers with correct ones
+ $status = $questtypeAgent->matchAnswersofCharacter($seminary, $questgroup, $quest, $character, $answers);
+ if($status === true)
+ {
+ // Mark Quest as solved
+ $this->Quests->setQuestSolved($quest['id'], $character['id']);
+
+ // Redirect
+ $this->redirect($this->linker->link(array(), 5, true, array('status'=>'solved'), false, 'task'));
+ }
+ elseif($status === false)
+ {
+ // Mark Quest as unsolved
+ $this->Quests->setQuestUnsolved($quest['id'], $character['id']);
+
+ // Redirect
+ $this->redirect($this->linker->link(array(), 5, true, array('status'=>'unsolved'), false, 'task'));
+ }
+ else {
+ // Mark Quest as submitted
+ $this->Quests->setQuestSubmitted($quest['id'], $character['id']);
+
+ // Redirect
+ $this->redirect($this->linker->link(array(), 5, true, false, 'task'));
+ }
+ }
+ catch(\hhu\z\exceptions\SubmissionNotValidException $e) {
+ $response->addParam($e);
+ }
+ }
+
+ // Render Task
+ $task = $this->runQuesttypeAgent($questtypeAgent, $request, $response);
+ }
+ catch(\nre\exceptions\ViewNotFoundException $e) {
+ $task = $e->getMessage();
+ }
+ catch(\nre\exceptions\ActionNotFoundException $e) {
+ $task = $e->getMessage();
+ }
+ catch(\hhu\z\exceptions\QuesttypeModelNotValidException $e) {
+ $task = $e->getMessage();
+ }
+ catch(\hhu\z\exceptions\QuesttypeModelNotFoundException $e) {
+ $task = $e->getMessage();
+ }
+ catch(\hhu\z\exceptions\QuesttypeControllerNotValidException $e) {
+ $task = $e->getMessage();
+ }
+ catch(\hhu\z\exceptions\QuesttypeControllerNotFoundException $e) {
+ $task = $e->getMessage();
+ }
+ catch(\hhu\z\exceptions\QuesttypeAgentNotValidException $e) {
+ $task = $e->getMessage();
+ }
+ catch(\hhu\z\exceptions\QuesttypeAgentNotFoundException $e) {
+ $task = $e->getMessage();
+ }
+
+
+ // Return rendered output
+ return $task;
+ }
+
+
+ /**
+ * Render and handle a Character submission for a Quest.
+ *
+ * @param string $questtypeClassname Name of the class for the Questtype of a Quest
+ * @param array $seminary Seminary data
+ * @param array $questgroup Questgroup data
+ * @param array $quest Quest data
+ * @param array $character Character data
+ * @return string Rendered output
+ */
+ private function renderTaskSubmission($questtypeClassname, $seminary, $questgroup, $quest, $character)
+ {
+ $task = null;
+ try {
+ // Generate request and response
+ $request = clone $this->request;
+ $response = $this->createQuesttypeResponse('submission', $seminary, $questgroup, $quest, $character);
+
+ // Load Questtype Agent
+ $questtypeAgent = $this->loadQuesttypeAgent($questtypeClassname, $request, $response);
+
+ // Solve Quest
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('submit')))
+ {
+ // Set status
+ if($this->request->getPostParam('submit') == _('solved'))
+ {
+ // Mark Quest as solved
+ $this->Quests->setQuestSolved($quest['id'], $character['id']);
+ }
+ else
+ {
+ // Mark Quest as unsolved
+ $this->Quests->setQuestUnsolved($quest['id'], $character['id']);
+ }
+
+ // Save additional data for Character answers
+ $questtypeAgent->controller->saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $this->request->getPostParam('characterdata'));
+
+ // Redirect
+ $this->redirect($this->linker->link(array('submissions', $seminary['url'], $questgroup['url'], $quest['url']), 1));
+ }
+
+ // Render task submissions
+ $task = $this->runQuesttypeAgent($questtypeAgent, $request, $response);
+ }
+ catch(\nre\exceptions\ViewNotFoundException $e) {
+ $task = $e->getMessage();
+ }
+ catch(\nre\exceptions\ActionNotFoundException $e) {
+ $task = $e->getMessage();
+ }
+ catch(\hhu\z\exceptions\QuesttypeModelNotValidException $e) {
+ $task = $e->getMessage();
+ }
+ catch(\hhu\z\exceptions\QuesttypeModelNotFoundException $e) {
+ $task = $e->getMessage();
+ }
+ catch(\hhu\z\exceptions\QuesttypeControllerNotValidException $e) {
+ $task = $e->getMessage();
+ }
+ catch(\hhu\z\exceptions\QuesttypeControllerNotFoundException $e) {
+ $task = $e->getMessage();
+ }
+ catch(\hhu\z\exceptions\QuesttypeAgentNotValidException $e) {
+ $task = $e->getMessage();
+ }
+ catch(\hhu\z\exceptions\QuesttypeAgentNotFoundException $e) {
+ $task = $e->getMessage();
+ }
+
+
+ // Return rendered output
+ return $task;
+ }
+
+
+ /**
+ * Create a response for the Questtype rendering.
+ *
+ * @param string $action Action to run
+ * @param mixed $param Additional parameters to add to the response
+ * @return Response Generated response
+ */
+ private function createQuesttypeResponse($action, $param1)
+ {
+ // Clone current response
+ $response = clone $this->response;
+ // Clear parameters
+ $response->clearParams(1);
+
+ // Add Action
+ $response->addParams(
+ null,
+ $action
+ );
+
+ // Add additional parameters
+ foreach(array_slice(func_get_args(), 1) as $param) {
+ $response->addParam($param);
+ }
+
+
+ // Return response
+ return $response;
+ }
+
+
+ /**
+ * Load and construct the QuesttypeAgent for a Questtype.
+ *
+ * @param string $questtypeClassname Name of the class for the Questtype of a Quest
+ * @param Request $request Request
+ * @param Response $response Response
+ * @return QuesttypeAgent
+ */
+ private function loadQuesttypeAgent($questtypeClassname, $request, $response)
+ {
+ // Load Agent
+ \hhu\z\QuesttypeAgent::load($questtypeClassname);
+
+
+ // Construct and return Agent
+ return \hhu\z\QuesttypeAgent::factory($questtypeClassname, $request, $response);
+ }
+
+
+ /**
+ * Run and render the Agent for a QuesttypeAgent and return ist output.
+ *
+ * @param Agent $questtypeAgent QuesttypeAgent to run and render
+ * @param Request $request Request
+ * @param Response $response Response
+ * @return string Rendered output
+ */
+ private function runQuesttypeAgent($questtypeAgent, $request, $response)
+ {
+ // Run Agent
+ $questtypeAgent->run($request, $response);
+
+
+ // Render and return output
+ return $questtypeAgent->render();
+ }
+
+ }
+
+?>
diff --git a/controllers/SeminariesController.inc b/controllers/SeminariesController.inc
new file mode 100644
index 00000000..345d4085
--- /dev/null
+++ b/controllers/SeminariesController.inc
@@ -0,0 +1,254 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the Agent to list registered seminaries.
+ *
+ * @author Oliver Hanraths
+ */
+ class SeminariesController extends \hhu\z\controllers\SeminaryController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('seminaries', 'users', 'characterroles', 'questgroupshierarchy', 'questgroups', 'media');
+ /**
+ * User permissions
+ *
+ * @var array
+ */
+ public $permissions = array(
+ 'index' => array('admin', 'moderator', 'user'),
+ 'seminary' => array('admin', 'moderator', 'user'),
+ 'create' => array('admin', 'moderator'),
+ 'edit' => array('admin', 'moderator', 'user'),
+ 'delete' => array('admin', 'moderator', 'user')
+ );
+ /**
+ * User seminary permissions
+ *
+ * @var array
+ */
+ public $seminaryPermissions = array(
+ 'seminary' => array('admin', 'moderator', 'user', 'guest'),
+ 'edit' => array('admin'),
+ 'delete' => array('admin')
+ );
+
+
+
+
+ /**
+ * Action: index.
+ *
+ * List registered seminaries.
+ */
+ public function index()
+ {
+ // Get seminaries
+ $seminaries = $this->Seminaries->getSeminaries();
+
+ // Get additional data
+ foreach($seminaries as &$seminary)
+ {
+ $seminary['description'] = \hhu\z\Utils::shortenString($seminary['description'], 100, 120).' …';
+
+ // Created user
+ $seminary['creator'] = $this->Users->getUserById($seminary['created_user_id']);
+
+ // Character of currently logged-in user
+ try {
+ $seminary['usercharacter'] = $this->Characters->getCharacterForUserAndSeminary($this->Auth->getUserId(), $seminary['id']);
+ $seminary['usercharacter']['characterroles'] = $this->Characterroles->getCharacterrolesForCharacterById($seminary['usercharacter']['id']);
+ }
+ catch(\nre\exceptions\IdNotFoundException $e) {
+ }
+
+ }
+
+
+ // Pass data to view
+ $this->set('seminaries', $seminaries);
+ }
+
+
+ /**
+ * Action: seminary.
+ *
+ * Show a seminary and its details.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of a seminary
+ */
+ public function seminary($seminaryUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Created user
+ $seminary['creator'] = $this->Users->getUserById($seminary['created_user_id']);
+
+ // Get Character
+ $character = $this->Characters->getCharacterForUserAndSeminary($this->Auth->getUserId(), $seminary['id']);
+
+ // Questgrouphierarchy and Questgroups
+ $questgroupshierarchy = $this->Questgroupshierarchy->getHierarchyOfSeminary($seminary['id']);
+ foreach($questgroupshierarchy as &$hierarchy)
+ {
+ // Get Questgroups
+ $hierarchy['questgroups'] = $this->Questgroups->getQuestgroupsForHierarchy($hierarchy['id']);
+
+ // Get additional data
+ foreach($hierarchy['questgroups'] as $i => &$questgroup)
+ {
+ // Check permission of Questgroups
+ if($i >= 1 && count(array_intersect(array('admin','moderator'), SeminaryController::$character['characterroles'])) == 0)
+ {
+ if(!$this->Questgroups->hasCharacterSolvedQuestgroup($hierarchy['questgroups'][$i-1]['id'], $character['id']))
+ {
+ $hierarchy['questgroups'] = array_slice($hierarchy['questgroups'], 0, $i);
+ break;
+ }
+ }
+
+ // Get first Questgroup text
+ $text = $this->Questgroups->getFirstQuestgroupText($questgroup['id']);
+ if(!empty($text))
+ {
+ $text = \hhu\z\Utils::shortenString($text['text'], 100, 120).' …';
+ $questgroup['text'] = $text;
+ }
+
+ // Get cumulated data
+ $data = $this->Questgroups->getCumulatedDataForQuestgroup($questgroup['id'], $character['id']);
+ $questgroup['xps'] = $data['xps'];
+ $questgroup['character_xps'] = $data['character_xps'];
+
+ // Get Media
+ $questgroup['picture'] = null;
+ try {
+ $questgroup['picture'] = $this->Media->getSeminaryMediaById($questgroup['questgroupspicture_id']);
+ }
+ catch(\nre\exceptions\IdNotFoundException $e) {
+ }
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('questgroupshierarchy', $questgroupshierarchy);
+ }
+
+
+ /**
+ * Action: create.
+ *
+ * Create a new seminary.
+ */
+ public function create()
+ {
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('create')))
+ {
+ // Create new seminary
+ $seminaryId = $this->Seminaries->createSeminary(
+ $this->request->getPostParam('title'),
+ $this->Auth->getUserId()
+ );
+
+ // Redirect to seminary
+ $user = $this->Seminaries->getSeminaryById($seminaryId);
+ $this->redirect($this->linker->link(array($seminary['url']), 1));
+ }
+ }
+
+
+ /**
+ * Action: edit.
+ *
+ * Edit a seminary.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of a seminary
+ */
+ public function edit($seminaryUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Check request method
+ if($this->request->getRequestMethod() == 'POST')
+ {
+ // Save changes
+ if(!is_null($this->request->getPostParam('save')))
+ {
+ // Edit seminary
+ $this->Seminaries->editSeminary(
+ $seminary['id'],
+ $this->request->getPostParam('title')
+ );
+ $seminary = $this->Seminaries->getSeminaryById($seminary['id']);
+ }
+
+
+ // Redirect to entry
+ $this->redirect($this->linker->link(array($seminary['url']), 1));
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ }
+
+
+ /**
+ * Action: delete.
+ *
+ * Delete a seminary.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of a seminary
+ */
+ public function delete($seminaryUrl)
+ {
+ // Get seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Check request method
+ if($this->request->getRequestMethod() == 'POST')
+ {
+ // Check confirmation
+ if(!is_null($this->request->getPostParam('delete')))
+ {
+ // Delete seminary
+ $this->Seminaries->deleteSeminary($seminary['id']);
+
+ // Redirect to overview
+ $this->redirect($this->linker->link(null, 1));
+ }
+
+ // Redirect to entry
+ $this->redirect($this->linker->link(array('seminary', $seminary['url']), 1));
+ }
+
+
+ // Show confirmation
+ $this->set('seminary', $seminary);
+ }
+
+ }
+
+?>
diff --git a/controllers/SeminarybarController.inc b/controllers/SeminarybarController.inc
new file mode 100644
index 00000000..8b1e18bc
--- /dev/null
+++ b/controllers/SeminarybarController.inc
@@ -0,0 +1,86 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the Agent to display a sidebar with Seminary related
+ * information.
+ *
+ * @author Oliver Hanraths
+ */
+ class SeminarybarController extends \hhu\z\Controller
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('characters', 'quests', 'questgroups', 'achievements', 'charactergroups', 'avatars', 'media');
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index()
+ {
+ if(is_null(SeminaryController::$seminary)) {
+ return;
+ }
+
+ // Get Seminary
+ $seminary = SeminaryController::$seminary;
+
+ // Get Character
+ $character = SeminaryController::$character;
+ if(is_null($character)) {
+ return;
+ }
+ $character['xplevel'] = $this->Characters->getXPLevelOfCharacters($character['id']);
+ $character['rank'] = $this->Characters->getXPRank($seminary['id'], $character['xps']);
+
+ // Get “last” Quest
+ $lastQuest = $this->Quests->getLastQuestForCharacter($character['id']);
+ if(!is_null($lastQuest)) {
+ $lastQuest['questgroup'] = $this->Questgroups->getQuestgroupById($lastQuest['questgroup_id']);
+ }
+
+ // Get last achieved Achievement
+ $achievements = $this->Achievements->getAchievedAchievementsForCharacter($character['id']);
+ $lastAchievement = array_shift($achievements);
+
+ // Get Character group members
+ $characterGroups = array();
+ foreach($this->Charactergroups->getGroupsForCharacter($character['id']) as $group)
+ {
+ $groupsgroup = $this->Charactergroups->getGroupsgroupById($group['charactergroupsgroup_id']);
+ if($groupsgroup['preferred'])
+ {
+ $group['members'] = $this->Characters->getCharactersForGroup($group['id']);
+ $characterGroups[] = $group;
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('character', $character);
+ $this->set('lastQuest', $lastQuest);
+ $this->set('lastAchievement', $lastAchievement);
+ $this->set('characterGroups', $characterGroups);
+ }
+
+ }
+
+?>
diff --git a/controllers/SeminarymenuController.inc b/controllers/SeminarymenuController.inc
new file mode 100644
index 00000000..30311c93
--- /dev/null
+++ b/controllers/SeminarymenuController.inc
@@ -0,0 +1,52 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the Agent to display a menu with Seminary related
+ * links.
+ *
+ * @author Oliver Hanraths
+ */
+ class SeminarymenuController extends \hhu\z\controllers\SeminaryController
+ {
+
+
+
+
+ /**
+ * Prefilter.
+ *
+ * @param Request $request Current request
+ * @param Response $response Current response
+ */
+ public function preFilter(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ parent::preFilter($request, $response);
+
+ // Set userdata
+ $this->set('loggedUser', self::$user);
+ $this->set('loggedSeminary', self::$seminary);
+ }
+
+
+ /**
+ * Action: index.
+ */
+ public function index()
+ {
+ }
+
+ }
+
+?>
diff --git a/controllers/UploadsController.inc b/controllers/UploadsController.inc
new file mode 100644
index 00000000..4d1da994
--- /dev/null
+++ b/controllers/UploadsController.inc
@@ -0,0 +1,171 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the UploadsAgent to process and show user uploads.
+ *
+ * @author Oliver Hanraths
+ */
+ class UploadsController extends \hhu\z\controllers\SeminaryController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('uploads', 'users', 'userroles', 'characterroles', 'seminaries');
+ /**
+ * User permissions
+ *
+ * @var array
+ */
+ public $permissions = array(
+ 'index' => array('admin', 'moderator', 'user', 'guest')
+ );
+ /**
+ * User seminary permissions
+ *
+ * @var array
+ */
+ public $seminaryPermissions = array(
+ 'seminary' => array('admin', 'moderator', 'user', 'guest')
+ );
+
+
+
+
+ /**
+ * Prefilter.
+ *
+ * @param Request $request Current request
+ * @param Response $response Current response
+ */
+ public function preFilter(\nre\core\Request $request, \nre\core\Response $response)
+ {
+ parent::preFilter($request, $response);
+
+ // Set headers for caching control
+ $response->addHeader("Pragma: public");
+ $response->addHeader("Cache-control: public, max-age=".(60*60*24));
+ $response->addHeader("Expires: ".gmdate('r', time()+(60*60*24)));
+ $response->addHeader("Date: ".gmdate(\DateTime::RFC822));
+ }
+
+
+ /**
+ * Action: seminary.
+ *
+ * Display a Seminary upload.
+ *
+ * @throws AccessDeniedException
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-title of Seminary
+ * @param string $uploadUrl URL-name of the upload
+ */
+ public function seminary($seminaryUrl, $uploadUrl)
+ {
+ // Get Seminary
+ $seminary = $this->Seminaries->getSeminaryByUrl($seminaryUrl);
+
+ // Get Upload
+ $upload = $this->Uploads->getSeminaryuploadByUrl($seminary['id'], $uploadUrl);
+
+ // Check permissions
+ if(!$upload['public'])
+ {
+ $user = $this->Users->getUserById($this->Auth->getUserId());
+ $user['roles'] = array_map(function($r) { return $r['name']; }, $this->Userroles->getUserrolesForUserById($user['id']));
+
+ // System roles
+ if(count(array_intersect(array('admin', 'moderator'), $user['roles'])) == 0)
+ {
+ // Owner of file
+ if($upload['created_user_id'] != $user['id'])
+ {
+ // Seminary permissions
+ $characterRoles = array_map(function($r) { return $r['name']; }, $this->Characterroles->getCharacterrolesForCharacterById($character['id']));
+ if(count(array_intersect(array('admin', 'moderator'), $characterRoles)) == 0) {
+ throw new \nre\exceptions\AccessDeniedException();
+ }
+ }
+ }
+ }
+
+ // Set content-type
+ $this->response->addHeader("Content-type: ".$upload['mimetype']."");
+
+ // Set filename
+ $upload['filename'] = ROOT.DS.\nre\configs\AppConfig::$dirs['seminaryuploads'].DS.$upload['url'];
+ if(!file_exists($upload['filename'])) {
+ throw new \nre\exceptions\IdNotFoundException($uploadUrl);
+ }
+
+ // Cache
+ if($this->setCacheHeaders($upload['filename'])) {
+ return;
+ }
+
+ // Load file
+ $file = file_get_contents($upload['filename']);
+
+
+
+
+ // Pass data to view
+ $this->set('upload', $upload);
+ $this->set('file', $file);
+ }
+
+
+
+
+ /**
+ * Determine file information and set the HTTP-header for
+ * caching accordingly.
+ *
+ * @param string $fileName Filename
+ * @return boolean HTTP-status 304 was set (in cache)
+ */
+ private function setCacheHeaders($fileName)
+ {
+ // Determine last change of file
+ $fileLastModified = gmdate('r', filemtime($fileName));
+
+ // Generate E-Tag
+ $fileEtag = hash('sha256', $fileLastModified.$fileName);
+
+
+ // Set header
+ $this->response->addHeader("Last-Modified: ".$fileLastModified);
+ $this->response->addHeader("Etag: ".$fileEtag);
+ // HTTP-status
+ $headerModifiedSince = $this->request->getServerParam('HTTP_IF_MODIFIED_SINCE');
+ $headerNoneMatch = $this->request->getServerParam('HTTP_IF_NONE_MATCH');
+ if(
+ !is_null($headerModifiedSince) && $fileLastModified < strtotime($headerModifiedSince) &&
+ !is_null($headerNoneMatch) && $headerNoneMatch == $fileEtag
+ ) {
+ $this->response->setExit(true);
+ $this->response->addHeader(\nre\core\WebUtils::getHttpHeader(304));
+
+ return true;
+ }
+
+
+ return false;
+ }
+
+ }
+
+?>
diff --git a/controllers/UserrolesController.inc b/controllers/UserrolesController.inc
new file mode 100644
index 00000000..80c00347
--- /dev/null
+++ b/controllers/UserrolesController.inc
@@ -0,0 +1,47 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the Agent to display and manage userroles.
+ *
+ * @author Oliver Hanraths
+ */
+ class UserrolesController extends \hhu\z\Controller
+ {
+
+
+
+
+ /**
+ * Action: user.
+ *
+ * Show a user and its details.
+ *
+ * @throws IdNotFoundException
+ * @param string $userUrl URL-Username of an user
+ */
+ public function user($userUrl)
+ {
+ // Get userroles
+ $roles = $this->Userroles->getUserrolesForUserByUrl($userUrl);
+
+
+ // Pass data to view
+ $this->set('roles', $roles);
+ }
+
+
+ }
+
+?>
diff --git a/controllers/UsersController.inc b/controllers/UsersController.inc
new file mode 100644
index 00000000..70ab8904
--- /dev/null
+++ b/controllers/UsersController.inc
@@ -0,0 +1,371 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers;
+
+
+ /**
+ * Controller of the Agent to list registered users and their data.
+ *
+ * @author Oliver Hanraths
+ */
+ class UsersController extends \hhu\z\controllers\IntermediateController
+ {
+ /**
+ * User permissions
+ *
+ * @var array
+ */
+ public $permissions = array(
+ 'index' => array('admin', 'moderator'),
+ 'user' => array('admin', 'moderator', 'user'),
+ 'create' => array('admin', 'moderator'),
+ 'edit' => array('admin', 'moderator'),
+ 'delete' => array('admin')
+ );
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('users', 'characters', 'avatars', 'media', 'characterroles');
+ /**
+ * Required components
+ *
+ * @var array
+ */
+ public $components = array('validation');
+
+
+
+
+ /**
+ * Action: index.
+ */
+ public function index()
+ {
+ // Get registered users
+ $users = $this->Users->getUsers();
+
+
+ // Pass data to view
+ $this->set('users', $users);
+ }
+
+
+ /**
+ * Action: user.
+ *
+ * Show a user and its details.
+ *
+ * @throws IdNotFoundException
+ * @throws AccessDeniedException
+ * @param string $userUrl URL-Username of an user
+ */
+ public function user($userUrl)
+ {
+ // Get user
+ $user = $this->Users->getUserByUrl($userUrl);
+
+ // Check permissions
+ if(count(array_intersect(array('admin','moderator'), \hhu\z\controllers\IntermediateController::$user['roles'])) == 0 && $user['id'] != IntermediateController::$user['id']) {
+ throw new \nre\exceptions\AccessDeniedException();
+ }
+
+ // Get Characters
+ $characters = $this->Characters->getCharactersForUser($user['id']);
+
+ // Additional Character information
+ foreach($characters as &$character)
+ {
+ // Seminary roles
+ $character['characterroles'] = $this->Characterroles->getCharacterrolesForCharacterById($character['id']);
+ $character['characterroles'] = array_map(function($a) { return $a['name']; }, $character['characterroles']);
+
+ // Level
+ $character['xplevel'] = $this->Characters->getXPLevelOfCharacters($character['id']);
+
+ // Avatar
+ $avatar = $this->Avatars->getAvatarById($character['avatar_id']);
+ if(!is_null($avatar['small_avatarpicture_id']))
+ {
+ //$character['seminary'] =
+ $character['small_avatar'] = $this->Media->getSeminaryMediaById($avatar['small_avatarpicture_id']);
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('user', $user);
+ $this->set('characters', $characters);
+ }
+
+
+ /**
+ * Action: login.
+ *
+ * Log in a user.
+ */
+ public function login()
+ {
+ $username = '';
+ $referrer = null;
+
+ // Log the user in
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('login')))
+ {
+ $username = $this->request->getPostParam('username');
+ $referrer = $this->request->getPostParam('referrer');
+ $userId = $this->Users->login(
+ $username,
+ $this->request->getPostParam('password')
+ );
+
+ if(!is_null($userId))
+ {
+ $this->Auth->setUserId($userId);
+ $user = $this->Users->getUserById($userId);
+
+ if(!empty($referrer)) {
+ $this->redirect($referrer);
+ }
+ else {
+ $this->redirect($this->linker->link(array($user['url']), 1));
+ }
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('username', $username);
+ $this->set('referrer', $referrer);
+ $this->set('failed', ($this->request->getRequestMethod() == 'POST'));
+ }
+
+
+ /**
+ * Action: register.
+ *
+ * Register a new user.
+ */
+ public function register()
+ {
+ $username = '';
+ $prename = '';
+ $surname = '';
+ $email = '';
+
+ $fields = array('username', 'prename', 'surname', 'email', 'password');
+ $validation = array();
+
+ // Register a new user
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('register')))
+ {
+ // Get params and validate them
+ $validation = $this->Validation->validateParams($this->request->getPostParams(), $fields);
+ $username = $this->request->getPostParam('username');
+ if($this->Users->usernameExists($username)) {
+ $validation = $this->Validation->addValidationResult($validation, 'username', 'exist', true);
+ }
+ $prename = $this->request->getPostParam('prename');
+ $surname = $this->request->getPostParam('surname');
+ $email = $this->request->getPostParam('email');
+ if($this->Users->emailExists($email)) {
+ $validation = $this->Validation->addValidationResult($validation, 'email', 'exist', true);
+ }
+
+
+ // Register
+ if($validation === true)
+ {
+ $userId = $this->Users->createUser(
+ $username,
+ $prename,
+ $surname,
+ $email,
+ $this->request->getPostParam('password')
+ );
+
+ // Send mail
+ $this->sendRegistrationMail($username, $email);
+
+ // Login
+ $this->Auth->setUserId($userId);
+ $user = $this->Users->getUserById($userId);
+
+ // Redirect to user page
+ $this->redirect($this->linker->link(array($user['url']), 1));
+ }
+ }
+
+ // Get validation settings
+ $validationSettings = array();
+ foreach($fields as &$field) {
+ $validationSettings[$field] = \nre\configs\AppConfig::$validation[$field];
+ }
+
+
+ // Pass data to view
+ $this->set('username', $username);
+ $this->set('prename', $prename);
+ $this->set('surname', $surname);
+ $this->set('email', $email);
+ $this->set('validation', $validation);
+ $this->set('validationSettings', $validationSettings);
+ }
+
+
+ /**
+ * Action: logout.
+ *
+ * Log out a user.
+ */
+ public function logout()
+ {
+ // Unset the currently logged in user
+ $this->Auth->setUserId(null);
+
+ // Redirect
+ $this->redirect($this->linker->link(array()));
+ }
+
+
+ /**
+ * Action: create.
+ *
+ * Create a new user.
+ */
+ public function create()
+ {
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('create')))
+ {
+ // Create new user
+ $userId = $this->Users->createUser(
+ $this->request->getPostParam('username'),
+ $this->request->getPostParam('prename'),
+ $this->request->getPostParam('surname'),
+ $this->request->getPostParam('email'),
+ $this->request->getPostParam('password')
+ );
+
+ // Redirect to user
+ $user = $this->Users->getUserById($userId);
+ $this->redirect($this->linker->link(array($user['url']), 1));
+ }
+ }
+
+
+ /**
+ * Action: edit.
+ *
+ * Edit a user.
+ *
+ * @throws IdNotFoundException
+ * @param string $userUrl URL-Username of an user
+ */
+ public function edit($userUrl)
+ {
+ // User
+ $user = $this->Users->getUserByUrl($userUrl);
+
+ // Check request method
+ if($this->request->getRequestMethod() == 'POST')
+ {
+ // Save changes
+ if(!is_null($this->request->getPostParam('save')))
+ {
+ // Edit user
+ $this->Users->editUser(
+ $user['id'],
+ $this->request->getPostParam('username'),
+ $this->request->getPostParam('prename'),
+ $this->request->getPostParam('surname'),
+ $this->request->getPostParam('email'),
+ $this->request->getPostParam('password')
+ );
+ $user = $this->Users->getUserById($user['id']);
+ }
+
+
+ // Redirect to entry
+ $this->redirect($this->linker->link(array($user['url']), 1));
+ }
+
+
+ // Pass data to view
+ $this->set('user', $user);
+ }
+
+
+ /**
+ * Action: delete.
+ *
+ * Delete a user.
+ *
+ * @throws IdNotFoundException
+ * @param string $userUrl URL-Username of an user
+ */
+ public function delete($userUrl)
+ {
+ // User
+ $user = $this->Users->getUserByUrl($userUrl);
+
+ // Check request method
+ if($this->request->getRequestMethod() == 'POST')
+ {
+ // Check confirmation
+ if(!is_null($this->request->getPostParam('delete')))
+ {
+ // Delete user
+ $this->Users->deleteUser($user['id']);
+
+ // Redirect to overview
+ $this->redirect($this->linker->link(null, 1));
+ }
+
+ // Redirect to entry
+ $this->redirect($this->linker->link(array('user', $user['url']), 1));
+ }
+
+
+ // Show confirmation
+ $this->set('user', $user);
+ }
+
+
+
+
+ /**
+ * Send mail for new user registration.
+ *
+ * @param string $username Name of newly registered user
+ * @param string $email E‑mail address of newly registered user
+ */
+ private function sendRegistrationMail($username, $email)
+ {
+ $sender = \nre\configs\AppConfig::$app['mailsender'];
+ if(empty($sender)) {
+ return;
+ }
+
+ // Send notification mail to system moderators
+ $subject = sprintf('new user registration: %s', $username);
+ $message = sprintf('User “%s” <%s> has registered themself to %s', $username, $email, \nre\configs\AppConfig::$app['name']);
+ $moderators = $this->Users->getUsersWithRole('moderator');
+ foreach($moderators as &$moderator)
+ {
+ \hhu\z\Utils::sendMail($sender, $moderator['email'], $subject, $message);
+ }
+ }
+
+ }
+
+?>
diff --git a/controllers/components/AchievementComponent.inc b/controllers/components/AchievementComponent.inc
new file mode 100644
index 00000000..70601543
--- /dev/null
+++ b/controllers/components/AchievementComponent.inc
@@ -0,0 +1,41 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers\components;
+
+
+ /**
+ * Component to handle achievements.
+ *
+ * @author Oliver Hanraths
+ */
+ class AchievementComponent extends \nre\core\Component
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('achievements');
+
+
+
+
+ /**
+ * Construct a new Achievements-component.
+ */
+ public function __construct()
+ {
+ }
+
+ }
+
+?>
diff --git a/controllers/components/AuthComponent.inc b/controllers/components/AuthComponent.inc
new file mode 100644
index 00000000..0db6c69d
--- /dev/null
+++ b/controllers/components/AuthComponent.inc
@@ -0,0 +1,79 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers\components;
+
+
+ /**
+ * Component to handle authentication and authorization.
+ *
+ * @author Oliver Hanraths
+ */
+ class AuthComponent extends \nre\core\Component
+ {
+ /**
+ * Key to save a user-ID as
+ *
+ * @var string
+ */
+ const KEY_USER_ID = 'user_id';
+
+
+
+
+ /**
+ * Construct a new Auth-component.
+ */
+ public function __construct()
+ {
+ // Start session
+ if(session_id() === '') {
+ session_start();
+ }
+ }
+
+
+
+
+ /**
+ * Set the ID of the user that is currently logged in.
+ *
+ * @param int $userId ID of the currently logged in user
+ */
+ public function setUserId($userId)
+ {
+ if(is_null($userId)) {
+ unset($_SESSION[self::KEY_USER_ID]);
+ }
+ else {
+ $_SESSION[self::KEY_USER_ID] = $userId;
+ }
+ }
+
+
+ /**
+ * Get the ID of the user that is currently logged in.
+ *
+ * @return int ID of the currently logged in user
+ */
+ public function getUserId()
+ {
+ if(array_key_exists(self::KEY_USER_ID, $_SESSION)) {
+ return $_SESSION[self::KEY_USER_ID];
+ }
+
+
+ return null;
+ }
+
+ }
+
+?>
diff --git a/controllers/components/ValidationComponent.inc b/controllers/components/ValidationComponent.inc
new file mode 100644
index 00000000..950d45ed
--- /dev/null
+++ b/controllers/components/ValidationComponent.inc
@@ -0,0 +1,140 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\controllers\components;
+
+
+ /**
+ * Component to validate user input.
+ *
+ * @author Oliver Hanraths
+ */
+ class ValidationComponent extends \nre\core\Component
+ {
+ /**
+ * Validation settings
+ *
+ * @var array
+ */
+ private $config;
+
+
+
+
+ /**
+ * Construct a new Validation-component.
+ */
+ public function __construct()
+ {
+ // Get validation settings from configuration
+ $this->config = \nre\configs\AppConfig::$validation;
+ }
+
+
+
+
+ /**
+ * Validate an user input.
+ *
+ * @param mixed $input User input to validate
+ * @param array $settings Validation setting
+ * @return mixed True or the settings the validation fails on
+ */
+ public function validate($input, $settings)
+ {
+ $validation = array();
+
+ // Min string length
+ if(array_key_exists('minlength', $settings) && strlen($input) < $settings['minlength']) {
+ $validation['minlength'] = $settings['minlength'];
+ }
+ // Max string length
+ if(array_key_exists('maxlength', $settings) && strlen($input) > $settings['maxlength']) {
+ $validation['maxlength'] = $settings['maxlength'];
+ }
+
+ // Regex
+ if(array_key_exists('regex', $settings) && !preg_match($settings['regex'], $input)) {
+ $validation['regex'] = $settings['regex'];
+ }
+
+
+ // Return true or the failed fields
+ if(empty($validation)) {
+ return true;
+ }
+ return $validation;
+ }
+
+
+ /**
+ * Validate user input parameters.
+ *
+ * @param array $params User input parameters
+ * @param array $indices Names of parameters to validate and to validate against
+ * @return mixed True or the parameters with settings the validation failed on
+ */
+ public function validateParams($params, $indices)
+ {
+ $validation = array();
+ foreach($indices as $index)
+ {
+ if(!array_key_exists($index, $params)) {
+ throw new \nre\exceptions\ParamsNotValidException($index);
+ }
+
+ // Check parameter
+ if(array_key_exists($index, $this->config))
+ {
+ $param = $params[$index];
+ $check = $this->validate($param, $this->config[$index]);
+ if($check !== true) {
+ $validation[$index] = $check;
+ }
+ }
+ }
+
+
+ // Return true or the failed parameters with failed settings
+ if(empty($validation)) {
+ return true;
+ }
+ return $validation;
+ }
+
+
+ /**
+ * Add a custom determined validation result to a validation
+ * store.
+ *
+ * @param mixed $validation Validation store to add result to
+ * @param string $param Name of parameter of the custom validation result
+ * @param string $setting Name of setting of the custom validation result
+ * @param mixed $result Validation result
+ * @return mixed The altered validation store
+ */
+ public function addValidationResult($validation, $param, $setting, $result)
+ {
+ if(!is_array($validation)) {
+ $validation = array();
+ }
+ if(!array_key_exists($param, $validation)) {
+ $validation[$param] = array();
+ }
+ $validation[$param][$setting] = $result;
+
+
+ return $validation;
+ }
+
+ }
+
+?>
diff --git a/core/Agent.inc b/core/Agent.inc
new file mode 100644
index 00000000..00ec1a90
--- /dev/null
+++ b/core/Agent.inc
@@ -0,0 +1,607 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Abstract class for the implementation af an Agent.
+ *
+ * @author coderkun
+ */
+ abstract class Agent
+ {
+ /**
+ * Name of BottomlevelAgent for showing inline errors
+ *
+ * @var string
+ */
+ const INLINEERROR_AGENT = 'inlineerror';
+
+ /**
+ * Current request
+ *
+ * @var Request
+ */
+ private $request;
+ /**
+ * Current response
+ *
+ * @var Response
+ */
+ private $response;
+ /**
+ * Log-system
+ *
+ * @var Logger
+ */
+ protected $log;
+ /**
+ * SubAgents
+ *
+ * @var array
+ */
+ protected $subAgents = array();
+ /**
+ * Controller of this Agent
+ *
+ * @var Controller
+ */
+ public $controller = null;
+
+
+
+
+ /**
+ * Load the class of an Agent.
+ *
+ * @static
+ * @throws AgentNotFoundException
+ * @throws AgentNotValidException
+ * @param string $agentName Name of the Agent to load
+ */
+ public static function load($agentName)
+ {
+ // Determine full classname
+ $agentType = self::getAgentType();
+ $className = self::getClassName($agentName, $agentType);
+
+ try {
+ // Load class
+ ClassLoader::load($className);
+
+ // Validate class
+ $parentAgentClassName = ClassLoader::concatClassNames($agentType, 'agent');
+ $parentAgentClassName = "\\nre\\agents\\$parentAgentClassName";
+ ClassLoader::check($className, $parentAgentClassName);
+ }
+ catch(\nre\exceptions\ClassNotValidException $e) {
+ throw new \nre\exceptions\AgentNotValidException($e->getClassName());
+ }
+ catch(\nre\exceptions\ClassNotFoundException $e) {
+ throw new \nre\exceptions\AgentNotFoundException($e->getClassName());
+ }
+ }
+
+
+
+ /**
+ * Instantiate an Agent (Factory Pattern).
+ *
+ * @static
+ * @throws DatamodelException
+ * @throws DriverNotValidException
+ * @throws DriverNotFoundException
+ * @throws ViewNotFoundException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws ControllerNotValidException
+ * @throws ControllerNotFoundException
+ * @param string $agentName Name of the Agent to instantiate
+ * @param Request $request Current request
+ * @param Response $respone Current respone
+ * @param Logger $log Log-system
+ */
+ public static function factory($agentName, Request $request, Response $response, Logger $log=null)
+ {
+ // Determine full classname
+ $agentType = self::getAgentType();
+ $className = self::getClassName($agentName, $agentType);
+
+
+ // Construct and return Agent
+ return new $className($request, $response, $log);
+ }
+
+
+ /**
+ * Determine the type of an Agent.
+ *
+ * @static
+ * @return string Agent type
+ */
+ private static function getAgentType()
+ {
+ return strtolower(ClassLoader::getClassName(get_called_class()));
+ }
+
+
+ /**
+ * Determine the classname for the given Agent name.
+ *
+ * @static
+ * @param string $agentName Agent name to get classname of
+ * @param string $agentType Agent type of given Agent name
+ * @return string Classname for the Agent name
+ */
+ private static function getClassName($agentName, $agentType)
+ {
+ $className = ClassLoader::concatClassNames($agentName, 'agent');
+
+
+ return \nre\configs\AppConfig::$app['namespace']."agents\\$agentType\\$className";
+ }
+
+
+
+
+ /**
+ * Construct a new Agent.
+ *
+ * @throws DatamodelException
+ * @throws DriverNotValidException
+ * @throws DriverNotFoundException
+ * @throws ViewNotFoundException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws ControllerNotValidException
+ * @throws ControllerNotFoundException
+ * @param Request $request Current request
+ * @param Response $respone Current response
+ * @param Logger $log Log-system
+ */
+ protected function __construct(Request $request, Response $response, Logger $log=null)
+ {
+ // Store values
+ $this->request = $request;
+ $this->response = $response;
+ $this->log = $log;
+
+ // Construct SubAgent
+ $this->actionConstruct();
+
+ // Load corresponding Controller
+ $this->loadController();
+ }
+
+
+
+
+ /**
+ * Run the Controller of this Agent and its SubAgents.
+ *
+ * @throws ParamsNotValidException
+ * @throws IdNotFoundException
+ * @throws DatamodelException
+ * @throws ActionNotFoundException
+ * @param Request $request Current request
+ * @param Response $response Current response
+ * @return Exception Last occurred exception of SubAgents
+ */
+ public function run(Request $request, Response $response)
+ {
+ // Check Controller
+ if(!is_null($this->controller))
+ {
+ // Call prefilter
+ $this->controller->preFilter($request, $response);
+
+ // Run controller
+ $this->controller->run($request, $response);
+
+ // Call postfilter
+ $this->controller->postFilter($request, $response);
+ }
+
+
+ // Run SubAgents
+ $exception = null;
+ foreach($this->subAgents as &$subAgent)
+ {
+ try {
+ $subAgent['object']->run(
+ $request,
+ $subAgent['response']
+ );
+ }
+ catch(ParamsNotValidException $e) {
+ $subAgent = $this->errorInline($subAgent, $request, $e);
+ }
+ catch(IdNotFoundException $e) {
+ $subAgent = $this->errorInline($subAgent, $request, $e);
+ }
+ catch(DatamodelException $e) {
+ $exception = $e;
+ $subAgent = $this->errorInline($subAgent, $request, $e);
+ }
+ catch(ActionNotFoundException $e) {
+ $subAgent = $this->errorInline($subAgent, $request, $e);
+ }
+ }
+
+
+ // Return last occurred exception
+ return $exception;
+ }
+
+
+ /**
+ * Generate output of the Controller of this Agent and its
+ * SubAgents.
+ *
+ * @param array $data View data
+ * @return string Generated output
+ */
+ public function render($data=array())
+ {
+ // Check Controller
+ if(!is_null($this->controller))
+ {
+ // Render SubAgents
+ foreach($this->subAgents as $subAgent)
+ {
+ $label = array_key_exists('label', $subAgent) ? $subAgent['label'] : $subAgent['name'];
+ $data[$label] = $this->renderSubAgent($subAgent);
+ }
+
+ // Render the Controller of this agent
+ return $this->controller->render($data);
+ }
+ }
+
+
+
+
+ /**
+ * Construct SubAgents (per Action).
+ */
+ protected function actionConstruct()
+ {
+ // Action ermitteln
+ $action = $this->response->getParam(2);
+ if(is_null($action)) {
+ $action = $this->request->getParam(2, 'action');
+ $this->response->addParam($action);
+ }
+
+ // Initialisierungsmethode für diese Action ausführen
+ if(method_exists($this, $action))
+ {
+ call_user_func_array(
+ array(
+ $this,
+ $action
+ ),
+ array(
+ $this->request,
+ $this->response
+ )
+ );
+ }
+ }
+
+
+ /**
+ * Load the Controller of this Agent.
+ *
+ * @throws DatamodelException
+ * @throws DriverNotValidException
+ * @throws DriverNotFoundException
+ * @throws ViewNotFoundException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws ControllerNotValidException
+ * @throws ControllerNotFoundException
+ */
+ protected function loadController()
+ {
+ // Determine Controller name
+ $controllerName = ClassLoader::getClassName(get_class($this));
+
+ // Determine ToplevelAgent
+ $toplevelAgentName = $this->response->getParam(0);
+ if(is_null($toplevelAgentName)) {
+ $toplevelAgentName = $this->request->getParam(0, 'toplevel');
+ $this->response->addParam($toplevelAgentName);
+ }
+
+ // Determine Action
+ $action = $this->response->getParam(2);
+ if(is_null($action)) {
+ $action = $this->request->getParam(2, 'action');
+ $this->response->addParam($action);
+ }
+
+
+ // Load Controller
+ Controller::load($controllerName);
+
+ // Construct Controller
+ $this->controller = Controller::factory($controllerName, $toplevelAgentName, $action, $this);
+ }
+
+
+ /**
+ * Log an error.
+ *
+ * @param Exception $exception Occurred exception
+ * @param int $logMode Log mode
+ */
+ protected function log($exception, $logMode)
+ {
+ if(is_null($this->log)) {
+ return;
+ }
+
+ $this->log->log(
+ $exception->getMessage(),
+ $logMode
+ );
+ }
+
+
+ /**
+ * Load a SubAgent and add it.
+ *
+ * @throws ServiceUnavailableException
+ * @throws AgentNotFoundException
+ * @throws AgentNotValidException
+ * @param string $agentName Name of the Agent to load
+ * @param mixed … Additional parameters for the agent
+ */
+ protected function addSubAgent($agentName)
+ {
+ try {
+ call_user_func_array(
+ array(
+ $this,
+ '_addSubAgent'
+ ),
+ func_get_args()
+ );
+ }
+ catch(DatamodelException $e) {
+ $this->subAgents[] = $this->newInlineError($agentName, $e);
+ }
+ }
+
+
+ /**
+ * Load a SubAgent and add it.
+ *
+ * @throws ServiceUnavailableException
+ * @throws DatamodelException
+ * @throws AgentNotFoundException
+ * @throws AgentNotValidException
+ * @param string $agentName Name of the Agent to load
+ * @param mixed … Additional parameters for the agent
+ */
+ protected function _addSubAgent($agentName)
+ {
+ try {
+ // Load Agent
+ \nre\agents\BottomlevelAgent::load($agentName);
+
+ // Construct Agent
+ $this->subAgents[] = call_user_func_array(
+ array(
+ $this,
+ 'newSubAgent'
+ ),
+ func_get_args()
+ );
+ }
+ catch(ViewNotFoundException $e) {
+ $this->subAgents[] = $this->newInlineError($agentName, $e);
+ }
+ catch(DriverNotValidException $e) {
+ $this->subAgents[] = $this->newInlineError($agentName, $e);
+ }
+ catch(DriverNotFoundException $e) {
+ $this->subAgents[] = $this->newInlineError($agentName, $e);
+ }
+ catch(ModelNotValidException $e) {
+ $this->subAgents[] = $this->newInlineError($agentName, $e);
+ }
+ catch(ModelNotFoundException $e) {
+ $this->subAgents[] = $this->newInlineError($agentName, $e);
+ }
+ catch(ControllerNotValidException $e) {
+ $this->subAgents[] = $this->newInlineError($agentName, $e);
+ }
+ catch(ControllerNotFoundException $e) {
+ $this->subAgents[] = $this->newInlineError($agentName, $e);
+ }
+ catch(AgentNotValidException $e) {
+ $this->subAgents[] = $this->newInlineError($agentName, $e);
+ }
+ catch(AgentNotFoundException $e) {
+ $this->subAgents[] = $this->newInlineError($agentName, $e);
+ }
+ }
+
+
+
+
+ /**
+ * Create a new SubAgent.
+ *
+ * @throws DatamodelException
+ * @param string $agentName Agent name
+ * @return array SubAgent
+ */
+ private function newSubAgent($agentName)
+ {
+ // Response
+ $response = clone $this->response;
+ $response->clearParams(1);
+ $params = func_get_args();
+ if(count($params) < 2 || empty($params[1])) {
+ $params[1] = \nre\configs\CoreConfig::$defaults['action'];
+ }
+ call_user_func_array(
+ array(
+ $response,
+ 'addParams'
+ ),
+ $params
+ );
+
+ return array(
+ 'name' => strtolower($agentName),
+ 'response' => $response,
+ 'object' => \nre\agents\BottomlevelAgent::factory(
+ $agentName,
+ $this->request,
+ $response,
+ $this->log
+ )
+ );
+ }
+
+
+ /**
+ * Render a SubAgent.
+ *
+ * @param array $subAgent SubAgent to render
+ * @return string Generated output
+ */
+ private function renderSubAgent(&$subAgent)
+ {
+ // Check for InlineError
+ if(array_key_exists('inlineerror', $subAgent) && !empty($subAgent['inlineerror'])) {
+ return file_get_contents($subAgent['inlineerror']);
+ }
+
+
+ // Rendern SubAgent and return its output
+ return $subAgent['object']->render();
+ }
+
+
+ /**
+ * Handle the exception of a SubAgent.
+ *
+ * @param string $label Name of the original Agent
+ * @param Excepiton $exception Occurred exception
+ * @return array InlineError-SubAgent
+ */
+ private function errorInline($subAgent, $request, $exception)
+ {
+ // Create the SubAgent for the exception
+ $subAgent = $this->newInlineError($subAgent['name'], $exception);
+
+
+ // Run the InlineError-SubAgent
+ try {
+ $subAgent['object']->run(
+ $request,
+ $subAgent['response']
+ );
+ }
+ catch(ActionNotFoundException $e) {
+ $this->log($e, Logger::LOGMODE_AUTO);
+ $subAgent['inlineerror'] = $this->newInlineErrorService();
+ }
+
+
+ // Return the InlineError-SubAgent
+ return $subAgent;
+ }
+
+
+ /**
+ * Create a new InlineError.
+ *
+ * @param string $label Name of the original Agent
+ * @param Exception $exception Occurred exception
+ */
+ private function newInlineError($label, $exception)
+ {
+ // Log error
+ $this->log($exception, Logger::LOGMODE_AUTO);
+
+ // Determine Agent name
+ $agentName = self::INLINEERROR_AGENT;
+
+ // Create SugAgent
+ $subAgent = array();
+
+
+ try {
+ // Load Agenten
+ \nre\agents\BottomlevelAgent::load($agentName);
+
+ // Construct Agent
+ $subAgent = $this->newSubAgent($agentName);
+ $subAgent['label'] = $label;
+ $subAgent['response']->addParam($exception);
+ }
+ catch(ViewNotFoundException $e) {
+ $subAgent['inlineerror'] = $this->newInlineErrorService();
+ }
+ catch(DatamodelException $e) {
+ $subAgent['inlineerror'] = $this->newInlineErrorService();
+ }
+ catch(DriverNotValidException $e) {
+ $subAgent['inlineerror'] = $this->newInlineErrorService();
+ }
+ catch(DriverNotFoundException $e) {
+ $subAgent['inlineerror'] = $this->newInlineErrorService();
+ }
+ catch(ModelNotValidException $e) {
+ $subAgent['inlineerror'] = $this->newInlineErrorService();
+ }
+ catch(ModelNotFoundException $e) {
+ $subAgent['inlineerror'] = $this->newInlineErrorService();
+ }
+ catch(ControllerNotValidException $e) {
+ $subAgent['inlineerror'] = $this->newInlineErrorService();
+ }
+ catch(ControllerNotFoundException $e) {
+ $subAgent['inlineerror'] = $this->newInlineErrorService();
+ }
+ catch(AgentNotValidException $e) {
+ $subAgent['inlineerror'] = $this->newInlineErrorService();
+ }
+ catch(AgentNotFoundException $e) {
+ $subAgent['inlineerror'] = $this->newInlineErrorService();
+ }
+
+
+ // Return SubAgent
+ return $subAgent;
+ }
+
+
+ /**
+ * Handle a hardcore error that could not be handled by the
+ * system.
+ */
+ private function newInlineErrorService()
+ {
+ // Read and return static error file
+ return ROOT.DS.\nre\configs\CoreConfig::getClassDir('views').DS.\nre\configs\CoreConfig::$defaults['inlineErrorFile'].\nre\configs\Config::getFileExt('views');
+ }
+
+ }
+
+?>
diff --git a/core/Api.inc b/core/Api.inc
new file mode 100644
index 00000000..b89b12e7
--- /dev/null
+++ b/core/Api.inc
@@ -0,0 +1,163 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Abstract class to implement an API.
+ *
+ * The API is the center of each application and specifies how and what
+ * to run and render.
+ *
+ * @author coderkun
+ */
+ abstract class Api
+ {
+ /**
+ * Die aktuelle Anfrage
+ *
+ * @var Request
+ */
+ protected $request;
+ /**
+ * Der Toplevelagent
+ *
+ * @var ToplevelAgent
+ */
+ private $toplevelAgent = null;
+ /**
+ * Die aktuelle Antwort
+ *
+ * @var Response
+ */
+ protected $response;
+ /**
+ * Log-System
+ *
+ * @var Logger
+ */
+ protected $log;
+
+
+
+
+ /**
+ * Construct a new API.
+ *
+ * @param Request $request Current request
+ * @param Response $respone Current response
+ */
+ public function __construct(Request $request, Response $response)
+ {
+ // Store request
+ $this->request = $request;
+
+ // Store response
+ $this->response = $response;
+
+ // Init logging
+ $this->log = new \nre\core\Logger();
+ }
+
+
+
+
+ /**
+ * Run the application.
+ *
+ * @throws DatamodelException
+ * @throws DriverNotValidException
+ * @throws DriverNotFoundException
+ * @throws ViewNotFoundException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws ControllerNotValidException
+ * @throws ControllerNotFoundException
+ * @throws AgentNotValidException
+ * @throws AgentNotFoundException
+ * @return Exception Last occurred exception of an subagent
+ */
+ public function run()
+ {
+ // Load ToplevelAgent
+ $this->loadToplevelAgent();
+
+ // Run ToplevelAgent
+ return $this->toplevelAgent->run($this->request, $this->response);
+ }
+
+
+ /**
+ * Render the output.
+ */
+ public function render()
+ {
+ // Check exit-status
+ if($this->response->getExit()) {
+ return;
+ }
+
+ // Render ToplevelAgent
+ $this->response->setOutput($this->toplevelAgent->render());
+ }
+
+
+
+
+ /**
+ * Log an exception
+ *
+ * @param Exception $exception Occurred exception
+ * @param int $logMode Log-mode
+ */
+ protected function log($exception, $logMode)
+ {
+ $this->log->log(
+ $exception->getMessage(),
+ $logMode
+ );
+ }
+
+
+
+
+ /**
+ * Load the ToplevelAgent specified by the request.
+ *
+ * @throws ServiceUnavailableException
+ * @throws AgentNotValidException
+ * @throws AgentNotFoundException
+ */
+ private function loadToplevelAgent()
+ {
+ // Determine agent
+ $agentName = $this->response->getParam(0);
+ if(is_null($agentName)) {
+ $agentName = $this->request->getParam(0, 'toplevel');
+ $this->response->addParam($agentName);
+ }
+
+ // Load agent
+ \nre\agents\ToplevelAgent::load($agentName);
+
+ // Construct agent
+ $this->toplevelAgent = \nre\agents\ToplevelAgent::factory(
+ $agentName,
+ $this->request,
+ $this->response,
+ $this->log
+ );
+ }
+
+ }
+
+?>
diff --git a/core/Autoloader.inc b/core/Autoloader.inc
new file mode 100644
index 00000000..020b61f7
--- /dev/null
+++ b/core/Autoloader.inc
@@ -0,0 +1,98 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Autoloader.
+ *
+ * This class tries to load not yet used classes.
+ *
+ * @author coderkun
+ */
+ class Autoloader
+ {
+ /**
+ * Private construct().
+ */
+ private function __construct() {}
+
+ /**
+ * Private clone().
+ */
+ private function __clone() {}
+
+
+
+
+ /**
+ * Register load-method.
+ */
+ public static function register()
+ {
+ spl_autoload_register(
+ array(
+ get_class(),
+ 'load'
+ )
+ );
+ }
+
+
+ /**
+ * Look for the given class and try to load it.
+ *
+ * @param string $fullClassName Die zu ladende Klasse
+ */
+ public static function load($fullClassName)
+ {
+ $fullClassNameA = explode('\\', $fullClassName);
+
+ if(strpos($fullClassName, \nre\configs\CoreConfig::$core['namespace']) !== 0)
+ {
+ // App
+ $className = array_slice($fullClassNameA, substr_count(\nre\configs\AppConfig::$app['namespace'], '\\'));
+ array_unshift($className, \nre\configs\CoreConfig::getClassDir('app'));
+ $filename = ROOT.DS.implode(DS, $className).\nre\configs\CoreConfig::getFileExt('includes');
+ if(file_exists($filename)) {
+ require_once($filename);
+ }
+ }
+ else
+ {
+ // Core
+ $className = array_slice($fullClassNameA, substr_count(\nre\configs\CoreConfig::$core['namespace'], '\\'));
+ $filename = ROOT.DS.implode(DS, $className).\nre\configs\CoreConfig::getFileExt('includes');
+ if(file_exists($filename)) {
+ require_once($filename);
+ }
+ }
+
+
+ }
+
+
+ /**
+ * Determine classtype of a class.
+ *
+ * @param string $className Name of the class to determine the classtype of
+ * @return string Classtype of the given class
+ */
+ public static function getClassType($className)
+ {
+ // CamelCase
+ return strtolower(preg_replace('/^.*([A-Z][^A-Z]+)$/', '$1', $className));
+ }
+
+ }
+
+?>
diff --git a/core/ClassLoader.inc b/core/ClassLoader.inc
new file mode 100644
index 00000000..81cf537a
--- /dev/null
+++ b/core/ClassLoader.inc
@@ -0,0 +1,129 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Class for safely loading classes.
+ *
+ * @author coderkun
+ */
+ class ClassLoader
+ {
+
+
+
+
+ /**
+ * Load a class.
+ *
+ * @throws ClassNotFoundException
+ * @param string $className Name of the class to load
+ */
+ public static function load($fullClassName)
+ {
+ // Determine folder to look in
+ $className = explode('\\', $fullClassName);
+ $className = array_slice($className, substr_count(\nre\configs\AppConfig::$app['namespace'], '\\'));
+
+ // Determine filename
+ $fileName = ROOT.DS.implode(DS, $className). \nre\configs\CoreConfig::getFileExt('includes');
+
+
+ // Check file
+ if(!file_exists($fileName))
+ {
+ throw new \nre\exceptions\ClassNotFoundException(
+ $fullClassName
+ );
+ }
+
+ // Include file
+ include_once($fileName);
+ }
+
+
+ /**
+ * Check inheritance of a class.
+ *
+ * @throws ClassNotValidException
+ * @param string $className Name of the class to check
+ * @param string $parentClassName Name of the parent class
+ */
+ public static function check($className, $parentClassName)
+ {
+ // Check if class is subclass of parent class
+ if(!is_subclass_of($className, $parentClassName)) {
+ throw new \nre\exceptions\ClassNotValidException(
+ $className
+ );
+ }
+ }
+
+
+ /**
+ * Strip the namespace from a class name.
+ *
+ * @param string $class Name of a class including its namespace
+ * @return Name of the given class without its namespace
+ */
+ public static function stripNamespace($class)
+ {
+ return array_slice(explode('\\', $class), -1)[0];
+ }
+
+
+ /**
+ * Strip the class type from a class name.
+ *
+ * @param string $className Name of a class
+ * @return Name of the given class without its class type
+ */
+ public static function stripClassType($className)
+ {
+ return preg_replace('/^(.*)[A-Z][^A-Z]+$/', '$1', $className);
+ }
+
+
+ /**
+ * Strip the namespace and the class type of a full class name
+ * to get only its name.
+ *
+ * @param string $class Full name of a class
+ * @return Only the name of the given class
+ */
+ public static function getClassName($class)
+ {
+ return self::stripClassType(self::stripNamespace($class));
+ }
+
+
+ /**
+ * Concatenate strings to a class name following the CamelCase
+ * pattern.
+ *
+ * @param string $className1 Arbitrary number of strings to concat
+ * @return string Class name as CamelCase
+ */
+ public static function concatClassNames($className1)
+ {
+ return implode('', array_map(
+ function($arg) {
+ return ucfirst(strtolower($arg));
+ },
+ func_get_args()
+ ));
+ }
+
+ }
+
+?>
diff --git a/core/Component.inc b/core/Component.inc
new file mode 100644
index 00000000..a93363dc
--- /dev/null
+++ b/core/Component.inc
@@ -0,0 +1,85 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Abstract class to implement a (Controller) Component.
+ *
+ * @author coderkun
+ */
+ abstract class Component
+ {
+
+
+
+
+ /**
+ * Load the class of a Component.
+ *
+ * @throws ComponentNotFoundException
+ * @throws ComponentNotValidException
+ * @param string $componentName Name of the Component to load
+ */
+ public static function load($componentName)
+ {
+ // Determine full classname
+ $className = self::getClassName($componentName);
+
+ try {
+ // Load class
+ ClassLoader::load($className);
+
+ // Validate class
+ ClassLoader::check($className, get_class());
+ }
+ catch(\nre\exceptions\ClassNotValidException $e) {
+ throw new \nre\exceptions\ComponentNotValidException($e->getClassName());
+ }
+ catch(\nre\exceptions\ClassNotFoundException $e) {
+ throw new \nre\exceptions\ComponentNotFoundException($e->getClassName());
+ }
+ }
+
+
+ /**
+ * Instantiate a Component (Factory Pattern).
+ *
+ * @param string $componentName Name of the Component to instantiate
+ */
+ public static function factory($componentName)
+ {
+ // Determine full classname
+ $className = self::getClassName($componentName);
+
+ // Construct and return Controller
+ return new $className();
+ }
+
+
+ /**
+ * Determine the classname for the given Component name.
+ *
+ * @param string $componentName Component name to get classname of
+ * @return string Classname for the Component name
+ */
+ private static function getClassName($componentName)
+ {
+ $className = \nre\core\ClassLoader::concatClassNames($componentName, \nre\core\ClassLoader::stripNamespace(get_class()));
+
+
+ return \nre\configs\AppConfig::$app['namespace']."controllers\\components\\$className";
+ }
+
+ }
+
+?>
diff --git a/core/Config.inc b/core/Config.inc
new file mode 100644
index 00000000..b51f1e47
--- /dev/null
+++ b/core/Config.inc
@@ -0,0 +1,49 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Configuration.
+ *
+ * This class does not hold any configuration value but helps to
+ * determine values that can be hold by AppConfig or CoreConfig.
+ *
+ * @author coderkun
+ */
+ final class Config
+ {
+
+
+
+
+ /**
+ * Get a default value.
+ *
+ * @param string $index Index of value to get
+ */
+ public static function getDefault($index)
+ {
+ if(array_key_exists($index, \nre\configs\AppConfig::$defaults)) {
+ return \nre\configs\AppConfig::$defaults[$index];
+ }
+ if(array_key_exists($index, \nre\configs\CoreConfig::$defaults)) {
+ return \nre\configs\CoreConfig::$defaults[$index];
+ }
+
+
+ return null;
+ }
+
+ }
+
+?>
diff --git a/core/Controller.inc b/core/Controller.inc
new file mode 100644
index 00000000..941e7523
--- /dev/null
+++ b/core/Controller.inc
@@ -0,0 +1,433 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Abstract class for implementing a Controller.
+ *
+ * @author coderkun
+ */
+ abstract class Controller
+ {
+ /**
+ * Corresponding Agent
+ *
+ * @var Agent
+ */
+ protected $agent;
+ /**
+ * View of the Controller
+ *
+ * @var View
+ */
+ protected $view = null;
+ /**
+ * Data to pass to the View
+ *
+ * @var array
+ */
+ protected $viewData = array();
+ /**
+ * Current request
+ *
+ * @var Request
+ */
+ protected $request = null;
+ /**
+ * Current response
+ *
+ * @var Response
+ */
+ protected $response = null;
+
+
+
+
+ /**
+ * Load the class of a Controller.
+ *
+ * @throws ControllerNotFoundException
+ * @throws ControllerNotValidException
+ * @param string $controllerName Name of the Controller to load
+ */
+ public static function load($controllerName)
+ {
+ // Determine full classname
+ $className = self::getClassName($controllerName);
+
+ try {
+ // Load class
+ ClassLoader::load($className);
+
+ // Validate class
+ ClassLoader::check($className, get_class());
+ }
+ catch(\nre\exceptions\ClassNotValidException $e) {
+ throw new \nre\exceptions\ControllerNotValidException($e->getClassName());
+ }
+ catch(\nre\exceptions\ClassNotFoundException $e) {
+ throw new \nre\exceptions\ControllerNotFoundException($e->getClassName());
+ }
+ }
+
+
+ /**
+ * Instantiate a Controller (Factory Pattern).
+ *
+ * @throws DatamodelException
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws ViewNotFoundException
+ * @param string $controllerName Name of the Controller to instantiate
+ * @param string $layoutName Name of the current Layout
+ * @param string $action Current Action
+ */
+ public static function factory($controllerName, $layoutName, $action, $agent)
+ {
+ // Determine full classname
+ $className = self::getClassName($controllerName);
+
+ // Construct and return Controller
+ return new $className($layoutName, $action, $agent);
+ }
+
+
+ /**
+ * Determine the classname for the given Controller name.
+ *
+ * @param string $controllerName Controller name to get classname of
+ * @return string Classname for the Controller name
+ */
+ private static function getClassName($controllerName)
+ {
+ $className = \nre\core\ClassLoader::concatClassNames($controllerName, \nre\core\ClassLoader::stripNamespace(get_class()));
+
+
+ return \nre\configs\AppConfig::$app['namespace']."controllers\\$className";
+ }
+
+
+
+
+ /**
+ * Construct a new Controller.
+ *
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ * @throws ViewNotFoundException
+ * @param string $layoutName Name of the current Layout
+ * @param string $action Current Action
+ * @param Agent $agent Corresponding Agent
+ */
+ protected function __construct($layoutName, $action, $agent)
+ {
+ // Store values
+ $this->agent = $agent;
+
+ // Load Components
+ $this->loadComponents();
+
+ // Load Models
+ $this->loadModels();
+
+ // Load View
+ $this->loadView($layoutName, $action);
+ }
+
+
+
+
+ /**
+ * Prefilter that is executed before running the Controller.
+ *
+ * @param Request $request Current request
+ * @param Response $response Current response
+ */
+ public function preFilter(Request $request, Response $response)
+ {
+ // Request speichern
+ $this->request = $request;
+
+ // Response speichern
+ $this->response = $response;
+
+
+ // Linker erstellen
+ $this->set('linker', new \nre\core\Linker($request));
+
+ }
+
+
+ /**
+ * Prefilter that is executed after running the Controller.
+ *
+ * @param Request $request Current request
+ * @param Response $response Current response
+ */
+ public function postFilter(Request $request, Response $response)
+ {
+ }
+
+
+ /**
+ * Run the Controller.
+ *
+ * This method executes the Action of the Controller defined by
+ * the current Request.
+ *
+ * @throws ParamsNotValidException
+ * @throws IdNotFoundException
+ * @throws DatamodelException
+ * @throws ActionNotFoundException
+ * @param Request $request Current request
+ * @param Response $response Current response
+ */
+ public function run(Request $request, Response $response)
+ {
+ // Determine Action
+ $action = $response->getParam(2, 'action');
+ if(!method_exists($this, $action)) {
+ throw new \nre\exceptions\ActionNotFoundException($action);
+ }
+
+ // Determine parameters
+ $params = $response->getParams(3);
+ if(empty($params)) {
+ $params = $request->getParams(3);
+ }
+
+ // Fill missing parameters
+ $rc = new \ReflectionClass($this);
+ $nullParamsCount = $rc->getMethod($action)->getNumberOfParameters() - count($params);
+ $nullParams = ($nullParamsCount > 0 ? array_fill(0, $nullParamsCount, NULL) : array());
+
+
+ // Call Action
+ call_user_func_array(
+ array(
+ $this,
+ $action
+ ),
+ array_merge(
+ $params,
+ $nullParams
+ )
+ );
+ }
+
+
+ /**
+ * Generate the output.
+ *
+ * @param array $viewData Data to pass to the View
+ * @return string Generated output
+ */
+ public function render($viewData=null)
+ {
+ // Combine given data and data of this Controller
+ $data = $this->viewData;
+ if(!is_null($viewData)) {
+ $data = array_merge($viewData, $data);
+ }
+
+ // Rendern and return output
+ return $this->view->render($data);
+ }
+
+
+
+
+ /**
+ * Set data for the View.
+ *
+ * @param string $name Key
+ * @param mixed $data Value
+ */
+ protected function set($name, $data)
+ {
+ $this->viewData[$name] = $data;
+ }
+
+
+ /**
+ * Redirect to the given URL.
+ *
+ * @param string $url Relative URL
+ */
+ protected function redirect($url)
+ {
+ $url = 'http://'.$_SERVER['HTTP_HOST'].$url;
+ header('Location: '.$url);
+ exit;
+ }
+
+
+ /**
+ * Check if Models of this Controller are loaded and available.
+ *
+ * @param string $modelName Arbitrary number of Models to check
+ * @return bool All given Models are loaded and available
+ */
+ protected function checkModels($modelName)
+ {
+ foreach(func_get_args() as $modelName)
+ {
+ if(!isset($this->$modelName) || !is_subclass_of($this->$modelName, 'Model')) {
+ return false;
+ }
+ }
+
+
+ return true;
+ }
+
+
+ /**
+ * Get the View of the Controller
+ *
+ * @return View View of the Controller
+ */
+ protected function getView()
+ {
+ return $this->view;
+ }
+
+
+
+
+ /**
+ * Load the Components of this Controller.
+ *
+ * @throws ComponentNotValidException
+ * @throws ComponentNotFoundException
+ */
+ private function loadComponents()
+ {
+ // Determine components
+ $components = array();
+ if(property_exists($this, 'components')) {
+ $components = $this->components;
+ }
+ if(!is_array($components)) {
+ $components = array($components);
+ }
+ // Components of parent classes
+ $parent = $this;
+ while($parent = get_parent_class($parent))
+ {
+ $properties = get_class_vars($parent);
+ if(array_key_exists('components', $properties)) {
+ $components = array_merge($components, $properties['components']);
+ }
+ }
+
+ // Load components
+ foreach($components as &$component)
+ {
+ // Load class
+ Component::load($component);
+
+ // Construct component
+ $componentName = ucfirst(strtolower($component));
+ $this->$componentName = Component::factory($component);
+ }
+ }
+
+
+ /**
+ * Load the Models of this Controller.
+ *
+ * @throws DatamodelException
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ */
+ protected function loadModels()
+ {
+ // Determine Models
+ $explicit = false;
+ $models = \nre\core\ClassLoader::stripClassType(\nre\core\ClassLoader::stripNamespace(get_class($this)));
+ if(property_exists($this, 'models'))
+ {
+ $models = $this->models;
+ $explicit = true;
+ }
+ if(!is_array($models)) {
+ $models = array($models);
+ }
+ // Models of parent classes
+ $parent = $this;
+ while($parent = get_parent_class($parent))
+ {
+ $properties = get_class_vars($parent);
+ if(array_key_exists('models', $properties)) {
+ $models = array_merge($models, $properties['models']);
+ }
+ }
+
+ // Load Models
+ foreach($models as &$model)
+ {
+ try {
+ // Load class
+ Model::load($model);
+
+ // Construct Model
+ $modelName = ucfirst(strtolower($model));
+ $this->$modelName = Model::factory($model);
+ }
+ catch(\nre\exceptions\ModelNotValidException $e) {
+ if($explicit) {
+ throw $e;
+ }
+ }
+ catch(\nre\exceptions\ModelNotFoundException $e) {
+ if($explicit) {
+ throw $e;
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Load the View of this Controller.
+ *
+ * @throws ViewNotFoundException
+ * @param string $layoutName Name of the current Layout
+ * @param string $action Current Action
+ */
+ protected function loadView($layoutName, $action)
+ {
+ // Check Layout name
+ if(is_null($layoutName)) {
+ return;
+ }
+
+ // Determine controller name
+ $controllerName = \nre\core\ClassLoader::getClassName(get_class($this));
+
+
+ // Load view
+ $isToplevel = is_subclass_of($this->agent, '\nre\agents\ToplevelAgent');
+ $this->view = View::loadAndFactory($layoutName, $controllerName, $action, $isToplevel);
+ }
+
+ }
+
+?>
diff --git a/core/Driver.inc b/core/Driver.inc
new file mode 100644
index 00000000..eec59143
--- /dev/null
+++ b/core/Driver.inc
@@ -0,0 +1,96 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Abstract class for implementing a Driver.
+ *
+ * @author coderkun
+ */
+ abstract class Driver
+ {
+
+
+
+
+ /**
+ * Load the class of a Driver.
+ *
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @param string $driverName Name of the Driver to load
+ */
+ public static function load($driverName)
+ {
+ // Determine full classname
+ $className = self::getClassName($driverName);
+
+ try {
+ // Load class
+ ClassLoader::load($className);
+
+ // Validate class
+ ClassLoader::check($className, get_class());
+ }
+ catch(\nre\exceptions\ClassNotValidException $e) {
+ throw new \nre\exceptions\DriverNotValidException($e->getClassName());
+ }
+ catch(\nre\exceptions\ClassNotFoundException $e) {
+ throw new \nre\exceptions\DriverNotFoundException($e->getClassName());
+ }
+ }
+
+
+ /**
+ * Instantiate a Driver (Factory Pattern).
+ *
+ * @param string $driverName Name of the Driver to instantiate
+ */
+ public static function factory($driverName, $config)
+ {
+ // Determine full classname
+ $className = self::getClassName($driverName);
+
+
+ // Construct and return Driver
+ return $className::singleton($config);
+ }
+
+
+ /**
+ * Determine the classname for the given Driver name.
+ *
+ * @param string $driverName Driver name to get classname of
+ * @return string Classname fore the Driver name
+ */
+ private static function getClassName($driverName)
+ {
+ $className = ClassLoader::concatClassNames($driverName, ClassLoader::stripNamespace(get_class()));
+
+
+ return "\\nre\\drivers\\$className";
+ }
+
+
+
+
+ /**
+ * Construct a new Driver.
+ */
+ protected function __construct()
+ {
+ }
+
+ }
+
+?>
diff --git a/core/Exception.inc b/core/Exception.inc
new file mode 100644
index 00000000..a17a700f
--- /dev/null
+++ b/core/Exception.inc
@@ -0,0 +1,65 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Exception class.
+ *
+ * @author coderkun
+ */
+ class Exception extends \Exception
+ {
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $message Error message
+ * @param int $code Error code
+ * @param string $name Name to insert
+ */
+ function __construct($message, $code, $name=null)
+ {
+ parent::__construct(
+ $this->concat(
+ $message,
+ $name
+ ),
+ $code
+ );
+ }
+
+
+
+
+ /**
+ * Insert the name in a message
+ *
+ * @param string $message Error message
+ * @param string $name Name to insert
+ */
+ private function concat($message, $name)
+ {
+ if(is_null($name)) {
+ return $message;
+ }
+
+
+ return "$message: $name";
+ }
+
+ }
+
+?>
diff --git a/core/Linker.inc b/core/Linker.inc
new file mode 100644
index 00000000..e8c4fa0f
--- /dev/null
+++ b/core/Linker.inc
@@ -0,0 +1,311 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Class to create web links based on the current request.
+ *
+ * @author coderkun
+ */
+ class Linker
+ {
+ /**
+ * Current request
+ *
+ * @var Request
+ */
+ private $request;
+
+
+
+
+ /**
+ * Construct a new linker.
+ *
+ * @param Request $request Current request
+ */
+ function __construct(\nre\requests\WebRequest $request)
+ {
+ $this->request = $request;
+ }
+
+
+
+
+ /**
+ * Mask parameters to be used in an URL.
+ *
+ * @param string $param1 First parameter
+ * @return string Masked parameters as string
+ */
+ public static function createLinkParam($param1)
+ {
+ return implode(
+ \nre\configs\CoreConfig::$classes['linker']['url']['delimiter'],
+ call_user_func_array(
+ '\nre\core\Linker::createLinkParams',
+ func_get_args()
+ )
+ );
+ }
+
+
+ /**
+ * Mask parameters to be used in an URL.
+ *
+ * @param string $param1 First parameter
+ * @return string Masked parameters as array
+ */
+ public static function createLinkParams($param1)
+ {
+ // Parameters
+ $linkParams = array();
+ $params = func_get_args();
+
+ foreach($params as $param)
+ {
+ // Delete critical signs
+ $specials = array('/', '?', '&');
+ foreach($specials as &$special) {
+ $param = str_replace($special, '', $param);
+ }
+
+ // Process parameter
+ $param = str_replace(
+ ' ',
+ \nre\configs\CoreConfig::$classes['linker']['url']['delimiter'],
+ substr(
+ $param,
+ 0,
+ \nre\configs\CoreConfig::$classes['linker']['url']['length']
+ )
+ );
+
+ // Encode parameter
+ $linkParams[] = $param;
+ }
+
+
+ // Return link parameters
+ return $linkParams;
+ }
+
+
+
+
+ /**
+ * Create a web link.
+ *
+ * @param array $params Parameters to use
+ * @param int $offset Ignore first parameters
+ * @param bool $exclusiveParams Use only the given parameters
+ * @param array $getParams GET-parameter to use
+ * @param bool $exclusiveGetParams Use only the given GET-parameters
+ * @param string $anchor Target anchor
+ * @param bool $absolute Include hostname etc. for an absolute URL
+ * @return string Created link
+ */
+ public function link($params=null, $offset=0, $exclusiveParams=true, $getParams=null, $exclusiveGetParams=true, $anchor=null, $absolute=false)
+ {
+ // Make current request to response
+ $response = new \nre\responses\WebResponse();
+
+
+ // Check parameters
+ if(is_null($params)) {
+ $params = array();
+ }
+ elseif(!is_array($params)) {
+ $params = array($params);
+ }
+
+ // Set parameters from request
+ $reqParams = array_slice($this->request->getParams(), 1, $offset);
+ if(count($reqParams) < $offset && $offset > 0) {
+ $reqParams[] = $this->request->getParam(1, 'intermediate');
+ }
+ if(count($reqParams) < $offset && $offset > 1) {
+ $reqParams[] = $this->request->getParam(2, 'action');
+ }
+ $params = array_map('rawurlencode', $params);
+ $params = array_merge($reqParams, $params);
+
+ // Use Layout
+ $layout = \nre\configs\AppConfig::$defaults['toplevel'];
+ if(!empty($getParams) && array_key_exists('layout', $getParams)) {
+ $layout = $getParams['layout'];
+ }
+ array_unshift($params, $layout);
+
+ // Use parameters from request only
+ if(!$exclusiveParams)
+ {
+ $params = array_merge(
+ $params,
+ array_slice(
+ $this->request->getParams(),
+ count($params)
+ )
+ );
+ }
+
+ // Set parameters
+ call_user_func_array(
+ array(
+ $response,
+ 'addParams'
+ ),
+ $params
+ );
+
+
+ // Check GET-parameters
+ if(is_null($getParams)) {
+ $getParams = array();
+ }
+ elseif(!is_array($params)) {
+ $params = array($params);
+ }
+ if(!$exclusiveGetParams)
+ {
+ $getParams = array_merge(
+ $this->request->getGetParams(),
+ $getParams
+ );
+ }
+
+ // Set GET-parameters
+ $response->addGetParams($getParams);
+
+
+ // Create and return link
+ return self::createLink($this->request, $response, $anchor, $absolute);
+ }
+
+
+ /**
+ * Create a link from an URL.
+ *
+ * @param string $url URL to create link from
+ * @param bool $absolute Create absolute URL
+ * @return string Created link
+ */
+ public function hardlink($url, $absolute=false)
+ {
+ return $this->createUrl($url, $this->request, $absolute);
+ }
+
+
+
+
+ /**
+ * Create a link.
+ *
+ * @param Request $request Current request
+ * @param Response $response Current response
+ * @param bool $absolute Create absolute link
+ * @param string $anchor Anchor on target
+ * @return string Created link
+ */
+ private static function createLink(Request $request, Response $response, $anchor=null, $absolute=false)
+ {
+ // Check response
+ if(is_null($response)) {
+ return null;
+ }
+
+
+ // Get parameters
+ $params = $response->getParams(1);
+
+ // Check Action
+ if(count($params) == 2 && $params[1] == \nre\configs\CoreConfig::$defaults['action']) {
+ array_pop($params);
+ }
+
+ // Set parameters
+ $link = implode('/', $params);
+
+ // Apply reverse-routes
+ $link = $request->applyReverseRoutes($link);
+
+
+ // Get GET-parameters
+ $getParams = $response->getGetParams();
+
+ // Layout überprüfen
+ if(array_key_exists('layout', $getParams) && $getParams['layout'] == \nre\configs\AppConfig::$defaults['toplevel']) {
+ unset($getParams['layout']);
+ }
+
+ // Set GET-parameters
+ if(array_key_exists('url', $getParams)) {
+ unset($getParams['url']);
+ }
+ if(count($getParams) > 0) {
+ $link .= '?'.http_build_query($getParams);
+ }
+
+ // Add anchor
+ if(!is_null($anchor)) {
+ $link .= "#$anchor";
+ }
+
+
+ // Create URL
+ $url = self::createUrl($link, $request, $absolute);
+
+
+ return $url;
+ }
+
+
+ /**
+ * Adapt a link to the environment.
+ *
+ * @param string $url URL
+ * @param Request $request Current request
+ * @param bool $absolute Create absolute URL
+ * @return string Adapted URL
+ */
+ private static function createUrl($url, Request $request, $absolute=false)
+ {
+ // Variables
+ $server = $_SERVER['SERVER_NAME'];
+ $uri = $_SERVER['REQUEST_URI'];
+ $prefix = '';
+
+
+ // Determine prefix
+ $ppos = array(strlen($uri));
+ if(($p = strpos($uri, '/'.$request->getParam(1, 'intermediate'))) !== false) {
+ $ppos[] = $p;
+ }
+ $prefix = substr($uri, 0, min($ppos));
+
+ // Create absolute URL
+ if($absolute) {
+ $prefix = "http://$server/".trim($prefix, '/');
+ }
+
+ // Put URL together
+ $url = rtrim($prefix, '/').'/'.ltrim($url, '/');
+
+
+ // Return URL
+ return $url;
+ }
+
+ }
+
+?>
diff --git a/core/Logger.inc b/core/Logger.inc
new file mode 100644
index 00000000..b5ac7dc9
--- /dev/null
+++ b/core/Logger.inc
@@ -0,0 +1,132 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Class to log messages to different targets.
+ *
+ * @author coderkun
+ */
+ class Logger
+ {
+ /**
+ * Log mode: Detect automatic
+ *
+ * @var int
+ */
+ const LOGMODE_AUTO = 0;
+ /**
+ * Log mode: Print to screen
+ *
+ * @var int
+ */
+ const LOGMODE_SCREEN = 1;
+ /**
+ * Log mode: Use PHP-logging mechanism
+ *
+ * @var int
+ */
+ const LOGMODE_PHP = 2;
+
+ /**
+ * Do not auto-log to screen
+ *
+ * @var boolean
+ */
+ private $autoLogToScreenDisabled = false;
+
+
+
+
+ /**
+ * Construct a new logger.
+ */
+ public function __construct()
+ {
+ }
+
+
+
+
+ /**
+ * Log a message.
+ *
+ * @param string $message Message to log
+ * @param int $logMode Log mode to use
+ */
+ public function log($message, $logMode=self::LOGMODE_SCREEN)
+ {
+ // Choose log mode automatically
+ if($logMode == self::LOGMODE_AUTO) {
+ $logMode = $this->getAutoLogMode();
+ }
+
+ // Print message to screen
+ if($logMode & self::LOGMODE_SCREEN) {
+ $this->logToScreen($message);
+ }
+
+ // Use PHP-logging mechanism
+ if($logMode & self::LOGMODE_PHP) {
+ $this->logToPhp($message);
+ }
+ }
+
+
+ /**
+ * Disable logging to screen when the log mode is automatically
+ * detected.
+ */
+ public function disableAutoLogToScreen()
+ {
+ $this->autoLogToScreenDisabled = true;
+ }
+
+
+
+ /**
+ * Print a message to screen.
+ *
+ * @param string $message Message to print
+ */
+ private function logToScreen($message)
+ {
+ echo "$message \n";
+ }
+
+
+ /**
+ * Log a message by using PHP-logging mechanism.
+ *
+ * @param string $message Message to log
+ */
+ private function logToPhp($message)
+ {
+ error_log($message, 0);
+ }
+
+
+ /**
+ * Detect log mode automatically by distinguishing between
+ * production and test environment.
+ *
+ * @return int Automatically detected log mode
+ */
+ private function getAutoLogMode()
+ {
+ return ($_SERVER['SERVER_ADDR'] == '127.0.0.1' && !$this->autoLogToScreenDisabled) ? self::LOGMODE_SCREEN : self::LOGMODE_PHP;
+ }
+
+ }
+
+?>
diff --git a/core/Model.inc b/core/Model.inc
new file mode 100644
index 00000000..c0475b4a
--- /dev/null
+++ b/core/Model.inc
@@ -0,0 +1,141 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Abstract class for implementing a Model.
+ *
+ * @author coderkun
+ */
+ abstract class Model
+ {
+
+
+
+
+ /**
+ * Load the class of a Model.
+ *
+ * @throws ModelNotFoundException
+ * @throws ModelNotValidException
+ * @param string $modelName Name of the Model to load
+ */
+ public static function load($modelName)
+ {
+ // Determine full classname
+ $className = self::getClassName($modelName);
+
+ try {
+ // Load class
+ ClassLoader::load($className);
+
+ // Validate class
+ ClassLoader::check($className, get_class());
+ }
+ catch(\nre\exceptions\ClassNotValidException $e) {
+ throw new \nre\exceptions\ModelNotValidException($e->getClassName());
+ }
+ catch(\nre\exceptions\ClassNotFoundException $e) {
+ throw new \nre\exceptions\ModelNotFoundException($e->getClassName());
+ }
+ }
+
+
+ /**
+ * Instantiate a Model (Factory Pattern).
+ *
+ * @param string $modelName Name of the Model to instantiate
+ */
+ public static function factory($modelName)
+ {
+ // Determine full classname
+ $className = self::getClassName($modelName);
+
+ // Construct and return Model
+ return new $className();
+ }
+
+
+ /**
+ * Determine the classname for the given Model name.
+ *
+ * @param string $modelName Model name to get classname of
+ * @return string Classname fore the Model name
+ */
+ private static function getClassName($modelName)
+ {
+ $className = ClassLoader::concatClassNames($modelName, ClassLoader::stripNamespace(get_class()));
+
+
+ return \nre\configs\AppConfig::$app['namespace']."models\\$className";
+ }
+
+
+
+
+ /**
+ * Construct a new Model.
+ *
+ * TODO Catch exception
+ *
+ * @throws DatamodelException
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ */
+ protected function __construct()
+ {
+ // Load Models
+ $this->loadModels();
+ }
+
+
+
+
+ /**
+ * Load the Models of this Model.
+ *
+ * @throws DatamodelException
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @throws ModelNotValidException
+ * @throws ModelNotFoundException
+ */
+ private function loadModels()
+ {
+ // Determine Models
+ $models = array();
+ if(property_exists($this, 'models')) {
+ $models = $this->models;
+ }
+ if(!is_array($models)) {
+ $models = array($models);
+ }
+
+
+ // Load Models
+ foreach($models as $model)
+ {
+ // Load class
+ Model::load($model);
+
+ // Construct Model
+ $modelName = ucfirst(strtolower($model));
+ $this->$modelName = Model::factory($model);
+ }
+ }
+
+ }
+
+?>
diff --git a/core/Request.inc b/core/Request.inc
new file mode 100644
index 00000000..5fda187e
--- /dev/null
+++ b/core/Request.inc
@@ -0,0 +1,64 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Base class to represent a request.
+ *
+ * @author coderkun
+ */
+ class Request
+ {
+ /**
+ * Passed parameters
+ *
+ * @var array
+ */
+ protected $params = array();
+
+
+
+
+ /**
+ * Get a parameter.
+ *
+ * @param int $index Index of parameter
+ * @param string $defaultIndex Index of default configuration value for this parameter
+ * @return string Value of parameter
+ */
+ public function getParam($index, $defaultIndex=null)
+ {
+ // Return parameter
+ if(count($this->params) > $index) {
+ return $this->params[$index];
+ }
+
+ // Return default value
+ return \nre\core\Config::getDefault($defaultIndex);
+ }
+
+
+ /**
+ * Get all parameters from index on.
+ *
+ * @param int $offset Offset-index
+ * @return array Parameter values
+ */
+ public function getParams($offset=0)
+ {
+ return array_slice($this->params, $offset);
+ }
+
+ }
+
+?>
diff --git a/core/Response.inc b/core/Response.inc
new file mode 100644
index 00000000..4781b2ab
--- /dev/null
+++ b/core/Response.inc
@@ -0,0 +1,158 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Base class to represent a response.
+ *
+ * @author coderkun
+ */
+ class Response
+ {
+ /**
+ * Applied parameters
+ *
+ * @var array
+ */
+ protected $params = array();
+ /**
+ * Generated output
+ *
+ * @var string
+ */
+ private $output = '';
+ /**
+ * Abort futher execution
+ *
+ * @var bool
+ */
+ private $exit = false;
+
+
+
+
+ /**
+ * Add a parameter.
+ *
+ * @param mixed $value Value of parameter
+ */
+ public function addParam($value)
+ {
+ $this->params[] = $value;
+ }
+
+
+ /**
+ * Add multiple parameters.
+ *
+ * @param mixed $value1 Value of first parameter
+ * @param mixed … Values of further parameters
+ */
+ public function addParams($value1)
+ {
+ $this->params = array_merge(
+ $this->params,
+ func_get_args()
+ );
+ }
+
+
+ /**
+ * Delete all stored parameters (from offset on).
+ *
+ * @param int $offset Offset-index
+ */
+ public function clearParams($offset=0)
+ {
+ $this->params = array_slice($this->params, 0, $offset);
+ }
+
+
+ /**
+ * Get a parameter.
+ *
+ * @param int $index Index of parameter
+ * @param string $defaultIndex Index of default configuration value for this parameter
+ * @return string Value of parameter
+ */
+ public function getParam($index, $defaultIndex=null)
+ {
+ // Return parameter
+ if(count($this->params) > $index) {
+ return $this->params[$index];
+ }
+
+
+ // Return default value
+ return \nre\core\Config::getDefault($defaultIndex);
+ }
+
+
+ /**
+ * Get all parameters from index on.
+ *
+ * @param int $offset Offset-index
+ * @return array Parameter values
+ */
+ public function getParams($offset=0)
+ {
+ return array_slice($this->params, $offset);
+ }
+
+
+ /**
+ * Set output.
+ *
+ * @param string $output Generated output
+ */
+ public function setOutput($output)
+ {
+ $this->output = $output;
+ }
+
+
+ /**
+ * Get generated output.
+ *
+ * @return string Generated output
+ */
+ public function getOutput()
+ {
+ return $this->output;
+ }
+
+
+ /**
+ * Set exit-state.
+ *
+ * @param bool $exit Abort further execution
+ */
+ public function setExit($exit=true)
+ {
+ $this->exit = $exit;
+ }
+
+
+ /**
+ * Get exit-state.
+ *
+ * @return bool Abort further execution
+ */
+ public function getExit()
+ {
+ return $this->exit;
+ }
+
+ }
+
+?>
diff --git a/core/View.inc b/core/View.inc
new file mode 100644
index 00000000..546ddb6e
--- /dev/null
+++ b/core/View.inc
@@ -0,0 +1,124 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * View.
+ *
+ * Class to encapsulate a template file and to provide rendering methods.
+ *
+ * @author coderkun
+ */
+ class View
+ {
+ /**
+ * Template filename
+ *
+ * @var string
+ */
+ protected $templateFilename;
+
+
+
+
+ /**
+ * Load and instantiate the View of an Agent.
+ *
+ * @throws ViewNotFoundException
+ * @param string $layoutName Name of Layout in use
+ * @param string $agentName Name of the Agent
+ * @param string $action Current Action
+ * @param bool $isToplevel Agent is a ToplevelAgent
+ */
+ public static function loadAndFactory($layoutName, $agentName=null, $action=null, $isToplevel=false)
+ {
+ return new View($layoutName, $agentName, $action, $isToplevel);
+ }
+
+
+
+
+ /**
+ * Construct a new View.
+ *
+ * @throws ViewNotFoundException
+ * @param string $layoutName Name of Layout in use
+ * @param string $agentName Name of the Agent
+ * @param string $action Current Action
+ * @param bool $isToplevel Agent is a ToplevelAgent
+ */
+ protected function __construct($layoutName, $agentName=null, $action=null, $isToplevel=false)
+ {
+ // Create template filename
+ // LayoutName
+ $fileName = ROOT.DS. \nre\configs\CoreConfig::getClassDir('views').DS. strtolower($layoutName).DS;
+ // AgentName and Action
+ if(strtolower($agentName) != $layoutName || !$isToplevel) {
+ $fileName .= strtolower($agentName).DS.strtolower($action);
+ }
+ else {
+ $fileName .= strtolower($layoutName);
+ }
+ // File extension
+ $fileName .= \nre\configs\CoreConfig::getFileExt('views');
+
+
+ // Check template file
+ if(!file_exists($fileName)) {
+ throw new \nre\exceptions\ViewNotFoundException($fileName);
+ }
+
+ // Save filename
+ $this->templateFilename = $fileName;
+ }
+
+
+
+
+ /**
+ * Generate output
+ *
+ * @param array $data Data to have available in the template file
+ */
+ public function render($data)
+ {
+ // Extract data
+ if(!is_null($data)) {
+ extract($data, EXTR_SKIP);
+ }
+
+ // Buffer output
+ ob_start();
+
+ // Include template
+ include($this->templateFilename);
+
+
+ // Return buffered output
+ return ob_get_clean();
+ }
+
+
+ /**
+ * Get template filename.
+ *
+ * @return string Template filename
+ */
+ public function getTemplateFilename()
+ {
+ return $this->templateFilename;
+ }
+
+ }
+
+?>
diff --git a/core/WebUtils.inc b/core/WebUtils.inc
new file mode 100644
index 00000000..5a4bb6f0
--- /dev/null
+++ b/core/WebUtils.inc
@@ -0,0 +1,75 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\core;
+
+
+ /**
+ * Class that holds several web-specific methods and properties.
+ *
+ * @author coderkun
+ */
+ class WebUtils
+ {
+ /**
+ * HTTP-statuscode 403: Forbidden
+ *
+ * @var int
+ */
+ const HTTP_FORBIDDEN = 403;
+ /**
+ * HTTP-statuscode 404: Not Found
+ *
+ * @var int
+ */
+ const HTTP_NOT_FOUND = 404;
+ /**
+ * HTTP-statuscode 503: Service Unavailable
+ *
+ * @var int
+ */
+ const HTTP_SERVICE_UNAVAILABLE = 503;
+
+ /**
+ * HTTP-header strings
+ *
+ * @var array
+ */
+ public static $httpStrings = array(
+ 200 => 'OK',
+ 304 => 'Not Modified',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 503 => 'Service Unavailable'
+ );
+
+
+
+
+ /**
+ * Get the HTTP-header of a HTTP-statuscode
+ *
+ * @param int $httpStatusCode HTTP-statuscode
+ * @return string HTTP-header of HTTP-statuscode
+ */
+ public static function getHttpHeader($httpStatusCode)
+ {
+ if(!array_key_exists($httpStatusCode, self::$httpStrings)) {
+ $httpStatusCode = 200;
+ }
+
+
+ return sprintf("HTTP/1.1 %d %s\n", $httpStatusCode, self::$httpStrings[$httpStatusCode]);
+ }
+
+ }
+
+?>
diff --git a/drivers/DatabaseDriver.inc b/drivers/DatabaseDriver.inc
new file mode 100644
index 00000000..dfd5c0fd
--- /dev/null
+++ b/drivers/DatabaseDriver.inc
@@ -0,0 +1,87 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\drivers;
+
+
+ /**
+ * Abstract class for implementing a database driver.
+ *
+ * @author coderkun
+ */
+ abstract class DatabaseDriver extends \nre\core\Driver
+ {
+ /**
+ * Driver class instance
+ *
+ * @static
+ * @var DatabaseDriver
+ */
+ protected static $driver = null;
+ /**
+ * Connection resource
+ *
+ * @var resource
+ */
+ protected $connection = null;
+
+
+
+
+ /**
+ * Singleton-pattern.
+ *
+ * @param array $config Database driver configuration
+ * @return DatabaseDriver Singleton-instance of database driver class
+ */
+ public static function singleton($config)
+ {
+ // Singleton
+ if(self::$driver !== null) {
+ return self::$driver;
+ }
+
+ // Construct
+ $className = get_called_class();
+ self::$driver = new $className($config);
+
+
+ return self::$driver;
+ }
+
+
+ /**
+ * Construct a new database driver.
+ *
+ * @param array $config Connection and login settings
+ */
+ protected function __construct($config)
+ {
+ parent::__construct();
+
+ // Establish connection
+ $this->connect($config);
+ }
+
+
+
+
+ /**
+ * Establish a connect to a MqSQL-database.
+ *
+ * @throws DatamodelException
+ * @param array $config Connection and login settings
+ */
+ protected abstract function connect($config);
+
+ }
+
+?>
diff --git a/drivers/MysqliDriver.inc b/drivers/MysqliDriver.inc
new file mode 100644
index 00000000..fe4fa81a
--- /dev/null
+++ b/drivers/MysqliDriver.inc
@@ -0,0 +1,169 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\drivers;
+
+
+ /**
+ * Implementation of a database driver for MySQL-databases.
+ *
+ * @author coderkun
+ */
+ class MysqliDriver extends \nre\drivers\DatabaseDriver
+ {
+
+
+
+
+ /**
+ * Construct a MySQL-driver.
+ *
+ * @throws DatamodelException
+ * @param array $config Connection and login settings
+ */
+ function __construct($config)
+ {
+ parent::__construct($config);
+ }
+
+
+
+
+ /**
+ * Execute a SQL-query.
+ *
+ * @throws DatamodelException
+ * @param string $query Query to run
+ * @param mixed … Params
+ * @return array Result
+ */
+ public function query($query)
+ {
+ // Prepare statement
+ if(!($stmt = $this->connection->prepare($query))) {
+ throw new \nre\exceptions\DatamodelException($this->connection->error, $this->connection->errno);
+ }
+
+ try {
+ // Prepare data
+ $data = array();
+
+ // Bind parameters
+ $p = array_slice(func_get_args(), 1);
+ $params = array();
+ foreach($p as $key => $value) {
+ $params[$key] = &$p[$key];
+ }
+ if(count($params) > 0)
+ {
+ if(!(call_user_func_array(array($stmt, 'bind_param'), $params))) {
+ throw new \nre\exceptions\DatamodelException($this->connection->error, $this->connection->errno);
+ }
+ }
+
+ // Execute query
+ if(!$stmt->execute()) {
+ throw new \nre\exceptions\DatamodelException($this->connection->error, $this->connection->errno);
+ }
+
+ // Fetch result
+ if($result = $stmt->get_result()) {
+ while($row = $result->fetch_assoc()) {
+ $data[] = $row;
+ }
+ }
+
+
+ $stmt->close();
+ return $data;
+ }
+ catch(Exception $e) {
+ $stmt->close();
+ throw $e;
+ }
+ }
+
+
+ /**
+ * Return the last insert id (of the last insert-query).
+ *
+ * @return int Last insert id
+ */
+ public function getInsertId()
+ {
+ return $this->connection->insert_id;
+ }
+
+
+ /**
+ * Enable/disable autocommit feature.
+ *
+ * @param boolean $autocommit Enable/disable autocommit
+ */
+ public function setAutocommit($autocommit)
+ {
+ $this->connection->autocommit($autocommit);
+ }
+
+
+ /**
+ * Rollback the current transaction.
+ */
+ public function rollback()
+ {
+ $this->connection->rollback();
+ }
+
+
+ /**
+ * Commit the current transaction.
+ */
+ public function commit()
+ {
+ $this->connection->commit();
+ }
+
+
+
+
+ /**
+ * Establish a connect to a MqSQL-database.
+ *
+ * @throws DatamodelException
+ * @param array $config Connection and login settings
+ */
+ protected function connect($config)
+ {
+ // Connect
+ $con = @new \mysqli(
+ $config['host'],
+ $config['user'],
+ $config['password'],
+ $config['db']
+ );
+
+ // Check connection
+ if($con->connect_error) {
+ throw new \nre\exceptions\DatamodelException($con->connect_error, $con->connect_errno);
+ }
+
+ // Set character encoding
+ if(!$con->set_charset('utf8')) {
+ throw new \nre\exceptions\DatamodelException($con->connect_error, $con->connect_errno);
+ }
+
+ // Save connection
+ $this->connection = $con;
+ }
+
+ }
+
+?>
diff --git a/exceptions/AccessDeniedException.inc b/exceptions/AccessDeniedException.inc
new file mode 100644
index 00000000..31bba929
--- /dev/null
+++ b/exceptions/AccessDeniedException.inc
@@ -0,0 +1,51 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Access denied.
+ *
+ * @author coderkun
+ */
+ class AccessDeniedException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 85;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'access denied';
+
+
+
+
+ /**
+ * Consturct a new exception.
+ */
+ function __construct()
+ {
+ parent::__construct(
+ self::MESSAGE,
+ self::CODE
+ );
+ }
+
+ }
+
+?>
diff --git a/exceptions/ActionNotFoundException.inc b/exceptions/ActionNotFoundException.inc
new file mode 100644
index 00000000..418dd49c
--- /dev/null
+++ b/exceptions/ActionNotFoundException.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Action not found.
+ *
+ * @author coderkun
+ */
+ class ActionNotFoundException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 70;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'action not found';
+
+ /**
+ * Name of the action that was not found
+ *
+ * @var string
+ */
+ private $action;
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $action Name of the action that was not found
+ */
+ function __construct($action)
+ {
+ parent::__construct(
+ self::MESSAGE,
+ self::CODE,
+ $action
+ );
+
+ // Store values
+ $this->action = $action;
+ }
+
+
+
+
+ /**
+ * Get the name of the action that was not found.
+ *
+ * @return string Name of the action that was not found
+ */
+ public function getAction()
+ {
+ return $this->action;
+ }
+
+ }
+
+?>
diff --git a/exceptions/AgentNotFoundException.inc b/exceptions/AgentNotFoundException.inc
new file mode 100644
index 00000000..f0b3a2a7
--- /dev/null
+++ b/exceptions/AgentNotFoundException.inc
@@ -0,0 +1,67 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Agent not found.
+ *
+ * @author coderkun
+ */
+ class AgentNotFoundException extends \nre\exceptions\ClassNotFoundException
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 66;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'agent not found';
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $agentName Name of the Agent that was not found
+ */
+ function __construct($agentName)
+ {
+ parent::__construct(
+ $agentName,
+ self::MESSAGE,
+ self::CODE
+ );
+ }
+
+
+
+
+ /**
+ * Get the name of the Agent that was not found.
+ *
+ * @return string Name of the Agent that was not found
+ */
+ public function getAgentName()
+ {
+ return $this->getClassName();
+ }
+
+ }
+
+?>
diff --git a/exceptions/AgentNotValidException.inc b/exceptions/AgentNotValidException.inc
new file mode 100644
index 00000000..1c752051
--- /dev/null
+++ b/exceptions/AgentNotValidException.inc
@@ -0,0 +1,67 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Agent not valid.
+ *
+ * @author coderkun
+ */
+ class AgentNotValidException extends \nre\exceptions\ClassNotValidException
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 76;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'agent not valid';
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $agentName Name of the invalid Agent
+ */
+ function __construct($agentName)
+ {
+ parent::__construct(
+ $agentName,
+ self::MESSAGE,
+ self::CODE
+ );
+ }
+
+
+
+
+ /**
+ * Get the name of the invalid Agent.
+ *
+ * @return string Name of the invalid Agent
+ */
+ public function getAgentName()
+ {
+ return $this->getClassName();
+ }
+
+ }
+
+?>
diff --git a/exceptions/ClassNotFoundException.inc b/exceptions/ClassNotFoundException.inc
new file mode 100644
index 00000000..070f383f
--- /dev/null
+++ b/exceptions/ClassNotFoundException.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Class not found.
+ *
+ * @author coderkun
+ */
+ class ClassNotFoundException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 64;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'class not found';
+
+ /**
+ * Name of the class that was not found
+ *
+ * @var string
+ */
+ private $className;
+
+
+
+
+ /**
+ * Construct a new exception
+ *
+ * @param string $className Name of the class that was not found
+ */
+ function __construct($className, $message=self::MESSAGE, $code=self::CODE)
+ {
+ parent::__construct(
+ $message,
+ $code,
+ $className
+ );
+
+ // Store values
+ $this->className = $className;
+ }
+
+
+
+
+ /**
+ * Get the name of the class that was not found.
+ *
+ * @return string Name of the class that was not found
+ */
+ public function getClassName()
+ {
+ return $this->className;
+ }
+
+ }
+
+?>
diff --git a/exceptions/ClassNotValidException.inc b/exceptions/ClassNotValidException.inc
new file mode 100644
index 00000000..bdd36d5e
--- /dev/null
+++ b/exceptions/ClassNotValidException.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Class not valid.
+ *
+ * @author coderkun
+ */
+ class ClassNotValidException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 74;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'class not valid';
+
+ /**
+ * Name of the invalid class
+ *
+ * @var string
+ */
+ private $className;
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $className Name of the invalid class
+ */
+ function __construct($className, $message=self::MESSAGE, $code=self::CODE)
+ {
+ parent::__construct(
+ $message,
+ $code,
+ $className
+ );
+
+ // Store value
+ $this->className = $className;
+ }
+
+
+
+
+ /**
+ * Get the name of the invalid class.
+ *
+ * @return string Name of the invalid class
+ */
+ public function getClassName()
+ {
+ return $this->className;
+ }
+
+ }
+
+?>
diff --git a/exceptions/ComponentNotFoundException.inc b/exceptions/ComponentNotFoundException.inc
new file mode 100644
index 00000000..5e75de44
--- /dev/null
+++ b/exceptions/ComponentNotFoundException.inc
@@ -0,0 +1,67 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Component not found.
+ *
+ * @author coderkun
+ */
+ class ComponentNotFoundException extends \nre\exceptions\ClassNotFoundException
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 67;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'component not found';
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $componentName Name of the Component that was not found
+ */
+ function __construct($componentName)
+ {
+ parent::__construct(
+ $componentName,
+ self::MESSAGE,
+ self::CODE
+ );
+ }
+
+
+
+
+ /**
+ * Get the name of the Component that was not found.
+ *
+ * @return string Name of the Component that was not found
+ */
+ public function getComponentName()
+ {
+ return $this->getClassName();
+ }
+
+ }
+
+?>
diff --git a/exceptions/ComponentNotValidException.inc b/exceptions/ComponentNotValidException.inc
new file mode 100644
index 00000000..a03b0c0d
--- /dev/null
+++ b/exceptions/ComponentNotValidException.inc
@@ -0,0 +1,67 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Component not valid.
+ *
+ * @author coderkun
+ */
+ class ComponentNotValidException extends \nre\exceptions\ClassNotValidException
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 77;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'component not valid';
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $componentName Name of the invalid Component
+ */
+ function __construct($componentName)
+ {
+ parent::__construct(
+ $componentName,
+ self::MESSAGE,
+ self::CODE
+ );
+ }
+
+
+
+
+ /**
+ * Get the name of the invalid Component.
+ *
+ * @return string Name of the invalid Component
+ */
+ public function getComponentName()
+ {
+ return $this->getClassName();
+ }
+
+ }
+
+?>
diff --git a/exceptions/ControllerNotFoundException.inc b/exceptions/ControllerNotFoundException.inc
new file mode 100644
index 00000000..90859c49
--- /dev/null
+++ b/exceptions/ControllerNotFoundException.inc
@@ -0,0 +1,67 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Controller not found.
+ *
+ * @author coderkun
+ */
+ class ControllerNotFoundException extends \nre\exceptions\ClassNotFoundException
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 67;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'controller not found';
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $controllerName Name of the Controller that was not found
+ */
+ function __construct($controllerName)
+ {
+ parent::__construct(
+ $controllerName,
+ self::MESSAGE,
+ self::CODE
+ );
+ }
+
+
+
+
+ /**
+ * Get the name of the Controller that was not found.
+ *
+ * @return string Name of the Controller that was not found
+ */
+ public function getControllerName()
+ {
+ return $this->getClassName();
+ }
+
+ }
+
+?>
diff --git a/exceptions/ControllerNotValidException.inc b/exceptions/ControllerNotValidException.inc
new file mode 100644
index 00000000..0c945bcb
--- /dev/null
+++ b/exceptions/ControllerNotValidException.inc
@@ -0,0 +1,67 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Controller not valid.
+ *
+ * @author coderkun
+ */
+ class ControllerNotValidException extends \nre\exceptions\ClassNotValidException
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 77;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'controller not valid';
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $controllerName Name of the invalid Controller
+ */
+ function __construct($controllerName)
+ {
+ parent::__construct(
+ $controllerName,
+ self::MESSAGE,
+ self::CODE
+ );
+ }
+
+
+
+
+ /**
+ * Get the name of the invalid Controller.
+ *
+ * @return string Name of the invalid Controller
+ */
+ public function getControllerName()
+ {
+ return $this->getClassName();
+ }
+
+ }
+
+?>
diff --git a/exceptions/DatamodelException.inc b/exceptions/DatamodelException.inc
new file mode 100644
index 00000000..7785cd21
--- /dev/null
+++ b/exceptions/DatamodelException.inc
@@ -0,0 +1,99 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Datamodel.
+ *
+ * This exception is thrown when an error occurred during the execution
+ * of a datamodel.
+ *
+ * @author coderkun
+ */
+ class DatamodelException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 84;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'datamodel error';
+
+ /**
+ * Error message of datamodel
+ *
+ * @var string
+ */
+ private $datamodelMessage;
+ /**
+ * Error code of datamodel
+ *
+ * @var int
+ */
+ private $datamodelErrorNumber;
+
+
+
+
+ /**
+ * Consturct a new exception.
+ *
+ * @param string $datamodelMessage Error message of datamodel
+ * @param int $datamodelErrorNumber Error code of datamodel
+ */
+ function __construct($datamodelMessage, $datamodelErrorNumber)
+ {
+ parent::__construct(
+ self::MESSAGE,
+ self::CODE,
+ $datamodelMessage." ($datamodelErrorNumber)"
+ );
+
+ // Store values
+ $this->datamodelMessage = $datamodelMessage;
+ $this->datamodelErrorNumber = $datamodelErrorNumber;
+ }
+
+
+
+
+ /**
+ * Get the error message of datamodel.
+ *
+ * @return string Error message of datamodel
+ */
+ public function getDatamodelMessage()
+ {
+ return $this->datamodelMessage;
+ }
+
+
+ /**
+ * Get the error code of datamodel.
+ *
+ * @return string Error code of datamodel
+ */
+ public function getDatamodelErrorNumber()
+ {
+ return $this->datamodelErrorNumber;
+ }
+
+ }
+
+?>
diff --git a/exceptions/DriverNotFoundException.inc b/exceptions/DriverNotFoundException.inc
new file mode 100644
index 00000000..9b218f29
--- /dev/null
+++ b/exceptions/DriverNotFoundException.inc
@@ -0,0 +1,68 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Driver not found.
+ *
+ * @author coderkun
+ */
+ class DriverNotFoundException extends \nre\exceptions\ClassNotFoundException
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 71;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'driver not found';
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $driverName Name of the driver that was not found
+ */
+ function __construct($driverName)
+ {
+ // Elternkonstruktor aufrufen
+ parent::__construct(
+ $driverName,
+ self::MESSAGE,
+ self::CODE
+ );
+ }
+
+
+
+
+ /**
+ * Get the name of the driver that was not found.
+ *
+ * @return string Name of the driver that was not found
+ */
+ public function getDriverName()
+ {
+ return $this->getClassName();
+ }
+
+ }
+
+?>
diff --git a/exceptions/DriverNotValidException.inc b/exceptions/DriverNotValidException.inc
new file mode 100644
index 00000000..fa9022e8
--- /dev/null
+++ b/exceptions/DriverNotValidException.inc
@@ -0,0 +1,68 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Driver not valid.
+ *
+ * @author coderkun
+ */
+ class DriverNotValidException extends \nre\exceptions\ClassNotValidException
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 81;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'driver not valid';
+
+
+
+
+ /**
+ * Konstruktor.
+ *
+ * @param string $driverName Name of the invalid driver
+ */
+ function __construct($driverName)
+ {
+ // Elternkonstruktor aufrufen
+ parent::__construct(
+ $driverName,
+ self::MESSAGE,
+ self::CODE
+ );
+ }
+
+
+
+
+ /**
+ * Get the name of the invalid driver.
+ *
+ * @return string Name of the invalid driver
+ */
+ public function getDriverName()
+ {
+ return $this->getClassName();
+ }
+
+ }
+
+?>
diff --git a/exceptions/FatalDatamodelException.inc b/exceptions/FatalDatamodelException.inc
new file mode 100644
index 00000000..afd80b86
--- /dev/null
+++ b/exceptions/FatalDatamodelException.inc
@@ -0,0 +1,96 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Datamodel exception that is fatal for the application.
+ *
+ * @author coderkun
+ */
+ class FatalDatamodelException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 85;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'fatal datamodel error';
+
+ /**
+ * Error message of datamodel
+ *
+ * @var string
+ */
+ private $datamodelMessage;
+ /**
+ * Error code of datamodel
+ *
+ * @var int
+ */
+ private $datamodelErrorNumber;
+
+
+
+
+ /**
+ * Consturct a new exception.
+ *
+ * @param string $datamodelMessage Error message of datamodel
+ * @param int $datamodelErrorNumber Error code of datamodel
+ */
+ function __construct($datamodelMessage, $datamodelErrorNumber)
+ {
+ parent::__construct(
+ self::MESSAGE,
+ self::CODE,
+ $datamodelMessage." ($datamodelErrorNumber)"
+ );
+
+ // Store values
+ $this->datamodelMessage = $datamodelMessage;
+ $this->datamodelErrorNumber = $datamodelErrorNumber;
+ }
+
+
+
+
+ /**
+ * Get the error message of datamodel.
+ *
+ * @return string Error message of datamodel
+ */
+ public function getDatamodelMessage()
+ {
+ return $this->datamodelMessage;
+ }
+
+
+ /**
+ * Get the error code of datamodel.
+ *
+ * @return string Error code of datamodel
+ */
+ public function getDatamodelErrorNumber()
+ {
+ return $this->datamodelErrorNumber;
+ }
+
+ }
+
+?>
diff --git a/exceptions/IdNotFoundException.inc b/exceptions/IdNotFoundException.inc
new file mode 100644
index 00000000..fc3315c3
--- /dev/null
+++ b/exceptions/IdNotFoundException.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: ID not found.
+ *
+ * @author coderkun
+ */
+ class IdNotFoundException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 85;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'id not found';
+
+ /**
+ * ID that was not found
+ *
+ * @var mixed
+ */
+ private $id;
+
+
+
+
+ /**
+ * Consturct a new exception.
+ *
+ * @param mixed $id ID that was not found
+ */
+ function __construct($id)
+ {
+ parent::__construct(
+ self::MESSAGE,
+ self::CODE,
+ $id
+ );
+
+ // Store values
+ $this->id = $id;
+ }
+
+
+
+
+ /**
+ * Get the ID that was not found.
+ *
+ * @return mixed ID that was not found
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ }
+
+?>
diff --git a/exceptions/LayoutNotFoundException.inc b/exceptions/LayoutNotFoundException.inc
new file mode 100644
index 00000000..0eb8c89c
--- /dev/null
+++ b/exceptions/LayoutNotFoundException.inc
@@ -0,0 +1,68 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Layout not found.
+ *
+ * @author coderkun
+ */
+ class LayoutNotFoundException extends \nre\exceptions\AgentNotFoundException
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 65;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'layout not found';
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $layoutName Name of the Layout that was not found
+ */
+ function __construct($layoutName)
+ {
+ // Elternkonstruktor aufrufen
+ parent::__construct(
+ $layoutName,
+ self::MESSAGE,
+ self::CODE
+ );
+ }
+
+
+
+
+ /**
+ * Get the name of the Layout that was not found.
+ *
+ * @return string Name of the Layout that was not found
+ */
+ public function getLayoutName()
+ {
+ return $this->getAgentName();
+ }
+
+ }
+
+?>
diff --git a/exceptions/LayoutNotValidException.inc b/exceptions/LayoutNotValidException.inc
new file mode 100644
index 00000000..b3af184f
--- /dev/null
+++ b/exceptions/LayoutNotValidException.inc
@@ -0,0 +1,67 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Layout not valid.
+ *
+ * @author coderkun
+ */
+ class LayoutNotValidException extends \nre\exceptions\AgentNotFoundException
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 75;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'layout not valid';
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $layoutName Name of the invalid Layout
+ */
+ function __construct($layoutName)
+ {
+ parent::__construct(
+ $layoutName,
+ self::MESSAGE,
+ self::CODE
+ );
+ }
+
+
+
+
+ /**
+ * Get the name of the invalid Layout.
+ *
+ * @return string Name of the invalid Layout
+ */
+ public function getLayoutName()
+ {
+ return $this->getAgentName();
+ }
+
+ }
+
+?>
diff --git a/exceptions/ModelNotFoundException.inc b/exceptions/ModelNotFoundException.inc
new file mode 100644
index 00000000..48e8c0df
--- /dev/null
+++ b/exceptions/ModelNotFoundException.inc
@@ -0,0 +1,67 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Action not found.
+ *
+ * @author coderkun
+ */
+ class ModelNotFoundException extends \nre\exceptions\ClassNotFoundException
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 68;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'model not found';
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $modelName Name of the Model that was not found
+ */
+ function __construct($modelName)
+ {
+ parent::__construct(
+ $modelName,
+ self::MESSAGE,
+ self::CODE
+ );
+ }
+
+
+
+
+ /**
+ * Get the name of the Model that was not found
+ *
+ * @return string Name of the Model that was not found
+ */
+ public function getModelName()
+ {
+ return $this->getClassName();
+ }
+
+ }
+
+?>
diff --git a/exceptions/ModelNotValidException.inc b/exceptions/ModelNotValidException.inc
new file mode 100644
index 00000000..267e460e
--- /dev/null
+++ b/exceptions/ModelNotValidException.inc
@@ -0,0 +1,68 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Action not found.
+ *
+ * @author coderkun
+ */
+ class ModelNotValidException extends \nre\exceptions\ClassNotValidException
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 78;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'model not valid';
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $modelName Name of the invalid Model
+ */
+ function __construct($modelName)
+ {
+ // Elternkonstruktor aufrufen
+ parent::__construct(
+ $modelName,
+ self::MESSAGE,
+ self::CODE
+ );
+ }
+
+
+
+
+ /**
+ * Get the name of the invalid Model
+ *
+ * @return string Name of the invalid Model
+ */
+ public function getModelName()
+ {
+ return $this->getClassName();
+ }
+
+ }
+
+?>
diff --git a/exceptions/ParamsNotValidException.inc b/exceptions/ParamsNotValidException.inc
new file mode 100644
index 00000000..be650ce2
--- /dev/null
+++ b/exceptions/ParamsNotValidException.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception Parameters not valid.
+ *
+ * @author coderkun
+ */
+ class ParamsNotValidException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 86;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'parameters not valid';
+
+ /**
+ * Invalid parameters.
+ *
+ * @var array
+ */
+ private $params;
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param mixed $param1 Invalid parameters as argument list
+ */
+ function __construct($param1)
+ {
+ parent::__construct(
+ self::MESSAGE,
+ self::CODE,
+ implode(', ', func_get_args())
+ );
+
+ // Store values
+ $this->params = func_get_args();
+ }
+
+
+
+
+ /**
+ * Get invalid parameters.
+ *
+ * @return array Invalid parameters
+ */
+ public function getParams()
+ {
+ return $this->params;
+ }
+
+ }
+
+?>
diff --git a/exceptions/ServiceUnavailableException.inc b/exceptions/ServiceUnavailableException.inc
new file mode 100644
index 00000000..bafc297f
--- /dev/null
+++ b/exceptions/ServiceUnavailableException.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: Service is unavailable.
+ *
+ * @author coderkun
+ */
+ class ServiceUnavailableException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 84;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'service unavailable';
+
+ /**
+ * Throws exception
+ *
+ * @var Exception
+ */
+ private $exception;
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param Exception $exception Exception that has occurred
+ */
+ function __construct($exception)
+ {
+ parent::__construct(
+ self::MESSAGE,
+ self::CODE,
+ $exception->getMessage()
+ );
+
+ // Store values
+ $this->exception = $exception;
+ }
+
+
+
+
+ /**
+ * Get the exception that hat occurred
+ *
+ * @return Exception Exception that has occurred
+ */
+ public function getException()
+ {
+ return $this->exception;
+ }
+
+ }
+
+?>
diff --git a/exceptions/ViewNotFoundException.inc b/exceptions/ViewNotFoundException.inc
new file mode 100644
index 00000000..30366201
--- /dev/null
+++ b/exceptions/ViewNotFoundException.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\exceptions;
+
+
+ /**
+ * Exception: View not found.
+ *
+ * @author coderkun
+ */
+ class ViewNotFoundException extends \nre\core\Exception
+ {
+ /**
+ * Error code
+ *
+ * @var int
+ */
+ const CODE = 69;
+ /**
+ * Error message
+ *
+ * @var string
+ */
+ const MESSAGE = 'view not found';
+
+ /**
+ * Filename of the view that was not found
+ *
+ * @var string
+ */
+ private $fileName;
+
+
+
+
+ /**
+ * Construct a new exception.
+ *
+ * @param string $fileName Filename of the view that was not found
+ */
+ function __construct($fileName)
+ {
+ parent::__construct(
+ self::MESSAGE,
+ self::CODE,
+ $fileName
+ );
+
+ // Save values
+ $this->fileName = $fileName;
+ }
+
+
+
+
+ /**
+ * Get the filename of the view that was not found.
+ *
+ * @return string Filename of the view that was not found
+ */
+ public function getFileName()
+ {
+ return $this->fileName;
+ }
+
+ }
+
+?>
diff --git a/locale/de_DE/LC_MESSAGES/The Legend of Z.mo b/locale/de_DE/LC_MESSAGES/The Legend of Z.mo
new file mode 100644
index 00000000..5e8d1b22
Binary files /dev/null and b/locale/de_DE/LC_MESSAGES/The Legend of Z.mo differ
diff --git a/locale/de_DE/LC_MESSAGES/The Legend of Z.po b/locale/de_DE/LC_MESSAGES/The Legend of Z.po
new file mode 100644
index 00000000..8be6bc87
--- /dev/null
+++ b/locale/de_DE/LC_MESSAGES/The Legend of Z.po
@@ -0,0 +1,811 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: The Legend of Z\n"
+"POT-Creation-Date: 2014-04-25 02:24+0100\n"
+"PO-Revision-Date: 2014-04-25 02:26+0100\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Language: de_DE\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 1.6.4\n"
+"X-Poedit-Basepath: ../../../\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"X-Poedit-SearchPath-0: views\n"
+"X-Poedit-SearchPath-1: questtypes\n"
+
+#: questtypes/bossfight/html/quest.tpl:11
+#: questtypes/bossfight/html/quest.tpl:24
+#: questtypes/bossfight/html/submission.tpl:21
+#: questtypes/bossfight/html/submission.tpl:33
+msgid "lost"
+msgstr "verloren"
+
+#: questtypes/bossfight/html/quest.tpl:41
+msgid "Choose"
+msgstr "Wählen"
+
+#: questtypes/bossfight/html/quest.tpl:47
+#: questtypes/choiceinput/html/quest.tpl:14
+#: questtypes/crossword/html/quest.tpl:30
+#: questtypes/dragndrop/html/quest.tpl:16
+#: questtypes/multiplechoice/html/quest.tpl:19
+#: questtypes/submit/html/quest.tpl:23 questtypes/textinput/html/quest.tpl:10
+msgid "solve"
+msgstr "lösen"
+
+#: questtypes/crossword/html/quest.tpl:22
+#: questtypes/crossword/html/submission.tpl:22
+msgid "vertical"
+msgstr "vertikal"
+
+#: questtypes/crossword/html/quest.tpl:24
+#: questtypes/crossword/html/submission.tpl:24
+msgid "horizontal"
+msgstr "horizontal"
+
+#: questtypes/multiplechoice/html/quest.tpl:3
+#, php-format
+msgid "Question %d of %d"
+msgstr "Frage %d von %d"
+
+#: questtypes/multiplechoice/html/quest.tpl:17
+msgid "solve Question"
+msgstr "Frage lösen"
+
+#: questtypes/submit/html/quest.tpl:4
+#, php-format
+msgid "File has wrong type “%s”"
+msgstr "Der Dateityp „%s“ ist nicht erlaubt"
+
+#: questtypes/submit/html/quest.tpl:6
+msgid "File exceeds size maximum"
+msgstr "Die Datei ist zu groß"
+
+#: questtypes/submit/html/quest.tpl:8
+#, php-format
+msgid "Error during file upload: %s"
+msgstr "Fehler beim Dateiupload: %s"
+
+#: questtypes/submit/html/quest.tpl:17
+msgid "Allowed file types"
+msgstr "Erlaubte Dateiformate"
+
+#: questtypes/submit/html/quest.tpl:20
+#, php-format
+msgid "%s-files"
+msgstr "%s-Dateien"
+
+#: questtypes/submit/html/quest.tpl:20
+msgid "max."
+msgstr "max."
+
+#: questtypes/submit/html/quest.tpl:28
+msgid "Past submissions"
+msgstr "Vorherige Lösungen"
+
+#: questtypes/submit/html/quest.tpl:33 questtypes/submit/html/submission.tpl:6
+#, php-format
+msgid "submitted at %s on %s h"
+msgstr "eingereicht am %s um %s Uhr"
+
+#: questtypes/submit/html/quest.tpl:35
+msgid "This submission is waiting for approval"
+msgstr "Diese Lösungen wartet auf freigabe"
+
+#: questtypes/submit/html/quest.tpl:42
+#: questtypes/submit/html/submission.tpl:12
+#, php-format
+msgid "Comment from %s on %s at %s"
+msgstr "Kommentar von %s am %s um %s Uhr"
+
+#: questtypes/submit/html/submission.tpl:27
+msgid "Comment"
+msgstr "Kommentar"
+
+#: questtypes/submit/html/submission.tpl:30 views/html/quests/quest.tpl:45
+#: views/html/quests/submissions.tpl:28
+msgid "solved"
+msgstr "Richtig!"
+
+#: questtypes/submit/html/submission.tpl:31 views/html/quests/quest.tpl:50
+#: views/html/quests/submissions.tpl:19
+msgid "unsolved"
+msgstr "Leider falsch!"
+
+#: views/binary/error/index.tpl:1 views/html/error/index.tpl:1
+msgid "Error"
+msgstr "Fehler"
+
+#: views/html/achievements/index.tpl:9 views/html/seminarymenu/index.tpl:4
+msgid "Achievements"
+msgstr "Achievements"
+
+#: views/html/achievements/index.tpl:10
+msgid "Achievement description"
+msgstr ""
+"Achievements sind Auszeichnungen für deine Erfolge im Verlauf der Reise. Sie "
+"dienen als historische Erinnerungen an Meilensteine, besondere Taten und "
+"interessante oder lustige Ereignisse, die du erlebst."
+
+#: views/html/achievements/index.tpl:13
+msgid "Seldom Achievements"
+msgstr "Die seltensten Achievements"
+
+#: views/html/achievements/index.tpl:27
+#, php-format
+msgid "Achievement has been achieved only %d times"
+msgstr "wurde erst %d mal gefunden"
+
+#: views/html/achievements/index.tpl:33
+msgid "Most successful collectors"
+msgstr "Die erfolgreichsten Sammler"
+
+#: views/html/achievements/index.tpl:39
+#, php-format
+msgid "Character has achieved %d Achievements"
+msgstr "hat %d Achievement erhalten"
+
+#: views/html/achievements/index.tpl:45
+msgid "Personal Achievements"
+msgstr "Deine Achievements"
+
+#: views/html/achievements/index.tpl:47
+#, php-format
+msgid "Own progress: %d %%"
+msgstr "Persönlicher Fortschritt: %d %%"
+
+#: views/html/achievements/index.tpl:52
+#: views/html/charactergroups/group.tpl:24
+#: views/html/charactergroups/managegroup.tpl:17
+#: views/html/characters/character.tpl:36 views/html/seminarybar/index.tpl:7
+msgid "Rank"
+msgstr "Platz"
+
+#: views/html/achievements/index.tpl:52
+#, php-format
+msgid "You achieved %d of %d Achievements so far"
+msgstr "Du hast bislang %d von insgesamt %d Achievements erreicht"
+
+#: views/html/achievements/index.tpl:59 views/html/seminarybar/index.tpl:28
+#, php-format
+msgid "achieved at: %s"
+msgstr "erhalten am: %s"
+
+#: views/html/achievements/index.tpl:68
+msgid "Secret Achievement"
+msgstr "Geheimes Achievement"
+
+#: views/html/achievements/index.tpl:72
+msgid "Continue playing to unlock this secret Achievement"
+msgstr "Spiele weiter, um diesen geheimen Erfolg freizuschalten"
+
+#: views/html/charactergroups/group.tpl:8
+#: views/html/charactergroups/groupsgroup.tpl:8
+#: views/html/charactergroups/index.tpl:9
+#: views/html/charactergroups/managegroup.tpl:8
+#: views/html/characters/character.tpl:85 views/html/seminarymenu/index.tpl:3
+msgid "Character Groups"
+msgstr "Gruppen"
+
+#: views/html/charactergroups/group.tpl:14 views/html/characters/index.tpl:13
+#: views/html/characters/manage.tpl:10
+msgid "Manage"
+msgstr "Verwalten"
+
+#: views/html/charactergroups/group.tpl:26
+#: views/html/charactergroups/managegroup.tpl:19
+msgid "Members"
+msgstr "Mitglieder"
+
+#: views/html/charactergroups/group.tpl:26
+#: views/html/charactergroups/managegroup.tpl:19
+msgid "Member"
+msgstr "Mitglied"
+
+#: views/html/charactergroups/group.tpl:30
+#: views/html/charactergroups/managegroup.tpl:23
+#: views/html/characters/character.tpl:11
+#: views/html/characters/character.tpl:13 views/html/characters/index.tpl:9
+#: views/html/characters/manage.tpl:8 views/html/seminarymenu/index.tpl:2
+#: views/html/users/user.tpl:15
+msgid "Characters"
+msgstr "Charaktere"
+
+#: views/html/charactergroups/group.tpl:45
+#: views/html/charactergroups/groupsgroup.tpl:20
+#: views/html/charactergroupsquests/quest.tpl:9
+#, php-format
+msgid "%s-Quests"
+msgstr "%squests"
+
+#: views/html/charactergroups/managegroup.tpl:40
+msgid "Remove Characters"
+msgstr "Entferne Charaktere"
+
+#: views/html/charactergroups/managegroup.tpl:43
+msgid "Filter Characters"
+msgstr "Filtere Charaktere"
+
+#: views/html/charactergroups/managegroup.tpl:49
+msgid "Add Characters"
+msgstr "Füge Charaktere hinzu"
+
+#: views/html/charactergroups/managegroup.tpl:54
+#: views/html/questgroups/questgroup.tpl:57 views/html/quests/create.tpl:9
+#: views/html/quests/index.tpl:9
+msgid "Quests"
+msgstr "Quests"
+
+#: views/html/charactergroupsquests/quest.tpl:16
+msgid "Description"
+msgstr "Beschreibung"
+
+#: views/html/charactergroupsquests/quest.tpl:24
+msgid "Rules"
+msgstr "Regeln"
+
+#: views/html/charactergroupsquests/quest.tpl:31
+msgid "Won Quest"
+msgstr "Gewonnene Quest"
+
+#: views/html/charactergroupsquests/quest.tpl:37
+msgid "Lost Quest"
+msgstr "Verlorene Quest"
+
+#: views/html/characters/character.tpl:24
+msgid "Total progress"
+msgstr "Fortschritt"
+
+#: views/html/characters/character.tpl:28
+#: views/html/characters/character.tpl:67
+#: views/html/characters/character.tpl:73
+#: views/html/characters/character.tpl:79 views/html/seminarybar/index.tpl:42
+#: views/html/users/user.tpl:29
+msgid "Level"
+msgstr "Level"
+
+#: views/html/characters/character.tpl:38
+msgid "Milestones"
+msgstr "Meilensteine"
+
+#: views/html/characters/character.tpl:61
+msgid "Ranking"
+msgstr "Ranking"
+
+#: views/html/characters/character.tpl:67
+#: views/html/characters/character.tpl:73
+#: views/html/characters/character.tpl:79
+#: views/html/characters/character.tpl:90 views/html/quests/index.tpl:37
+#: views/html/seminarybar/index.tpl:6 views/html/seminarybar/index.tpl:42
+#, php-format
+msgid "%d XPs"
+msgstr "%d XP"
+
+#: views/html/characters/character.tpl:99 views/html/seminarybar/index.tpl:14
+msgid "Last Quest"
+msgstr "Letzter Speicherpunkt"
+
+#: views/html/characters/character.tpl:105
+msgid "Topic progress"
+msgstr "Thematischer Fortschritt"
+
+#: views/html/characters/manage.tpl:14
+msgid "Selection"
+msgstr "Auswahl"
+
+#: views/html/characters/manage.tpl:23 views/html/characters/manage.tpl:34
+#: views/html/characters/manage.tpl:42
+msgid "Admin"
+msgstr "Administrator"
+
+#: views/html/characters/manage.tpl:24 views/html/characters/manage.tpl:35
+#: views/html/characters/manage.tpl:43
+msgid "Moderator"
+msgstr "Moderator"
+
+#: views/html/characters/manage.tpl:25 views/html/characters/manage.tpl:37
+#: views/html/characters/manage.tpl:45
+msgid "User"
+msgstr "Benutzer"
+
+#: views/html/characters/manage.tpl:32
+msgid "Add role"
+msgstr "Füge Rolle hinzu"
+
+#: views/html/characters/manage.tpl:40
+msgid "Remove role"
+msgstr "Entferne Rolle"
+
+#: views/html/characters/register.tpl:9
+msgid "Create Character"
+msgstr "Charakter erstellen"
+
+#: views/html/characters/register.tpl:21
+#, php-format
+msgid "Character name is too short (min. %d chars)"
+msgstr "Der Charaktername ist zu kurz (min. %d Zeichen)"
+
+#: views/html/characters/register.tpl:23
+#, php-format
+msgid "Character name is too long (max. %d chars)"
+msgstr "Der Charaktername ist zu lang (max. %d Zeichen)"
+
+#: views/html/characters/register.tpl:25
+msgid "Character name contains illegal characters"
+msgstr "Der Charaktername enthält ungültige Zeichen"
+
+#: views/html/characters/register.tpl:27
+msgid "Character name already exists"
+msgstr "Der Charaktername existiert bereits"
+
+#: views/html/characters/register.tpl:29
+msgid "Character name invalid"
+msgstr "Der Charaktername ist ungültig"
+
+#: views/html/characters/register.tpl:41
+msgid "Character properties"
+msgstr "Charaktereigenschaften"
+
+#: views/html/characters/register.tpl:42 views/html/characters/register.tpl:43
+msgid "Character name"
+msgstr "Charaktername"
+
+#: views/html/characters/register.tpl:60
+#, php-format
+msgid "The Seminary field “%s” is invalid"
+msgstr "Das Kursfeld „%s“ ist ungültig"
+
+#: views/html/characters/register.tpl:65
+msgid "Seminary fields"
+msgstr "Kursfelder"
+
+#: views/html/characters/register.tpl:87 views/html/seminaries/create.tpl:14
+#: views/html/users/create.tpl:17
+msgid "create"
+msgstr "erstellen"
+
+#: views/html/error/index.tpl:5 views/html/error/index.tpl:14
+#: views/html/introduction/index.tpl:3 views/html/introduction/index.tpl:11
+#: views/html/menu/index.tpl:6 views/html/users/login.tpl:3
+#: views/html/users/login.tpl:17
+msgid "Login"
+msgstr "Login"
+
+#: views/html/error/index.tpl:8 views/html/error/index.tpl:9
+#: views/html/introduction/index.tpl:6 views/html/introduction/index.tpl:7
+#: views/html/users/create.tpl:6 views/html/users/create.tpl:7
+#: views/html/users/edit.tpl:6 views/html/users/edit.tpl:7
+#: views/html/users/login.tpl:9 views/html/users/login.tpl:10
+#: views/html/users/register.tpl:78 views/html/users/register.tpl:79
+msgid "Username"
+msgstr "Benutzername"
+
+#: views/html/error/index.tpl:10 views/html/error/index.tpl:11
+#: views/html/introduction/index.tpl:8 views/html/introduction/index.tpl:9
+#: views/html/users/create.tpl:14 views/html/users/create.tpl:15
+#: views/html/users/edit.tpl:14 views/html/users/edit.tpl:15
+#: views/html/users/login.tpl:11 views/html/users/login.tpl:12
+#: views/html/users/register.tpl:86 views/html/users/register.tpl:87
+msgid "Password"
+msgstr "Passwort"
+
+#: views/html/error/index.tpl:15 views/html/introduction/index.tpl:12
+msgid "or"
+msgstr "oder"
+
+#: views/html/error/index.tpl:15 views/html/introduction/index.tpl:12
+msgid "register yourself"
+msgstr "registriere dich"
+
+#: views/html/introduction/index.tpl:1
+msgid "Introduction"
+msgstr "Einführung"
+
+#: views/html/library/index.tpl:9 views/html/library/topic.tpl:8
+#: views/html/seminarymenu/index.tpl:5
+msgid "Library"
+msgstr "Bibliothek"
+
+#: views/html/library/index.tpl:10
+#, php-format
+msgid "Library description, %s, %s"
+msgstr ""
+"Hier findest du alle Themen aus der Vorlesung „%s“ und die passenden Quests "
+"zum Nachschlagen und Wiederholen. Dein Fortschritt in „%s“ beeinflusst den "
+"Umfang der Bibliothek, spiele also regelmäßig weiter und schalte so Quest "
+"für Quest alle Inhalte frei."
+
+#: views/html/library/index.tpl:12
+#, php-format
+msgid "Total progress: %d %%"
+msgstr "Gesamtfortschritt: %d %%"
+
+#: views/html/menu/index.tpl:2 views/html/users/create.tpl:1
+#: views/html/users/delete.tpl:1 views/html/users/edit.tpl:1
+#: views/html/users/index.tpl:1 views/html/users/login.tpl:1
+#: views/html/users/register.tpl:1 views/html/users/user.tpl:1
+msgid "Users"
+msgstr "Benutzer"
+
+#: views/html/menu/index.tpl:3 views/html/seminaries/create.tpl:6
+#: views/html/seminaries/delete.tpl:6 views/html/seminaries/edit.tpl:6
+#: views/html/seminaries/index.tpl:1
+msgid "Seminaries"
+msgstr "Kurse"
+
+#: views/html/menu/index.tpl:8
+msgid "Logout"
+msgstr "Logout"
+
+#: views/html/questgroups/create.tpl:8
+msgid "Questgroups"
+msgstr "Questgruppen"
+
+#: views/html/questgroups/create.tpl:10
+msgid "Create Questgroup"
+msgstr "Questgruppe erstellen"
+
+#: views/html/questgroups/create.tpl:14 views/html/questgroups/create.tpl:15
+#: views/html/seminaries/create.tpl:11 views/html/seminaries/create.tpl:12
+#: views/html/seminaries/edit.tpl:11 views/html/seminaries/edit.tpl:12
+msgid "Title"
+msgstr "Titel"
+
+#: views/html/questgroups/create.tpl:17 views/html/quests/create.tpl:43
+msgid "Create"
+msgstr "Erstellen"
+
+#: views/html/questgroups/questgroup.tpl:45
+msgid "Found optional Questline"
+msgstr "Optionale Questline gefunden"
+
+#: views/html/quests/create.tpl:11
+msgid "Create Quest"
+msgstr "Quest erstellen"
+
+#: views/html/quests/create.tpl:15 views/html/quests/create.tpl:16
+#: views/html/users/user.tpl:11
+msgid "Name"
+msgstr "Name"
+
+#: views/html/quests/create.tpl:17 views/html/quests/index.tpl:14
+msgid "Questgroup"
+msgstr "Questgruppe"
+
+#: views/html/quests/create.tpl:29 views/html/quests/create.tpl:30
+msgid "XPs"
+msgstr "XP"
+
+#: views/html/quests/create.tpl:37
+msgid "Entry text"
+msgstr "Einstiegstext"
+
+#: views/html/quests/create.tpl:39
+msgid "Wrong text"
+msgstr "Text für falsche Antwort"
+
+#: views/html/quests/create.tpl:40 views/html/quests/quest.tpl:41
+msgid "Task"
+msgstr "Aufgabe"
+
+#: views/html/quests/index.tpl:21
+msgid "Questtype"
+msgstr "Questtyp"
+
+#: views/html/quests/index.tpl:29
+msgid "Apply filters"
+msgstr "Filter anwenden"
+
+#: views/html/quests/index.tpl:30
+msgid "Reset filters"
+msgstr "Filter zurücksetzen"
+
+#: views/html/quests/quest.tpl:11
+msgid "Prolog"
+msgstr "Prolog"
+
+#: views/html/quests/quest.tpl:46
+#, php-format
+msgid "Quest completed. You have earned %d XPs."
+msgstr "Quest abgeschlossen. Du hast %d XP erhalten."
+
+#: views/html/quests/quest.tpl:62
+msgid "Task already successfully solved"
+msgstr "Du hast die Aufgabe bereits erfolgreich gelöst"
+
+#: views/html/quests/quest.tpl:64
+msgid "Show answer"
+msgstr "Lösung anzeigen"
+
+#: views/html/quests/quest.tpl:74
+msgid "Epilog"
+msgstr "Epilog"
+
+#: views/html/quests/quest.tpl:100
+msgid "Continuation"
+msgstr "Fortsetzung"
+
+#: views/html/quests/quest.tpl:106 views/html/quests/quest.tpl:119
+msgid "Quest"
+msgstr "Quest"
+
+#: views/html/quests/submission.tpl:9
+#, php-format
+msgid "Submission of %s"
+msgstr "Lösung von %s"
+
+#: views/html/quests/submissions.tpl:10
+msgid "submitted"
+msgstr "eingereicht"
+
+#: views/html/seminaries/create.tpl:7
+msgid "New seminary"
+msgstr "Neuer Kurs"
+
+#: views/html/seminaries/delete.tpl:7 views/html/seminaries/seminary.tpl:10
+msgid "Delete seminary"
+msgstr "Kurs löschen"
+
+#: views/html/seminaries/delete.tpl:9
+#, php-format
+msgid "Should the seminary “%s” really be deleted?"
+msgstr "Soll der Kurs „%s“ wirklich gelöscht werden?"
+
+#: views/html/seminaries/delete.tpl:11 views/html/users/delete.tpl:6
+msgid "delete"
+msgstr "löschen"
+
+#: views/html/seminaries/delete.tpl:12 views/html/users/delete.tpl:7
+msgid "cancel"
+msgstr "abbrechen"
+
+#: views/html/seminaries/edit.tpl:7 views/html/seminaries/seminary.tpl:9
+msgid "Edit seminary"
+msgstr "Kurs bearbeiten"
+
+#: views/html/seminaries/edit.tpl:14 views/html/users/edit.tpl:17
+msgid "save"
+msgstr "speichern"
+
+#: views/html/seminaries/index.tpl:4
+msgid "Create new seminary"
+msgstr "Neuen Kurs erstellen"
+
+#: views/html/seminaries/index.tpl:21
+#, php-format
+msgid "created by %s on %s"
+msgstr "erstellt von %s am %s"
+
+#: views/html/seminaries/index.tpl:24
+msgid "Create a Character"
+msgstr "Erstelle einen Charakter"
+
+#: views/html/seminaries/index.tpl:26
+#, php-format
+msgid "Your Character “%s” has not been activated yet"
+msgstr "Dein Charakter „%s“ wurde noch nicht aktiviert"
+
+#: views/html/seminaries/seminary.tpl:11
+msgid "Show Quests"
+msgstr "Quests anzeigen"
+
+#: views/html/seminarybar/index.tpl:21
+msgid "Last Achievement"
+msgstr "Letztes Achievement"
+
+#: views/html/seminarybar/index.tpl:46
+#, php-format
+msgid "Show %s-Profile"
+msgstr "%s-Profil anzeigen"
+
+#: views/html/users/create.tpl:2
+msgid "New user"
+msgstr "Neuer Benutzer"
+
+#: views/html/users/create.tpl:8 views/html/users/create.tpl:9
+#: views/html/users/edit.tpl:8 views/html/users/edit.tpl:9
+#: views/html/users/register.tpl:80 views/html/users/register.tpl:81
+msgid "Prename"
+msgstr "Vorname"
+
+#: views/html/users/create.tpl:10 views/html/users/create.tpl:11
+#: views/html/users/edit.tpl:10 views/html/users/edit.tpl:11
+#: views/html/users/register.tpl:82 views/html/users/register.tpl:83
+msgid "Surname"
+msgstr "Nachname"
+
+#: views/html/users/create.tpl:12 views/html/users/create.tpl:13
+#: views/html/users/edit.tpl:12 views/html/users/edit.tpl:13
+#: views/html/users/register.tpl:84 views/html/users/register.tpl:85
+#: views/html/users/user.tpl:12
+msgid "E‑mail address"
+msgstr "E‑Mail-Adresse"
+
+#: views/html/users/delete.tpl:2 views/html/users/user.tpl:5
+msgid "Delete user"
+msgstr "Benutzer löschen"
+
+#: views/html/users/delete.tpl:4
+#, php-format
+msgid "Should the user “%s” (%s) really be deleted?"
+msgstr "Soll der Benutzer „%s“ (%s) wirklich gelöscht werden?"
+
+#: views/html/users/edit.tpl:2 views/html/users/user.tpl:4
+msgid "Edit user"
+msgstr "Benutzer bearbeiten"
+
+#: views/html/users/index.tpl:3
+msgid "Create new user"
+msgstr "Neuen Benutzer erstellen"
+
+#: views/html/users/index.tpl:7 views/html/users/user.tpl:10
+#, php-format
+msgid "registered on %s"
+msgstr "registriert am %s"
+
+#: views/html/users/login.tpl:5
+msgid "Login failed"
+msgstr "Die Anmeldung war nicht korrekt"
+
+#: views/html/users/register.tpl:3
+msgid "Registration"
+msgstr "Registrierung"
+
+#: views/html/users/register.tpl:14
+#, php-format
+msgid "Username is too short (min. %d chars)"
+msgstr "Der Benutzername ist zu kurz (min. %d Zeichen)"
+
+#: views/html/users/register.tpl:16
+#, php-format
+msgid "Username is too long (max. %d chars)"
+msgstr "Der Benutzername ist zu lang (max. %d Zeichen)"
+
+#: views/html/users/register.tpl:18
+msgid "Username contains illegal characters"
+msgstr "Der Benutzername enthält ungültige Zeichen"
+
+#: views/html/users/register.tpl:20
+msgid "Username already exists"
+msgstr "Der Benutzername existiert bereits"
+
+#: views/html/users/register.tpl:22
+msgid "Username invalid"
+msgstr "Der Benutzername ist ungültig"
+
+#: views/html/users/register.tpl:27
+#, php-format
+msgid "Prename is too short (min. %d chars)"
+msgstr "Der Vorname ist zu kurz (min. %d Zeichen)"
+
+#: views/html/users/register.tpl:29
+#, php-format
+msgid "Prename is too long (max. %d chars)"
+msgstr "Der Vorname ist zu lang (max. %d Zeichen)"
+
+#: views/html/users/register.tpl:31
+#, php-format
+msgid "Prename contains illegal characters"
+msgstr "Der Vorname enthält ungültige Zeichen"
+
+#: views/html/users/register.tpl:33
+msgid "Prename invalid"
+msgstr "Der Vorname ist ungültig"
+
+#: views/html/users/register.tpl:38
+#, php-format
+msgid "Surname is too short (min. %d chars)"
+msgstr "Der Nachname ist zu kurz (min. %d Zeichen)"
+
+#: views/html/users/register.tpl:40
+#, php-format
+msgid "Surname is too long (max. %d chars)"
+msgstr "Der Nachname ist zu lang (max. %d Zeichen)"
+
+#: views/html/users/register.tpl:42
+#, php-format
+msgid "Surname contains illegal characters"
+msgstr "Der Nachname enthält ungültige Zeichen"
+
+#: views/html/users/register.tpl:44
+msgid "Surname invalid"
+msgstr "Der Nachname ist ungültig"
+
+#: views/html/users/register.tpl:49 views/html/users/register.tpl:53
+msgid "E‑mail address invalid"
+msgstr "Die E‑Mail-Adresse ist ungültig"
+
+#: views/html/users/register.tpl:51
+msgid "E‑mail address already exists"
+msgstr "E‑Mail-Adresse existiert bereits"
+
+#: views/html/users/register.tpl:58
+#, php-format
+msgid "Password is too short (min. %d chars)"
+msgstr "Das Passwort ist zu kurz (min. %d Zeichen)"
+
+#: views/html/users/register.tpl:60
+#, php-format
+msgid "Password is too long (max. %d chars)"
+msgstr "Das Passwort ist zu lang (max. %d Zeichen)"
+
+#: views/html/users/register.tpl:62
+msgid "Password invalid"
+msgstr "Das Passwort ist ungültig"
+
+#: views/html/users/register.tpl:89
+msgid "Register"
+msgstr "Registrieren"
+
+#: views/html/users/user.tpl:34
+msgid "Roles"
+msgstr "Rollen"
+
+#, fuzzy
+#~ msgid "%stQuests"
+#~ msgstr "Quests"
+
+#~ msgid "Character Groups Quests"
+#~ msgstr "Gruppenquests"
+
+#~ msgid "Skip task"
+#~ msgstr "Aufgabe überspringen"
+
+#~ msgid "continue"
+#~ msgstr "fortfahren"
+
+#~ msgid "Character type"
+#~ msgstr "Charaktertyp"
+
+#~ msgid "Questtopics"
+#~ msgstr "Themen"
+
+#~ msgid "Questname"
+#~ msgstr "Questname"
+
+#, fuzzy
+#~ msgid "achieved at %s"
+#~ msgstr "erhalten am: %s"
+
+#~ msgid "Usergroups"
+#~ msgstr "Benutzergruppen"
+
+#~ msgid "E‑Mail"
+#~ msgstr "E‑Mail"
+
+#~ msgid "E‑Mail-Address"
+#~ msgstr "E‑Mail-Adresse"
+
+#~ msgid "E‑mail address not valid"
+#~ msgstr "Die E‑Mail-Adresse ist nicht gültig"
+
+#~ msgid "Username is too long"
+#~ msgstr "Der Benutzername ist zu lang"
+
+#~ msgid "Registration failed: %s"
+#~ msgstr "Registrierung fehlgeschlagen: %s"
+
+#~ msgid "Words"
+#~ msgstr "Wörter"
+
+#~ msgid "Go on"
+#~ msgstr "Hier geht es weiter"
+
+#, fuzzy
+#~ msgid "Character groups"
+#~ msgstr "Charaktergruppen"
+
+#~ msgid "locked"
+#~ msgstr "gesperrt"
+
+#~ msgid "Group Leader"
+#~ msgstr "Gruppenleiter"
+
+#~ msgid "as"
+#~ msgstr "als"
+
+#~ msgid "This Quest is optional"
+#~ msgstr "Diese Quest ist optional"
diff --git a/logs/empty b/logs/empty
new file mode 100644
index 00000000..e69de29b
diff --git a/media/empty b/media/empty
new file mode 100644
index 00000000..e69de29b
diff --git a/models/AchievementsModel.inc b/models/AchievementsModel.inc
new file mode 100644
index 00000000..543a2a30
--- /dev/null
+++ b/models/AchievementsModel.inc
@@ -0,0 +1,572 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Achievements-tables.
+ *
+ * @author Oliver Hanraths
+ */
+ class AchievementsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new AchievementsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get an Achievement by its URL.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param string $achievementUrl URL-title of Achievement
+ * @return array Achievement data
+ */
+ public function getAchievementByUrl($seminaryId, $achievementUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT achievements.id, achievementconditions.condition, title, url, description, progress, unachieved_achievementsmedia_id, achieved_achievementsmedia_id '.
+ 'FROM achievements '.
+ 'LEFT JOIN achievementconditions ON achievementconditions.id = achievements.achievementcondition_id '.
+ 'WHERE seminary_id = ? AND url = ?',
+ 'is',
+ $seminaryId, $achievementUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($achievementUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get all not yet achieved Achievements for a Seminary that can
+ * only be achieved once (only by one Character).
+ *
+ * @param int $seminaryId ID of Seminary
+ * @return array Achievements data
+ */
+ public function getUnachievedOnlyOnceAchievementsForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT achievements.id, achievementconditions.condition, title, url, description, progress, hidden, only_once, all_conditions, deadline, unachieved_achievementsmedia_id, achieved_achievementsmedia_id '.
+ 'FROM achievements '.
+ 'LEFT JOIN achievementconditions ON achievementconditions.id = achievements.achievementcondition_id '.
+ 'WHERE achievements.seminary_id = ? AND only_once = 1 AND NOT EXISTS ('.
+ 'SELECT character_id '.
+ 'FROM achievements_characters '.
+ 'WHERE achievements_characters.achievement_id = achievements.id'.
+ ')',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get all Achievements that have a deadline.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @return array Achievements data
+ */
+ public function getDeadlineAchievements($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT achievements.id, achievementconditions.condition, title, url, description, progress, hidden, only_once, all_conditions, deadline, unachieved_achievementsmedia_id, achieved_achievementsmedia_id '.
+ 'FROM achievements '.
+ 'LEFT JOIN achievementconditions ON achievementconditions.id = achievements.achievementcondition_id '.
+ 'WHERE achievements.seminary_id = ? AND deadline IS NOT NULL '.
+ 'ORDER BY deadline ASC',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get seldom Achievements.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $count Number of Achievements to retrieve
+ * @return array List of seldom Achievements
+ */
+ public function getSeldomAchievements($seminaryId, $count, $alsoWithDeadline=true)
+ {
+ return $this->db->query(
+ 'SELECT id, title, url, description, progress, hidden, unachieved_achievementsmedia_id, achieved_achievementsmedia_id, count(DISTINCT character_id) AS c '.
+ 'FROM achievements_characters '.
+ 'LEFT JOIN achievements ON achievements.id = achievements_characters.achievement_id '.
+ 'WHERE achievements.seminary_id = ? AND only_once = 0 '.
+ (!$alsoWithDeadline ? 'AND achievements.deadline IS NULL ' : null).
+ 'GROUP BY achievement_id '.
+ 'ORDER BY count(DISTINCT character_id) ASC '.
+ 'LIMIT ?',
+ 'ii',
+ $seminaryId,
+ $count
+ );
+ }
+
+
+ /**
+ * Get all achieved Achievements for a Character.
+ *
+ * @param int $characterId ID of Character
+ * @return array Achievements data
+ */
+ public function getAchievedAchievementsForCharacter($characterId, $alsoWithDeadline=true)
+ {
+ return $this->db->query(
+ 'SELECT achievements.id, achievements_characters.created, achievements.title, achievements.url, achievements.description, achievements.progress, unachieved_achievementsmedia_id, achieved_achievementsmedia_id '.
+ 'FROM achievements '.
+ 'INNER JOIN achievements_characters ON achievements_characters.achievement_id = achievements.id '.
+ 'WHERE achievements_characters.character_id = ? '.
+ (!$alsoWithDeadline ? 'AND achievements.deadline IS NULL ' : null).
+ 'ORDER BY achievements_characters.created DESC',
+ 'i',
+ $characterId
+ );
+ }
+
+
+ /**
+ * Get all not yet achieved Achievements for a Character.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $characterId ID of Character
+ * @return array Achievements data
+ */
+ public function getUnachhievedAchievementsForCharacter($seminaryId, $characterId, $includeOnlyOnce=false, $alsoWithDeadline=true)
+ {
+ return $this->db->query(
+ 'SELECT achievements.id, achievementconditions.condition, title, url, description, progress, hidden, only_once, all_conditions, deadline, unachieved_achievementsmedia_id, achieved_achievementsmedia_id '.
+ 'FROM achievements '.
+ 'LEFT JOIN achievementconditions ON achievementconditions.id = achievements.achievementcondition_id '.
+ 'WHERE achievements.seminary_id = ? AND only_once <= ? AND NOT EXISTS ('.
+ 'SELECT character_id '.
+ 'FROM achievements_characters '.
+ 'WHERE '.
+ 'achievements_characters.achievement_id = achievements.id AND '.
+ 'achievements_characters.character_id = ?'.
+ ') '.
+ (!$alsoWithDeadline ? 'AND achievements.deadline IS NULL ' : null).
+ 'ORDER BY achievements.pos ASC',
+ 'iii',
+ $seminaryId,
+ $includeOnlyOnce,
+ $characterId
+ );
+ }
+
+
+ /**
+ * Get the rank for the number of achieved Achievements.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $xps Amount of achieved Achievements
+ * @return int Rank of Achievements count
+ */
+ public function getCountRank($seminaryId, $count)
+ {
+ $data = $this->db->query(
+ 'SELECT count(*) AS c '.
+ 'FROM ('.
+ 'SELECT count(DISTINCT achievement_id) '.
+ 'FROM achievements_characters '.
+ 'LEFT JOIN achievements ON achievements.id = achievements_characters.achievement_id '.
+ 'WHERE achievements.seminary_id = ? '.
+ 'GROUP BY character_id '.
+ 'HAVING count(DISTINCT achievement_id) > ?'.
+ ') AS ranking',
+ 'ii',
+ $seminaryId,
+ $count
+ );
+ if(!empty($data)) {
+ return $data[0]['c'] + 1;
+ }
+
+
+ return 1;
+ }
+
+
+ /**
+ * Get all date conditions for an Achievement.
+ *
+ * @param int $achievementId ID of Achievement
+ * @return array Date conditions
+ */
+ public function getAchievementConditionsDate($achievementId)
+ {
+ return $this->db->query(
+ 'SELECT `select` '.
+ 'FROM achievementconditions_date '.
+ 'WHERE achievement_id = ?',
+ 'i',
+ $achievementId
+ );
+ }
+
+
+ /**
+ * Check a date condition.
+ *
+ * @param string $select SELECT-string with date-functions
+ * @return boolean Result
+ */
+ public function checkAchievementConditionDate($select)
+ {
+ $data = $this->db->query(
+ 'SELECT ('.$select.') AS got '
+ );
+ if(!empty($data)) {
+ return ($data[0]['got'] == 1);
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Get all Character conditions for an Achievement.
+ *
+ * @param int $achievementId ID of Achievement
+ * @return array Character conditions
+ */
+ public function getAchievementConditionsCharacter($achievementId)
+ {
+ return $this->db->query(
+ 'SELECT field, value '.
+ 'FROM achievementconditions_character '.
+ 'WHERE achievement_id = ?',
+ 'i',
+ $achievementId
+ );
+ }
+
+
+ /**
+ * Check a Character condition.
+ *
+ * @param string $field Field to check
+ * @param int $value The value the field has to match
+ * @param int $characterId ID of Character
+ * @return boolean Result
+ */
+ public function checkAchievementConditionCharacter($field, $value, $characterId)
+ {
+ $data = $this->db->query(
+ "SELECT ($field >= $value) AS got ".
+ 'FROM v_characters '.
+ 'WHERE user_id = ?',
+ 'i',
+ $characterId
+ );
+ if(!empty($data)) {
+ return ($data[0]['got'] == 1);
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Get the progress for a Character condition.
+ *
+ * @param string $field Field to check
+ * @param int $value The value the field has to match
+ * @param int $characterId ID of Character
+ * @return float Percentage progress
+ */
+ public function getAchievementConditionCharacterProgress($field, $value, $characterId)
+ {
+ $data = $this->db->query(
+ "SELECT $field AS field ".
+ 'FROM v_characters '.
+ 'WHERE user_id = ?',
+ 'i',
+ $characterId
+ );
+ if(!empty($data)) {
+ return $data[0]['field'] / $value;
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Get all Quest conditions for an Achievement.
+ *
+ * @param int $achievementId ID of Achievement
+ * @return array Quest conditions
+ */
+ public function getAchievementConditionsQuest($achievementId)
+ {
+ return $this->db->query(
+ 'SELECT field, `count`, value, quest_id, status, groupby '.
+ 'FROM achievementconditions_quest '.
+ 'WHERE achievement_id = ?',
+ 'i',
+ $achievementId
+ );
+ }
+
+
+ /**
+ * Check a Quest condition.
+ *
+ * @param string $field Field to check
+ * @param boolean $count Conut field-value
+ * @param int $value The value the field has to match
+ * @param int $status Status of Quest or NULL
+ * @param string $groupby Field to group or NULL
+ * @param int $questId ID of related Quest or NULL
+ * @param int $characterId ID of Character
+ * @return boolean Result
+ */
+ public function checkAchievementConditionQuest($field, $count, $value, $status, $groupby, $questId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT ('.(
+ $count
+ ? "count($field) >= $value"
+ : "$field = $value"
+ ). ') AS got '.
+ 'FROM quests_characters '.
+ 'WHERE '.
+ 'character_id = ?'.
+ (!is_null($questId) ? " AND quest_id = $questId" : '').
+ (!is_null($status) ? " AND status = $status" : '').
+ (!is_null($groupby) ? " GROUP BY $groupby" : ''),
+ 'i',
+ $characterId
+ );
+ if(!empty($data)) {
+ foreach($data as &$datum) {
+ if($datum['got'] == 1) {
+ return true;
+ }
+ }
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Get the progress for a Quest condition.
+ *
+ * @param string $field Field to check
+ * @param boolean $count Conut field-value
+ * @param int $value The value the field has to match
+ * @param int $status Status of Quest or NULL
+ * @param string $groupby Field to group or NULL
+ * @param int $questId ID of related Quest or NULL
+ * @param int $characterId ID of Character
+ * @return float Percentage progress
+ */
+ public function getAchievementConditionQuestProgress($field, $count, $value, $status, $groupby, $questId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT '.(
+ $count
+ ? "count($field)"
+ : "$field"
+ ). ' AS field '.
+ 'FROM quests_characters '.
+ 'WHERE '.
+ 'character_id = ?'.
+ (!is_null($questId) ? " AND quest_id = $questId" : '').
+ (!is_null($status) ? " AND status = $status" : '').
+ (!is_null($groupby) ? " GROUP BY $groupby" : ''),
+ 'i',
+ $characterId
+ );
+ if(!empty($data))
+ {
+ $maxField = 0;
+ foreach($data as &$datum) {
+ $maxField = max($maxField, intval($datum['field']));
+ }
+
+ return $maxField / $value;
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Get all Metaachievement conditions for an Achievement.
+ *
+ * @param int $achievementId ID of Achievement
+ * @return array Metaachievement conditions
+ */
+ public function getAchievementConditionsAchievement($achievementId)
+ {
+ return $this->db->query(
+ 'SELECT field, `count`, value, meta_achievement_id, groupby '.
+ 'FROM achievementconditions_achievement '.
+ 'WHERE achievement_id = ?',
+ 'i',
+ $achievementId
+ );
+ }
+
+
+ /**
+ * Check a Metaachievement condition.
+ *
+ * @param string $field Field to check
+ * @param boolean $count Conut field-value
+ * @param int $value The value the field has to match
+ * @param string $groupby Field to group or NULL
+ * @param int $metaAchievementId ID of related Achievement or NULL
+ * @param int $characterId ID of Character
+ * @return boolean Result
+ */
+ public function checkAchievementConditionAchievement($field, $count, $value, $groupby, $metaAchievementId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT ('.(
+ $count
+ ? "count($field) >= $value"
+ : "$field = $value"
+ ). ') AS got '.
+ 'FROM achievements_characters '.
+ 'WHERE '.
+ 'character_id = ?'.
+ (!is_null($metaAchievementId) ? " AND achievement_id = $metaAchievementId" : '').
+ (!is_null($groupby) ? " GROUP BY $groupby" : ''),
+ 'i',
+ $characterId
+ );
+ if(!empty($data)) {
+ foreach($data as &$datum) {
+ if($datum['got'] == 1) {
+ return true;
+ }
+ }
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Get the progress for a Metaachievement condition.
+ *
+ * @param string $field Field to check
+ * @param boolean $count Conut field-value
+ * @param int $value The value the field has to match
+ * @param string $groupby Field to group or NULL
+ * @param int $metaAchievementId ID of related Achievement or NULL
+ * @param int $characterId ID of Character
+ * @return float Percentage progress
+ */
+ public function getAchievementConditionAchievementProgress($field, $count, $value, $groupby, $metaAchievementId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT '.(
+ $count
+ ? "count($field)"
+ : "$field"
+ ). ' AS field '.
+ 'FROM achievements_characters '.
+ 'WHERE '.
+ 'character_id = ?'.
+ (!is_null($metaAchievementId) ? " AND achievement_id = $metaAchievementId" : '').
+ (!is_null($groupby) ? " GROUP BY $groupby" : ''),
+ 'i',
+ $characterId
+ );
+ if(!empty($data))
+ {
+ $maxField = 0;
+ foreach($data as &$datum) {
+ $maxField = max($maxField, intval($datum['field']));
+ }
+
+ return $maxField / $value;
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Set an Achievement as achieved for a Character.
+ *
+ * @param int $achievementId ID of Achievement
+ * @param int $characterId ID of Character
+ */
+ public function setAchievementAchieved($achievementId, $characterId)
+ {
+ $this->db->query(
+ 'INSERT INTO achievements_characters '.
+ '(achievement_id, character_id) '.
+ 'VALUES '.
+ '(?, ?)',
+ 'ii',
+ $achievementId, $characterId
+ );
+ }
+
+
+ /**
+ * Check if a Character has achieved an Achievement.
+ *
+ * @param int $achievementId ID of Achievement
+ * @param int $characterId ID of Character
+ * @return boolean Whether Character has achieved the Achievement or not
+ */
+ public function hasCharacterAchievedAchievement($achievementId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT achievement_id, character_id, created '.
+ 'FROM achievements_characters '.
+ 'WHERE achievement_id = ? AND character_id = ?',
+ 'ii',
+ $achievementId, $characterId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return false;
+ }
+
+ }
+
+?>
diff --git a/models/AvatarsModel.inc b/models/AvatarsModel.inc
new file mode 100644
index 00000000..9c3d3701
--- /dev/null
+++ b/models/AvatarsModel.inc
@@ -0,0 +1,92 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Avatars-tables.
+ *
+ * @author Oliver Hanraths
+ */
+ class AvatarsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new AvatarsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get an Avatar by its ID
+ *
+ * @param int $avatarId ID of Avatar
+ * @return array Avatar data
+ */
+ public function getAvatarById($avatarId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, charactertype_id, xplevel_id, avatarpicture_id, small_avatarpicture_id '.
+ 'FROM avatars '.
+ 'WHERE id = ?',
+ 'i',
+ $avatarId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get an Avatar by its Character type and XP-level.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param string $charactertypeUrl URL-title of Character type
+ * @param int $xplevel XP-level
+ * @return array Avatar data
+ */
+ public function getAvatarByTypeAndLevel($seminaryId, $charactertypeUrl, $xplevel)
+ {
+ $data = $this->db->query(
+ 'SELECT avatars.id, charactertype_id, xplevel_id, avatarpicture_id, small_avatarpicture_id '.
+ 'FROM avatars '.
+ 'INNER JOIN charactertypes ON charactertypes.id = avatars.charactertype_id '.
+ 'INNER JOIN xplevels ON xplevels.id = avatars.xplevel_id AND xplevels.seminary_id = charactertypes.seminary_id '.
+ 'WHERE charactertypes.seminary_id = ? AND charactertypes.url = ? AND xplevels.level = ?',
+ 'isi',
+ $seminaryId,
+ $charactertypeUrl,
+ $xplevel
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($charactertypeUrl);
+ }
+
+
+ return $data[0];
+ }
+
+ }
+
+?>
diff --git a/models/CharactergroupsModel.inc b/models/CharactergroupsModel.inc
new file mode 100644
index 00000000..3f24a9dc
--- /dev/null
+++ b/models/CharactergroupsModel.inc
@@ -0,0 +1,235 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model of the CharactergroupsAgent to interact with
+ * Charactergroups-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharactergroupsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new CharactergroupsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get Character groups-groups of a Seminary.
+ *
+ * @param int $seminaryId ID of the corresponding Seminary
+ * @return array Character groups-groups data
+ */
+ public function getGroupsroupsForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT id, name, url, preferred '.
+ 'FROM charactergroupsgroups '.
+ 'WHERE seminary_id = ?',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get a Character groups-group by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param int $seminaryId ID of the corresponding Seminary
+ * @param string $groupsgroupUrl URL-name of the Character groups-group
+ * @return array Character groups-group data
+ */
+ public function getGroupsgroupByUrl($seminaryId, $groupsgroupUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, name, url, preferred '.
+ 'FROM charactergroupsgroups '.
+ 'WHERE seminary_id = ? AND url = ?',
+ 'is',
+ $seminaryId, $groupsgroupUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($groupsgroupUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a Character groups-group by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param string $groupsgroupId ID of the Character groups-group
+ * @return array Character groups-group data
+ */
+ public function getGroupsgroupById($groupsgroupId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, name, url, preferred '.
+ 'FROM charactergroupsgroups '.
+ 'WHERE id = ?',
+ 'i',
+ $groupsgroupId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($groupsgroupId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get Character groups for a Character groups-group.
+ *
+ * @param int $groupsgroupId ID of the Character groups-group
+ * @return array Character groups
+ */
+ public function getGroupsForGroupsgroup($groupsgroupId)
+ {
+ return $this->db->query(
+ 'SELECT id, name, url, xps, motto '.
+ 'FROM v_charactergroups '.
+ 'WHERE charactergroupsgroup_id = ?',
+ 'i',
+ $groupsgroupId
+ );
+ }
+
+
+ /**
+ * Get Character groups for a Character.
+ *
+ * @param int $characterId ID of the Character
+ * @return array Character groups
+ */
+ public function getGroupsForCharacter($characterId)
+ {
+ return $this->db->query(
+ 'SELECT charactergroups.id, charactergroups.charactergroupsgroup_id, charactergroups.name, charactergroups.url, charactergroups.xps, charactergroupsgroups.id AS charactergroupsgroup_id, charactergroupsgroups.name AS charactergroupsgroup_name, charactergroupsgroups.url AS charactergroupsgroup_url '.
+ 'FROM characters_charactergroups '.
+ 'LEFT JOIN v_charactergroups AS charactergroups ON charactergroups.id = characters_charactergroups.charactergroup_id '.
+ 'LEFT JOIN charactergroupsgroups ON charactergroupsgroups.id = charactergroups.charactergroupsgroup_id '.
+ 'WHERE characters_charactergroups.character_id = ?',
+ 'i',
+ $characterId
+ );
+ }
+
+
+ /**
+ * Get a Character group by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param int $groupsgroupId ID of the Character groups-group
+ * @param string $groupUrl URL-name of the Character group
+ * @return array Character group data
+ */
+ public function getGroupByUrl($groupsgroupId, $groupUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, name, url, xps, motto '.
+ 'FROM v_charactergroups '.
+ 'WHERE charactergroupsgroup_id = ? AND url = ?',
+ 'is',
+ $groupsgroupId, $groupUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($groupUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get the rank of a XP-value of a Character.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $xps XP-value to get rank for
+ * @return int Rank of XP-value
+ */
+ public function getXPRank($groupsgroupId, $xps)
+ {
+ $data = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM v_charactergroups '.
+ 'WHERE charactergroupsgroup_id = ? AND xps > ?',
+ 'id',
+ $groupsgroupId, $xps
+ );
+ if(!empty($data)) {
+ return $data[0]['c'] + 1;
+ }
+
+
+ return 1;
+ }
+
+
+ /**
+ * Add a Character to a Character group.
+ *
+ * @param int $groupId ID of Character group
+ * @param int $characterId ID of Character to add
+ */
+ public function addCharacterToCharactergroup($groupId, $characterId)
+ {
+ $this->db->query(
+ 'INSERT INTO characters_charactergroups '.
+ '(character_id, charactergroup_id) '.
+ 'VALUES '.
+ '(?, ?)',
+ 'ii',
+ $characterId,
+ $groupId
+ );
+ }
+
+
+ /**
+ * Remove a Character from a Character group.
+ *
+ * @param int $groupId ID of Character group
+ * @param int $characterId ID of Character to remove
+ */
+ public function removeCharacterFromCharactergroup($groupId, $characterId)
+ {
+ $this->db->query(
+ 'DELETE FROM characters_charactergroups '.
+ 'WHERE charactergroup_id = ? AND character_id = ?',
+ 'ii',
+ $groupId,
+ $characterId
+ );
+ }
+
+ }
+
+?>
diff --git a/models/CharactergroupsquestsModel.inc b/models/CharactergroupsquestsModel.inc
new file mode 100644
index 00000000..4490168d
--- /dev/null
+++ b/models/CharactergroupsquestsModel.inc
@@ -0,0 +1,136 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model of the CharactergroupsquestsAgent to interact with
+ * Charactergroupsquests-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharactergroupsquestsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new CharactergroupsquestsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get Character groups Quests of a Character groups-groups.
+ *
+ * @param int $groupsgroupId ID of the Character groups-group
+ * @return array Character groups Quest data
+ */
+ public function getQuestsForCharactergroupsgroup($groupsgroupId)
+ {
+ return $this->db->query(
+ 'SELECT id, questgroups_id, title, url, xps '.
+ 'FROM charactergroupsquests '.
+ 'WHERE charactergroupsgroup_id = ?',
+ 'i',
+ $groupsgroupId
+ );
+ }
+
+
+ /**
+ * Get a Character groups Quest by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param int $groupsgroupId ID of the Character groups-group
+ * @param string $questUrl URL-title of the Character groups Quest
+ * @return array Character groups Quest data
+ */
+ public function getQuestByUrl($groupsgroupId, $questUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, questgroups_id, title, url, description, xps, rules, won_text, lost_text, questsmedia_id '.
+ 'FROM charactergroupsquests '.
+ 'WHERE charactergroupsgroup_id = ? AND url = ?',
+ 'is',
+ $groupsgroupId,
+ $questUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get the Character groups for a Quest.
+ *
+ * @param int $questId ID of the Character groups Quest
+ * @return array Character groups
+ */
+ public function getGroupsForQuest($questId)
+ {
+ $groups = $this->db->query(
+ 'SELECT charactergroups.id, charactergroups.name, charactergroups.url, charactergroupsquests_groups.created, charactergroupsquests_groups.xps_factor, charactergroupsquests.xps '.
+ 'FROM charactergroupsquests_groups '.
+ 'LEFT JOIN charactergroups ON charactergroups.id = charactergroupsquests_groups.charactergroup_id '.
+ 'LEFT JOIN charactergroupsquests ON charactergroupsquests.id = charactergroupsquests_groups.charactergroupsquest_id '.
+ 'WHERE charactergroupsquests_groups.charactergroupsquest_id = ?',
+ 'i',
+ $questId
+ );
+ foreach($groups as &$group) {
+ $group['xps'] = round($group['xps'] * $group['xps_factor'], 1);
+ }
+
+
+ return $groups;
+ }
+
+
+ /**
+ * Get Character groups Quests for a Character group.
+ *
+ * @param int $groupId ID of the Character group
+ * @return array Character groups Quests
+ */
+ public function getQuestsForGroup($groupId)
+ {
+ $quests = $this->db->query(
+ 'SELECT charactergroupsquests.id, charactergroupsquests_groups.created, charactergroupsquests.title, charactergroupsquests.url, charactergroupsquests.xps, charactergroupsquests_groups.xps_factor '.
+ 'FROM charactergroupsquests_groups '.
+ 'LEFT JOIN charactergroupsquests ON charactergroupsquests.id = charactergroupsquests_groups.charactergroupsquest_id '.
+ 'WHERE charactergroupsquests_groups.charactergroup_id = ?',
+ 'i',
+ $groupId
+ );
+ foreach($quests as &$quest) {
+ $quest['group_xps'] = round($quest['xps'] * $quest['xps_factor'], 1);
+ }
+
+
+ return $quests;
+ }
+
+
+ }
+
+?>
diff --git a/models/CharacterrolesModel.inc b/models/CharacterrolesModel.inc
new file mode 100644
index 00000000..752d4a0a
--- /dev/null
+++ b/models/CharacterrolesModel.inc
@@ -0,0 +1,100 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with characterroles-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharacterrolesModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new CharacterrolesModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all characterroles for a Character referenced by its ID.
+ *
+ * @param int $userId ID of an user
+ * @return array Characterroles for a Character
+ */
+ public function getCharacterrolesForCharacterById($characterId)
+ {
+ return $this->db->query(
+ 'SELECT characterroles.id, characterroles.created, characterroles.name '.
+ 'FROM characters_characterroles '.
+ 'LEFT JOIN characterroles ON characterroles.id = characters_characterroles.characterrole_id '.
+ 'WHERE characters_characterroles.character_id = ?',
+ 'i',
+ $characterId
+ );
+ }
+
+
+ /**
+ * Add a role to a Character.
+ *
+ * @param int $characterId ID of Character to add role to
+ * @param string $characterrole Role to add
+ */
+ public function addCharacterroleToCharacter($characterId, $characterrole)
+ {
+ $this->db->query(
+ 'INSERT IGNORE INTO characters_characterroles '.
+ '(character_id, characterrole_id) '.
+ 'SELECT ?, id '.
+ 'FROM characterroles '.
+ 'WHERE name = ?',
+ 'is',
+ $characterId,
+ $characterrole
+ );
+ }
+
+
+ /**
+ * Remove a role from a Character.
+ *
+ * @param int $characterId ID of Character to remove role from
+ * @param string $characterrole Role to remove
+ */
+ public function removeCharacterroleFromCharacter($characterId, $characterrole)
+ {
+ $this->db->query(
+ 'DELETE FROM characters_characterroles '.
+ 'WHERE character_id = ? AND characterrole_id = ('.
+ 'SELECT id '.
+ 'FROM characterroles '.
+ 'WHERE name = ?'.
+ ')',
+ 'is',
+ $characterId,
+ $characterrole
+ );
+ }
+
+ }
+
+?>
diff --git a/models/CharactersModel.inc b/models/CharactersModel.inc
new file mode 100644
index 00000000..329a7403
--- /dev/null
+++ b/models/CharactersModel.inc
@@ -0,0 +1,522 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Characters-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharactersModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new CharactersModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all characters for an user.
+ *
+ * @param int $userId ID of the user
+ * @return array Characters
+ */
+ public function getCharactersForUser($userId)
+ {
+ return $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url, seminaries.id AS seminary_id, seminaries.url AS seminary_url, seminaries.title AS seminary_title, seminaries.url AS seminary_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'LEFT JOIN seminaries ON seminaries.id = charactertypes.seminary_id '.
+ 'WHERE user_id = ?',
+ 'i',
+ $userId
+ );
+ }
+
+
+ /**
+ * Get Characters for a Seminary.
+ *
+ * @param int $seminaryId ID of the Seminary
+ * @return array Characters
+ */
+ public function getCharactersForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url, seminaries.id AS seminary_url, seminaries.title AS seminary_title, seminaries.url AS seminary_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'LEFT JOIN seminaries ON seminaries.id = charactertypes.seminary_id '.
+ 'WHERE seminaries.id = ?',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get Characters for a Character group.
+ *
+ * @param int $groupId ID of the Character group
+ * @return array Characters
+ */
+ public function getCharactersForGroup($groupId)
+ {
+ return $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN characters_charactergroups ON characters_charactergroups.character_id = characters.id '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE characters_charactergroups.charactergroup_id = ?',
+ 'i',
+ $groupId
+ );
+ }
+
+
+ /**
+ * Get the character of a user for a Seminary.
+ *
+ * @throws IdNotFoundException
+ * @param int $userId ID of the user
+ * @param int $seminaryId ID of the Seminary
+ * @return array Character data
+ */
+ public function getCharacterForUserAndSeminary($userId, $seminaryId)
+ {
+ $data = $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE characters.user_id = ? AND charactertypes.seminary_id = ?',
+ 'ii',
+ $userId, $seminaryId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($userId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a Character by its Url.
+ *
+ * @throws IdNotFoundException
+ * @param int $seminaryId ID of the Seminary
+ * @param string $characterUrl URL-name of the Character
+ * @return array Character data
+ */
+ public function getCharacterByUrl($seminaryId, $characterUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE charactertypes.seminary_id = ? AND characters.url = ?',
+ 'is',
+ $seminaryId, $characterUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($characterUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a Character by its Id.
+ *
+ * @throws IdNotFoundException
+ * @param string $characterId ID of the Character
+ * @return array Character data
+ */
+ public function getCharacterById($characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE characters.id = ?',
+ 'i',
+ $characterId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($characterUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get Characters with the most amount of Achievements.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $conut Amount of Characters to retrieve
+ * @return array List of Characters
+ */
+ public function getCharactersWithMostAchievements($seminaryId, $count, $alsoWithDeadline=true)
+ {
+ return $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url, count(DISTINCT achievement_id) AS c '.
+ 'FROM achievements_characters '.
+ 'INNER JOIN achievements ON achievements.id = achievements_characters.achievement_id '.
+ 'INNER JOIN v_characters AS characters ON characters.id = achievements_characters.character_id '.
+ 'INNER JOIN characters_characterroles ON characters_characterroles.character_id = characters.id '.
+ 'INNER JOIN characterroles ON characterroles.id = characters_characterroles.characterrole_id AND characterroles.name = ? '.
+ 'INNER JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE achievements.seminary_id = ? AND deadline IS NULL '.
+ (!$alsoWithDeadline ? 'AND achievements.deadline IS NULL ' : null).
+ 'GROUP BY achievements_characters.character_id '.
+ 'ORDER BY count(DISTINCT achievements_characters.achievement_id) DESC '.
+ 'LIMIT ?',
+ 'sii',
+ 'user',
+ $seminaryId,
+ $count
+ );
+ }
+
+
+ /**
+ * Calculate only XPs for a Character achieved through Quests.
+ *
+ * @param int $characterId ID of Character
+ * @return int Quest-XPs for Character
+ */
+ public function getQuestXPsOfCharacter($characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT quest_xps '.
+ 'FROM v_charactersxps '.
+ 'WHERE character_id = ?',
+ 'i',
+ $characterId
+ );
+ if(!empty($data)) {
+ return $data[0]['quest_xps'];
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Get the XP-level of a Character.
+ *
+ * @param string $characterId ID of the Character
+ * @return array XP-level of Character
+ */
+ public function getXPLevelOfCharacters($characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT xplevels.xps, xplevels.level, xplevels.name '.
+ 'FROM v_charactersxplevels '.
+ 'INNER JOIN xplevels ON xplevels.id = v_charactersxplevels.xplevel_id '.
+ 'WHERE v_charactersxplevels.character_id = ?',
+ 'i',
+ $characterId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get the rank of a XP-value of a Character.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $xps XP-value to get rank for
+ * @return int Rank of XP-value
+ */
+ public function getXPRank($seminaryId, $xps)
+ {
+ $data = $this->db->query(
+ 'SELECT count(characters.id) AS c '.
+ 'FROM charactertypes '.
+ 'INNER JOIN v_characters AS characters ON characters.charactertype_id = charactertypes.id '.
+ 'INNER JOIN characters_characterroles ON characters_characterroles.character_id = characters.id '.
+ 'INNER JOIN characterroles ON characterroles.id = characters_characterroles.characterrole_id AND characterroles.name = ? '.
+ 'WHERE seminary_id = ? AND characters.xps > ?',
+ 'sid',
+ 'user',
+ $seminaryId, $xps
+ );
+ if(!empty($data)) {
+ return $data[0]['c'] + 1;
+ }
+
+
+ return 1;
+ }
+
+
+ /**
+ * Get the superior $count Characters in the ranking.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $xps XP-value of Character
+ * @param int $count Count of Characters to determine
+ * @return array List of superior Characters
+ */
+ public function getSuperiorCharacters($seminaryId, $xps, $count)
+ {
+ return $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'INNER JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'INNER JOIN characters_characterroles ON characters_characterroles.character_id = characters.id '.
+ 'INNER JOIN characterroles ON characterroles.id = characters_characterroles.characterrole_id AND characterroles.name = ? '.
+ 'WHERE charactertypes.seminary_id = ? AND characters.xps > ? '.
+ 'ORDER BY characters.xps ASC '.
+ 'LIMIT ?',
+ 'sidd',
+ 'user',
+ $seminaryId, $xps, $count
+ );
+ }
+
+
+ /**
+ * Get the inferior $count Characters in the ranking.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $xps XP-value of Character
+ * @param int $count Count of Characters to determine
+ * @return array List of inferior Characters
+ */
+ public function getInferiorCharacters($seminaryId, $xps, $count)
+ {
+ return $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'INNER JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'INNER JOIN characters_characterroles ON characters_characterroles.character_id = characters.id '.
+ 'INNER JOIN characterroles ON characterroles.id = characters_characterroles.characterrole_id AND characterroles.name = ? '.
+ 'WHERE charactertypes.seminary_id = ? AND characters.xps < ? '.
+ 'ORDER BY characters.xps DESC '.
+ 'LIMIT ?',
+ 'sidd',
+ 'user',
+ $seminaryId, $xps, $count
+ );
+ }
+
+
+ /**
+ * Get Characters that solved a Quest.
+ *
+ * @param int $questId ID of Quest to get Characters for
+ * @return array Characters data
+ */
+ public function getCharactersSolvedQuest($questId)
+ {
+ return $data = $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE EXISTS ('.
+ 'SELECT character_id FROM quests_characters WHERE character_id = characters.id AND quest_id = ? AND status = ?'.
+ ')',
+ 'ii',
+ $questId,
+ QuestsModel::QUEST_STATUS_SOLVED
+ );
+ }
+
+
+ /**
+ * Get Characters that did not solv a Quest.
+ *
+ * @param int $questId ID of Quest to get Characters for
+ * @return array Characters data
+ */
+ public function getCharactersUnsolvedQuest($questId)
+ {
+ return $data = $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE EXISTS ('.
+ 'SELECT character_id FROM quests_characters WHERE character_id = characters.id AND quest_id = ? AND status = ?'.
+ ') AND NOT EXISTS ('.
+ 'SELECT character_id FROM quests_characters WHERE character_id = characters.id AND quest_id = ? AND status = ?'.
+ ')',
+ 'iiii',
+ $questId, QuestsModel::QUEST_STATUS_UNSOLVED,
+ $questId, QuestsModel::QUEST_STATUS_SOLVED
+ );
+ }
+
+
+ /**
+ * Get Characters that sent a submission for a Quest.
+ *
+ * @param int $questId ID of Quest to get Characters for
+ * @return array Characters data
+ */
+ public function getCharactersSubmittedQuest($questId)
+ {
+ return $data = $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'WHERE ('.
+ 'SELECT status '.
+ 'FROM quests_characters '.
+ 'WHERE quest_id = ? AND character_id = characters.id '.
+ 'ORDER BY created DESC '.
+ 'LIMIT 1'.
+ ') = ?',
+ 'ii',
+ $questId, QuestsModel::QUEST_STATUS_SUBMITTED
+ );
+ }
+
+
+ public function getXPLevelsForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT id, xps, level, name '.
+ 'FROM xplevels '.
+ 'WHERE seminary_id = ? '.
+ 'ORDER BY level ASC',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Check if a Character name already exists.
+ *
+ * @param string $name Character name to check
+ * @return boolean Whether Character name exists or not
+ */
+ public function characterNameExists($name)
+ {
+ $data = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM characters '.
+ 'WHERE name = ? OR url = ?',
+ 'ss',
+ $name,
+ \nre\core\Linker::createLinkParam($name)
+ );
+ if(!empty($data)) {
+ return ($data[0]['c'] > 0);
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Create a new Character.
+ *
+ * @param int $userId User-ID that creates the new character
+ * @param int $charactertypeId ID of type of new Character
+ * @param string $characterName Name for the new Character
+ * @return int ID of Character
+ */
+ public function createCharacter($userId, $charactertypeId, $characterName)
+ {
+ $this->db->query(
+ 'INSERT INTO characters '.
+ '(user_id, charactertype_id, name, url) '.
+ 'VALUES '.
+ '(?, ?, ?, ?)',
+ 'iiss',
+ $userId,
+ $charactertypeId,
+ $characterName,
+ \nre\core\Linker::createLinkParam($characterName)
+ );
+
+
+ return $this->db->getInsertId();
+ }
+
+
+ /**
+ * Set the value of a Seminary field for a Character.
+ *
+ * @param int $characterId ID of Character
+ * @param int $seminarycharacterfieldId ID of seminarycharacterfield to set value of
+ * @param string $value Value to set
+ */
+ public function setSeminaryFieldOfCharacter($characterId, $seminarycharacterfieldId, $value)
+ {
+ $this->db->query(
+ 'INSERT INTO characters_seminarycharacterfields '.
+ '(character_id, seminarycharacterfield_id, value) '.
+ 'VALUES '.
+ '(?, ?, ?) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'value = ?',
+ 'iiss',
+ $characterId,
+ $seminarycharacterfieldId,
+ $value,
+ $value
+ );
+ }
+
+
+ /**
+ * Get Characters with the given Character role.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param string $characterrole Character role
+ * @return array List of users
+ */
+ public function getCharactersWithCharacterRole($seminaryId, $characterrole)
+ {
+ return $this->db->query(
+ 'SELECT characters.id, characters.created, characters.charactertype_id, characters.name, characters.url, characters.user_id, characters.xps, characters.xplevel, characters.avatar_id, charactertypes.name AS charactertype_name, charactertypes.url AS charactertype_url, seminaries.id AS seminary_url, seminaries.title AS seminary_title, seminaries.url AS seminary_url '.
+ 'FROM v_characters AS characters '.
+ 'LEFT JOIN charactertypes ON charactertypes.id = characters.charactertype_id '.
+ 'LEFT JOIN seminaries ON seminaries.id = charactertypes.seminary_id '.
+ 'LEFT JOIN characters_characterroles ON characters_characterroles.character_id = characters.id '.
+ 'LEFT JOIN characterroles ON characterroles.id = characters_characterroles.characterrole_id '.
+ 'WHERE seminaries.id = ? AND characterroles.name = ?',
+ 'is',
+ $seminaryId, $characterrole
+ );
+ }
+
+ }
+
+?>
diff --git a/models/CharactertypesModel.inc b/models/CharactertypesModel.inc
new file mode 100644
index 00000000..07c8d5e9
--- /dev/null
+++ b/models/CharactertypesModel.inc
@@ -0,0 +1,57 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Charactertypes-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class CharactertypesModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new CharactertypesModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all Character types of a Seminary.
+ *
+ * @param int $seminaryId ID of Seminary to get types of
+ * @return array Character types
+ */
+ public function getCharacterTypesForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT id, name, url '.
+ 'FROM charactertypes '.
+ 'WHERE seminary_id = ? '.
+ 'ORDER BY name ASC',
+ 'i',
+ $seminaryId
+ );
+ }
+
+ }
+
+?>
diff --git a/models/DatabaseModel.inc b/models/DatabaseModel.inc
new file mode 100644
index 00000000..51259f4f
--- /dev/null
+++ b/models/DatabaseModel.inc
@@ -0,0 +1,83 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\models;
+
+
+ /**
+ * Default implementation of a database model.
+ *
+ * @author coderkun
+ */
+ class DatabaseModel extends \nre\core\Model
+ {
+ /**
+ * Database connection
+ *
+ * @static
+ * @var DatabaseDriver
+ */
+ protected $db = NULL;
+
+
+
+
+ /**
+ * Construct a new datamase model.
+ *
+ * @throws DatamodelException
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @param string $type Database type
+ * @param array $config Connection settings
+ */
+ function __construct($type, $config)
+ {
+ parent::__construct();
+
+ // Load database driver
+ $this->loadDriver($type);
+
+ // Establish database connection
+ $this->connect($type, $config);
+ }
+
+
+
+
+ /**
+ * Load the database driver.
+ *
+ * @throws DriverNotFoundException
+ * @throws DriverNotValidException
+ * @param string $driverName Name of the database driver
+ */
+ private function loadDriver($driverName)
+ {
+ \nre\core\Driver::load($driverName);
+ }
+
+
+ /**
+ * Establish a connection to the database.
+ *
+ * @throws DatamodelException
+ * @param string $driverName Name of the database driver
+ * @param array $config Connection settings
+ */
+ private function connect($driverName, $config)
+ {
+ $this->db = \nre\core\Driver::factory($driverName, $config);
+ }
+
+ }
+
+?>
diff --git a/models/MediaModel.inc b/models/MediaModel.inc
new file mode 100644
index 00000000..031df753
--- /dev/null
+++ b/models/MediaModel.inc
@@ -0,0 +1,195 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with the Media-tables.
+ *
+ * @author Oliver Hanraths
+ */
+ class MediaModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new MediaModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get a medium by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param string $mediaURL URL-name of the Medium
+ * @return array Medium data
+ */
+ public function getMediaByUrl($mediaUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, name, url, description, mimetype '.
+ 'FROM media '.
+ 'WHERE url = ?',
+ 's',
+ $mediaUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($mediaUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a medium by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param int $mediaId ID of the Medium
+ * @return array Medium data
+ */
+ public function getMediaById($mediaId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, name, url, description, mimetype '.
+ 'FROM media '.
+ 'WHERE id = ?',
+ 'i',
+ $mediaId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($mediaId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a Seminary medium by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param int $seminaryId ID of the seminary
+ * @param string $seminaryMediaUrl URL-name of the Seminary medium
+ * @return array Seminary medium data
+ */
+ public function getSeminaryMediaByUrl($seminaryId, $seminaryMediaUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, name, url, description, mimetype '.
+ 'FROM seminarymedia '.
+ 'WHERE url = ?',
+ 's',
+ $seminaryMediaUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($seminaryMediaUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a Seminary medium by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param int $seminaryMediaId ID of the Seminary medium
+ * @return array Seminary medium data
+ */
+ public function getSeminaryMediaById($mediaId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, name, url, description, mimetype '.
+ 'FROM seminarymedia '.
+ 'WHERE id = ?',
+ 'i',
+ $mediaId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($mediaId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Create a new Questsmedia by creating a new Seminarymedia and
+ * adding it to the list of Questsmedia.
+ * TODO Currently only temporary for easier data import.
+ */
+ public function createQuestMedia($userId, $seminaryId, $filename, $description, $mimetype, $tmpFilename)
+ {
+ $uploadId = false;
+ $this->db->setAutocommit(false);
+
+ try {
+ // Create database record
+ $this->db->query(
+ 'INSERT INTO seminarymedia '.
+ '(created_user_id, seminary_id, name, url, description, mimetype) '.
+ 'VALUES '.
+ '(?, ? ,? ,?, ?, ?)',
+ 'iissss',
+ $userId,
+ $seminaryId,
+ $filename,
+ \nre\core\Linker::createLinkParam($filename),
+ $description,
+ $mimetype
+ );
+ $uploadId = $this->db->getInsertId();
+
+ $this->db->query(
+ 'INSERT INTO questsmedia '.
+ '(media_id, created_user_id) '.
+ 'VALUES '.
+ '(?, ?)',
+ 'ii',
+ $uploadId,
+ $userId
+ );
+
+ // Create filename
+ $filename = ROOT.DS.'seminarymedia'.DS.$uploadId;
+ if(!move_uploaded_file($tmpFilename, $filename))
+ {
+ $this->db->rollback();
+ $uploadId = false;
+ }
+ }
+ catch(\nre\exceptions\DatamodelException $e) {
+ $this->db->rollback();
+ $this->db->setAutocommit(true);
+ }
+
+
+ $this->db->setAutocommit(true);
+ return $uploadId;
+ }
+
+ }
+
+?>
diff --git a/models/QuestgroupsModel.inc b/models/QuestgroupsModel.inc
new file mode 100644
index 00000000..21ee1452
--- /dev/null
+++ b/models/QuestgroupsModel.inc
@@ -0,0 +1,767 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Questgroups-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuestgroupsModel extends \hhu\z\Model
+ {
+ /**
+ * Questgroup-status: Entered
+ *
+ * @var int;
+ */
+ const QUESTGROUP_STATUS_ENTERED = 0;
+
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('questgroupshierarchy', 'quests', 'questtexts');
+
+
+
+
+ /**
+ * Construct a new QuestgroupsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all Questgroups for a Questgroup hierarchy.
+ *
+ * @param int $hierarchyId ID of the Questgroup hierarchy to get Questgroups for
+ * @param int $parentQuestgroupId ID of the parent Questgroup hierarchy
+ * @return array Questgroups for the given hierarchy
+ */
+ public function getQuestgroupsForHierarchy($hierarchyId, $parentQuestgroupId=null)
+ {
+ // Get Questgroups
+ $questgroups = array();
+ if(is_null($parentQuestgroupId))
+ {
+ $questgroups = $this->db->query(
+ 'SELECT questgroups.id, questgroups_questgroupshierarchy.questgroupshierarchy_id, questgroups_questgroupshierarchy.pos, questgroups.title, questgroups.url, questgroups.questgroupspicture_id '.
+ 'FROM questgroups_questgroupshierarchy '.
+ 'INNER JOIN questgroups ON questgroups.id = questgroups_questgroupshierarchy.questgroup_id '.
+ 'WHERE questgroups_questgroupshierarchy.questgroupshierarchy_id = ? AND questgroups_questgroupshierarchy.parent_questgroup_id IS NULL '.
+ 'ORDER BY questgroups_questgroupshierarchy.pos ASC',
+ 'i',
+ $hierarchyId
+ );
+ }
+ else
+ {
+ $questgroups = $this->db->query(
+ 'SELECT questgroups.id, questgroups_questgroupshierarchy.questgroupshierarchy_id, questgroups_questgroupshierarchy.pos, questgroups.title, questgroups.url, questgroups.questgroupspicture_id '.
+ 'FROM questgroups_questgroupshierarchy '.
+ 'INNER JOIN questgroups ON questgroups.id = questgroups_questgroupshierarchy.questgroup_id '.
+ 'WHERE questgroups_questgroupshierarchy.questgroupshierarchy_id = ? AND questgroups_questgroupshierarchy.parent_questgroup_id = ? '.
+ 'ORDER BY questgroups_questgroupshierarchy.pos ASC',
+ 'ii',
+ $hierarchyId, $parentQuestgroupId
+ );
+ }
+
+
+ // Return Questgroups
+ return $questgroups;
+ }
+
+
+ /**
+ * Get all Questgroups for a Seminary.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @return array List of Questgroups
+ */
+ public function getQuestgroupsForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT id, title, url '.
+ 'FROM questgroups '.
+ 'WHERE seminary_id = ? '.
+ 'ORDER BY title ASC',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get a Questgroup by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param int $questgroupId ID of a Questgroup
+ * @return array Questgroup data
+ */
+ public function getQuestgroupById($questgroupId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, title, url, questgroupspicture_id '.
+ 'FROM questgroups '.
+ 'WHERE questgroups.id = ?',
+ 'i',
+ $questgroupId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questgroupId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a Questgroup by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param int $seminaryId ID of the corresponding seminary
+ * @param string $questgroupURL URL-title of a Questgroup
+ * @return array Questgroup data
+ */
+ public function getQuestgroupByUrl($seminaryId, $questgroupUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, title, url, questgroupspicture_id '.
+ 'FROM questgroups '.
+ 'WHERE seminary_id = ? AND url = ?',
+ 'is',
+ $seminaryId, $questgroupUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questgroupUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get texts of a Questgroup.
+ *
+ * @param int $questgroupId ID of a Questgroup
+ * @return array Texts of this Questgroup
+ */
+ public function getQuestgroupTexts($questgroupId)
+ {
+ return $this->db->query(
+ 'SELECT id, pos, text '.
+ 'FROM questgrouptexts '.
+ 'WHERE questgroup_id = ? '.
+ 'ORDER BY pos ASC',
+ 'i',
+ $questgroupId
+ );
+ }
+
+
+ /**
+ * Get first texts of a Questgroup.
+ *
+ * @param int $questgroupId ID of a Questgroup
+ * @return array First Text of this Questgroup
+ */
+ public function getFirstQuestgroupText($questgroupId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, pos, text '.
+ 'FROM questgrouptexts '.
+ 'WHERE questgroup_id = ? '.
+ 'ORDER BY pos ASC '.
+ 'LIMIT 1',
+ 'i',
+ $questgroupId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get the next Questgroup.
+ *
+ * Determine the next Questgroup. If there is no next Questgroup
+ * on the same level as the given Quest then the followed-up
+ * Questgroup from a higher hierarchy level is returned.
+ *
+ * @param int $questgroupId ID of Questgroup to get next Questgroup of
+ * @return array Questgroup data
+ */
+ public function getNextQuestgroup($questgroupId)
+ {
+ $currentQuestgroup = $this->getQuestgroupById($questgroupId);
+ $currentQuestgroup['hierarchy'] = $this->Questgroupshierarchy->getHierarchyForQuestgroup($currentQuestgroup['id']);
+ if(empty($currentQuestgroup['hierarchy'])) {
+ return null;
+ }
+
+ $nextQuestgroup = $this->_getNextQuestgroup($currentQuestgroup['hierarchy']['parent_questgroup_id'], $currentQuestgroup['hierarchy']['questgroup_pos']);
+ if(is_null($nextQuestgroup) && !is_null($currentQuestgroup['hierarchy']['parent_questgroup_id'])) {
+ $nextQuestgroup = $this->getNextQuestgroup($currentQuestgroup['hierarchy']['parent_questgroup_id']);
+ }
+
+
+ return $nextQuestgroup;
+ }
+
+
+ /**
+ * Get the previous Questgroup.
+ *
+ * Determine the previous Questgroup. If there is no previous
+ * Questgroup on the same level as the given Quest then the
+ * followed-up Questgroup from a higher hierarchy level is
+ * returned.
+ *
+ * @param int $questgroupId ID of Questgroup to get previous Questgroup of
+ * @return array Questgroup data
+ */
+ public function getPreviousQuestgroup($questgroupId)
+ {
+ $currentQuestgroup = $this->getQuestgroupById($questgroupId);
+ $currentQuestgroup['hierarchy'] = $this->Questgroupshierarchy->getHierarchyForQuestgroup($currentQuestgroup['id']);
+ if(empty($currentQuestgroup['hierarchy'])) {
+ return null;
+ }
+
+ $previousQuestgroup = $this->_getPreviousQuestgroup($currentQuestgroup['hierarchy']['parent_questgroup_id'], $currentQuestgroup['hierarchy']['questgroup_pos']);
+ if(is_null($previousQuestgroup) && !is_null($currentQuestgroup['hierarchy']['parent_questgroup_id'])) {
+ $previousQuestgroup = $this->getPreviousQuestgroup($currentQuestgroup['hierarchy']['parent_questgroup_id']);
+ }
+
+
+ return $previousQuestgroup;
+ }
+
+
+ /**
+ * Mark a Questgroup as entered for a Character.
+ *
+ * @param int $questId ID of Quest to mark as entered
+ * @param int $characterId ID of Character that entered the Quest
+ */
+ public function setQuestgroupEntered($questgroupId, $characterId)
+ {
+ $this->setQuestgroupStatus($questgroupId, $characterId, self::QUESTGROUP_STATUS_ENTERED, false);
+ }
+
+
+ /**
+ * Determine if the given Character has entered a Questgroup.
+ *
+ * @param int $questgroupId ID of Questgroup to check
+ * @param int $characterId ID of Character to check
+ * @result boolean Whether Character has entered the Questgroup or not
+ */
+ public function hasCharacterEnteredQuestgroup($questgroupId, $characterId)
+ {
+ $count = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM questgroups_characters '.
+ 'WHERE questgroup_id = ? AND character_id = ? AND status IN (?)',
+ 'iii',
+ $questgroupId,
+ $characterId,
+ self::QUESTGROUP_STATUS_ENTERED
+ );
+
+
+ return (!empty($count) && intval($count[0]['c']) > 0);
+ }
+
+
+ /**
+ * Determine if the given Character has solved the Quests form
+ * this Questgroup.
+ *
+ * @param int $questgroupId ID of Questgroup to check
+ * @param int $characterId ID of Character to check
+ * @result boolean Whether Character has solved the Questgroup or not
+ */
+ public function hasCharacterSolvedQuestgroup($questgroupId, $characterId)
+ {
+ // Get data of Questgroup
+ $questgroup = $this->getQuestgroupById($questgroupId);
+
+ // Chack all Quests
+ $currentQuest = $this->Quests->getFirstQuestOfQuestgroup($questgroup['id']);
+ if(!is_null($currentQuest))
+ {
+ if(!$this->Quests->hasCharacterSolvedQuest($currentQuest['id'], $characterId)) {
+ return false;
+ }
+
+ // Get next Quests
+ $nextQuests = !is_null($currentQuest) ? $this->Quests->getNextQuests($currentQuest['id']) : null;
+ while(!is_null($currentQuest) && !empty($nextQuests))
+ {
+ // Get choosed Quest
+ $currentQuest = null;
+ foreach($nextQuests as &$nextQuest) {
+ if($this->Quests->hasCharacterEnteredQuest($nextQuest['id'], $characterId)) {
+ $currentQuest = $nextQuest;
+ }
+ }
+
+ // Check Quest
+ if(is_null($currentQuest)) {
+ return false;
+ }
+
+ // Check status
+ if(!$this->Quests->hasCharacterSolvedQuest($currentQuest['id'], $characterId)) {
+ return false;
+ }
+
+ $nextQuests = !is_null($currentQuest) ? $this->Quests->getNextQuests($currentQuest['id']) : null;
+ }
+ }
+
+ // Check all child Questgroups
+ $questgroup['hierarchy'] = $this->Questgroupshierarchy->getHierarchyForQuestgroup($questgroup['id']);
+ if(!empty($questgroup['hierarchy']))
+ {
+ $childQuestgroupshierarchy = $this->Questgroupshierarchy->getChildQuestgroupshierarchy($questgroup['hierarchy']['id']);
+ foreach($childQuestgroupshierarchy as &$hierarchy)
+ {
+ // Get Questgroups
+ $questgroups = $this->getQuestgroupsForHierarchy($hierarchy['id'], $questgroup['id']);
+ foreach($questgroups as &$group) {
+ if(!$this->hasCharacterSolvedQuestgroup($group['id'], $characterId)) {
+ return false;
+ }
+ }
+ }
+ }
+
+
+ return true;
+ }
+
+
+ /**
+ * Get all related Questgroups of a Questtext.
+ *
+ * @param int $questtextId ID of the Questtext
+ * @return array Related Questgroups for the Questtext
+ */
+ public function getRelatedQuestsgroupsOfQuesttext($questtextId)
+ {
+ return $this->db->query(
+ 'SELECT questgroups.id, questgroups_questtexts.questtext_id, questgroups.title, questgroups.url, questgroups_questtexts.entry_text '.
+ 'FROM questgroups_questtexts '.
+ 'INNER JOIN questgroups ON questgroups.id = questgroups_questtexts.questgroup_id '.
+ 'WHERE questgroups_questtexts.questtext_id = ?',
+ 'i',
+ $questtextId
+ );
+ }
+
+
+ /**
+ * Get all related Questgroups of a Quest.
+ *
+ * @param int $questId ID of the Quest
+ * @return array Related Quests for the Quest
+ */
+ public function getRelatedQuestsgroupsOfQuest($questId)
+ {
+ return $this->db->query(
+ 'SELECT questgroups_questtexts.questgroup_id AS id '.
+ 'FROM quests '.
+ 'INNER JOIN questtexts ON questtexts.quest_id = quests.id '.
+ 'INNER JOIN questgroups_questtexts ON questgroups_questtexts.questtext_id = questtexts.id '.
+ 'WHERE quests.id = ?',
+ 'i',
+ $questId
+ );
+
+ }
+
+
+ /**
+ * Get all related Questgroups of a Questgroup.
+ *
+ * @param int $questgroupId ID of the Questgroup
+ * @return array Related Questgroups for the Questgroup
+ */
+ public function getRelatedQuestsgroupsOfQuestgroup($questgroupId)
+ {
+ return $this->db->query(
+ 'SELECT questgroups_questtexts.questgroup_id AS id '.
+ 'FROM questgroups '.
+ 'INNER JOIN quests ON quests.questgroup_id = questgroups.id '.
+ 'INNER JOIN questtexts ON questtexts.quest_id = quests.id '.
+ 'INNER JOIN questgroups_questtexts ON questgroups_questtexts.questtext_id = questtexts.id '.
+ 'WHERE questgroups.id = ?',
+ 'i',
+ $questgroupId
+ );
+ }
+
+
+ /**
+ * Calculate cumulated data for a Questgroup, its
+ * sub-Questgroups and all its Quests.
+ *
+ * @param int $questgroupId ID of Questgroup
+ * @param int $characterId ID of Character
+ * @param array $calculatedQuests IDs of already calculated Quests
+ * @return array Cumulated data for Questgroup
+ */
+ public function getCumulatedDataForQuestgroup($questgroupId, $characterId=null, &$calculatedQuests=array())
+ {
+ // Cumulated data
+ $data = array(
+ 'xps' => 0,
+ 'character_xps' => 0
+ );
+
+ // Current Questgroup
+ $questgroup = $this->getQuestgroupById($questgroupId);
+
+ // Quests of current Questgroup
+ $quest = $this->Quests->getFirstQuestOfQuestgroup($questgroup['id']);
+ if(!is_null($quest))
+ {
+ $questData = $this->getCumulatedDataForQuest($quest, $characterId, $calculatedQuests);
+ $data['xps'] += $questData['xps'];
+ $data['character_xps'] += $questData['character_xps'];
+ }
+
+ // XPs of child Questgroups
+ $questgroupHierarchy = $this->Questgroupshierarchy->getHierarchyForQuestgroup($questgroup['id']);
+ if(!empty($questgroupHierarchy))
+ {
+ $childQuestgroupshierarchy = $this->Questgroupshierarchy->getChildQuestgroupshierarchy($questgroupHierarchy['id']);
+ foreach($childQuestgroupshierarchy as &$hierarchy)
+ {
+ $questgroups = $this->getQuestgroupsForHierarchy($hierarchy['id'], $questgroup['id']);
+ foreach($questgroups as &$questgroup)
+ {
+ $childData = $this->getCumulatedDataForQuestgroup($questgroup['id'], $characterId, $calculatedQuests);
+ $data['xps'] += $childData['xps'];
+ $data['character_xps'] += $childData['character_xps'];
+ }
+ }
+ }
+
+
+ // Return cumulated data
+ return $data;
+ }
+
+
+ /**
+ * Calculate cumulated data of the given Quest, its following
+ * Quests and its related Questgroups.
+ *
+ * @param array $quest Quest data
+ * @param int $characterId ID of Character
+ * @param array $calculatedQuests IDs of already calculated Quests
+ * @return array Cumulated data for Quest
+ */
+ public function getCumulatedDataForQuest($quest, $characterId=null, &$calculatedQuests=array())
+ {
+ // Cumulated data
+ $data = array(
+ 'xps' => $quest['xps'],
+ 'character_xps' => (!is_null($characterId) && $this->Quests->hasCharacterSolvedQuest($quest['id'], $characterId)) ? $quest['xps'] : 0
+ );
+
+ // Related Questgroups
+ $relatedQuestgroups = $this->getRelatedQuestsgroupsOfQuest($quest['id']);
+ foreach($relatedQuestgroups as &$relatedQuestgroup)
+ {
+ $relatedData = $this->getCumulatedDataForQuestgroup($relatedQuestgroup['id'], $characterId, $calculatedQuests);
+ $data['xps'] += $relatedData['xps'];
+ $data['character_xps'] += $relatedData['character_xps'];
+ }
+
+ // Next Quests
+ $nextQuests = $this->Quests->getNextQuests($quest['id']);
+ $allNextData = array(
+ 'xps' => array(0),
+ 'character_xps' => array(0),
+ );
+ foreach($nextQuests as &$nextQuest)
+ {
+ if(!in_array($nextQuest['id'], $calculatedQuests))
+ {
+ $nextData = $this->getCumulatedDataForQuest($nextQuest, $characterId, $calculatedQuests);
+ $allNextData['xps'][] = $nextData['xps'];
+ $allNextData['character_xps'][] = $nextData['character_xps'];
+ $calculatedQuests[] = $nextQuest['id'];
+ }
+ }
+ $data['xps'] += max($allNextData['xps']);
+ $data['character_xps'] += max($allNextData['character_xps']);
+
+
+ // Return cumulated data
+ return $data;
+ }
+
+
+ /**
+ * Summarize XPs of all Quests for a Questgroup and its
+ * sub-Questgroups solved by a Character.
+ *
+ * @param int $questgroupId ID of Questgroup
+ * @param int $characterId ID of Character
+ * @return int Sum of XPs
+ */
+ public function getAchievedXPsForQuestgroup($questgroupId, $characterId)
+ {
+ // Sum of XPs
+ $xps = 0;
+
+ // Current Questgroup
+ $questgroup = $this->getQuestgroupById($questgroupId);
+
+ // Quests of current Questgroup
+ $quest = $this->Quests->getFirstQuestOfQuestgroup($questgroup['id']);
+ if(!is_null($quest)) {
+ $xps += $this->getAchievedXPsForQuest($quest, $characterId);
+ }
+
+ // XPs of child Questgroups
+ $questgroupHierarchy = $this->Questgroupshierarchy->getHierarchyForQuestgroup($questgroup['id']);
+ if(empty($questgroupHierarchy)) {
+ return $xps;
+ }
+ $childQuestgroupshierarchy = $this->Questgroupshierarchy->getChildQuestgroupshierarchy($questgroupHierarchy['id']);
+ foreach($childQuestgroupshierarchy as &$hierarchy)
+ {
+ $questgroups = $this->getQuestgroupsForHierarchy($hierarchy['id'], $questgroup['id']);
+ foreach($questgroups as &$questgroup) {
+ $xps += $this->getAchievedXPsForQuestgroup($questgroup['id'], $characterId);
+ }
+ }
+
+
+ // Return summarized XPs
+ return $xps;
+ }
+
+
+ /**
+ * Summarize XPs of the given Quest, its following Quests and
+ * its related Questgroups solved by a Character.
+ *
+ * @param int $quest Quest to summarize XPs for
+ * @param int $characterId ID of Character
+ * @return int Sum of XPs
+ */
+ public function getAchievedXPsForQuest($quest, $characterId)
+ {
+ $xps = 0;
+
+ // XPs for the given Quest
+ if($this->Quests->hasCharacterSolvedQuest($quest['id'], $characterId))
+ {
+ $xps += $quest['xps'];
+
+ // Next Quests
+ $nextQuests = $this->Quests->getNextQuests($quest['id']);
+ foreach($nextQuests as &$nextQuest)
+ {
+ if($this->Quests->hasCharacterEnteredQuest($nextQuest['id'], $characterId))
+ {
+ $xps += $this->getAchievedXPsForQuest($nextQuest, $characterId);
+ break;
+ }
+ }
+ }
+
+ // Related Questgroups
+ $relatedQuestgroups = $this->getRelatedQuestsgroupsOfQuest($quest['id']);
+ foreach($relatedQuestgroups as &$relatedQuestgroup) {
+ $xps += $this->getAchievedXPsForQuestgroup($relatedQuestgroup['id'], $characterId);
+ }
+
+
+ // Return summarized XPs
+ return $xps;
+ }
+
+
+ /**
+ * Create a new Questgroup.
+ *
+ * @param int $userId User-ID that creates the new character
+ * @param int $seminaryId ID of Seminary
+ * @param string $title Title for new Questgroup
+ * @return int ID of new Questgroup
+ */
+ public function createQuestgroup($userId, $seminaryId, $title)
+ {
+ $this->db->query(
+ 'INSERT INTO questgroups '.
+ '(created_user_id, seminary_id, title, url) '.
+ 'VALUES '.
+ '(?, ?, ?, ?)',
+ 'iiss',
+ $userId,
+ $seminaryId,
+ $title,
+ \nre\core\Linker::createLinkParam($title)
+ );
+
+
+ return $this->db->getInsertId();
+ }
+
+
+
+
+ /**
+ * Get the next (direct) Questgroup.
+ *
+ * @param int $parentQuestgroupId ID of parent Questgroup to get next Questgroup of
+ * @param int $questgroupPos Position of Questgroup to get next Questgroup of
+ * @return array Data of next Questgroup or NULL
+ */
+ private function _getNextQuestgroup($parentQuestgroupId, $questgroupPos)
+ {
+ if(!is_null($parentQuestgroupId))
+ {
+ $data = $this->db->query(
+ 'SELECT * '.
+ 'FROM questgroups_questgroupshierarchy '.
+ 'INNER JOIN questgroups ON questgroups.id = questgroups_questgroupshierarchy.questgroup_id '.
+ 'WHERE parent_questgroup_id = ? AND pos = ? + 1',
+ 'ii',
+ $parentQuestgroupId, $questgroupPos
+ );
+ }
+ else
+ {
+ $data = $this->db->query(
+ 'SELECT * '.
+ 'FROM questgroups_questgroupshierarchy '.
+ 'INNER JOIN questgroups ON questgroups.id = questgroups_questgroupshierarchy.questgroup_id '.
+ 'WHERE parent_questgroup_id IS NULL AND pos = ? + 1',
+ 'i',
+ $questgroupPos
+ );
+ }
+ if(empty($data)) {
+ return null;
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get the previous (direct) Questgroup.
+ *
+ * @param int $parentQuestgroupId ID of parent Questgroup to get previous Questgroup of
+ * @param int $questgroupPos Position of Questgroup to get previous Questgroup of
+ * @return array Data of previous Questgroup or NULL
+ */
+ private function _getPreviousQuestgroup($parentQuestgroupId, $questgroupPos)
+ {
+ if(!is_null($parentQuestgroupId))
+ {
+ $data = $this->db->query(
+ 'SELECT * '.
+ 'FROM questgroups_questgroupshierarchy '.
+ 'INNER JOIN questgroups ON questgroups.id = questgroups_questgroupshierarchy.questgroup_id '.
+ 'WHERE parent_questgroup_id = ? AND pos = ? - 1',
+ 'ii',
+ $parentQuestgroupId, $questgroupPos
+ );
+ }
+ else
+ {
+ $data = $this->db->query(
+ 'SELECT * '.
+ 'FROM questgroups_questgroupshierarchy '.
+ 'INNER JOIN questgroups ON questgroups.id = questgroups_questgroupshierarchy.questgroup_id '.
+ 'WHERE parent_questgroup_id IS NULL AND pos = ? - 1',
+ 'i',
+ $questgroupPos
+ );
+ }
+ if(empty($data)) {
+ return null;
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Mark a Questgroup for a Character.
+ *
+ * @param int $questgroupId ID of Questgroup to mark
+ * @param int $characterId ID of Character to mark the Questgroup for
+ * @param int $status Questgroup status to mark
+ * @param boolean $repeated Insert although status is already set for this Questgroup and Character
+ */
+ private function setQuestgroupStatus($questgroupId, $characterId, $status, $repeated=true)
+ {
+ // Check if status is already set
+ if(!$repeated)
+ {
+ $count = $this->db->query(
+ 'SELECT count(*) AS c '.
+ 'FROM questgroups_characters '.
+ 'WHERE questgroup_id = ? AND character_id = ? AND status = ?',
+ 'iii',
+ $questgroupId,
+ $characterId,
+ $status
+ );
+ if(!empty($count) && intval($count[0]['c']) > 0) {
+ return;
+ }
+ }
+
+ // Set status
+ $this->db->query(
+ 'INSERT INTO questgroups_characters '.
+ '(questgroup_id, character_id, status) '.
+ 'VALUES '.
+ '(?, ?, ?) ',
+ 'iii',
+ $questgroupId,
+ $characterId,
+ $status
+ );
+ }
+
+ }
+
+?>
diff --git a/models/QuestgroupshierarchyModel.inc b/models/QuestgroupshierarchyModel.inc
new file mode 100644
index 00000000..c1dae116
--- /dev/null
+++ b/models/QuestgroupshierarchyModel.inc
@@ -0,0 +1,128 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Questgroupshierarchy-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuestgroupshierarchyModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new QuestgroupshierarchyModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get a Questgroup hierarchy by its ID.
+ *
+ * throws IdNotFoundException
+ * @param int $questgroupshierarchyId ID of a Questgroup hierarchy
+ * @return array Questgroup hierarchy
+ */
+ public function getHierarchyById($questgroupshierarchyId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, seminary_id, parent_questgroupshierarchy_id, pos, title_singular, title_plural, url '.
+ 'FROM questgroupshierarchy '.
+ 'WHERE questgroupshierarchy.id = ?',
+ 'i',
+ $questgroupshierarchyId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questgroupshierarchyId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get the toplevel hierarchy entries of a Seminary.
+ *
+ * @param int $seminaryId ID of the seminary to get hierarchy for
+ * @return array Toplevel hierarchy
+ */
+ public function getHierarchyOfSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT id, seminary_id, parent_questgroupshierarchy_id, pos, title_singular, title_plural, url '.
+ 'FROM questgroupshierarchy '.
+ 'WHERE '.
+ 'questgroupshierarchy.seminary_id = ? AND '.
+ 'questgroupshierarchy.parent_questgroupshierarchy_id IS NULL '.
+ 'ORDER BY questgroupshierarchy.pos ASC',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get the Questgroup-Hierarchy for a Questgroup.
+ *
+ * @param int $questgroupId ID of Questgroup
+ * @return array Hierarchy for Questgroup
+ */
+ public function getHierarchyForQuestgroup($questgroupId)
+ {
+ $data = $this->db->query(
+ 'SELECT questgroups_questgroupshierarchy.parent_questgroup_id, questgroups_questgroupshierarchy.pos AS questgroup_pos, questgroupshierarchy.id, questgroupshierarchy.seminary_id, questgroupshierarchy.parent_questgroupshierarchy_id, questgroupshierarchy.pos, questgroupshierarchy.title_singular, questgroupshierarchy.title_plural, questgroupshierarchy.url '.
+ 'FROM questgroups_questgroupshierarchy '.
+ 'INNER JOIN questgroupshierarchy ON questgroupshierarchy.id = questgroups_questgroupshierarchy.questgroupshierarchy_id '.
+ 'WHERE questgroups_questgroupshierarchy.questgroup_id = ?',
+ 'i',
+ $questgroupId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get the child hierarchy entries of a Questgroup hierarchy.
+ *
+ * @param int $questgroupshierarchyId ID of a Questgroup hierarchy
+ * @return array Child Questgroup hierarchy entries
+ */
+ public function getChildQuestgroupshierarchy($questgroupshierarchyId)
+ {
+ return $this->db->query(
+ 'SELECT id, seminary_id, parent_questgroupshierarchy_id, pos, title_singular, title_plural, url '.
+ 'FROM questgroupshierarchy '.
+ 'WHERE questgroupshierarchy.parent_questgroupshierarchy_id = ? '.
+ 'ORDER BY questgroupshierarchy.pos ASC',
+ 'i',
+ $questgroupshierarchyId
+ );
+ }
+
+ }
+
+?>
diff --git a/models/QuestsModel.inc b/models/QuestsModel.inc
new file mode 100644
index 00000000..d4175545
--- /dev/null
+++ b/models/QuestsModel.inc
@@ -0,0 +1,487 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Quests-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuestsModel extends \hhu\z\Model
+ {
+ /**
+ * Quest-status: Entered
+ *
+ * @var int;
+ */
+ const QUEST_STATUS_ENTERED = 0;
+ /**
+ * Quest-status: submitted
+ *
+ * @var int;
+ */
+ const QUEST_STATUS_SUBMITTED = 1;
+ /**
+ * Quest-status: Unsolved
+ *
+ * @var int;
+ */
+ const QUEST_STATUS_UNSOLVED = 2;
+ /**
+ * Quest-status: Solved
+ *
+ * @var int;
+ */
+ const QUEST_STATUS_SOLVED = 3;
+
+
+
+
+ /**
+ * Construct a new QuestsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get a Quest and its data by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param int $seminaryId ID of the corresponding Seminary
+ * @param int $questgroupId ID of the corresponding Questgroup
+ * @param string $questURL URL-title of a Quest
+ * @return array Quest data
+ */
+ public function getQuestByUrl($seminaryId, $questgroupId, $questUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT quests.id, quests.questgroup_id, quests.questtype_id, quests.title, quests.url, quests.xps, quests.entry_text, quests.task, quests.wrong_text, quests.questsmedia_id '.
+ 'FROM quests '.
+ 'LEFT JOIN questgroups ON questgroups.id = quests.questgroup_id '.
+ 'WHERE questgroups.seminary_id = ? AND questgroups.id = ? AND quests.url = ?',
+ 'iis',
+ $seminaryId, $questgroupId, $questUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get a Quest and its data by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param string $questId ID of a Quest
+ * @return array Quest data
+ */
+ public function getQuestById($questId)
+ {
+ $data = $this->db->query(
+ 'SELECT quests.id, quests.questgroup_id, quests.questtype_id, quests.title, quests.url, quests.xps, quests.entry_text, quests.task, quests.wrong_text, quests.questsmedia_id '.
+ 'FROM quests '.
+ 'LEFT JOIN questgroups ON questgroups.id = quests.questgroup_id '.
+ 'WHERE quests.id = ?',
+ 'i',
+ $questId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get the first Quest of a Qusetgroup.
+ *
+ * @param int $questId ID of Questgroup
+ * @return array Data of first Quest
+ */
+ public function getFirstQuestOfQuestgroup($questgroupId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, questtype_id, title, url, xps, task '.
+ 'FROM quests '.
+ 'LEFT JOIN quests_previousquests ON quests_previousquests.quest_id = quests.id '.
+ 'WHERE questgroup_id = ? AND quests_previousquests.previous_quest_id IS NULL',
+ 'i',
+ $questgroupId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get Quests that follow-up a Quest.
+ *
+ * @param int $questId ID of Quest to get next Quests of
+ * @return array Quests data
+ */
+ public function getNextQuests($questId)
+ {
+ return $this->db->query(
+ 'SELECT quests.id, quests.questtype_id, quests.title, quests.url, quests.xps, quests.entry_text, quests.task, questgroups.title AS questgroup_title, questgroups.url AS questgroup_url '.
+ 'FROM quests_previousquests '.
+ 'INNER JOIN quests ON quests.id = quests_previousquests.quest_id '.
+ 'INNER JOIN questgroups ON questgroups.id = quests.questgroup_id '.
+ 'WHERE quests_previousquests.previous_quest_id = ?',
+ 'i',
+ $questId
+ );
+ }
+
+
+ /**
+ * Get Quests that the given Quests follows-up to.
+ *
+ * @param int $questId ID of Quest to get previous Quests of
+ * @return array Quests data
+ */
+ public function getPreviousQuests($questId)
+ {
+ return $this->db->query(
+ 'SELECT quests.id, quests.title, quests.url, quests.entry_text, questgroups.title AS questgroup_title, questgroups.url AS questgroup_url '.
+ 'FROM quests_previousquests '.
+ 'INNER JOIN quests ON quests.id = quests_previousquests.previous_quest_id '.
+ 'INNER JOIN questgroups ON questgroups.id = quests.questgroup_id '.
+ 'WHERE quests_previousquests.quest_id = ?',
+ 'i',
+ $questId
+ );
+ }
+
+
+ /**
+ * Mark a Quest as entered for a Character.
+ *
+ * @param int $questId ID of Quest to mark as entered
+ * @param int $characterId ID of Character that entered the Quest
+ */
+ public function setQuestEntered($questId, $characterId)
+ {
+ $this->setQuestStatus($questId, $characterId, self::QUEST_STATUS_ENTERED, false);
+ }
+
+
+ /**
+ * Mark a Quest as submitted for a Character.
+ *
+ * @param int $questId ID of Quest to mark as unsolved
+ * @param int $characterId ID of Character that unsolved the Quest
+ */
+ public function setQuestSubmitted($questId, $characterId)
+ {
+ $this->setQuestStatus($questId, $characterId, self::QUEST_STATUS_SUBMITTED);
+ }
+
+
+ /**
+ * Mark a Quest as unsolved for a Character.
+ *
+ * @param int $questId ID of Quest to mark as unsolved
+ * @param int $characterId ID of Character that unsolved the Quest
+ */
+ public function setQuestUnsolved($questId, $characterId)
+ {
+ $this->setQuestStatus($questId, $characterId, self::QUEST_STATUS_UNSOLVED);
+ }
+
+
+ /**
+ * Mark a Quest as solved for a Character.
+ *
+ * @param int $questId ID of Quest to mark as solved
+ * @param int $characterId ID of Character that solved the Quest
+ */
+ public function setQuestSolved($questId, $characterId)
+ {
+ $this->setQuestStatus($questId, $characterId, self::QUEST_STATUS_SOLVED, false);
+ }
+
+
+ /**
+ * Determine if the given Character has entered a Quest.
+ *
+ * @param int $questId ID of Quest to check
+ * @param int $characterId ID of Character to check
+ * @result boolean Whether Character has entered the Quest or not
+ */
+ public function hasCharacterEnteredQuest($questId, $characterId)
+ {
+ $count = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM quests_characters '.
+ 'WHERE quest_id = ? AND character_id = ? AND status IN (?,?,?)',
+ 'iiiii',
+ $questId,
+ $characterId,
+ self::QUEST_STATUS_ENTERED, self::QUEST_STATUS_SOLVED, self::QUEST_STATUS_UNSOLVED
+ );
+
+
+ return (!empty($count) && intval($count[0]['c']) > 0);
+ }
+
+
+ /**
+ * Determine if the given Character has tried to solve a Quest.
+ *
+ * @param int $questId ID of Quest to check
+ * @param int $characterId ID of Character to check
+ * @result boolean Whether Character has tried to solved the Quest or not
+ */
+ public function hasCharacterTriedQuest($questId, $characterId)
+ {
+ $count = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM quests_characters '.
+ 'WHERE quest_id = ? AND character_id = ? AND status IN (?,?)',
+ 'iiii',
+ $questId,
+ $characterId,
+ self::QUEST_STATUS_SOLVED, self::QUEST_STATUS_UNSOLVED
+ );
+
+
+ return (!empty($count) && intval($count[0]['c']) > 0);
+ }
+
+
+ /**
+ * Determine if the given Character has solved the given Quest.
+ *
+ * @param int $questId ID of Quest to check
+ * @param int $characterId ID of Character to check
+ * @result boolean Whether Character has solved the Quest or not
+ */
+ public function hasCharacterSolvedQuest($questId, $characterId)
+ {
+ $count = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM quests_characters '.
+ 'WHERE quest_id = ? AND character_id = ? AND status = ?',
+ 'iii',
+ $questId,
+ $characterId,
+ self::QUEST_STATUS_SOLVED
+ );
+
+
+ return (!empty($count) && intval($count[0]['c']) > 0);
+ }
+
+
+ /**
+ * Get the last Quests for a Character.
+ *
+ * @param int $characterId ID of Character
+ * @retrun array Quest data
+ */
+ public function getLastQuestForCharacter($characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT quests.id, quests.questgroup_id, quests.questtype_id, quests.title, quests.url, quests.xps, quests.task, quests.wrong_text, quests.questsmedia_id '.
+ 'FROM quests_characters '.
+ 'LEFT JOIN quests ON quests.id = quests_characters.quest_id '.
+ 'WHERE quests_characters.character_id = ? AND quests_characters.status IN (?, ?, ?) '.
+ 'ORDER BY quests_characters.created desc '.
+ 'LIMIT 1',
+ 'iiii',
+ $characterId,
+ self::QUEST_STATUS_ENTERED, self::QUEST_STATUS_SUBMITTED, self::QUEST_STATUS_SOLVED
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get all Quests for a Seminary.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @return array Quests for this Seminary
+ */
+ public function getQuestsForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT DISTINCT quests.id, quests.questgroup_id, quests.questtype_id, quests.title, quests.url, quests.xps, quests.task, quests.wrong_text, quests.questsmedia_id '.
+ 'FROM questgroups '.
+ 'INNER JOIN quests ON quests.questgroup_id = questgroups.id '.
+ 'WHERE questgroups.seminary_id = ?',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get all Quests that are linked to a Questtopic.
+ *
+ * @param int $questtopicId ID of Questtopic
+ * @return array Quests for this Questtopic
+ */
+ public function getQuestsForQuesttopic($questtopicId)
+ {
+ return $this->db->query(
+ 'SELECT DISTINCT quests.id, quests.questgroup_id, quests.questtype_id, quests.title, quests.url, quests.xps, quests.task, quests.wrong_text, quests.questsmedia_id '.
+ 'FROM quests_questsubtopics '.
+ 'INNER JOIN quests ON quests.id = quests_questsubtopics.quest_id '.
+ 'WHERE quests_questsubtopics.questsubtopic_id = ?',
+ 'i',
+ $questtopicId
+ );
+ }
+
+
+
+
+ /**
+ * Mark a Quest for a Character.
+ *
+ * @param int $questId ID of Quest to mark
+ * @param int $characterId ID of Character to mark the Quest for
+ * @param int $status Quest status to mark
+ * @param boolean $repeated Insert although status is already set for this Quest and Character
+ */
+ private function setQuestStatus($questId, $characterId, $status, $repeated=true)
+ {
+ // Check if status is already set
+ if(!$repeated)
+ {
+ $count = $this->db->query(
+ 'SELECT count(*) AS c '.
+ 'FROM quests_characters '.
+ 'WHERE quest_id = ? AND character_id = ? AND status = ?',
+ 'iii',
+ $questId,
+ $characterId,
+ $status
+ );
+ if(!empty($count) && intval($count[0]['c']) > 0) {
+ return;
+ }
+ }
+
+ // Set status
+ $this->db->query(
+ 'INSERT INTO quests_characters '.
+ '(quest_id, character_id, status) '.
+ 'VALUES '.
+ '(?, ?, ?) ',
+ 'iii',
+ $questId,
+ $characterId,
+ $status
+ );
+ }
+
+
+ /**
+ * Get the last status of a Quest for a Character.
+ *
+ * @param int $questId ID of Quest
+ * @param int $characterId ID of Character to get status for
+ * @return int Last status
+ */
+ public function getLastQuestStatus($questId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, created, status '.
+ 'FROM quests_characters '.
+ 'WHERE quest_id = ? AND character_id = ? '.
+ 'ORDER BY created DESC '.
+ 'LIMIT 1',
+ 'ii',
+ $questId, $characterId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Create a new Quest.
+ *
+ * @param int $userId User-ID that creates the new character
+ * @param string $name Name for new Quest
+ * @param int $questgroupId ID of Questgroup
+ * @param int $questtypeId ID of Questtype
+ * @param int $xps XPs for new Quest
+ * @param string $entrytext Entrytext for new Quest
+ * @param string $wrongtext Wrongtext for new Quest
+ * @param string $task Task for new Quest
+ * @return int ID of new Quest
+ */
+ public function createQuest($userId, $name, $questgroupId, $questtypeId, $xps, $entrytext, $wrongtext, $task)
+ {
+ $this->db->query(
+ 'INSERT INTO quests '.
+ '(created_user_id, questgroup_id, questtype_id, title, url, xps, entry_text, wrong_text, task) '.
+ 'VALUES '.
+ '(?, ?, ?, ?, ?, ?, ?, ?, ?)',
+ 'iiississs',
+ $userId, $questgroupId, $questtypeId,
+ $name, \nre\core\Linker::createLinkParam($name),
+ $xps, $entrytext, $wrongtext, $task
+ );
+
+
+ return $this->db->getInsertId();
+ }
+
+
+ /**
+ * Set the media for a Quest.
+ *
+ * @param int $questId ID of Quest to set media for
+ * @param int $questmediaId ID of Questsmedia to set
+ */
+ public function setQuestmedia($questId, $questsmediaId)
+ {
+ $this->db->query(
+ 'UPDATE quests '.
+ 'SET questsmedia_id = ? '.
+ 'WHERE id = ?',
+ 'ii',
+ $questsmediaId,
+ $questId
+ );
+ }
+
+ }
+
+?>
diff --git a/models/QuesttextsModel.inc b/models/QuesttextsModel.inc
new file mode 100644
index 00000000..7aca28b9
--- /dev/null
+++ b/models/QuesttextsModel.inc
@@ -0,0 +1,220 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Questtexts-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuesttextsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new QuesttextsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all Questtexts for a Quest.
+ *
+ * @param int $questId ID of the Quest
+ * @param string $questtexttypeUrl URL of the Questtexttype
+ * @return array All Questtexts for a Quest
+ */
+ public function getQuesttextsOfQuest($questId, $questtexttypeUrl=null)
+ {
+ if(is_null($questtexttypeUrl))
+ {
+ return $this->db->query(
+ 'SELECT questtexts.id, questtexts.text, questtexts.pos, questtexts.out_text, questtexts.abort_text, questtexts.questsmedia_id, questtexttypes.id AS type_id, questtexttypes.type, questtexttypes.url AS type_url '.
+ 'FROM questtexts '.
+ 'LEFT JOIN questtexttypes ON questtexttypes.id = questtexts.questtexttype_id '.
+ 'WHERE questtexts.quest_id = ?',
+ 'i',
+ $questId
+ );
+ }
+ else
+ {
+ return $this->db->query(
+ 'SELECT questtexts.id, questtexts.text, questtexts.pos, questtexts.out_text, questtexts.abort_text, questtexts.questsmedia_id, questtexttypes.id AS type_id, questtexttypes.type, questtexttypes.url AS type_url '.
+ 'FROM questtexts '.
+ 'LEFT JOIN questtexttypes ON questtexttypes.id = questtexts.questtexttype_id '.
+ 'WHERE questtexts.quest_id = ? and questtexttypes.url = ?',
+ 'is',
+ $questId,
+ $questtexttypeUrl
+ );
+ }
+ }
+
+
+ /**
+ * Get count of Questtexts for a Quest.
+ *
+ * @param int $questId ID of the Quest
+ * @param string $questtexttypeUrl URL of the Questtexttype
+ * @return int Amount of Questtexts for a Quest
+ */
+ public function getQuesttextCountOfQuest($questId, $questtexttypeUrl=null)
+ {
+ if(is_null($questtexttypeUrl))
+ {
+ $data = $this->db->query(
+ 'SELECT count(questtexts.id) AS c '.
+ 'FROM questtexts '.
+ 'LEFT JOIN questtexttypes ON questtexttypes.id = questtexts.questtexttype_id '.
+ 'WHERE questtexts.quest_id = ?',
+ 'i',
+ $questId
+ );
+ }
+ else
+ {
+ $data = $this->db->query(
+ 'SELECT count(questtexts.id) AS c '.
+ 'FROM questtexts '.
+ 'LEFT JOIN questtexttypes ON questtexttypes.id = questtexts.questtexttype_id '.
+ 'WHERE questtexts.quest_id = ? and questtexttypes.url = ?',
+ 'is',
+ $questId,
+ $questtexttypeUrl
+ );
+ }
+ if(!empty($data)) {
+ return $data[0]['c'];
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Get corresponding Questtext for a Sidequest.
+ *
+ * @throws IdNotFoundException
+ * @param int $sidequestId ID of the Sidequest to get the Questtext for
+ * @param array Questtext data
+ */
+ public function getRelatedQuesttextForQuestgroup($questgroupId)
+ {
+ $data = $this->db->query(
+ 'SELECT questtexts.id, questtexts.text, questtexts.pos, questtexts.quest_id, questtexttypes.id AS type_id, questtexttypes.type, questtexttypes.url AS type_url '.
+ 'FROM questgroups_questtexts '.
+ 'LEFT JOIN questtexts ON questtexts.id = questgroups_questtexts.questtext_id '.
+ 'LEFT JOIN questtexttypes ON questtexttypes.id = questtexts.questtexttype_id '.
+ 'WHERE questgroups_questtexts.questgroup_id = ?',
+ 'i',
+ $questgroupId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get all registered Questtexttypes.
+ *
+ * @return array Registered Questtexttypes
+ */
+ public function getQuesttexttypes()
+ {
+ return $this->db->query(
+ 'SELECT id, type, url '.
+ 'FROM questtexttypes'
+ );
+ }
+
+
+ /**
+ * Get a Questtexttype by its URL.
+ *
+ * @param string $questtexttypeUrl URL-type of Questtexttype
+ * @return array Questtexttype data
+ */
+ public function getQuesttexttypeByUrl($questtexttypeUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, type, url '.
+ 'FROM questtexttypes '.
+ 'WHERE url = ?',
+ 's',
+ $questtexttypeUrl
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Add a list of Questtexts to a Quest.
+ *
+ * @param int $userId ID of user
+ * @param int $questId ID of Quest to add texts to
+ * @param string $questtexttypeUrl URL-type of Questtexttype of texts
+ * @param array $texts List of texts to add.
+ */
+ public function addQuesttextsToQuest($userId, $questId, $questtexttypeUrl, $texts)
+ {
+ $questtexttype = $this->getQuesttexttypeByUrl($questtexttypeUrl);
+ if(is_null($questtexttype)) {
+ return;
+ }
+ foreach($texts as &$text)
+ {
+ $pos = $this->db->query(
+ 'SELECT COALESCE(MAX(pos),0)+1 AS pos '.
+ 'FROM questtexts '.
+ 'WHERE quest_id = ?',
+ 'i',
+ $questId
+ );
+ $pos = $pos[0]['pos'];
+
+ $this->db->query(
+ 'INSERT INTO questtexts '.
+ '(created_user_id, quest_id, questtexttype_id, pos, text) '.
+ 'VALUES '.
+ '(?, ?, ?, ?, ?)',
+ 'iiiis',
+ $userId, $questId, $questtexttype['id'], $pos,
+ $text
+ );
+ }
+ }
+
+
+
+
+ }
+
+?>
diff --git a/models/QuesttopicsModel.inc b/models/QuesttopicsModel.inc
new file mode 100644
index 00000000..abd246f7
--- /dev/null
+++ b/models/QuesttopicsModel.inc
@@ -0,0 +1,154 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Questtopics-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuesttopicsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new QuesttopicsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get a Questtopic by its URL.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param string $questtopicUrl URL-Title of Questtopic
+ * @return array Questtopic data
+ */
+ public function getQuesttopicByUrl($seminaryId, $questtopicUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, title, url '.
+ 'FROM questtopics '.
+ 'WHERE seminary_id = ? AND url = ?',
+ 'is',
+ $seminaryId, $questtopicUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questtopicUrl);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get all Questtopics for a Seminary.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @return array List of Questtopics
+ */
+ public function getQuesttopicsForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT id, title, url '.
+ 'FROM questtopics '.
+ 'WHERE seminary_id = ?',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get count of Quests that are linked to a Questtopic.
+ *
+ * @param int $questtopicId ID of Questtopic
+ * @return int Count of Quests
+ */
+ public function getQuestCountForQuesttopic($questtopicId)
+ {
+ $data = $this->db->query(
+ 'SELECT count(DISTINCT quests_questsubtopics.quest_id) AS c ' .
+ 'FROM questsubtopics '.
+ 'LEFT JOIN quests_questsubtopics ON quests_questsubtopics.questsubtopic_id = questsubtopics.id '.
+ 'WHERE questsubtopics.questtopic_id = ?',
+ 'i',
+ $questtopicId
+ );
+ if(!empty($data)) {
+ return $data[0]['c'];
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Get count of Quests that are linked to a Questtopic and are
+ * unlocked by a Character.
+ *
+ * @param int $questtopicId ID of Questtopic
+ * @param int $characterId ID of Character
+ * @return int Count of Quests
+ */
+ public function getCharacterQuestCountForQuesttopic($questtopicId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT count(DISTINCT quests_characters.quest_id) AS c '.
+ 'FROM questsubtopics '.
+ 'LEFT JOIN quests_questsubtopics ON quests_questsubtopics.questsubtopic_id = questsubtopics.id '.
+ 'INNER JOIN quests_characters ON quests_characters.quest_id = quests_questsubtopics.quest_id AND quests_characters.character_id = ? AND quests_characters.status = 3 '.
+ 'WHERE questsubtopics.questtopic_id = ?',
+ 'ii',
+ $characterId,
+ $questtopicId
+ );
+ if(!empty($data)) {
+ return $data[0]['c'];
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Get all Questsubtopics for a Quest.
+ *
+ * @param int $questId ID of Quest
+ * @return array List of Questsubtopics
+ */
+ public function getQuestsubtopicsForQuest($questId)
+ {
+ return $this->db->query(
+ 'SELECT DISTINCT id, questtopic_id, title, url '.
+ 'FROM quests_questsubtopics '.
+ 'INNER JOIN questsubtopics ON questsubtopics.id = quests_questsubtopics.questsubtopic_id '.
+ 'WHERE quests_questsubtopics.quest_id = ?',
+ 'i',
+ $questId
+ );
+ }
+
+ }
+
+?>
diff --git a/models/QuesttypesModel.inc b/models/QuesttypesModel.inc
new file mode 100644
index 00000000..58b36648
--- /dev/null
+++ b/models/QuesttypesModel.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with Questtypes-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class QuesttypesModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new QuesttypesModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all registered Questtypes.
+ *
+ * @return array List of registered Questtypes
+ */
+ public function getQuesttypes()
+ {
+ return $this->db->query(
+ 'SELECT id, title, url, classname '.
+ 'FROM questtypes '.
+ 'ORDER BY title ASC'
+ );
+ }
+
+
+ /**
+ * Get a Questtype by its ID
+ *
+ * @param int $questtypeId ID of Questtype
+ * @return array Questtype data
+ */
+ public function getQuesttypeById($questtypeId)
+ {
+ $data = $this->db->query(
+ 'SELECT title, classname '.
+ 'FROM questtypes '.
+ 'WHERE id = ?',
+ 'i',
+ $questtypeId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questtypeId);
+ }
+
+
+ return $data = $data[0];
+ }
+
+ }
+
+?>
diff --git a/models/SeminariesModel.inc b/models/SeminariesModel.inc
new file mode 100644
index 00000000..207411df
--- /dev/null
+++ b/models/SeminariesModel.inc
@@ -0,0 +1,195 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model of the SeminariesAgent to list registered seminaries.
+ *
+ * @author Oliver Hanraths
+ */
+ class SeminariesModel extends \hhu\z\Model
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('questgroupshierarchy', 'questgroups');
+
+
+
+
+ /**
+ * Construct a new SeminariesModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get registered seminaries.
+ *
+ * @return array Seminaries
+ */
+ public function getSeminaries()
+ {
+ // Get seminaries
+ return $this->db->query(
+ 'SELECT id, created, created_user_id, title, url, course, description, seminarymedia_id '.
+ 'FROM seminaries '.
+ 'ORDER BY created DESC'
+ );
+ }
+
+
+ /**
+ * Get a seminary and its data by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryId ID of a seminary
+ * @return array Seminary
+ */
+ public function getSeminaryById($seminaryId)
+ {
+ $seminary = $this->db->query(
+ 'SELECT id, created, created_user_id, title, url, course, description, seminarymedia_id '.
+ 'FROM seminaries '.
+ 'WHERE id = ?',
+ 'i',
+ $seminaryId
+ );
+ if(empty($seminary)) {
+ throw new \nre\exceptions\IdNotFoundException($seminaryId);
+ }
+
+
+ return $seminary[0];
+ }
+
+
+ /**
+ * Get a seminary and its data by its URL-title.
+ *
+ * @throws IdNotFoundException
+ * @param string $seminaryUrl URL-Title of a seminary
+ * @return array Seminary
+ */
+ public function getSeminaryByUrl($seminaryUrl)
+ {
+ $seminary = $this->db->query(
+ 'SELECT id, created, created_user_id, title, url, course, description, seminarymedia_id '.
+ 'FROM seminaries '.
+ 'WHERE url = ?',
+ 's',
+ $seminaryUrl
+ );
+ if(empty($seminary)) {
+ throw new \nre\exceptions\IdNotFoundException($seminaryUrl);
+ }
+
+
+ return $seminary[0];
+ }
+
+
+ /*
+ * Calculate sum of XPs for a Seminary.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @return int Total sum of XPs
+ */
+ public function getTotalXPs($seminaryId)
+ {
+ $xps = 0;
+
+ // Questgroups
+ $questgroupshierarchy = $this->Questgroupshierarchy->getHierarchyOfSeminary($seminaryId);
+ foreach($questgroupshierarchy as &$hierarchy)
+ {
+ // Get Questgroups
+ $questgroups = $this->Questgroups->getQuestgroupsForHierarchy($hierarchy['id']);
+ foreach($questgroups as &$questgroup)
+ {
+ $data = $this->Questgroups->getCumulatedDataForQuestgroup($questgroup['id']);
+ $xps += $data['xps'];
+ }
+ }
+
+
+ return $xps;
+ }
+
+
+ /**
+ * Create a new seminary.
+ *
+ * @param string $title Title of seminary to create
+ * @param int $userId ID of creating user
+ * @return int ID of the newly created seminary
+ */
+ public function createSeminary($title, $userId)
+ {
+ $this->db->query(
+ 'INSERT INTO seminaries '.
+ '(created_user_id, title, url) '.
+ 'VALUES '.
+ '(?, ?, ?)',
+ 'iss',
+ $userId,
+ $title,
+ \nre\core\Linker::createLinkParam($title)
+ );
+
+
+ return $this->db->getInsertId();
+ }
+
+
+ /**
+ * Edit a seminary.
+ *
+ * @throws DatamodelException
+ * @param int $seminaryId ID of the seminary to delete
+ * @param string $title New title of seminary
+ */
+ public function editSeminary($seminaryId, $title)
+ {
+ $this->db->query(
+ 'UPDATE seminaries '.
+ 'SET title = ?, url = ? '.
+ 'WHERE id = ?',
+ 'ssi',
+ $title,
+ \nre\core\Linker::createLinkParam($title),
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Delete a seminary.
+ *
+ * @param int $seminaryId ID of the seminary to delete
+ */
+ public function deleteSeminary($seminaryId)
+ {
+ $this->db->query('DELETE FROM seminaries WHERE id = ?', 'i', $seminaryId);
+ }
+
+ }
+
+?>
diff --git a/models/SeminarycharacterfieldsModel.inc b/models/SeminarycharacterfieldsModel.inc
new file mode 100644
index 00000000..efad3bde
--- /dev/null
+++ b/models/SeminarycharacterfieldsModel.inc
@@ -0,0 +1,78 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with the Seminarycharacterfields-tables.
+ *
+ * @author Oliver Hanraths
+ */
+ class SeminarycharacterfieldsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new SeminarycharacterfieldsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all Character fields of a Seminary.
+ *
+ * @param int $seminaryId ID of Seminary to get fields of
+ * @param array Seminary Character fields
+ */
+ public function getFieldsForSeminary($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT seminarycharacterfields.id, seminarycharacterfields.title, seminarycharacterfields.url, seminarycharacterfields.regex, seminarycharacterfields.required, seminarycharacterfieldtypes.id AS type_id, seminarycharacterfieldtypes.title AS type_title, seminarycharacterfieldtypes.url AS type_url '.
+ 'FROM seminarycharacterfields '.
+ 'LEFT JOIN seminarycharacterfieldtypes ON seminarycharacterfieldtypes.id = seminarycharacterfields.seminarycharacterfieldtype_id '.
+ 'WHERE seminarycharacterfields.seminary_id = ? '.
+ 'ORDER BY pos ASC',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get Seminary Character fields of a Character.
+ *
+ * @param int $characterId ID of the Character
+ * @return array Seminary Character fields
+ */
+ public function getFieldsForCharacter($characterId)
+ {
+ return $this->db->query(
+ 'SELECT seminarycharacterfields.title, characters_seminarycharacterfields.value '.
+ 'FROM characters_seminarycharacterfields '.
+ 'LEFT JOIN seminarycharacterfields ON seminarycharacterfields.id = characters_seminarycharacterfields.seminarycharacterfield_id '.
+ 'LEFT JOIN seminarycharacterfieldtypes ON seminarycharacterfieldtypes.id = seminarycharacterfields.seminarycharacterfieldtype_id '.
+ 'WHERE characters_seminarycharacterfields.character_id = ?',
+ 'i',
+ $characterId
+ );
+ }
+
+ }
+
+?>
diff --git a/models/UploadsModel.inc b/models/UploadsModel.inc
new file mode 100644
index 00000000..fc4ec2fd
--- /dev/null
+++ b/models/UploadsModel.inc
@@ -0,0 +1,141 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to handle files to upload.
+ *
+ * @author Oliver Hanraths
+ */
+ class UploadsModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new UploadsModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Upload a file and create a database record.
+ *
+ * @param int $userId ID of user that uploads the file
+ * @param int $seminaryId ID of Seminary
+ * @param string $name Name of file to upload
+ * @param string $filename Filename of file to upload
+ * @param string $tmpFilename Name of temporary uploaded file
+ * @param string $mimetype Mimetype of file to upload
+ * @return mixed ID of database record or false
+ */
+ public function uploadSeminaryFile($userId, $seminaryId, $name, $filename, $tmpFilename, $mimetype)
+ {
+ $uploadId = false;
+ $this->db->setAutocommit(false);
+
+ try {
+ // Create database record
+ $this->db->query(
+ 'INSERT INTO seminaryuploads '.
+ '(created_user_id, seminary_id, name, url, mimetype) '.
+ 'VALUES '.
+ '(?, ? ,? ,?, ?)',
+ 'iisss',
+ $userId,
+ $seminaryId,
+ $name,
+ \nre\core\Linker::createLinkParam($filename),
+ $mimetype
+ );
+ $uploadId = $this->db->getInsertId();
+
+ // Create filename
+ $filename = ROOT.DS.\nre\configs\AppConfig::$dirs['seminaryuploads'].DS.$filename;
+ if(!move_uploaded_file($tmpFilename, $filename))
+ {
+ $this->db->rollback();
+ $uploadId = false;
+ }
+ }
+ catch(\nre\exceptions\DatamodelException $e) {
+ $this->db->rollback();
+ $this->db->setAutocommit(true);
+ }
+
+
+ $this->db->setAutocommit(true);
+ return $uploadId;
+ }
+
+
+ /**
+ * Get an upload by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param int $uploadId ID of the uploaded file
+ * @return array Upload data
+ */
+ public function getSeminaryuploadById($seminaryuploadId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, created, created_user_id, seminary_id, name, url, mimetype, public '.
+ 'FROM seminaryuploads '.
+ 'WHERE id = ?',
+ 'i',
+ $seminaryuploadId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($seminaryuploadId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get an upload by its URL.
+ *
+ * @throws IdNotFoundException
+ * @param int $seminaryId ID of Seminary
+ * @param int $uploadId ID of the uploaded file
+ * @return array Upload data
+ */
+ public function getSeminaryuploadByUrl($seminaryId, $seminaryuploadUrl)
+ {
+ $data = $this->db->query(
+ 'SELECT id, created, created_user_id, seminary_id, name, url, mimetype, public '.
+ 'FROM seminaryuploads '.
+ 'WHERE seminary_id = ? AND url = ?',
+ 'is',
+ $seminaryId,
+ $seminaryuploadUrl
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($seminaryuploadUrl);
+ }
+
+
+ return $data[0];
+ }
+
+ }
+
+?>
diff --git a/models/UserrolesModel.inc b/models/UserrolesModel.inc
new file mode 100644
index 00000000..33121a21
--- /dev/null
+++ b/models/UserrolesModel.inc
@@ -0,0 +1,77 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model to interact with userroles-table.
+ *
+ * @author Oliver Hanraths
+ */
+ class UserrolesModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new UserrolesModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get all userroles for an user referenced by its ID.
+ *
+ * @param int $userId ID of an user
+ * @return array Userroles for an user
+ */
+ public function getUserrolesForUserById($userId)
+ {
+ return $this->db->query(
+ 'SELECT userroles.id, userroles.created, userroles.name '.
+ 'FROM users_userroles '.
+ 'LEFT JOIN userroles ON userroles.id = users_userroles.userrole_id '.
+ 'WHERE users_userroles.user_id = ?',
+ 'i',
+ $userId
+ );
+ }
+
+
+ /**
+ * Get all userroles for an user referenced by its URL-username.
+ *
+ * @param string $userUrl URL-Username of an user
+ * @return array Userroles for an user
+ */
+ public function getUserrolesForUserByUrl($userUrl)
+ {
+ return $this->db->query(
+ 'SELECT userroles.id, userroles.created, userroles.name '.
+ 'FROM users '.
+ 'LEFT JOIN users_userroles ON users_userroles.user_id = users.id '.
+ 'LEFT JOIN userroles ON userroles.id = users_userroles.userrole_id '.
+ 'WHERE users.url = ?',
+ 's',
+ $userUrl
+ );
+ }
+
+ }
+
+?>
diff --git a/models/UsersModel.inc b/models/UsersModel.inc
new file mode 100644
index 00000000..d2a69003
--- /dev/null
+++ b/models/UsersModel.inc
@@ -0,0 +1,348 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\models;
+
+
+ /**
+ * Model of the UsersAgent to list users and get their data.
+ *
+ * @author Oliver Hanraths
+ */
+ class UsersModel extends \hhu\z\Model
+ {
+
+
+
+
+ /**
+ * Construct a new UsersModel.
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+
+
+
+ /**
+ * Get registered users.
+ *
+ * @return array Users
+ */
+ public function getUsers()
+ {
+ return $this->db->query(
+ 'SELECT id, created, username, url, surname, prename, email '.
+ 'FROM users '.
+ 'ORDER BY username ASC'
+ );
+ }
+
+
+ /**
+ * Get users with the given user role.
+ *
+ * @param string $userrole User role
+ * @return array List of users
+ */
+ public function getUsersWithRole($userrole)
+ {
+ return $this->db->query(
+ 'SELECT users.id, users.created, users.username, users.url, users.surname, users.prename, users.email '.
+ 'FROM users '.
+ 'LEFT JOIN users_userroles ON users_userroles.user_id = users.id '.
+ 'LEFT JOIN userroles ON userroles.id = users_userroles.userrole_id '.
+ 'WHERE userroles.name = ? '.
+ 'ORDER BY username ASC',
+ 's',
+ $userrole
+ );
+ }
+
+
+ /**
+ * Get a user and its data by its ID.
+ *
+ * @throws IdNotFoundException
+ * @param int $userId ID of an user
+ * @return array Userdata
+ */
+ public function getUserById($userId)
+ {
+ // Get user
+ $user = $this->db->query(
+ 'SELECT id, created, username, url, surname, prename, email '.
+ 'FROM users '.
+ 'WHERE id = ?',
+ 'i',
+ $userId
+ );
+ if(empty($user)) {
+ throw new \nre\exceptions\IdNotFoundException($userId);
+ }
+
+
+ return $user[0];
+ }
+
+
+ /**
+ * Get a user and its data by its URL-username.
+ *
+ * @throws IdNotFoundException
+ * @param string $userUrl URL-Username of an user
+ * @return array Userdata
+ */
+ public function getUserByUrl($userUrl)
+ {
+ // Get user
+ $user = $this->db->query(
+ 'SELECT id, created, username, url, surname, prename, email '.
+ 'FROM users '.
+ 'WHERE url = ?',
+ 's',
+ $userUrl
+ );
+ if(empty($user)) {
+ throw new \nre\exceptions\IdNotFoundException($userUrl);
+ }
+
+
+ return $user[0];
+ }
+
+
+ /**
+ * Log a user in if its credentials are valid.
+ *
+ * @throws DatamodelException
+ * @param string $username The name of the user to log in
+ * @param string $password Plaintext password of the user to log in
+ */
+ public function login($username, $password)
+ {
+ $data = $this->db->query('SELECT id, password FROM users WHERE username = ?', 's', $username);
+ if(!empty($data))
+ {
+ $data = $data[0];
+ if($this->verify($password, $data['password'])) {
+ return $data['id'];
+ }
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Check if an username already exists.
+ *
+ * @param string $username Username to check
+ * @return boolean Whether username exists or not
+ */
+ public function usernameExists($username)
+ {
+ $data = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM users '.
+ 'WHERE username = ? OR url = ?',
+ 'ss',
+ $username,
+ \nre\core\Linker::createLinkParam($username)
+ );
+ if(!empty($data)) {
+ return ($data[0]['c'] > 0);
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Check if an e‑mail address already exists.
+ *
+ * @param string $email E‑mail address to check
+ * @return boolean Whether e‑mail address exists or not
+ */
+ public function emailExists($email)
+ {
+ $data = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM users '.
+ 'WHERE email = ?',
+ 's',
+ $email
+ );
+ if(!empty($data)) {
+ return ($data[0]['c'] > 0);
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Create a new user.
+ *
+ * @param string $username Username of the user to create
+ * @param string $email E‑Mail-Address of the user to create
+ * @param string $password Password of the user to create
+ * @return int ID of the newly created user
+ */
+ public function createUser($username, $prename, $surname, $email, $password)
+ {
+ $userId = null;
+ $this->db->setAutocommit(false);
+ try {
+ // Create user
+ $this->db->query(
+ 'INSERT INTO users '.
+ '(username, url, surname, prename, email, password) '.
+ 'VALUES '.
+ '(?, ?, ?, ?, ?, ?)',
+ 'ssssss',
+ $username,
+ \nre\core\Linker::createLinkParam($username),
+ $surname,
+ $prename,
+ $email,
+ $this->hash($password)
+ );
+ $userId = $this->db->getInsertId();
+
+ // Add role “user”
+ $this->db->query(
+ 'INSERT INTO users_userroles '.
+ '(user_id, userrole_id) '.
+ 'SELECT ?, userroles.id '.
+ 'FROM userroles '.
+ 'WHERE userroles.name = ?',
+ 'is',
+ $userId,
+ 'user'
+ );
+ }
+ catch(Exception $e) {
+ $this->db->rollback();
+ $this->db->setAutocommit(true);
+ throw $e;
+ }
+ $this->db->setAutocommit(true);
+
+
+ return $userId;
+ }
+
+
+ /**
+ * Edit a user.
+ *
+ * @throws DatamodelException
+ * @param int $userId ID of the user to delete
+ * @param string $username New name of user
+ * @param string $email Changed e‑mail-address of user
+ * @param string $password Changed plaintext password of user
+ */
+ public function editUser($userId, $username, $prename, $surname, $email, $password)
+ {
+ $this->db->setAutocommit(false);
+ try {
+ // Update user data
+ $this->db->query(
+ 'UPDATE users '.
+ 'SET username = ?, url = ?, prename = ?, surname = ?, email = ? '.
+ 'WHERE id = ?',
+ 'sssssi',
+ $username,
+ \nre\core\Linker::createLinkParam($username),
+ $prename,
+ $surname,
+ $email,
+ $userId
+ );
+
+ // Set new password
+ if(!empty($password))
+ {
+ $this->db->query(
+ 'UPDATE users '.
+ 'SET password = ? '.
+ 'WHERE id = ?',
+ 'si',
+ $this->hash($password),
+ $userId
+ );
+ }
+ }
+ catch(Exception $e) {
+ $this->db->rollback();
+ $this->db->setAutocommit(true);
+ throw $e;
+ }
+ $this->db->setAutocommit(true);
+ }
+
+
+ /**
+ * Delete a user.
+ *
+ * @param int $userId ID of the user to delete
+ */
+ public function deleteUser($userId)
+ {
+ $this->db->query('DELETE FROM users WHERE id = ?', 'i', $userId);
+ }
+
+
+
+
+ /**
+ * Hash a password.
+ *
+ * @param string $password Plaintext password
+ * @return string Hashed password
+ */
+ public function hash($password)
+ {
+ if(!function_exists('password_hash')) {
+ \hhu\z\lib\Password::load();
+ }
+
+
+ return password_hash($password, PASSWORD_DEFAULT);
+ }
+
+
+ /**
+ * Verify a password.
+ *
+ * @param string $password Plaintext password to verify
+ * @param string $hash Hashed password to match with
+ * @return boolean Verified
+ */
+ private function verify($password, $hash)
+ {
+ if(!function_exists('password_verify')) {
+ \hhu\z\lib\Password::load();
+ }
+
+
+ return password_verify($password, $hash);
+ }
+
+ }
+
+?>
diff --git a/questtypes/bossfight/BossfightQuesttypeAgent.inc b/questtypes/bossfight/BossfightQuesttypeAgent.inc
new file mode 100644
index 00000000..6d99ec44
--- /dev/null
+++ b/questtypes/bossfight/BossfightQuesttypeAgent.inc
@@ -0,0 +1,24 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * QuesttypeAgent for a boss-fight.
+ *
+ * @author Oliver Hanraths
+ */
+ class BossfightQuesttypeAgent extends \hhu\z\QuesttypeAgent
+ {
+ }
+
+?>
diff --git a/questtypes/bossfight/BossfightQuesttypeController.inc b/questtypes/bossfight/BossfightQuesttypeController.inc
new file mode 100644
index 00000000..4f8001e7
--- /dev/null
+++ b/questtypes/bossfight/BossfightQuesttypeController.inc
@@ -0,0 +1,258 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Controller of the BossfightQuesttypeAgent for a boss-fight.
+ *
+ * @author Oliver Hanraths
+ */
+ class BossfightQuesttypeController extends \hhu\z\QuesttypeController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('media');
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Prepare session
+ $this->prepareSession($quest['id']);
+
+ // Remove previous answers
+ $this->Bossfight->clearCharacterSubmissions($quest['id'], $character['id']);
+
+ // Save answers
+ foreach($_SESSION['quests'][$quest['id']]['stages'] as &$stage) {
+ $this->Bossfight->setCharacterSubmission($stage['id'], $character['id']);
+ }
+ }
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data)
+ {
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get Boss-Fight
+ $fight = $this->Bossfight->getBossFight($quest['id']);
+
+ // Prepare session
+ $this->prepareSession($quest['id']);
+
+ // Calculate lives
+ $lives = array(
+ 'character' => $fight['lives_character'],
+ 'boss' => $fight['lives_boss']
+ );
+ foreach($_SESSION['quests'][$quest['id']]['stages'] as &$stage)
+ {
+ $lives['character'] += $stage['livedrain_character'];
+ $lives['boss'] += $stage['livedrain_boss'];
+ }
+
+
+ return ($lives['boss'] == 0 && $lives['character'] > 0);
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Display a stage with a text and the answers for the following
+ * stages.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public function quest($seminary, $questgroup, $quest, $character, $exception)
+ {
+ // Get Boss-Fight
+ $fight = $this->Bossfight->getBossFight($quest['id']);
+ if(!is_null($fight['boss_seminarymedia_id'])) {
+ $fight['bossmedia'] = $this->Media->getSeminaryMediaById($fight['boss_seminarymedia_id']);
+ }
+
+ // Prepare session
+ $this->prepareSession($quest['id']);
+
+ // Get Stage
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('submit_stages')))
+ {
+ $stages = $this->request->getPostParam('submit_stages');
+ $stageId = array_keys($stages)[0];
+ $stage = $this->Bossfight->getStageById($stageId);
+ }
+ else
+ {
+ $_SESSION['quests'][$quest['id']]['stages'] = array();
+ $stage = $this->Bossfight->getFirstStage($quest['id']);
+ }
+
+ // Store Stage in session
+ if(count($_SESSION['quests'][$quest['id']]['stages']) == 0 || $_SESSION['quests'][$quest['id']]['stages'][count($_SESSION['quests'][$quest['id']]['stages'])-1]['id'] != $stage['id']) {
+ $_SESSION['quests'][$quest['id']]['stages'][] = $stage;
+ }
+
+ // Calculate lives
+ $lives = array(
+ 'character' => $fight['lives_character'],
+ 'boss' => $fight['lives_boss']
+ );
+ foreach($_SESSION['quests'][$quest['id']]['stages'] as &$stage)
+ {
+ $lives['character'] += $stage['livedrain_character'];
+ $lives['boss'] += $stage['livedrain_boss'];
+ }
+
+ // Get Child-Stages
+ $childStages = $this->Bossfight->getChildStages($stage['id']);
+
+ // Get answer of Character
+ if($this->request->getGetParam('show-answer') == 'true') {
+ foreach($childStages as &$childStage) {
+ $childStage['answer'] = $this->Bossfight->getCharacterSubmission($childStage['id'], $character['id']);
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('character', $character);
+ $this->set('fight', $fight);
+ $this->set('stage', $stage);
+ $this->set('lives', $lives);
+ $this->set('childStages', $childStages);
+ }
+
+
+ /**
+ * Action: submission.
+ *
+ * Display all stages with the answers the character has
+ * choosen.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public function submission($seminary, $questgroup, $quest, $character)
+ {
+ // Get Boss-Fight
+ $fight = $this->Bossfight->getBossFight($quest['id']);
+ if(!is_null($fight['boss_seminarymedia_id'])) {
+ $fight['bossmedia'] = $this->Media->getSeminaryMediaById($fight['boss_seminarymedia_id']);
+ }
+
+ // Get stages
+ $stages = array();
+ $stage = $this->Bossfight->getFirstStage($quest['id']);
+ while(!is_null($stage))
+ {
+ $stages[] = $stage;
+
+ $childStages = $this->Bossfight->getChildStages($stage['id']);
+ $stage = null;
+ foreach($childStages as &$childStage)
+ {
+ if($this->Bossfight->getCharacterSubmission($childStage['id'], $character['id']))
+ {
+ $stage = $childStage;
+ break;
+ }
+ }
+ }
+
+ // Calculate lives
+ $stages[0]['lives'] = array(
+ 'character' => $fight['lives_character'],
+ 'boss' => $fight['lives_boss']
+ );
+ for($i=1; $i $stages[$i-1]['lives']['character'] + $stages[$i]['livedrain_character'],
+ 'boss' => $stages[$i-1]['lives']['boss'] + $stages[$i]['livedrain_boss'],
+ );
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('character', $character);
+ $this->set('fight', $fight);
+ $this->set('stages', $stages);
+ }
+
+
+
+
+ /**
+ * Prepare the session to store stage information in
+ *
+ * @param int $questId ID of Quest
+ */
+ private function prepareSession($questId)
+ {
+ if(!array_key_exists('quests', $_SESSION)) {
+ $_SESSION['quests'] = array();
+ }
+ if(!array_key_exists($questId, $_SESSION['quests'])) {
+ $_SESSION['quests'][$questId] = array();
+ }
+ if(!array_key_exists('stages', $_SESSION['quests'][$questId])) {
+ $_SESSION['quests'][$questId]['stages'] = array();
+ }
+ }
+
+ }
+
+?>
diff --git a/questtypes/bossfight/BossfightQuesttypeModel.inc b/questtypes/bossfight/BossfightQuesttypeModel.inc
new file mode 100644
index 00000000..a05927f0
--- /dev/null
+++ b/questtypes/bossfight/BossfightQuesttypeModel.inc
@@ -0,0 +1,183 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Model of the BossfightQuesttypeAgent for a boss-fight.
+ *
+ * @author Oliver Hanraths
+ */
+ class BossfightQuesttypeModel extends \hhu\z\QuesttypeModel
+ {
+
+
+
+
+ /**
+ * Get a Boss-Fight.
+ *
+ * @throws IdNotFoundException
+ * @param int $questId ID of Quest
+ * @return array Boss-Fight data
+ */
+ public function getBossFight($questId)
+ {
+ $data = $this->db->query(
+ 'SELECT bossname, boss_seminarymedia_id, lives_character, lives_boss '.
+ 'FROM questtypes_bossfight '.
+ 'WHERE quest_id = ?',
+ 'i',
+ $questId
+ );
+ if(empty($data)) {
+ throw new \nre\exceptions\IdNotFoundException($questId);
+ }
+
+
+ return $data[0];
+ }
+
+
+ /**
+ * Get the first Stage to begin the Boss-Fight with.
+ *
+ * @param int $questId ID of Quest
+ * @return array Data of first Stage
+ */
+ public function getFirstStage($questId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, text, question, livedrain_character, livedrain_boss '.
+ 'FROM questtypes_bossfight_stages '.
+ 'WHERE questtypes_bossfight_quest_id = ? AND parent_stage_id IS NULL',
+ 'i',
+ $questId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get a Stage by its ID.
+ *
+ * @param int $stageId ID of Stage
+ * @return array Stage data or null
+ */
+ public function getStageById($stageId)
+ {
+ $data = $this->db->query(
+ 'SELECT id, text, question, livedrain_character, livedrain_boss '.
+ 'FROM questtypes_bossfight_stages '.
+ 'WHERE id = ?',
+ 'i',
+ $stageId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get the follow-up Stages for a Stage.
+ *
+ * @param int $parentStageId ID of Stage to get follow-up Stages for
+ * @return array List of follow-up Stages
+ */
+ public function getChildStages($parentStageId)
+ {
+ return $this->db->query(
+ 'SELECT id, text, question, livedrain_character, livedrain_boss '.
+ 'FROM questtypes_bossfight_stages '.
+ 'WHERE parent_stage_id = ?',
+ 'i',
+ $parentStageId
+ );
+ }
+
+
+ /**
+ * Reset all Character submissions of a Boss-Fight.
+ *
+ * @param int $questId ID of Quest
+ * @param int $characterId ID of Character
+ */
+ public function clearCharacterSubmissions($questId, $characterId)
+ {
+ $this->db->query(
+ 'DELETE FROM questtypes_bossfight_stages_characters '.
+ 'WHERE questtypes_bossfight_stage_id IN ('.
+ 'SELECT id '.
+ 'FROM questtypes_bossfight_stages '.
+ 'WHERE questtypes_bossfight_quest_id = ?'.
+ ') AND character_id = ?',
+ 'ii',
+ $questId,
+ $characterId
+ );
+ }
+
+
+ /**
+ * Save Character’s submitted answer for one Boss-Fight-Stage.
+ *
+ * @param int $regexId ID of list
+ * @param int $characterId ID of Character
+ */
+ public function setCharacterSubmission($stageId, $characterId)
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_bossfight_stages_characters '.
+ '(questtypes_bossfight_stage_id, character_id) '.
+ 'VALUES '.
+ '(?, ?) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'questtypes_bossfight_stage_id = ?',
+ 'iii',
+ $stageId, $characterId, $stageId
+ );
+ }
+
+
+ /**
+ * Get answer of one Boss-Fight-Stage submitted by Character.
+ *
+ * @param int $regexId ID of list
+ * @param int $characterId ID of Character
+ * @return boolean Stage taken
+ */
+ public function getCharacterSubmission($stageId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT questtypes_bossfight_stage_id '.
+ 'FROM questtypes_bossfight_stages_characters '.
+ 'WHERE questtypes_bossfight_stage_id = ? AND character_id = ? ',
+ 'ii',
+ $stageId, $characterId
+ );
+
+
+ return (!empty($data));
+ }
+
+ }
+
+?>
diff --git a/questtypes/bossfight/html/quest.tpl b/questtypes/bossfight/html/quest.tpl
new file mode 100644
index 00000000..e97569c2
--- /dev/null
+++ b/questtypes/bossfight/html/quest.tpl
@@ -0,0 +1,51 @@
+
+
+ =$character['name']?>
+
+
+ 0) : ?>
+
+
+
+
+ =_('lost')?>
+
+
+
+
+ =$fight['bossname']?>
+
+
+ 0) : ?>
+
+
+
+
+ =_('lost')?>
+
+
+
+
+
+=\hhu\z\Utils::t($stage['text'])?>
+
+
diff --git a/questtypes/bossfight/html/submission.tpl b/questtypes/bossfight/html/submission.tpl
new file mode 100644
index 00000000..3696964e
--- /dev/null
+++ b/questtypes/bossfight/html/submission.tpl
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+=$stage['question']?>
+
+
+ =$character['name']?>
+
+ 0) : ?>
+
+
+
+
+ =_('lost')?>
+
+
+
+
+ =$fight['bossname']?>
+
+ 0) : ?>
+
+
+
+
+ =_('lost')?>
+
+
+
+
+
diff --git a/questtypes/choiceinput/ChoiceinputQuesttypeAgent.inc b/questtypes/choiceinput/ChoiceinputQuesttypeAgent.inc
new file mode 100644
index 00000000..b418f01e
--- /dev/null
+++ b/questtypes/choiceinput/ChoiceinputQuesttypeAgent.inc
@@ -0,0 +1,24 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * QuesttypeAgent for choosing between predefined input values.
+ *
+ * @author Oliver Hanraths
+ */
+ class ChoiceinputQuesttypeAgent extends \hhu\z\QuesttypeAgent
+ {
+ }
+
+?>
diff --git a/questtypes/choiceinput/ChoiceinputQuesttypeController.inc b/questtypes/choiceinput/ChoiceinputQuesttypeController.inc
new file mode 100644
index 00000000..a7edf905
--- /dev/null
+++ b/questtypes/choiceinput/ChoiceinputQuesttypeController.inc
@@ -0,0 +1,171 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Controller of the ChoiceinputQuesttypeAgent for choosing between
+ * predefined input values.
+ *
+ * @author Oliver Hanraths
+ */
+ class ChoiceinputQuesttypeController extends \hhu\z\QuesttypeController
+ {
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get lists
+ $choiceLists = $this->Choiceinput->getChoiceinputLists($quest['id']);
+
+ // Save answers
+ foreach($choiceLists as &$list)
+ {
+ $pos = intval($list['number']) - 1;
+ $answer = (array_key_exists($pos, $answers)) ? $answers[$pos] : null;
+ $this->Choiceinput->setCharacterSubmission($list['id'], $character['id'], $answer);
+ }
+ }
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data)
+ {
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get lists
+ $choiceLists = $this->Choiceinput->getChoiceinputLists($quest['id']);
+
+ // Match lists with user answers
+ foreach($choiceLists as $i => &$list)
+ {
+ if(!array_key_exists($i, $answers)) {
+ return false;
+ }
+ if($list['questtypes_choiceinput_choice_id'] != $answers[$i]) {
+ return false;
+ }
+ }
+
+
+ // All answer right
+ return true;
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Display a text with lists with predefined values.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public function quest($seminary, $questgroup, $quest, $character, $exception)
+ {
+ // Get Task
+ $task = $this->Choiceinput->getChoiceinputQuest($quest['id']);
+
+ // Process text
+ $textParts = preg_split('/(\$\$)/', ' '.$task['text'].' ');
+
+ // Get lists
+ $choiceLists = $this->Choiceinput->getChoiceinputLists($quest['id']);
+ foreach($choiceLists as &$list) {
+ $list['values'] = $this->Choiceinput->getChoiceinputChoices($list['id']);
+ }
+
+ // Get Character answers
+ if($this->request->getGetParam('show-answer') == 'true' || !$this->Quests->hasCharacterSolvedQuest($quest['id'], $character['id']) || $this->request->getGetParam('status') == 'solved')
+ {
+ foreach($choiceLists as &$list) {
+ $list['answer'] = $this->Choiceinput->getCharacterSubmission($list['id'], $character['id']);
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('texts', $textParts);
+ $this->set('choiceLists', $choiceLists);
+ }
+
+
+ /**
+ * Action: submission.
+ *
+ * Show the submission of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public function submission($seminary, $questgroup, $quest, $character)
+ {
+ // Get Task
+ $task = $this->Choiceinput->getChoiceinputQuest($quest['id']);
+
+ // Process text
+ $textParts = preg_split('/(\$\$)/', ' '.$task['text'].' ');
+
+ // Get lists
+ $choiceLists = $this->Choiceinput->getChoiceinputLists($quest['id']);
+ foreach($choiceLists as &$list)
+ {
+ $list['values'] = $this->Choiceinput->getChoiceinputChoices($list['id']);
+ $list['answer'] = $this->Choiceinput->getCharacterSubmission($list['id'], $character['id']);
+ $list['right'] = ($list['questtypes_choiceinput_choice_id'] == $list['answer']);
+ }
+
+
+ // Pass data to view
+ $this->set('texts', $textParts);
+ $this->set('choiceLists', $choiceLists);
+ }
+
+ }
+
+?>
diff --git a/questtypes/choiceinput/ChoiceinputQuesttypeModel.inc b/questtypes/choiceinput/ChoiceinputQuesttypeModel.inc
new file mode 100644
index 00000000..e908600f
--- /dev/null
+++ b/questtypes/choiceinput/ChoiceinputQuesttypeModel.inc
@@ -0,0 +1,153 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Model of the ChoiceinputQuesttypeAgent for choosing between
+ * predefined input values.
+ *
+ * @author Oliver Hanraths
+ */
+ class ChoiceinputQuesttypeModel extends \hhu\z\QuesttypeModel
+ {
+
+
+
+
+ /**
+ * Get choiceinput-text for a Quest.
+ *
+ * @param int $questId ID of Quest
+ * @return array Choiceinput-text
+ */
+ public function getChoiceinputQuest($questId)
+ {
+ $data = $this->db->query(
+ 'SELECT text '.
+ 'FROM questtypes_choiceinput '.
+ 'WHERE quest_id = ?',
+ 'i',
+ $questId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get all lists of input values for a choiceinput-text.
+ *
+ * @param int $questId ID of Quest
+ * @return array List
+ */
+ public function getChoiceinputLists($questId)
+ {
+ return $this->db->query(
+ 'SELECT id, number, questtypes_choiceinput_choice_id '.
+ 'FROM questtypes_choiceinput_lists '.
+ 'WHERE questtypes_choiceinput_quest_id = ? '.
+ 'ORDER BY number ASC',
+ 'i',
+ $questId
+ );
+ }
+
+
+ /**
+ * Get the list of values for a choiceinput-list.
+ *
+ * @param int $listId ID of list
+ * @return array Input values
+ */
+ public function getChoiceinputChoices($listId)
+ {
+ return $this->db->query(
+ 'SELECT id, pos, text '.
+ 'FROM questtypes_choiceinput_choices '.
+ 'WHERE questtypes_choiceinput_list_id = ? '.
+ 'ORDER BY pos ASC',
+ 'i',
+ $listId
+ );
+ }
+
+
+ /**
+ * Save Character’s submitted answer for one choiceinput-list.
+ *
+ * @param int $regexId ID of list
+ * @param int $characterId ID of Character
+ * @param string $answer Submitted answer for this list
+ */
+ public function setCharacterSubmission($listId, $characterId, $answer)
+ {
+ if(is_null($answer))
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_choiceinput_lists_characters '.
+ '(questtypes_choiceinput_list_id, character_id, questtypes_choiceinput_choice_id) '.
+ 'VALUES '.
+ '(?, ?, NULL) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'questtypes_choiceinput_choice_id = NULL',
+ 'ii',
+ $listId, $characterId
+ );
+ }
+ else
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_choiceinput_lists_characters '.
+ '(questtypes_choiceinput_list_id, character_id, questtypes_choiceinput_choice_id) '.
+ 'VALUES '.
+ '(?, ?, ?) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'questtypes_choiceinput_choice_id = ?',
+ 'iiii',
+ $listId, $characterId, $answer, $answer
+ );
+ }
+ }
+
+
+ /**
+ * Get answer of one choiceinput-list submitted by Character.
+ *
+ * @param int $regexId ID of list
+ * @param int $characterId ID of Character
+ * @return int Submitted answer for this list or null
+ */
+ public function getCharacterSubmission($listId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT questtypes_choiceinput_choice_id '.
+ 'FROM questtypes_choiceinput_lists_characters '.
+ 'WHERE questtypes_choiceinput_list_id = ? AND character_id = ? ',
+ 'ii',
+ $listId, $characterId
+ );
+ if(!empty($data)) {
+ return $data[0]['questtypes_choiceinput_choice_id'];
+ }
+
+
+ return null;
+ }
+
+ }
+
+?>
diff --git a/questtypes/choiceinput/html/quest.tpl b/questtypes/choiceinput/html/quest.tpl
new file mode 100644
index 00000000..43a7d2df
--- /dev/null
+++ b/questtypes/choiceinput/html/quest.tpl
@@ -0,0 +1,15 @@
+
diff --git a/questtypes/choiceinput/html/submission.tpl b/questtypes/choiceinput/html/submission.tpl
new file mode 100644
index 00000000..40f60505
--- /dev/null
+++ b/questtypes/choiceinput/html/submission.tpl
@@ -0,0 +1,12 @@
+
diff --git a/questtypes/crossword/CrosswordQuesttypeAgent.inc b/questtypes/crossword/CrosswordQuesttypeAgent.inc
new file mode 100644
index 00000000..9b137fe9
--- /dev/null
+++ b/questtypes/crossword/CrosswordQuesttypeAgent.inc
@@ -0,0 +1,24 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * QuesttypeAgent for solving a crossword.
+ *
+ * @author Oliver Hanraths
+ */
+ class CrosswordQuesttypeAgent extends \hhu\z\QuesttypeAgent
+ {
+ }
+
+?>
diff --git a/questtypes/crossword/CrosswordQuesttypeController.inc b/questtypes/crossword/CrosswordQuesttypeController.inc
new file mode 100644
index 00000000..e5510786
--- /dev/null
+++ b/questtypes/crossword/CrosswordQuesttypeController.inc
@@ -0,0 +1,365 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Controller of the CrosswordQuesttypeAgent for solving a crossword.
+ *
+ * @author Oliver Hanraths
+ */
+ class CrosswordQuesttypeController extends \hhu\z\QuesttypeController
+ {
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get words
+ $words = $this->Crossword->getWordsForQuest($quest['id']);
+
+ // Iterate words
+ foreach($words as &$word)
+ {
+ // Assemble answer for word
+ $answer = '';
+ if($word['vertical'])
+ {
+ $x = $word['pos_x'];
+ $startY = $word['pos_y'];
+ $endY = $startY + mb_strlen($word['word'], 'UTF-8') - 1;
+
+ foreach(range($startY, $endY) as $y)
+ {
+ if(array_key_exists($x, $answers) && array_key_exists($y, $answers[$x]) && !empty($answers[$x][$y])) {
+ $answer .= $answers[$x][$y];
+ }
+ else {
+ $answer .= ' ';
+ }
+ }
+ }
+ else
+ {
+ $startX = $word['pos_x'];
+ $endX = $startX + mb_strlen($word['word'], 'UTF-8') - 1;
+ $y = $word['pos_y'];
+
+ foreach(range($startX, $endX) as $x)
+ {
+ if(array_key_exists($x, $answers) && array_key_exists($y, $answers[$x]) && !empty($answers[$x][$y])) {
+ $answer .= $answers[$x][$y];
+ }
+ else {
+ $answer .= ' ';
+ }
+ }
+ }
+
+ // Save answer
+ $this->Crossword->setCharacterSubmission($word['id'], $character['id'], $answer);
+ }
+ }
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data)
+ {
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get words
+ $words = $this->Crossword->getWordsForQuest($quest['id']);
+
+ // Iterate words
+ foreach($words as &$word)
+ {
+ // Assemble answer for word
+ $answer = '';
+ if($word['vertical'])
+ {
+ $x = $word['pos_x'];
+ $startY = $word['pos_y'];
+ $endY = $startY + mb_strlen($word['word'], 'UTF-8') - 1;
+
+ foreach(range($startY, $endY) as $y)
+ {
+ if(array_key_exists($x, $answers) && array_key_exists($y, $answers[$x]) && !empty($answers[$x][$y])) {
+ $answer .= $answers[$x][$y];
+ }
+ else {
+ $answer .= ' ';
+ }
+ }
+ }
+ else
+ {
+ $startX = $word['pos_x'];
+ $endX = $startX + mb_strlen($word['word'], 'UTF-8') - 1;
+ $y = $word['pos_y'];
+
+ foreach(range($startX, $endX) as $x)
+ {
+ if(array_key_exists($x, $answers) && array_key_exists($y, $answers[$x]) && !empty($answers[$x][$y])) {
+ $answer .= $answers[$x][$y];
+ }
+ else {
+ $answer .= ' ';
+ }
+ }
+ }
+
+ // Check answer
+ if(mb_strtolower($word['word'], 'UTF-8') != mb_strtolower($answer, 'UTF-8')) {
+ return false;
+ }
+ }
+
+
+ // All answer right
+ return true;
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Display a text with lists with predefined values.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public function quest($seminary, $questgroup, $quest, $character, $exception)
+ {
+ // Get words
+ $words = $this->Crossword->getWordsForQuest($quest['id']);
+
+ // Create 2D-matrix
+ $matrix = array();
+ $maxX = 0;
+ $maxY = 0;
+ foreach($words as $index => &$word)
+ {
+ if($this->request->getGetParam('show-answer') == 'true' || !$this->Quests->hasCharacterSolvedQuest($quest['id'], $character['id']) || $this->request->getGetParam('status') == 'solved') {
+ $word['answer'] = $this->Crossword->getCharacterSubmission($word['id'], $character['id']);
+ }
+ // Insert word
+ if($word['vertical'])
+ {
+ $x = $word['pos_x'];
+ $startY = $word['pos_y'];
+ $endY = $startY + mb_strlen($word['word'], 'UTF-8') - 1;
+
+ $matrix = array_pad($matrix, $x+1, array());
+ $matrix[$x] = array_pad($matrix[$x], $endY+1, null);
+ $maxX = max($maxX, $x);
+ $maxY = max($maxY, $endY);
+
+ foreach(range($startY, $endY) as $y)
+ {
+ $matrix[$x][$y] = array(
+ 'char' => mb_substr($word['word'], $y-$startY, 1, 'UTF-8'),
+ 'indices' => (array_key_exists($x, $matrix) && array_key_exists($y, $matrix[$x]) && !is_null($matrix[$x][$y]) && array_key_exists('indices', $matrix[$x][$y])) ? $matrix[$x][$y]['indices'] : array(),
+ 'answer' => null
+ );
+ if($y == $startY) {
+ $matrix[$x][$y]['indices'][] = $index;
+ }
+ if(array_key_exists('answer', $word))
+ {
+ $answer = mb_substr($word['answer'], $y-$startY, 1, 'UTF-8');
+ if($answer != ' ') {
+ $matrix[$x][$y]['answer'] = $answer;
+ }
+ }
+ }
+ }
+ else
+ {
+ $startX = $word['pos_x'];
+ $endX = $startX + mb_strlen($word['word'], 'UTF-8') - 1;
+ $y = $word['pos_y'];
+
+ $matrix = array_pad($matrix, $endX+1, array());
+ $maxX = max($maxX, $endX);
+ $maxY = max($maxY, $y);
+
+ foreach(range($startX, $endX) as $x)
+ {
+ $matrix[$x] = array_pad($matrix[$x], $y+1, null);
+
+ $matrix[$x][$y] = array(
+ 'char' => mb_substr($word['word'], $x-$startX, 1, 'UTF-8'),
+ 'indices' => (array_key_exists($x, $matrix) && array_key_exists($y, $matrix[$x]) && !is_null($matrix[$x][$y]) && array_key_exists('indices', $matrix[$x][$y])) ? $matrix[$x][$y]['indices'] : array(),
+ 'answer' => null
+ );
+ if($x == $startX) {
+ $matrix[$x][$y]['indices'][] = $index;
+ }
+ if(array_key_exists('answer', $word))
+ {
+ $answer = mb_substr($word['answer'], $x-$startX, 1, 'UTF-8');
+ if($answer != ' ') {
+ $matrix[$x][$y]['answer'] = $answer;
+ }
+ }
+ }
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('words', $words);
+ $this->set('maxX', $maxX);
+ $this->set('maxY', $maxY);
+ $this->set('matrix', $matrix);
+ }
+
+
+ /**
+ * Action: submission.
+ *
+ * Show the submission of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public function submission($seminary, $questgroup, $quest, $character)
+ {
+ // Get words
+ $words = $this->Crossword->getWordsForQuest($quest['id']);
+
+ // Create 2D-matrix
+ $matrix = array();
+ $maxX = 0;
+ $maxY = 0;
+ foreach($words as $index => &$word)
+ {
+ // Character answer
+ $word['answer'] = $this->Crossword->getCharacterSubmission($word['id'], $character['id']);
+
+ // Insert word
+ if($word['vertical'])
+ {
+ $x = $word['pos_x'];
+ $startY = $word['pos_y'];
+ $endY = $startY + mb_strlen($word['word'], 'UTF-8') - 1;
+
+ $matrix = array_pad($matrix, $x+1, array());
+ $matrix[$x] = array_pad($matrix[$x], $endY+1, null);
+ $maxX = max($maxX, $x);
+ $maxY = max($maxY, $endY);
+
+ foreach(range($startY, $endY) as $y)
+ {
+ $matrix[$x][$y] = array(
+ 'char' => mb_substr($word['word'], $y-$startY, 1, 'UTF-8'),
+ 'indices' => (array_key_exists($x, $matrix) && array_key_exists($y, $matrix[$x]) && !is_null($matrix[$x][$y]) && array_key_exists('indices', $matrix[$x][$y])) ? $matrix[$x][$y]['indices'] : array(),
+ 'answer' => null,
+ 'right' => false
+ );
+ if($y == $startY) {
+ $matrix[$x][$y]['indices'][] = $index;
+ }
+
+ if(!is_null($word['answer']))
+ {
+ $answer = mb_substr($word['answer'], $y-$startY, 1, 'UTF-8');
+ if($answer != ' ')
+ {
+ $matrix[$x][$y]['answer'] = $answer;
+ $matrix[$x][$y]['right'] = (mb_strtolower($matrix[$x][$y]['char'], 'UTF-8') == mb_strtolower($answer, 'UTF-8'));
+ }
+ }
+ }
+ }
+ else
+ {
+ $startX = $word['pos_x'];
+ $endX = $startX + mb_strlen($word['word'], 'UTF-8') - 1;
+ $y = $word['pos_y'];
+
+ $matrix = array_pad($matrix, $endX+1, array());
+ $maxX = max($maxX, $endX);
+ $maxY = max($maxY, $y);
+
+ foreach(range($startX, $endX) as $x)
+ {
+ $matrix[$x] = array_pad($matrix[$x], $y+1, null);
+
+ $matrix[$x][$y] = array(
+ 'char' => mb_substr($word['word'], $x-$startX, 1, 'UTF-8'),
+ 'indices' => (array_key_exists($x, $matrix) && array_key_exists($y, $matrix[$x]) && !is_null($matrix[$x][$y]) && array_key_exists('indices', $matrix[$x][$y])) ? $matrix[$x][$y]['indices'] : array(),
+ 'answer' => null,
+ 'right' => false
+ );
+ if($x == $startX) {
+ $matrix[$x][$y]['indices'][] = $index;
+ }
+ if(!is_null($word['answer']))
+ {
+ $answer = mb_substr($word['answer'], $x-$startX, 1, 'UTF-8');
+ if($answer != ' ')
+ {
+ $matrix[$x][$y]['answer'] = $answer;
+ $matrix[$x][$y]['right'] = (mb_strtolower($matrix[$x][$y]['char'], 'UTF-8') == mb_strtolower($answer, 'UTF-8'));
+ }
+ }
+ }
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('words', $words);
+ $this->set('maxX', $maxX);
+ $this->set('maxY', $maxY);
+ $this->set('matrix', $matrix);
+ }
+
+ }
+
+?>
diff --git a/questtypes/crossword/CrosswordQuesttypeModel.inc b/questtypes/crossword/CrosswordQuesttypeModel.inc
new file mode 100644
index 00000000..78a42821
--- /dev/null
+++ b/questtypes/crossword/CrosswordQuesttypeModel.inc
@@ -0,0 +1,93 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Model of the CrosswordQuesttypeAgent for solving a crossword.
+ *
+ * @author Oliver Hanraths
+ */
+ class CrosswordQuesttypeModel extends \hhu\z\QuesttypeModel
+ {
+
+
+
+
+ /**
+ * Get all words for a crossword-Quest.
+ *
+ * @param int $questId ID of Quest
+ * @return array Words
+ */
+ public function getWordsForQuest($questId)
+ {
+ return $this->db->query(
+ 'SELECT id, question, word, vertical, pos_x, pos_y '.
+ 'FROM questtypes_crossword_words '.
+ 'WHERE quest_id = ? ',
+ 'i',
+ $questId
+ );
+ }
+
+
+ /**
+ * Save Character’s submitted answer for one crossword-word.
+ *
+ * @param int $regexId ID of word
+ * @param int $characterId ID of Character
+ * @param string $answer Submitted answer for this word
+ */
+ public function setCharacterSubmission($wordId, $characterId, $answer)
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_crossword_words_characters '.
+ '(questtypes_crossword_word_id, character_id, answer) '.
+ 'VALUES '.
+ '(?, ?, ?) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'answer = ?',
+ 'iiss',
+ $wordId, $characterId, $answer,
+ $answer
+ );
+ }
+
+
+ /**
+ * Get answer of one crossword-word submitted by Character.
+ *
+ * @param int $regexId ID of lisword
+ * @param int $characterId ID of Character
+ * @return int Submitted answer for this word or null
+ */
+ public function getCharacterSubmission($wordId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT answer '.
+ 'FROM questtypes_crossword_words_characters '.
+ 'WHERE questtypes_crossword_word_id = ? AND character_id = ? ',
+ 'ii',
+ $wordId, $characterId
+ );
+ if(!empty($data)) {
+ return $data[0]['answer'];
+ }
+
+
+ return null;
+ }
+
+ }
+
+?>
diff --git a/questtypes/crossword/html/quest.tpl b/questtypes/crossword/html/quest.tpl
new file mode 100644
index 00000000..d202eb8f
--- /dev/null
+++ b/questtypes/crossword/html/quest.tpl
@@ -0,0 +1,49 @@
+
+
diff --git a/questtypes/crossword/html/submission.tpl b/questtypes/crossword/html/submission.tpl
new file mode 100644
index 00000000..c47e414d
--- /dev/null
+++ b/questtypes/crossword/html/submission.tpl
@@ -0,0 +1,48 @@
+
+
diff --git a/questtypes/dragndrop/DragndropQuesttypeAgent.inc b/questtypes/dragndrop/DragndropQuesttypeAgent.inc
new file mode 100644
index 00000000..c7d0abdf
--- /dev/null
+++ b/questtypes/dragndrop/DragndropQuesttypeAgent.inc
@@ -0,0 +1,24 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * QuesttypeAgent for Drag&Drop.
+ *
+ * @author Oliver Hanraths
+ */
+ class DragndropQuesttypeAgent extends \hhu\z\QuesttypeAgent
+ {
+ }
+
+?>
diff --git a/questtypes/dragndrop/DragndropQuesttypeController.inc b/questtypes/dragndrop/DragndropQuesttypeController.inc
new file mode 100644
index 00000000..4f359faa
--- /dev/null
+++ b/questtypes/dragndrop/DragndropQuesttypeController.inc
@@ -0,0 +1,217 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Controller of the DragndropQuesttypeAgent for Drag&Drop.
+ *
+ * @author Oliver Hanraths
+ */
+ class DragndropQuesttypeController extends \hhu\z\QuesttypeController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('media');
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get Drag&Drop field
+ $dndField = $this->Dragndrop->getDragndrop($quest['id']);
+
+ // Get Drops
+ $drops = $this->Dragndrop->getDrops($dndField['quest_id']);
+
+ // Save user answers
+ foreach($drops as &$drop)
+ {
+ // Determine user answer
+ $answer = null;
+ if(array_key_exists($drop['id'], $answers) && !empty($answers[$drop['id']]))
+ {
+ $a = intval(substr($answers[$drop['id']], 4));
+ if($a !== false && $a > 0) {
+ $answer = $a;
+ }
+ }
+
+ // Update database record
+ $this->Dragndrop->setCharacterSubmission($drop['id'], $character['id'], $answer);
+ }
+ }
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data)
+ {
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get Drag&Drop field
+ $dndField = $this->Dragndrop->getDragndrop($quest['id']);
+
+ // Get Drags
+ $drags = $this->Dragndrop->getDrags($dndField['quest_id'], true);
+
+ // Match drags with user answers
+ foreach($drags as &$drag)
+ {
+ $founds = array_keys($answers, 'drag'.$drag['id']);
+ if(count($founds) != 1) {
+ return false;
+ }
+ if(!$this->Dragndrop->dragMatchesDrop($drag['id'], $founds[0])) {
+ return false;
+ }
+ }
+
+
+ // Set status
+ return true;
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Display a text with input fields and evaluate if user input
+ * matches with stored regular expressions.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public function quest($seminary, $questgroup, $quest, $character, $exception)
+ {
+ // Get Drag&Drop field
+ $dndField = $this->Dragndrop->getDragndrop($quest['id']);
+ $dndField['media'] = $this->Media->getSeminaryMediaById($dndField['questmedia_id']);
+
+ // Get Drags
+ $drags = array();
+ $dragsByIndex = $this->Dragndrop->getDrags($dndField['quest_id']);
+ foreach($dragsByIndex as &$drag) {
+ $drag['media'] = $this->Media->getSeminaryMediaById($drag['questmedia_id']);
+ $drags[$drag['id']] = $drag;
+ }
+
+ // Get Drops
+ $drops = $this->Dragndrop->getDrops($dndField['quest_id']);
+
+ // Get Character answers
+ if($this->request->getGetParam('show-answer') == 'true' || !$this->Quests->hasCharacterSolvedQuest($quest['id'], $character['id']) || $this->request->getGetParam('status') == 'solved')
+ {
+ foreach($drops as &$drop)
+ {
+ $drop['answer'] = $this->Dragndrop->getCharacterSubmission($drop['id'], $character['id']);
+ if(!is_null($drop['answer']))
+ {
+ $drop['answer'] = $drags[$drop['answer']];
+ unset($drags[$drop['answer']['id']]);
+ }
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('field', $dndField);
+ $this->set('drops', $drops);
+ $this->set('drags', $drags);
+ }
+
+
+ /**
+ * Action: submission.
+ *
+ * Show the submission of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public function submission($seminary, $questgroup, $quest, $character)
+ {
+ // Get Drag&Drop field
+ $dndField = $this->Dragndrop->getDragndrop($quest['id']);
+ $dndField['media'] = $this->Media->getSeminaryMediaById($dndField['questmedia_id']);
+
+ // Get Drags
+ $drags = array();
+ $dragsByIndex = $this->Dragndrop->getDrags($dndField['quest_id']);
+ foreach($dragsByIndex as &$drag) {
+ $drag['media'] = $this->Media->getSeminaryMediaById($drag['questmedia_id']);
+ $drags[$drag['id']] = $drag;
+ }
+
+ // Get Drops
+ $drops = $this->Dragndrop->getDrops($dndField['quest_id']);
+
+ // Get Character answers
+ foreach($drops as &$drop)
+ {
+ $drop['answer'] = $this->Dragndrop->getCharacterSubmission($drop['id'], $character['id']);
+ if(!is_null($drop['answer']))
+ {
+ $drop['answer'] = $drags[$drop['answer']];
+ unset($drags[$drop['answer']['id']]);
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('field', $dndField);
+ $this->set('drops', $drops);
+ $this->set('drags', $drags);
+ }
+
+ }
+
+?>
diff --git a/questtypes/dragndrop/DragndropQuesttypeModel.inc b/questtypes/dragndrop/DragndropQuesttypeModel.inc
new file mode 100644
index 00000000..2aadb335
--- /dev/null
+++ b/questtypes/dragndrop/DragndropQuesttypeModel.inc
@@ -0,0 +1,180 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Model of the DragndropQuesttypeAgent for Drag&Drop.
+ *
+ * @author Oliver Hanraths
+ */
+ class DragndropQuesttypeModel extends \hhu\z\QuesttypeModel
+ {
+
+
+
+
+ /**
+ * Get Drag&Drop-field.
+ *
+ * @param int $questId ID of Quest
+ * @return array Drag&Drop-field
+ */
+ public function getDragndrop($questId)
+ {
+ $data = $this->db->query(
+ 'SELECT quest_id, questmedia_id, width, height '.
+ 'FROM questtypes_dragndrop '.
+ 'WHERE quest_id = ?',
+ 'i',
+ $questId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get Drop-items.
+ *
+ * @param int $dragndropId ID of Drag&Drop-field
+ * @return array Drop-items
+ */
+ public function getDrops($dragndropId)
+ {
+ return $this->db->query(
+ 'SELECT id, top, `left`, width, height '. //, questtypes_dragndrop_drag_id '.
+ 'FROM questtypes_dragndrop_drops '.
+ 'WHERE questtypes_dragndrop_id = ?',
+ 'i',
+ $dragndropId
+ );
+ }
+
+
+ /**
+ * Get Drag-items.
+ *
+ * @param int $dragndropId ID of Drag&Drop-field
+ * @param boolean $onlyUsed Only Drag-items that are used for a Drop-item
+ * @return array Drag-items
+ */
+ public function getDrags($dragndropId, $onlyUsed=false)
+ {
+ return $this->db->query(
+ 'SELECT id, questmedia_id '.
+ 'FROM questtypes_dragndrop_drags '.
+ 'WHERE questtypes_dragndrop_id = ?'.
+ ($onlyUsed
+ ? ' AND EXISTS ('.
+ 'SELECT questtypes_dragndrop_drag_id '.
+ 'FROM questtypes_dragndrop_drops_drags '.
+ 'WHERE questtypes_dragndrop_drag_id = questtypes_dragndrop_drags.id'.
+ ')'
+ : null
+ ),
+ 'i',
+ $dragndropId
+ );
+ }
+
+
+ /**
+ * Check if a Drag-item mathes a Drop-item.
+ *
+ * @param int $dragId ID of Drag-field
+ * @param int $dropId ID of Drop-field
+ * @return boolean Drag-item is valid for Drop-item
+ */
+ public function dragMatchesDrop($dragId, $dropId)
+ {
+ $data = $this->db->query(
+ 'SELECT count(*) AS c '.
+ 'FROM questtypes_dragndrop_drops_drags '.
+ 'WHERE questtypes_dragndrop_drop_id = ? AND questtypes_dragndrop_drag_id = ?',
+ 'ii',
+ $dropId, $dragId
+ );
+ if(!empty($data)) {
+ return ($data[0]['c'] > 0);
+ }
+
+
+ return false;
+ }
+
+
+ /**
+ * Save Character’s submitted answer for one Drop-field.
+ *
+ * @param int $dropId ID of Drop-field
+ * @param int $characterId ID of Character
+ * @param string $answer Submitted Drag-field-ID for this field
+ */
+ public function setCharacterSubmission($dropId, $characterId, $answer)
+ {
+ if(is_null($answer))
+ {
+ $this->db->query(
+ 'DELETE FROM questtypes_dragndrop_drops_characters '.
+ 'WHERE questtypes_dragndrop_drop_id = ? AND character_id = ?',
+ 'ii',
+ $dropId, $characterId
+ );
+ }
+ else
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_dragndrop_drops_characters '.
+ '(questtypes_dragndrop_drop_id, character_id, questtypes_dragndrop_drag_id) '.
+ 'VALUES '.
+ '(?, ?, ?) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'questtypes_dragndrop_drag_id = ?',
+ 'iiii',
+ $dropId, $characterId, $answer, $answer
+ );
+ }
+ }
+
+
+ /**
+ * Get Character’s saved answer for one Drop-field.
+ *
+ * @param int $dropId ID of Drop-field
+ * @param int $characterId ID of Character
+ * @return int ID of Drag-field or null
+ */
+ public function getCharacterSubmission($dropId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT questtypes_dragndrop_drag_id '.
+ 'FROM questtypes_dragndrop_drops_characters '.
+ 'WHERE questtypes_dragndrop_drop_id = ? AND character_id = ?',
+ 'ii',
+ $dropId, $characterId
+ );
+ if(!empty($data)) {
+ return $data[0]['questtypes_dragndrop_drag_id'];
+ }
+
+
+ return null;
+ }
+
+ }
+
+?>
diff --git a/questtypes/dragndrop/html/quest.tpl b/questtypes/dragndrop/html/quest.tpl
new file mode 100644
index 00000000..4cb10b60
--- /dev/null
+++ b/questtypes/dragndrop/html/quest.tpl
@@ -0,0 +1,17 @@
+
diff --git a/questtypes/dragndrop/html/submission.tpl b/questtypes/dragndrop/html/submission.tpl
new file mode 100644
index 00000000..c136fe1e
--- /dev/null
+++ b/questtypes/dragndrop/html/submission.tpl
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/questtypes/multiplechoice/MultiplechoiceQuesttypeAgent.inc b/questtypes/multiplechoice/MultiplechoiceQuesttypeAgent.inc
new file mode 100644
index 00000000..2789c5f6
--- /dev/null
+++ b/questtypes/multiplechoice/MultiplechoiceQuesttypeAgent.inc
@@ -0,0 +1,24 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * QuesttypeAgent for multiple choice.
+ *
+ * @author Oliver Hanraths
+ */
+ class MultiplechoiceQuesttypeAgent extends \hhu\z\QuesttypeAgent
+ {
+ }
+
+?>
diff --git a/questtypes/multiplechoice/MultiplechoiceQuesttypeController.inc b/questtypes/multiplechoice/MultiplechoiceQuesttypeController.inc
new file mode 100644
index 00000000..09dbfe41
--- /dev/null
+++ b/questtypes/multiplechoice/MultiplechoiceQuesttypeController.inc
@@ -0,0 +1,276 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Controller of the MultiplechoiceQuesttypeAgent multiple choice.
+ *
+ * @author Oliver Hanraths
+ */
+ class MultiplechoiceQuesttypeController extends \hhu\z\QuesttypeController
+ {
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Save temporary user answer of last question
+ $answers = (!is_array($answers)) ? array() : $answers;
+ $pos = $this->Multiplechoice->getQuestionsCountOfQuest($quest['id']);
+ $question = $this->Multiplechoice->getQuestionOfQuest($quest['id'], $pos);
+ $this->saveUserAnswers($quest['id'], $question['id'], $answers);
+
+ // Save answers
+ $questions = $this->Multiplechoice->getQuestionsOfQuest($quest['id']);
+ foreach($questions as &$question)
+ {
+ $userAnswers = $this->getUserAnswers($quest['id'], $question['id']);
+ $answers = $this->Multiplechoice->getAnswersOfQuestion($question['id']);
+ foreach($answers as &$answer)
+ {
+ $userAnswer = (array_key_exists($answer['pos']-1, $userAnswers)) ? true : false;
+ $this->Multiplechoice->setCharacterSubmission($answer['id'], $character['id'], $userAnswer);
+ }
+ }
+ }
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data)
+ {
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Save temporary user answer of last question
+ $answers = (!is_array($answers)) ? array() : $answers;
+ $pos = $this->Multiplechoice->getQuestionsCountOfQuest($quest['id']);
+ $question = $this->Multiplechoice->getQuestionOfQuest($quest['id'], $pos);
+ $this->saveUserAnswers($quest['id'], $question['id'], $answers);
+
+ // Get questions
+ $questions = $this->Multiplechoice->getQuestionsOfQuest($quest['id']);
+
+ // Iterate questions
+ foreach($questions as &$question)
+ {
+ // Get answers
+ $userAnswers = $this->getUserAnswers($quest['id'], $question['id']);
+ $answers = $this->Multiplechoice->getAnswersOfQuestion($question['id']);
+
+ // Match answers with user answers
+ foreach($answers as &$answer)
+ {
+ if(is_null($answer['tick'])) {
+ continue;
+ }
+ if($answer['tick']) {
+ if(!array_key_exists($answer['pos']-1, $userAnswers)) {
+ return false;
+ }
+ }
+ else {
+ if(array_key_exists($answer['pos']-1, $userAnswers)) {
+ return false;
+ }
+ }
+ }
+ }
+
+
+ // All questions correct answerd
+ return true;
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Display questions with a checkbox to let the user choose the
+ * right ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public function quest($seminary, $questgroup, $quest, $character, $exception)
+ {
+ // Get count of questions
+ $count = $this->Multiplechoice->getQuestionsCountOfQuest($quest['id']);
+
+ // Get position
+ $pos = 1;
+ if($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('submit-answer')))
+ {
+ if(!is_null($this->request->getPostParam('question')))
+ {
+ // Get current position
+ $pos = intval($this->request->getPostParam('question'));
+ if($pos < 0 || $pos > $count) {
+ throw new \nre\exceptions\ParamsNotValidException($pos);
+ }
+
+ // Save temporary answer of user
+ $question = $this->Multiplechoice->getQuestionOfQuest($quest['id'], $pos);
+ $answers = ($this->request->getRequestMethod() == 'POST' && !is_null($this->request->getPostParam('answers'))) ? $this->request->getPostParam('answers') : array();
+ $this->saveUserAnswers($quest['id'], $question['id'], $answers);
+
+ // Go to next position
+ $pos++;
+ }
+ else {
+ throw new \nre\exceptions\ParamsNotValidException('pos');
+ }
+ }
+
+ // Get current question
+ $question = $this->Multiplechoice->getQuestionOfQuest($quest['id'], $pos);
+
+ // Get answers
+ $question['answers'] = $this->Multiplechoice->getAnswersOfQuestion($question['id']);
+
+
+ // Get previous user answers
+ if($this->request->getGetParam('show-answer') == 'true' || !$this->Quests->hasCharacterSolvedQuest($quest['id'], $character['id']) || $this->request->getGetParam('status') == 'solved')
+ {
+ foreach($question['answers'] as &$answer) {
+ $answer['useranswer'] = $this->Multiplechoice->getCharacterSubmission($answer['id'], $character['id']);
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('question', $question);
+ $this->set('pos', $pos);
+ $this->set('count', $count);
+ }
+
+
+ /**
+ * Action: submission.
+ *
+ * Show the submission of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public function submission($seminary, $questgroup, $quest, $character)
+ {
+ // Get questions
+ $questions = $this->Multiplechoice->getQuestionsOfQuest($quest['id']);
+
+ // Get answers
+ foreach($questions as &$question)
+ {
+ $question['answers'] = $this->Multiplechoice->getAnswersOfQuestion($question['id']);
+
+ // Get user answers
+ foreach($question['answers'] as &$answer) {
+ $answer['useranswer'] = $this->Multiplechoice->getCharacterSubmission($answer['id'], $character['id']);
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('questions', $questions);
+ }
+
+
+
+
+ /**
+ * Save the answers of a user for a question temporary in the
+ * session.
+ *
+ * @param int $questId ID of Quest
+ * @param int $questionId ID of multiple choice question
+ * @param array $userAnswers Answers of user for the question
+ */
+ private function saveUserAnswers($questId, $questionId, $userAnswers)
+ {
+ // Ensure session structure
+ if(!array_key_exists('answers', $_SESSION)) {
+ $_SESSION['answers'] = array();
+ }
+ if(!array_key_exists($questId, $_SESSION['answers'])) {
+ $_SESSION['answers'][$questId] = array();
+ }
+ $_SESSION['answers'][$questId][$questionId] = array();
+
+ // Save answres
+ foreach($userAnswers as $pos => &$answer) {
+ $_SESSION['answers'][$questId][$questionId][$pos] = $answer;
+ }
+ }
+
+
+ /**
+ * Get the temporary saved answers of a user for a question.
+ *
+ * @param int $questId ID of Quest
+ * @param int $questionId ID of multiple choice question
+ * @return array Answers of user for the question
+ */
+ private function getUserAnswers($questId, $questionId)
+ {
+ // Ensure session structure
+ if(!array_key_exists('answers', $_SESSION)) {
+ $_SESSION['answers'] = array();
+ }
+ if(!array_key_exists($questId, $_SESSION['answers'])) {
+ $_SESSION['answers'][$questId] = array();
+ }
+ if(!array_key_exists($questionId, $_SESSION['answers'][$questId])) {
+ $_SESSION['answers'][$questId][$questionId] = array();
+ }
+
+
+ // Return answers
+ return $_SESSION['answers'][$questId][$questionId];
+ }
+
+ }
+
+?>
diff --git a/questtypes/multiplechoice/MultiplechoiceQuesttypeModel.inc b/questtypes/multiplechoice/MultiplechoiceQuesttypeModel.inc
new file mode 100644
index 00000000..6c4931b7
--- /dev/null
+++ b/questtypes/multiplechoice/MultiplechoiceQuesttypeModel.inc
@@ -0,0 +1,159 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Model of the MultiplechoiceQuesttypeAgent for multiple choice.
+ *
+ * @author Oliver Hanraths
+ */
+ class MultiplechoiceQuesttypeModel extends \hhu\z\QuesttypeModel
+ {
+
+
+
+
+ /**
+ * Get the count of multiple choice questions for a Quest.
+ *
+ * @param int $questId ID of Quest to get count for
+ * @return int Conut of questions
+ */
+ public function getQuestionsCountOfQuest($questId)
+ {
+ $data = $this->db->query(
+ 'SELECT count(id) AS c '.
+ 'FROM questtypes_multiplechoice '.
+ 'WHERE quest_id = ?',
+ 'i',
+ $questId
+ );
+ if(!empty($data)) {
+ return $data[0]['c'];
+ }
+
+
+ return 0;
+ }
+
+
+ /**
+ * Get all multiple choice questions of a Quest.
+ *
+ * @param int $questId ID of Quest
+ * @return array Multiple choice questions
+ */
+ public function getQuestionsOfQuest($questId)
+ {
+ return $this->db->query(
+ 'SELECT id, pos, question '.
+ 'FROM questtypes_multiplechoice '.
+ 'WHERE quest_id = ?',
+ 'i',
+ $questId
+ );
+ }
+
+
+ /**
+ * Get one multiple choice question of a Quest.
+ *
+ * @param int $questId ID of Quest
+ * @param int $pos Position of question
+ * @return array Question data
+ */
+ public function getQuestionOfQuest($questId, $pos)
+ {
+ $data = $this->db->query(
+ 'SELECT id, pos, question '.
+ 'FROM questtypes_multiplechoice '.
+ 'WHERE quest_id = ? AND pos = ?',
+ 'ii',
+ $questId, $pos
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get all answers of a multiple choice question.
+ *
+ * @param int $questionId ID of multiple choice question
+ * @return array Answers of question
+ */
+ public function getAnswersOfQuestion($questionId)
+ {
+ return $this->db->query(
+ 'SELECT id, pos, answer, tick '.
+ 'FROM questtypes_multiplechoice_answers '.
+ 'WHERE questtypes_multiplechoice_id = ?',
+ 'i',
+ $questionId
+ );
+ }
+
+
+ /**
+ * Save Character’s submitted answer for one option.
+ *
+ * @param int $answerId ID of multiple choice answer
+ * @param int $characterId ID of Character
+ * @param boolean $answer Submitted answer for this option
+ */
+ public function setCharacterSubmission($answerId, $characterId, $answer)
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_multiplechoice_characters '.
+ '(questtypes_multiplechoice_answer_id, character_id, ticked) '.
+ 'VALUES '.
+ '(?, ?, ?) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'ticked = ?',
+ 'iiii',
+ $answerId, $characterId, $answer, $answer
+ );
+ }
+
+
+ /**
+ * Get answer of one option submitted by Character.
+ *
+ * @param int $answerId ID of multiple choice answer
+ * @param int $characterId ID of Character
+ * @return boolean Submitted answer of Character or false
+ */
+ public function getCharacterSubmission($answerId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT ticked '.
+ 'FROM questtypes_multiplechoice_characters '.
+ 'WHERE questtypes_multiplechoice_answer_id = ? AND character_id = ? ',
+ 'ii',
+ $answerId, $characterId
+ );
+ if(!empty($data)) {
+ return $data[0]['ticked'];
+ }
+
+
+ return false;
+ }
+
+ }
+
+?>
diff --git a/questtypes/multiplechoice/html/quest.tpl b/questtypes/multiplechoice/html/quest.tpl
new file mode 100644
index 00000000..eba80286
--- /dev/null
+++ b/questtypes/multiplechoice/html/quest.tpl
@@ -0,0 +1,21 @@
+
diff --git a/questtypes/multiplechoice/html/submission.tpl b/questtypes/multiplechoice/html/submission.tpl
new file mode 100644
index 00000000..ebbbd493
--- /dev/null
+++ b/questtypes/multiplechoice/html/submission.tpl
@@ -0,0 +1,16 @@
+
+ &$question) : ?>
+
+ =$t->t($question['question'])?>
+
+
+
+ ☑☐
+ ✓×
+ =$answer['answer']?>
+
+
+
+
+
+
diff --git a/questtypes/submit/SubmitQuesttypeAgent.inc b/questtypes/submit/SubmitQuesttypeAgent.inc
new file mode 100644
index 00000000..68ca9ae5
--- /dev/null
+++ b/questtypes/submit/SubmitQuesttypeAgent.inc
@@ -0,0 +1,24 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * QuesttypeAgent for submitting.
+ *
+ * @author Oliver Hanraths
+ */
+ class SubmitQuesttypeAgent extends \hhu\z\QuesttypeAgent
+ {
+ }
+
+?>
diff --git a/questtypes/submit/SubmitQuesttypeController.inc b/questtypes/submit/SubmitQuesttypeController.inc
new file mode 100644
index 00000000..e064c542
--- /dev/null
+++ b/questtypes/submit/SubmitQuesttypeController.inc
@@ -0,0 +1,221 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Controller of the SubmitQuesttypeAgent for a submit task.
+ *
+ * @author Oliver Hanraths
+ */
+ class SubmitQuesttypeController extends \hhu\z\QuesttypeController
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('quests', 'uploads', 'users');
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @throws SubmissionNotValidException
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Save answer
+ if(array_key_exists('answers', $_FILES))
+ {
+ $answer = $_FILES['answers'];
+
+ // Check error
+ if($answer['error'] !== 0) {
+ throw new \hhu\z\exceptions\SubmissionNotValidException(
+ new \hhu\z\exceptions\FileUploadException($answer['error'])
+ );
+ }
+
+ // Check mimetype
+ $mimetypes = $this->Submit->getAllowedMimetypes($seminary['id']);
+ $answerMimetype = null;
+ foreach($mimetypes as &$mimetype) {
+ if($mimetype['mimetype'] == $answer['type']) {
+ $answerMimetype = $mimetype;
+ break;
+ }
+ }
+ if(is_null($answerMimetype)) {
+ throw new \hhu\z\exceptions\SubmissionNotValidException(
+ new \hhu\z\exceptions\WrongFiletypeException($answer['type'])
+ );
+ }
+
+ // Check file size
+ if($answer['size'] > $answerMimetype['size']) {
+ throw new \hhu\z\exceptions\SubmissionNotValidException(
+ new \hhu\z\exceptions\MaxFilesizeException()
+ );
+ }
+
+ // Create filename
+ $filename = sprintf(
+ '%s,%s,%s.%s',
+ $character['url'],
+ mb_substr($quest['url'], 0, 32),
+ date('Ymd-His'),
+ mb_substr(mb_substr($answer['name'], strrpos($answer['name'], '.')+1), 0, 4)
+ );
+
+ // Save file
+ if(!$this->Submit->setCharacterSubmission($seminary['id'], $quest['id'], $this->Auth->getUserId(), $character['id'], $answer, $filename)) {
+ throw new \hhu\z\exceptions\SubmissionNotValidException(
+ new \hhu\z\exceptions\FileUploadException(error_get_last()['message'])
+ );
+ }
+ }
+ }
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data)
+ {
+ $this->Submit->addCommentForCharacterAnswer($this->Auth->getUserId(), $data['submission_id'], $data['comment']);
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // A moderator has to evaluate the answer
+ return null;
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Display a big textbox to let the user enter a text that has
+ * to be evaluated by a moderator.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public function quest($seminary, $questgroup, $quest, $character, $exception)
+ {
+ // Answers (Submissions)
+ $characterSubmissions = $this->Submit->getCharacterSubmissions($quest['id'], $character['id']);
+ foreach($characterSubmissions as &$submission)
+ {
+ $submission['upload'] = $this->Uploads->getSeminaryuploadById($submission['upload_id']);
+ $submission['comments'] = $this->Submit->getCharacterSubmissionComments($submission['id']);
+ foreach($submission['comments'] as &$comment)
+ {
+ try {
+ $comment['user'] = $this->Users->getUserById($comment['created_user_id']);
+ $comment['user']['character'] = $this->Characters->getCharacterForUserAndSeminary($comment['user']['id'], $seminary['id']);
+ }
+ catch(\nre\exceptions\IdNotFoundException $e) {
+ }
+ }
+ }
+
+ // Has Character already solved Quest?
+ $solved = $this->Quests->hasCharacterSolvedQuest($quest['id'], $character['id']);
+
+ // Last Quest status for Character
+ $lastStatus = $this->Quests->getLastQuestStatus($quest['id'], $character['id']);
+
+ // Get allowed mimetypes
+ $mimetypes = $this->Submit->getAllowedMimetypes($seminary['id']);
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('submissions', $characterSubmissions);
+ $this->set('solved', $solved);
+ $this->set('lastStatus', $lastStatus);
+ $this->set('mimetypes', $mimetypes);
+ $this->set('exception', $exception);
+ }
+
+
+ /**
+ * Action: submission.
+ *
+ * Show the submission of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public function submission($seminary, $questgroup, $quest, $character)
+ {
+ // Get Character submissions
+ $submissions = $this->Submit->getCharacterSubmissions($quest['id'], $character['id']);
+ foreach($submissions as &$submission)
+ {
+ $submission['upload'] = $this->Uploads->getSeminaryuploadById($submission['upload_id']);
+ $submission['comments'] = $this->Submit->getCharacterSubmissionComments($submission['id']);
+ foreach($submission['comments'] as &$comment)
+ {
+ try {
+ $comment['user'] = $this->Users->getUserById($comment['created_user_id']);
+ $comment['user']['character'] = $this->Characters->getCharacterForUserAndSeminary($comment['user']['id'], $seminary['id']);
+ }
+ catch(\nre\exceptions\IdNotFoundException $e) {
+ }
+ }
+ }
+
+ // Status
+ $solved = $this->Quests->hasCharacterSolvedQuest($quest['id'], $character['id']);
+
+
+ // Pass data to view
+ $this->set('seminary', $seminary);
+ $this->set('submissions', $submissions);
+ $this->set('solved', $solved);
+ }
+
+ }
+
+?>
diff --git a/questtypes/submit/SubmitQuesttypeModel.inc b/questtypes/submit/SubmitQuesttypeModel.inc
new file mode 100644
index 00000000..5d821655
--- /dev/null
+++ b/questtypes/submit/SubmitQuesttypeModel.inc
@@ -0,0 +1,142 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Model of the SubmitQuesttypeAgent for a submit task.
+ *
+ * @author Oliver Hanraths
+ */
+ class SubmitQuesttypeModel extends \hhu\z\QuesttypeModel
+ {
+ /**
+ * Required models
+ *
+ * @var array
+ */
+ public $models = array('uploads');
+
+
+
+
+ /**
+ * Save Character’s submitted upload.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @param int $questId ID of Quest
+ * @param int $characterId ID of Character
+ * @param array $file Submitted upload
+ */
+ public function setCharacterSubmission($seminaryId, $questId, $userId, $characterId, $file, $filename)
+ {
+ // Save file on harddrive
+ $uploadId = $this->Uploads->uploadSeminaryFile($userId, $seminaryId, $file['name'], $filename, $file['tmp_name'], $file['type']);
+ if($uploadId === false) {
+ return false;
+ }
+
+ // Create database record
+ $this->db->query(
+ 'INSERT INTO questtypes_submit_characters '.
+ '(quest_id, character_id, upload_id) '.
+ 'VALUES '.
+ '(?, ?, ?) ',
+ 'iii',
+ $questId, $characterId, $uploadId
+ );
+
+
+ return true;
+ }
+
+
+ /**
+ * Add a comment for the answer of a Character.
+ *
+ * @param int $userId ID of user that comments
+ * @param int $submissionId ID of Character answer to comment
+ * @param string $comment Comment text
+ */
+ public function addCommentForCharacterAnswer($userId, $submissionId, $comment)
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_submit_characters_comments '.
+ '(created_user_id, questtypes_submit_character_id, comment) '.
+ 'VALUES '.
+ '(?, ?, ?)',
+ 'iis',
+ $userId,
+ $submissionId,
+ $comment
+ );
+ }
+
+
+ /**
+ * Get all uploads submitted by Character.
+ *
+ * @param int $questId ID of Quest
+ * @param int $characterId ID of Character
+ * @return array Text submitted by Character or NULL
+ */
+ public function getCharacterSubmissions($questId, $characterId)
+ {
+ return $this->db->query(
+ 'SELECT id, created, upload_id '.
+ 'FROM questtypes_submit_characters '.
+ 'WHERE quest_id = ? AND character_id = ? '.
+ 'ORDER BY created ASC',
+ 'ii',
+ $questId, $characterId
+ );
+ }
+
+
+ /**
+ * Get allowed mimetypes for uploading a file.
+ *
+ * @param int $seminaryId ID of Seminary
+ * @return array Allowed mimetypes
+ */
+ public function getAllowedMimetypes($seminaryId)
+ {
+ return $this->db->query(
+ 'SELECT id, mimetype, size '.
+ 'FROM questtypes_submit_mimetypes '.
+ 'WHERE seminary_id = ?',
+ 'i',
+ $seminaryId
+ );
+ }
+
+
+ /**
+ * Get all comments for a Character submission.
+ *
+ * @param int $characterSubmissionId ID of Character submission
+ * @return array Comments for this submission
+ */
+ public function getCharacterSubmissionComments($characterSubmissionId)
+ {
+ return $this->db->query(
+ 'SELECT id, created, created_user_id, comment '.
+ 'FROM questtypes_submit_characters_comments '.
+ 'WHERE questtypes_submit_character_id = ?',
+ 'i',
+ $characterSubmissionId
+ );
+ }
+
+ }
+
+?>
diff --git a/questtypes/submit/html/quest.tpl b/questtypes/submit/html/quest.tpl
new file mode 100644
index 00000000..4581c40d
--- /dev/null
+++ b/questtypes/submit/html/quest.tpl
@@ -0,0 +1,52 @@
+
+
+ getNestedException() instanceof \hhu\z\exceptions\WrongFiletypeException) : ?>
+ =sprintf(_('File has wrong type “%s”'), $exception->getNestedException()->getType())?>
+ getNestedException() instanceof \hhu\z\exceptions\WrongFiletypeException) : ?>
+ =_('File exceeds size maximum')?>
+ getNestedException() instanceof \hhu\z\exceptions\FileUploadException) : ?>
+ =sprintf(_('Error during file upload: %s'), $exception->getNestedException()->getNestedMessage())?>
+
+ =$exception->getNestedException()->getMessage()?>
+
+
+
+ $submissions[count($submissions)-1]['created'])) : ?>
+
+
+
+ 0) : ?>
+=_('Past submissions')?>
+
+
+
+ =$submission['upload']['name']?>
+ =sprintf(_('submitted at %s on %s h'), $dateFormatter->format(new \DateTime($submission['created'])), $timeFormatter->format(new \DateTime($submission['created'])))?>
+
+ =_('This submission is waiting for approval')?>
+
+ 0) : ?>
+
+
+
+
+ =sprintf(_('Comment from %s on %s at %s'), $comment['user']['character']['name'], $dateFormatter->format(new \DateTime($comment['created'])), $timeFormatter->format(new \DateTime($comment['created'])))?>:
+
+ =\hhu\z\Utils::t($comment['comment'])?>
+
+
+
+
+
+
+
+
diff --git a/questtypes/submit/html/submission.tpl b/questtypes/submit/html/submission.tpl
new file mode 100644
index 00000000..da978a66
--- /dev/null
+++ b/questtypes/submit/html/submission.tpl
@@ -0,0 +1,33 @@
+ 0) : ?>
+
+
+
+ =$submission['upload']['name']?>
+ =sprintf(_('submitted at %s on %s h'), $dateFormatter->format(new \DateTime($submission['created'])), $timeFormatter->format(new \DateTime($submission['created'])))?>
+ 0) : ?>
+
+
+
+
+ =sprintf(_('Comment from %s on %s at %s'), $comment['user']['character']['name'], $dateFormatter->format(new \DateTime($comment['created'])), $timeFormatter->format(new \DateTime($comment['created'])))?>:
+
+ =\hhu\z\Utils::t($comment['comment'])?>
+
+
+
+
+
+
+
+
+
+
diff --git a/questtypes/textinput/TextinputQuesttypeAgent.inc b/questtypes/textinput/TextinputQuesttypeAgent.inc
new file mode 100644
index 00000000..8144c148
--- /dev/null
+++ b/questtypes/textinput/TextinputQuesttypeAgent.inc
@@ -0,0 +1,24 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * QuesttypeAgent for inserting text.
+ *
+ * @author Oliver Hanraths
+ */
+ class TextinputQuesttypeAgent extends \hhu\z\QuesttypeAgent
+ {
+ }
+
+?>
diff --git a/questtypes/textinput/TextinputQuesttypeController.inc b/questtypes/textinput/TextinputQuesttypeController.inc
new file mode 100644
index 00000000..aa666790
--- /dev/null
+++ b/questtypes/textinput/TextinputQuesttypeController.inc
@@ -0,0 +1,185 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Controller of the TextinputQuesttypeAgent for for inserting text.
+ *
+ * @author Oliver Hanraths
+ */
+ class TextinputQuesttypeController extends \hhu\z\QuesttypeController
+ {
+
+
+
+
+ /**
+ * Save the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $answers Character answers for the Quest
+ */
+ public function saveAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get regexs
+ $regexs = $this->Textinput->getTextinputRegexs($quest['id']);
+
+ // Save answers
+ foreach($regexs as &$regex)
+ {
+ $pos = intval($regex['number']) - 1;
+ $answer = (array_key_exists($pos, $answers)) ? $answers[$pos] : '';
+ $this->Textinput->setCharacterSubmission($regex['id'], $character['id'], $answer);
+ }
+ }
+
+
+ /**
+ * Save additional data for the answers of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param array $data Additional (POST-) data
+ */
+ public function saveDataForCharacterAnswers($seminary, $questgroup, $quest, $character, $data)
+ {
+ }
+
+
+ /**
+ * Check if answers of a Character for a Quest match the correct ones.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @return boolean True/false for a right/wrong answer or null for moderator evaluation
+ */
+ public function matchAnswersOfCharacter($seminary, $questgroup, $quest, $character, $answers)
+ {
+ // Get right answers
+ $regexs = $this->Textinput->getTextinputRegexs($quest['id']);
+
+ // Match regexs with user answers
+ $allSolved = true;
+ foreach($regexs as $i => &$regex)
+ {
+ if(!array_key_exists($i, $answers))
+ {
+ $allSolved = false;
+ break;
+ }
+
+ if(!$this->isMatching($regex['regex'], $answers[$i]))
+ {
+ $allSolved = false;
+ break;
+ }
+ }
+
+
+ // Set status
+ return $allSolved;
+ }
+
+
+ /**
+ * Action: quest.
+ *
+ * Display a text with input fields and evaluate if user input
+ * matches with stored regular expressions.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ * @param Exception $exception Character submission exception
+ */
+ public function quest($seminary, $questgroup, $quest, $character, $exception)
+ {
+ // Get Task
+ $task = $this->Textinput->getTextinputQuest($quest['id']);
+
+ // Process text
+ $textParts = preg_split('/(\$\$)/', ' '.$task['text'].' ', -1, PREG_SPLIT_NO_EMPTY);
+
+ // Has Character already solved Quest?
+ $solved = $this->Quests->hasCharacterSolvedQuest($quest['id'], $character['id']);
+
+ // Get Character answers
+ $regexs = null;
+ if(!$solved || $this->request->getGetParam('show-answer') == 'true' || $this->request->getGetParam('status') == 'solved')
+ {
+ $regexs = $this->Textinput->getTextinputRegexs($quest['id']);
+ foreach($regexs as &$regex) {
+ $regex['answer'] = $this->Textinput->getCharacterSubmission($regex['id'], $character['id']);
+ }
+ }
+
+
+ // Pass data to view
+ $this->set('texts', $textParts);
+ $this->set('regexs', $regexs);
+ }
+
+
+ /**
+ * Action: submission.
+ *
+ * Show the submission of a Character for a Quest.
+ *
+ * @param array $seminary Current Seminary data
+ * @param array $questgroup Current Questgroup data
+ * @param array $quest Current Quest data
+ * @param array $character Current Character data
+ */
+ public function submission($seminary, $questgroup, $quest, $character)
+ {
+ // Get Task
+ $task = $this->Textinput->getTextinputQuest($quest['id']);
+
+ // Process text
+ $textParts = preg_split('/(\$\$)/', $task['text'], -1, PREG_SPLIT_NO_EMPTY);
+
+ // Get Character answers
+ $regexs = $this->Textinput->getTextinputRegexs($quest['id']);
+ foreach($regexs as &$regex) {
+ $regex['answer'] = $this->Textinput->getCharacterSubmission($regex['id'], $character['id']);
+ $regex['right'] = $this->isMatching($regex['regex'], $regex['answer']);
+ }
+
+
+ // Pass data to view
+ $this->set('texts', $textParts);
+ $this->set('regexs', $regexs);
+ }
+
+
+
+
+ private function isMatching($regex, $answer)
+ {
+ $score = preg_match($regex, $answer);
+
+
+ return ($score !== false && $score > 0);
+ }
+
+ }
+
+?>
diff --git a/questtypes/textinput/TextinputQuesttypeModel.inc b/questtypes/textinput/TextinputQuesttypeModel.inc
new file mode 100644
index 00000000..3d00d038
--- /dev/null
+++ b/questtypes/textinput/TextinputQuesttypeModel.inc
@@ -0,0 +1,117 @@
+
+ * @copyright 2014 Heinrich-Heine-Universität Düsseldorf
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link https://bitbucket.org/coderkun/the-legend-of-z
+ */
+
+ namespace hhu\z\questtypes;
+
+
+ /**
+ * Model of the TextinputQuesttypeAgent for inserting text.
+ *
+ * @author Oliver Hanraths
+ */
+ class TextinputQuesttypeModel extends \hhu\z\QuesttypeModel
+ {
+
+
+
+
+ /**
+ * Get textinput-text for a Quest.
+ *
+ * @param int $questId ID of Quest
+ * @return array Textinput-text
+ */
+ public function getTextinputQuest($questId)
+ {
+ $data = $this->db->query(
+ 'SELECT text '.
+ 'FROM questtypes_textinput '.
+ 'WHERE quest_id = ?',
+ 'i',
+ $questId
+ );
+ if(!empty($data)) {
+ return $data[0];
+ }
+
+
+ return null;
+ }
+
+
+ /**
+ * Get regular expressions for a textinput-text.
+ *
+ * @param int $questId ID of Quest
+ * @return array Regexs
+ */
+ public function getTextinputRegexs($questId)
+ {
+ return $this->db->query(
+ 'SELECT id, number, regex '.
+ 'FROM questtypes_textinput_regexs '.
+ 'WHERE questtypes_textinput_quest_id = ? '.
+ 'ORDER BY number ASC',
+ 'i',
+ $questId
+ );
+ }
+
+
+ /**
+ * Save Character’s submitted answer for one textinput field.
+ *
+ * @param int $regexId ID of regex
+ * @param int $characterId ID of Character
+ * @param string $answer Submitted answer for this field
+ */
+ public function setCharacterSubmission($regexId, $characterId, $answer)
+ {
+ $this->db->query(
+ 'INSERT INTO questtypes_textinput_regexs_characters '.
+ '(questtypes_textinput_regex_id, character_id, value) '.
+ 'VALUES '.
+ '(?, ?, ?) '.
+ 'ON DUPLICATE KEY UPDATE '.
+ 'value = ?',
+ 'iiss',
+ $regexId, $characterId, $answer, $answer
+ );
+ }
+
+
+ /**
+ * Get answer of one regex input field submitted by Character.
+ *
+ * @param int $regexId ID of regex
+ * @param int $characterId ID of Character
+ * @return string Submitted answer for this field or empty string
+ */
+ public function getCharacterSubmission($regexId, $characterId)
+ {
+ $data = $this->db->query(
+ 'SELECT value '.
+ 'FROM questtypes_textinput_regexs_characters '.
+ 'WHERE questtypes_textinput_regex_id = ? AND character_id = ? ',
+ 'ii',
+ $regexId, $characterId
+ );
+ if(!empty($data)) {
+ return $data[0]['value'];
+ }
+
+
+ return '';
+ }
+
+ }
+
+?>
diff --git a/questtypes/textinput/html/quest.tpl b/questtypes/textinput/html/quest.tpl
new file mode 100644
index 00000000..6616ba4d
--- /dev/null
+++ b/questtypes/textinput/html/quest.tpl
@@ -0,0 +1,11 @@
+
+
+ &$text) : ?>
+ 0) : ?>
+
+
+ =$t->t($text)?>
+
+
+
+
diff --git a/questtypes/textinput/html/submission.tpl b/questtypes/textinput/html/submission.tpl
new file mode 100644
index 00000000..b3d606f4
--- /dev/null
+++ b/questtypes/textinput/html/submission.tpl
@@ -0,0 +1,7 @@
+ &$text) : ?>
+ 0) : ?>
+=$regexs[$i-1]['answer']?>
+✓✕
+
+=$t->t($text)?>
+
diff --git a/requests/WebRequest.inc b/requests/WebRequest.inc
new file mode 100644
index 00000000..2e0f19b9
--- /dev/null
+++ b/requests/WebRequest.inc
@@ -0,0 +1,401 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\requests;
+
+
+ /**
+ * Representation of a web-request.
+ *
+ * @author coderkun
+ */
+ class WebRequest extends \nre\core\Request
+ {
+ /**
+ * Passed GET-parameters
+ *
+ * @var array
+ */
+ private $getParams = array();
+ /**
+ * Passed POST-parameters
+ *
+ * @var array
+ */
+ private $postParams = array();
+ /**
+ * Stored routes
+ *
+ * @var array
+ */
+ private $routes = array();
+ /**
+ * Stored reverse-routes
+ *
+ * @var array
+ */
+ private $reverseRoutes = array();
+
+
+
+
+ /**
+ * Construct a new web-request.
+ */
+ public function __construct()
+ {
+ // Detect current request
+ $this->detectRequest();
+
+ // Load GET-parameters
+ $this->loadGetParams();
+
+ // Load POST-parameters
+ $this->loadPostParams();
+
+ // Detect AJAX
+ $this->detectAJAX();
+ }
+
+
+
+
+ /**
+ * Get a parameter.
+ *
+ * @param int $index Index of parameter
+ * @param string $defaultIndex Index of default configuration value for this parameter
+ * @return string Value of parameter
+ */
+ public function getParam($index, $defaultIndex=null)
+ {
+ if($index == 0) {
+ return $this->getGetParam('layout', $defaultIndex);
+ }
+ else {
+ return parent::getParam($index-1, $defaultIndex);
+ }
+ }
+
+
+ /**
+ * Get all parameters from index on.
+ *
+ * @param int $offset Offset-index
+ * @return array Parameters
+ */
+ public function getParams($offset=0)
+ {
+ if($offset == 0)
+ {
+ return array_merge(
+ array(
+ $this->getGetParam('layout', 'toplevel')
+ ),
+ parent::getParams()
+ );
+ }
+
+
+ return array_slice($this->params, $offset-1);
+ }
+
+
+ /**
+ * Get a GET-parameter.
+ *
+ * @param string $key Key of GET-parameter
+ * @param string $defaultIndex Index of default configuration value for this parameter
+ * @return string Value of GET-parameter
+ */
+ public function getGetParam($key, $defaultIndex=null)
+ {
+ // Check key
+ if(array_key_exists($key, $this->getParams))
+ {
+ // Return value
+ return $this->getParams[$key];
+ }
+
+
+ // Return default value
+ return \nre\core\Config::getDefault($defaultIndex);
+ }
+
+
+ /**
+ * Get all GET-parameters.
+ *
+ * @return array GET-Parameters
+ */
+ public function getGetParams()
+ {
+ return $this->getParams;
+ }
+
+
+ /**
+ * Get a POST-parameter.
+ *
+ * @param string $key Key of POST-parameter
+ * @param string $defaultValue Default value for this parameter
+ * @return string Value of POST-parameter
+ */
+ public function getPostParam($key, $defaultValue=null)
+ {
+ // Check key
+ if(array_key_exists($key, $this->postParams))
+ {
+ // Return value
+ return $this->postParams[$key];
+ }
+
+
+ // Return default value
+ return $defaultValue;
+ }
+
+
+ /**
+ * Get all POST-parameters.
+ *
+ * @return array POST-parameters
+ */
+ public function getPostParams()
+ {
+ return $this->postParams;
+ }
+
+
+ /**
+ * Get the method of the current request.
+ *
+ * @return string Current request method
+ */
+ public function getRequestMethod()
+ {
+ return $_SERVER['REQUEST_METHOD'];
+ }
+
+
+ /**
+ * Add a URL-route.
+ *
+ * @param string $pattern Regex-Pattern that defines the routing
+ * @param string $replacement Regex-Pattern for replacement
+ * @param bool $isLast Stop after that rule
+ */
+ public function addRoute($pattern, $replacement, $isLast=false)
+ {
+ // Store route
+ $this->routes[] = $this->newRoute($pattern, $replacement, $isLast);
+ }
+
+
+ /**
+ * Add a reverse URL-route.
+ *
+ * @param string $pattern Regex-Pattern that defines the reverse routing
+ * @param string $replacement Regex-Pattern for replacement
+ * @param bool $isLast Stop after that rule
+ */
+ public function addReverseRoute($pattern, $replacement, $isLast=false)
+ {
+ // Store reverse route
+ $this->reverseRoutes[] = $this->newRoute($pattern, $replacement, $isLast);
+ }
+
+
+ /**
+ * Apply stored reverse-routes to an URL
+ *
+ * @param string $url URL to apply reverse-routes to
+ * @return string Reverse-routed URL
+ */
+ public function applyReverseRoutes($url)
+ {
+ return $this->applyRoutes($url, $this->reverseRoutes);
+ }
+
+
+ /**
+ * Revalidate the current request
+ */
+ public function revalidate()
+ {
+ $this->detectRequest();
+ }
+
+
+ /**
+ * Get a SERVER-parameter.
+ *
+ * @param string $key Key of SERVER-parameter
+ * @return string Value of SERVER-parameter
+ */
+ public function getServerParam($key)
+ {
+ if(array_key_exists($key, $_SERVER)) {
+ return $_SERVER[$key];
+ }
+
+
+ return null;
+ }
+
+
+
+
+ /**
+ * Detect the current HTTP-request.
+ */
+ private function detectRequest()
+ {
+ // URL ermitteln
+ $url = isset($_GET) && array_key_exists('url', $_GET) ? $_GET['url'] : '';
+ $url = trim($url, '/');
+
+ // Routes anwenden
+ $url = $this->applyRoutes($url, $this->routes);
+
+ // URL splitten
+ $params = explode('/', $url);
+ if(empty($params[0])) {
+ $params = array();
+ }
+
+
+ // Parameter speichern
+ $this->params = $params;
+ }
+
+
+ /**
+ * Determine parameters passed by GET.
+ */
+ private function loadGetParams()
+ {
+ if(isset($_GET)) {
+ $this->getParams = $_GET;
+ }
+ }
+
+
+ /**
+ * Determine parameters passed by POST.
+ */
+ private function loadPostParams()
+ {
+ if(isset($_POST)) {
+ $this->postParams = $_POST;
+ }
+ }
+
+
+ /**
+ * Detect an AJAX-request by checking the X-Requested-With
+ * header and set the layout to 'ajax' in this case.
+ */
+ private function detectAjax()
+ {
+ // Get request headers
+ $headers = apache_request_headers();
+
+ // Check X-Requested-With header and set layout
+ if(array_key_exists('X-Requested-With', $headers) && $headers['X-Requested-With'] == 'XMLHttpRequest') {
+ if(!array_key_exists('layout', $this->getParams)) {
+ $this->getParams['layout'] = 'ajax';
+ }
+ }
+ }
+
+
+ /**
+ * Create a new URL-route.
+ *
+ * @param string $pattern Regex-Pattern that defines the reverse routing
+ * @param string $replacement Regex-Pattern for replacement
+ * @param bool $isLast Stop after that rule
+ * @return array New URL-route
+ */
+ private function newRoute($pattern, $replacement, $isLast=false)
+ {
+ return array(
+ 'pattern' => $pattern,
+ 'replacement' => $replacement,
+ 'isLast' => $isLast
+ );
+ }
+
+
+ /**
+ * Apply given routes to an URL
+ *
+ * @param string $url URL to apply routes to
+ * @param array $routes Routes to apply
+ * @return string Routed URL
+ */
+ private function applyRoutes($url, $routes)
+ {
+ // Traverse given routes
+ foreach($routes as &$route)
+ {
+ // Create and apply Regex
+ $urlR = preg_replace(
+ '>'.$route['pattern'].'>i',
+ $route['replacement'],
+ $url
+ );
+
+ // Split URL
+ $get = '';
+ if(($gpos = strrpos($urlR, '?')) !== false) {
+ $get = substr($urlR, $gpos+1);
+ $urlR = substr($urlR, 0, $gpos);
+ }
+
+ // Has current route changed anything?
+ if($urlR != $url || !empty($get))
+ {
+ // Extract GET-parameters
+ if(strlen($get) > 0)
+ {
+ $gets = explode('&', $get);
+ foreach($gets as $get)
+ {
+ $get = explode('=', $get);
+ if(!array_key_exists($get[0], $this->getParams)) {
+ $this->getParams[$get[0]] = $get[1];
+ }
+ }
+ }
+
+
+ // Stop when route “isLast”
+ if($route['isLast']) {
+ $url = $urlR;
+ break;
+ }
+ }
+
+
+ // Set new URL
+ $url = $urlR;
+ }
+
+
+ // Return routed URL
+ return $url;
+ }
+
+ }
+
+?>
diff --git a/responses/WebResponse.inc b/responses/WebResponse.inc
new file mode 100644
index 00000000..2ca25079
--- /dev/null
+++ b/responses/WebResponse.inc
@@ -0,0 +1,250 @@
+
+ * @copyright 2013 coderkun (http://www.coderkun.de)
+ * @license http://www.gnu.org/licenses/gpl.html
+ * @link http://www.coderkun.de/projects/nre
+ */
+
+ namespace nre\responses;
+
+
+ /**
+ * Representation of a web-response.
+ *
+ * @author coderkun
+ */
+ class WebResponse extends \nre\core\Response
+ {
+ /**
+ * Applied GET-parameters
+ *
+ * @var array
+ */
+ private $getParams = array();
+ /**
+ * Changed header lines
+ *
+ * @var array
+ */
+ private $headers = array();
+
+
+
+
+ /**
+ * Add a parameter.
+ *
+ * @param mixed $value Value of parameter
+ */
+ public function addParam($value)
+ {
+ if(array_key_exists('layout', $this->getParams)) {
+ parent::addParam($value);
+ }
+ else {
+ $this->addGetParam('layout', $value);
+ }
+ }
+
+
+ /**
+ * Add multiple parameters.
+ *
+ * @param mixed $value1 Value of first parameter
+ * @param mixed … Values of further parameters
+ */
+ public function addParams($value1)
+ {
+ $this->addParam($value1);
+
+ $this->params = array_merge(
+ $this->params,
+ array_slice(
+ func_get_args(),
+ 1
+ )
+ );
+ }
+
+
+ /**
+ * Delete all stored parameters (from offset on).
+ *
+ * @param int $offset Offset-index
+ */
+ public function clearParams($offset=0)
+ {
+ if($offset == 0) {
+ unset($this->getParams['layout']);
+ }
+
+ parent::clearParams(max(0, $offset-1));
+ }
+
+
+ /**
+ * Get a parameter.
+ *
+ * @param int $index Index of parameter
+ * @param string $defaultIndex Index of default configuration value for this parameter
+ * @return string Value of parameter
+ */
+ public function getParam($index, $defaultIndex=null)
+ {
+ if($index == 0) {
+ return $this->getGetParam('layout', $defaultIndex);
+ }
+ else {
+ return parent::getParam($index-1, $defaultIndex);
+ }
+ }
+
+
+ /**
+ * Get all parameters from index on.
+ *
+ * @param int $offset Offset-index
+ * @return array Parameter values
+ */
+ public function getParams($offset=0)
+ {
+ if($offset == 0)
+ {
+ if(!array_key_exists('layout', $this->getParams)) {
+ return array();
+ }
+
+ return array_merge(
+ array(
+ $this->getParams['layout']
+ ),
+ $this->params
+ );
+ }
+
+
+ return array_slice($this->params, $offset-1);
+ }
+
+
+ /**
+ * Add a GET-parameter.
+ *
+ * @param string $key Key of GET-parameter
+ * @param mixed $value Value of GET-parameter
+ */
+ public function addGetParam($key, $value)
+ {
+ $this->getParams[$key] = $value;
+ }
+
+
+ /**
+ * Add multiple GET-parameters.
+ *
+ * @param array $params Associative arary with key-value GET-parameters
+ */
+ public function addGetParams($params)
+ {
+ $this->getParams = array_merge(
+ $this->getParams,
+ $params
+ );
+ }
+
+
+ /**
+ * Get a GET-parameter.
+ *
+ * @param int $index Index of GET-parameter
+ * @param string $defaultIndex Index of default configuration value for this parameter
+ * @return string Value of GET-parameter
+ */
+ public function getGetParam($key, $defaultIndex=null)
+ {
+ // Check key
+ if(array_key_exists($key, $this->getParams))
+ {
+ // Return value
+ return $this->getParams[$key];
+ }
+
+ // Return default value
+ return \nre\core\Config::getDefault($defaultIndex);
+ }
+
+
+ /**
+ * Get all GET-parameters.
+ *
+ * @return array All GET-parameters
+ */
+ public function getGetParams()
+ {
+ return $this->getParams;
+ }
+
+
+ /**
+ * Add a line to the response header.
+ *
+ * @param string $headerLine Header line
+ * @param bool $replace Replace existing header line
+ * @param int $http_response_code HTTP-response code
+ */
+ public function addHeader($headerLine, $replace=true, $http_response_code=null)
+ {
+ $this->headers[] = $this->newHeader($headerLine, $replace, $http_response_code);
+ }
+
+
+ /**
+ * Clear all stored headers.
+ */
+ public function clearHeaders()
+ {
+ $this->headers = array();
+ }
+
+
+ /**
+ * Send stored headers.
+ */
+ public function header()
+ {
+ foreach($this->headers as $header)
+ {
+ header(
+ $header['string'],
+ $header['replace'],
+ $header['responseCode']
+ );
+ }
+ }
+
+
+
+
+ /**
+ * Create a new header line.
+ *
+ * @param string $headerLine Header line
+ * @param bool $replace Replace existing header line
+ * @param int $http_response_code HTTP-response code
+ */
+ private function newHeader($headerLine, $replace=true, $http_response_code=null)
+ {
+ return array(
+ 'string' => $headerLine,
+ 'replace' => $replace,
+ 'responseCode' => $http_response_code
+ );
+ }
+
+ }
+
+?>
diff --git a/seminarymedia/empty b/seminarymedia/empty
new file mode 100644
index 00000000..e69de29b
diff --git a/seminaryuploads/empty b/seminaryuploads/empty
new file mode 100644
index 00000000..e69de29b
diff --git a/tmp/empty b/tmp/empty
new file mode 100644
index 00000000..e69de29b
diff --git a/uploads/empty b/uploads/empty
new file mode 100644
index 00000000..e69de29b
diff --git a/views/binary/binary.tpl b/views/binary/binary.tpl
new file mode 100644
index 00000000..43a06a51
--- /dev/null
+++ b/views/binary/binary.tpl
@@ -0,0 +1 @@
+=$intermediate?>
diff --git a/views/binary/error/index.tpl b/views/binary/error/index.tpl
new file mode 100644
index 00000000..2d8440b4
--- /dev/null
+++ b/views/binary/error/index.tpl
@@ -0,0 +1 @@
+=_('Error')?>: =$code?>: =$string?>
diff --git a/views/binary/media/achievement.tpl b/views/binary/media/achievement.tpl
new file mode 100644
index 00000000..0d6fb0df
--- /dev/null
+++ b/views/binary/media/achievement.tpl
@@ -0,0 +1 @@
+=$file?>
diff --git a/views/binary/media/avatar.tpl b/views/binary/media/avatar.tpl
new file mode 100644
index 00000000..0d6fb0df
--- /dev/null
+++ b/views/binary/media/avatar.tpl
@@ -0,0 +1 @@
+=$file?>
diff --git a/views/binary/media/index.tpl b/views/binary/media/index.tpl
new file mode 100644
index 00000000..0d6fb0df
--- /dev/null
+++ b/views/binary/media/index.tpl
@@ -0,0 +1 @@
+=$file?>
diff --git a/views/binary/media/seminary.tpl b/views/binary/media/seminary.tpl
new file mode 100644
index 00000000..0d6fb0df
--- /dev/null
+++ b/views/binary/media/seminary.tpl
@@ -0,0 +1 @@
+=$file?>
diff --git a/views/binary/media/seminaryheader.tpl b/views/binary/media/seminaryheader.tpl
new file mode 100644
index 00000000..0d6fb0df
--- /dev/null
+++ b/views/binary/media/seminaryheader.tpl
@@ -0,0 +1 @@
+=$file?>
diff --git a/views/binary/uploads/seminary.tpl b/views/binary/uploads/seminary.tpl
new file mode 100644
index 00000000..0d6fb0df
--- /dev/null
+++ b/views/binary/uploads/seminary.tpl
@@ -0,0 +1 @@
+=$file?>
diff --git a/views/error.tpl b/views/error.tpl
new file mode 100644
index 00000000..f45b01a0
--- /dev/null
+++ b/views/error.tpl
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Service Unavailable
+
+
+
+ Die Anwendung steht zur Zeit leider nicht zur Verfügung.
+
+
+
diff --git a/views/fault/error/index.tpl b/views/fault/error/index.tpl
new file mode 100644
index 00000000..f7e42500
--- /dev/null
+++ b/views/fault/error/index.tpl
@@ -0,0 +1,2 @@
+Fehler
+=$code?>: =$string?>
diff --git a/views/fault/fault.tpl b/views/fault/fault.tpl
new file mode 100644
index 00000000..a6459f96
--- /dev/null
+++ b/views/fault/fault.tpl
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Questlab
+
+
+
+ Questlab
+
+ =$intermediate?>
+
+
+
+
diff --git a/views/html/achievements/index.tpl b/views/html/achievements/index.tpl
new file mode 100644
index 00000000..2d63d0e2
--- /dev/null
+++ b/views/html/achievements/index.tpl
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+ =_('Achievements')?>
+=_('Achievement description')?>
+
+
+ =_('Seldom Achievements')?>
+
+
+
+
+
+
+
+
+
+
+
+
+ =$achievement['title']?>
+ =sprintf(_('Achievement has been achieved only %d times'), $achievement['c'])?>
+
+
+
+
+
+ =_('Most successful collectors')?>
+
+
+
+
+ =$successfulCharacter['name']?>
+ =sprintf(_('Character has achieved %d Achievements'), $successfulCharacter['c'])?>
+
+
+
+
+
+=_('Personal Achievements')?>
+
+
=sprintf(_('Own progress: %d %%'), round(count($achievedAchievements) / (count($achievedAchievements)+count($unachievedAchievements)) * 100))?>
+
+
+
+
+=$character['rank']?>. =_('Rank')?>: =sprintf(_('You achieved %d of %d Achievements so far'), count($achievedAchievements), count($achievedAchievements)+count($unachievedAchievements))?>.
+
+
+
+
+
+
+ =$achievement['title']?>=sprintf(_('achieved at: %s'), $dateFormatter->format(new \DateTime($achievement['created'])))?>
+ =\hhu\z\Utils::t($achievement['description'])?>
+
+
+
+
+
+
+
+ =(!$achievement['hidden']) ? $achievement['title'] : _('Secret Achievement')?>
+
+ =\hhu\z\Utils::t($achievement['description'])?>
+
+ =_('Continue playing to unlock this secret Achievement')?>
+
+
+
+
+
+
+
diff --git a/views/html/charactergroups/group.tpl b/views/html/charactergroups/group.tpl
new file mode 100644
index 00000000..5ef03248
--- /dev/null
+++ b/views/html/charactergroups/group.tpl
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+ 0) : ?>
+
+ =_('Manage')?>
+
+
+
+
+
+
=$group['name']?>
+
"=$group['motto']?>"
+
+
+ =$group['rank']?>. =_('Rank')?>
+ =$group['xps']?> XP
+ =count($group['characters'])?> =(count($group['characters']) > 1) ? _('Members') : _('Member')?>
+
+
+
+
+
+ =sprintf(_('%s-Quests'),$groupsgroup['name'])?>
+
+
diff --git a/views/html/charactergroups/groupsgroup.tpl b/views/html/charactergroups/groupsgroup.tpl
new file mode 100644
index 00000000..daa1c973
--- /dev/null
+++ b/views/html/charactergroups/groupsgroup.tpl
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+=$groupsgroup['name']?>
+
+
+
+ =$group['name']?> =$group['xps']?> XP
+
+
+
+
+=sprintf(_('%s-Quests'),$groupsgroup['name'])?>
+
diff --git a/views/html/charactergroups/index.tpl b/views/html/charactergroups/index.tpl
new file mode 100644
index 00000000..d88d25eb
--- /dev/null
+++ b/views/html/charactergroups/index.tpl
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+=_('Character Groups')?>
+
diff --git a/views/html/charactergroups/managegroup.tpl b/views/html/charactergroups/managegroup.tpl
new file mode 100644
index 00000000..53e1f19a
--- /dev/null
+++ b/views/html/charactergroups/managegroup.tpl
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
=$group['name']?>
+
"=$group['motto']?>"
+
+
+ =$group['rank']?>. =_('Rank')?>
+ =$group['xps']?> XP
+ =count($group['characters'])?> =(count($group['characters']) > 1) ? _('Members') : _('Member')?>
+
+
+
+
+
+
+
+
+
+
diff --git a/views/html/charactergroupsquests/quest.tpl b/views/html/charactergroupsquests/quest.tpl
new file mode 100644
index 00000000..45267e1a
--- /dev/null
+++ b/views/html/charactergroupsquests/quest.tpl
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+=$quest['title']?>
+Maximale Belohnung: =$quest['xps']?> XP
+
+
+ =_('Description')?>
+
+
+
+
+ =\hhu\z\Utils::t($quest['description'])?>
+
+
+ =_('Rules')?>
+ =\hhu\z\Utils::t($quest['rules'])?>
+
+
+
+
+
+ =_('Won Quest')?>
+ =\hhu\z\Utils::t($quest['won_text'])?>
+
+
+
+
+ =_('Lost Quest')?>
+ =\hhu\z\Utils::t($quest['lost_text'])?>
+
+
+
+
+ =$groupsgroup['name']?>
+
+
+
+ =$dateFormatter->format(new \DateTime($group['created']))?>
+ =$group['name']?>
+ =$group['xps']?> XP
+
+
+
+
diff --git a/views/html/characters/character.tpl b/views/html/characters/character.tpl
new file mode 100644
index 00000000..a26f0a27
--- /dev/null
+++ b/views/html/characters/character.tpl
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+ =$character['name']?>
+
+
+
+
+
+
+
=_('Total progress')?>: =round($character['quest_xps']*100/$seminary['achievable_xps'])?> %
+
+
+
=$character['xplevel']['level']?>
+
=_('Level')?>
+
+
+
=$character['xps']?>
+
XP
+
+
+
=$character['rank']?>.
+
=_('Rank')?>
+
+
=_('Milestones')?>
+
+
+
+
+
+
+
+
+
+ =_('Ranking')?>
+
+ &$rankCharacter) : ?>
+
+
+ =$character['rank']-count($ranking['superior'])+$index?>. =$rankCharacter['name']?>
+ =_('Level')?> =$rankCharacter['xplevel']?> (=sprintf(_('%d XPs'), $rankCharacter['xps'])?>)
+
+
+
+
+ =$character['rank']?>. =$character['name']?>
+ =_('Level')?> =$character['xplevel']['level']?> (=sprintf(_('%d XPs'), $character['xps'])?>)
+
+ &$rankCharacter) : ?>
+
+
+ =$character['rank']+$index+1?>. =$rankCharacter['name']?>
+ =_('Level')?> =$rankCharacter['xplevel']?> (=sprintf(_('%d XPs'), $rankCharacter['xps'])?>)
+
+
+
+
+
+ =_('Character Groups')?>
+
+
+
+
+
+
+
+
+
+ =_('Topic progress')?>
+
+
+
diff --git a/views/html/characters/index.tpl b/views/html/characters/index.tpl
new file mode 100644
index 00000000..171d09d7
--- /dev/null
+++ b/views/html/characters/index.tpl
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+=_('Characters')?>
+
+ 0) : ?>
+
+ =_('Manage')?>
+
+
+
+
diff --git a/views/html/characters/manage.tpl b/views/html/characters/manage.tpl
new file mode 100644
index 00000000..15b16aa2
--- /dev/null
+++ b/views/html/characters/manage.tpl
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+=_('Manage')?>
+
+
+
+ =_('Selection')?>:
+
+
+
+ checked="checked" disabled="disabled"/>
+
+
+ =$character['name']?>
+ =$character['xps']?> XP
+ =_('Admin')?>
+ =_('Moderator')?>
+ =_('User')?>
+
+
+
+
+
+
+ =_('Add role')?>
+
+
+
+
+
+
+
+ =_('Remove role')?>
+
+
+
+
+
+
+
diff --git a/views/html/characters/register.tpl b/views/html/characters/register.tpl
new file mode 100644
index 00000000..970dd25b
--- /dev/null
+++ b/views/html/characters/register.tpl
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+=_('Create Character')?>
+
+
+
+ &$settings) : ?>
+
+
+
+
+
+
+
+ =_('Character properties')?>
+ =_('Character name')?>:
+
+
+
+
+
+
+ &$settings) : ?>
+ =sprintf(_('The Seminary field “%s” is invalid'), $field)?>
+
+
+
+
+ =_('Seminary fields')?>
+
+ =$field['title']?>:
+
+ value="=$field['uservalue']?>" required="required"/>
+
+ =(array_key_exists('uservalue', $field) ? $field['uservalue'] : null)?>
+
+
+
+ selected="selected">=mb_eregi_replace('\\\\','',$option)?>
+
+
+
+
+
+
+
+
diff --git a/views/html/error/index.tpl b/views/html/error/index.tpl
new file mode 100644
index 00000000..a0b02462
--- /dev/null
+++ b/views/html/error/index.tpl
@@ -0,0 +1,17 @@
+=_('Error')?>
+=$code?>: =$string?>
+
+
+=_('Login')?>
+
+
+ =_('Username')?>:
+
+ =_('Password')?>:
+
+
+
+
+ =_('or')?> =_('register yourself')?>
+
+
diff --git a/views/html/html.tpl b/views/html/html.tpl
new file mode 100644
index 00000000..d66846bb
--- /dev/null
+++ b/views/html/html.tpl
@@ -0,0 +1,76 @@
+
+
+
+
+
+ =\nre\configs\AppConfig::$app['name']?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ =$intermediate?>
+
+
+ 0) : ?>
+ =$seminarybar?>
+
+
+
+
+
+
+
diff --git a/views/html/introduction/index.tpl b/views/html/introduction/index.tpl
new file mode 100644
index 00000000..42979a96
--- /dev/null
+++ b/views/html/introduction/index.tpl
@@ -0,0 +1,50 @@
+=_('Introduction')?>
+
+=_('Login')?>
+
+
+ =_('Username')?>:
+
+ =_('Password')?>:
+
+
+
+ =_('or')?> =_('register yourself')?>
+
+
+
+Bereits im Sommersemester 2013 wurde das Projekt „Die Legende von Zyren“ unterstützt durch den Lehrförderungsfond der Heinrich-Heine-Universität Düsseldorf ins Leben gerufen, um die Inhalte der Vorlesung „Wissensrepräsentation“ den Studierenden des Faches Informationswissenschaft mit Hilfe von Spielelementen und -modellen zu vermitteln. Die innovative Lernumgebung besteht aus einem virtuellen Textadventure, dass über eine webbasierte Plattform zugänglich ist und realen Spielen in einer Präsenzveranstaltung, in denen die Studierenden unmittelbar in das Abenteuer eintauchen und in Teams spielerisch gegeneinander antreten.
+Auf der Plattform spielt sich jeder der Studierenden mit seinem virtuellen Avatar durch das Reich von Zyren und erlernt und vertieft auf spielerische Weise die Inhalte der Vorlesung „Wissensrepräsentation“, die in Form von Herausforderungen oder Rätseln in das Abenteuer eingebunden wurden und über den Fortlauf und den Erfolg des Spiels entscheiden.
+In der zusätzlichen Präsenzveranstaltung tauchen die Studierenden direkt in das Abenteuer ein und die vertiefen die Inhalte spielerisch. Hier schließen sich die Studierenden in Teams (Gilden) zusammen, müssen eigenverantwortlich Lerninhalte erarbeiten, Probleme lösen und in speziellen Gildenaufgaben gegen andere Teams antreten, um ihr kollaborativ erarbeitetes Wissen auf die Probe zu stellen.
+Für jede erfolgreiche absolvierte Herausforderung auf der Plattform oder in der Übung erhalten die Studierenden Erfahrungspunkte und Belohnungen.
+Um das Konzept auch anderen Fachbereichen und Lehrveranstaltungen zugänglich zu machen, wurde im Frühjahr 2014 das Projekt Questlab (Arbeitstitel „The Legend of Z“) gestartet um das Konzept zu generalisieren. Lehrende können die Plattform nun nutzen um eigene Aufgaben (Quests) zu kreieren und hochzuladen und sie optional in eine Geschichte einzubinden, die sie selbst gestalten können. Zudem wurde das Responsive Design überarbeitet und bietet nun optimalen Zugriff auf die Plattform über alle mobilen Endgeräte.
+
+Die Legende von Zyren in der Presse
+
+
+Das Team
+Projektleitung:
+
+Entwicklung und Evaluation des Prototypens:
+
+ Lisa Orszullok
+ Simone Soubusta
+ Julia Göretz
+ Anja Wintermeyer
+
+Entwicklung „The Legend of Z“:
+
+ Oliver Hanraths
+ Daniel Miskovic
+
diff --git a/views/html/library/index.tpl b/views/html/library/index.tpl
new file mode 100644
index 00000000..37b423ae
--- /dev/null
+++ b/views/html/library/index.tpl
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ =_('Library')?>
+=sprintf(_('Library description, %s, %s'), $seminary['course'], $seminary['title'])?>
+
+
=sprintf(_('Total progress: %d %%'), ($totalQuestcount > 0) ? $numberFormatter->format(round($totalCharacterQuestcount/$totalQuestcount*100)) : 0) ?>
+
+
+
+
+
diff --git a/views/html/library/topic.tpl b/views/html/library/topic.tpl
new file mode 100644
index 00000000..8f1888df
--- /dev/null
+++ b/views/html/library/topic.tpl
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ =$questtopic['title']?>
+
+
Themenfortschritt: =$questtopic['characterQuestcount']?> / =$questtopic['questcount']?>
+
+
+
+
+
+Quests zu diesem Thema:
+
diff --git a/views/html/menu/index.tpl b/views/html/menu/index.tpl
new file mode 100644
index 00000000..64627e74
--- /dev/null
+++ b/views/html/menu/index.tpl
@@ -0,0 +1,9 @@
+ =\nre\configs\AppConfig::$app['name']?>
+ 0) : ?> =_('Users')?>
+ =_('Seminaries')?>
+ 0) : ?>=$seminarymenu?>
+
+ =_('Login')?>
+
+ =_('Logout')?>
+
diff --git a/views/html/questgroups/create.tpl b/views/html/questgroups/create.tpl
new file mode 100644
index 00000000..17692935
--- /dev/null
+++ b/views/html/questgroups/create.tpl
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+=_('Create Questgroup')?>
+
+
+
+ =_('Title')?>:
+
+
+
+
diff --git a/views/html/questgroups/questgroup.tpl b/views/html/questgroups/questgroup.tpl
new file mode 100644
index 00000000..347880a7
--- /dev/null
+++ b/views/html/questgroups/questgroup.tpl
@@ -0,0 +1,72 @@
+
+
+
+
+
+=$questgroupshierarchypath?>
+
+
+=$questgroup['hierarchy']['title_singular']?> =$questgroup['hierarchy']['questgroup_pos']?>: =$questgroup['title']?>
+
+=$questgroup['title']?>
+
+ 0): ?>
+
+
+
=\hhu\z\Utils::t($text['text'])?>
+
+
+
+
+
+
+ 0) : ?>
+=$hierarchy['title_plural']?>
+
+
+
+
+
+
+
+
+
+
+
+
+
Fortschritt:
+
+
+
+
=$group['character_xps']?> / =$group['xps']?> XP
+
+
+
+
+
+
+
+
+
+
+
+ 0) : ?>
+=_('Quests')?>
+
+
diff --git a/views/html/questgroupshierarchypath/index.tpl b/views/html/questgroupshierarchypath/index.tpl
new file mode 100644
index 00000000..1f68e73c
--- /dev/null
+++ b/views/html/questgroupshierarchypath/index.tpl
@@ -0,0 +1,16 @@
+
diff --git a/views/html/quests/create.tpl b/views/html/quests/create.tpl
new file mode 100644
index 00000000..68f8b4b8
--- /dev/null
+++ b/views/html/quests/create.tpl
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+=_('Create Quest')?>
+
+
+
+ =_('Name')?>:
+
+ =_('Questgroup')?>:
+
+
+ =$questgroup['title']?>
+
+
+ =('Questtype')?>:
+
+
+ =$questtype['title']?>
+
+
+ =_('XPs')?>:
+
+
+
+
+ Prolog:
+
+ =('Entry text')?>:
+
+ =('Wrong text')?>:
+
+ =_('Task')?>:
+
+
+
+
diff --git a/views/html/quests/createmedia.tpl b/views/html/quests/createmedia.tpl
new file mode 100644
index 00000000..be46b2b8
--- /dev/null
+++ b/views/html/quests/createmedia.tpl
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+Create Questsmedia
+
+
+New mediaId: =var_dump($mediaid)?>
+
+
+Error: =$error?>
+
+
+
+
+
+
diff --git a/views/html/quests/index.tpl b/views/html/quests/index.tpl
new file mode 100644
index 00000000..8e927524
--- /dev/null
+++ b/views/html/quests/index.tpl
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+=_('Quests')?>
+
+
+
+ Filter
+ =_('Questgroup')?>:
+
+ alle
+
+ selected="selected">=$filter['title']?>
+
+
+ =_('Questtype')?>:
+
+ alle
+
+ selected="selected">=$filter['title']?>
+
+
+
+
+
+
+
+
diff --git a/views/html/quests/quest.tpl b/views/html/quests/quest.tpl
new file mode 100644
index 00000000..664d7d52
--- /dev/null
+++ b/views/html/quests/quest.tpl
@@ -0,0 +1,137 @@
+
+
+
+
+
+=$questgroupshierarchypath?>
+=$quest['title']?>
+
+ 0) : ?>
+
+ =_('Prolog')?>
+
+
+
+
+
+
+
+
+
+
+ =\hhu\z\Utils::t($questtext['text'])?>
+
+ 0 || !empty($questtext['abort_text'])) : ?>
+
+
+
+
+
+
+
+
+
+ =_('Task')?>
+
+
+
+
=_('solved')?>
+
=sprintf(_('Quest completed. You have earned %d XPs.'), $quest['xps'])?>
+
+
+
+
=_('unsolved')?>
+
=\hhu\z\Utils::t($quest['wrong_text'])?>
+
+
+
+
+
+ =$t->t($quest['task'])?>
+ =$task?>
+
+
+
+
+
+
+
+
+ 0) : ?>
+
+ =_('Epilog')?>
+
+
+
+
+
+
+ =\hhu\z\Utils::t($questtext['text'])?>
+
+ 0 || !empty($questtext['abort_text'])) : ?>
+
+
+
+
+
+
+
+
+
+
diff --git a/views/html/quests/submission.tpl b/views/html/quests/submission.tpl
new file mode 100644
index 00000000..b8add296
--- /dev/null
+++ b/views/html/quests/submission.tpl
@@ -0,0 +1,12 @@
+
+
+
+
+
+=$questgroupshierarchypath?>
+=$quest['title']?>
+
+=sprintf(_('Submission of %s'),$character['name'])?>
+
diff --git a/views/html/quests/submissions.tpl b/views/html/quests/submissions.tpl
new file mode 100644
index 00000000..363db991
--- /dev/null
+++ b/views/html/quests/submissions.tpl
@@ -0,0 +1,36 @@
+
+
+
+
+
+=$questgroupshierarchypath?>
+=$quest['title']?>
+
+
diff --git a/views/html/seminaries/create.tpl b/views/html/seminaries/create.tpl
new file mode 100644
index 00000000..22a9acaa
--- /dev/null
+++ b/views/html/seminaries/create.tpl
@@ -0,0 +1,15 @@
+
+
+
+
+
+=_('Seminaries')?>
+=_('New seminary')?>
+
+
+
+ =_('Title')?>:
+
+
+
+
diff --git a/views/html/seminaries/delete.tpl b/views/html/seminaries/delete.tpl
new file mode 100644
index 00000000..8e53fdb5
--- /dev/null
+++ b/views/html/seminaries/delete.tpl
@@ -0,0 +1,13 @@
+
+
+
+
+
+=_('Seminaries')?>
+=_('Delete seminary')?>
+
+=sprintf(_('Should the seminary “%s” really be deleted?'), $seminary['title'])?>
+
+
+
+
diff --git a/views/html/seminaries/edit.tpl b/views/html/seminaries/edit.tpl
new file mode 100644
index 00000000..27974c1b
--- /dev/null
+++ b/views/html/seminaries/edit.tpl
@@ -0,0 +1,15 @@
+
+
+
+
+
+=_('Seminaries')?>
+=_('Edit seminary')?>
+
+
+
+ =_('Title')?>:
+
+
+
+
diff --git a/views/html/seminaries/index.tpl b/views/html/seminaries/index.tpl
new file mode 100644
index 00000000..922a6e03
--- /dev/null
+++ b/views/html/seminaries/index.tpl
@@ -0,0 +1,31 @@
+=_('Seminaries')?>
+ 0) : ?>
+
+ =_('Create new seminary')?>
+
+
+
+
+
+
+
+
+
+
+ 0) : ?>
+ =$seminary['title']?>
+
+ =$seminary['title']?>
+
+
+ =sprintf(_('created by %s on %s'), $seminary['creator']['username'], $dateFormatter->format(new \DateTime($seminary['created'])))?>
+ =\hhu\z\Utils::t($seminary['description'])?>
+
+ =_('Create a Character')?>
+
+ =sprintf(_('Your Character “%s” has not been activated yet'), $seminary['usercharacter']['name'])?>
+
+
+
+
+
diff --git a/views/html/seminaries/seminary.tpl b/views/html/seminaries/seminary.tpl
new file mode 100644
index 00000000..7aa0ce44
--- /dev/null
+++ b/views/html/seminaries/seminary.tpl
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ 0) : ?>
+
+ =_('Edit seminary')?>
+ =_('Delete seminary')?>
+ 0) : ?>=_('Show Quests')?>
+
+
+=\hhu\z\Utils::t($seminary['description'])?>
+
+=$hierarchy['title_plural']?>
+
+
+
+
+
+
+
+ =$hierarchy['title_singular']?> =$group['pos']?>:
+ =$group['title']?>
+
+
+
+
+
=$group['character_xps']?> / =$group['xps']?> XP
+
+
+ =$group['text']?>
+
+ Auf ins Abenteuer!
+
+
+
+
+
diff --git a/views/html/seminarybar/index.tpl b/views/html/seminarybar/index.tpl
new file mode 100644
index 00000000..ec094e70
--- /dev/null
+++ b/views/html/seminarybar/index.tpl
@@ -0,0 +1,48 @@
+
+ =$character['name']?>
+
+
+ =('Level')?> =$character['xplevel']['level']?>
+ =sprintf(_('%d XPs'), $character['xps'])?>
+ =$character['rank']?>. =_('Rank')?>
+ Zum Profil
+
+
+
+
+
+
+
+
+
+ =_('Last Achievement')?>
+
+
+
+
+
+ =$lastAchievement['title']?>
+ =sprintf(_('achieved at: %s'), $dateFormatter->format(new \DateTime($lastAchievement['created'])))?>
+
+
+
+
+
+
diff --git a/views/html/seminarymenu/index.tpl b/views/html/seminarymenu/index.tpl
new file mode 100644
index 00000000..56b9307b
--- /dev/null
+++ b/views/html/seminarymenu/index.tpl
@@ -0,0 +1,5 @@
+ =$loggedSeminary['title']?>
+ 0) : ?> =_('Characters')?>
+ =_('Character Groups')?>
+ =_('Achievements')?>
+ =_('Library')?>
diff --git a/views/html/userroles/user.tpl b/views/html/userroles/user.tpl
new file mode 100644
index 00000000..df8b7a66
--- /dev/null
+++ b/views/html/userroles/user.tpl
@@ -0,0 +1,5 @@
+
diff --git a/views/html/users/create.tpl b/views/html/users/create.tpl
new file mode 100644
index 00000000..2d5484dc
--- /dev/null
+++ b/views/html/users/create.tpl
@@ -0,0 +1,18 @@
+=_('Users')?>
+=_('New user')?>
+
+
+
+ =_('Username')?>:
+
+ =_('Prename')?>:
+
+ =_('Surname')?>:
+
+ =_('E‑mail address')?>:
+
+ =_('Password')?>:
+
+
+
+
diff --git a/views/html/users/delete.tpl b/views/html/users/delete.tpl
new file mode 100644
index 00000000..10f280ab
--- /dev/null
+++ b/views/html/users/delete.tpl
@@ -0,0 +1,8 @@
+=_('Users')?>
+=_('Delete user')?>
+
+=sprintf(_('Should the user “%s” (%s) really be deleted?'), $user['username'], $user['email'])?>
+
+
+
+
diff --git a/views/html/users/edit.tpl b/views/html/users/edit.tpl
new file mode 100644
index 00000000..510aaed3
--- /dev/null
+++ b/views/html/users/edit.tpl
@@ -0,0 +1,18 @@
+=_('Users')?>
+=_('Edit user')?>
+
+
+
+ =_('Username')?>:
+
+ =_('Prename')?>:
+
+ =_('Surname')?>:
+
+ =_('E‑mail address')?>:
+
+ =_('Password')?>:
+
+
+
+
diff --git a/views/html/users/index.tpl b/views/html/users/index.tpl
new file mode 100644
index 00000000..9f36049c
--- /dev/null
+++ b/views/html/users/index.tpl
@@ -0,0 +1,9 @@
+=_('Users')?>
+
+ =_('Create new user')?>
+
+
+
+ =$user['username']?> =sprintf(_('registered on %s'), $dateFormatter->format(new \DateTime($user['created'])))?>
+
+
diff --git a/views/html/users/login.tpl b/views/html/users/login.tpl
new file mode 100644
index 00000000..d51423f0
--- /dev/null
+++ b/views/html/users/login.tpl
@@ -0,0 +1,18 @@
+=_('Users')?>
+
+=_('Login')?>
+
+=_('Login failed')?>.
+
+
+
+ =_('Username')?>:
+
+ =_('Password')?>:
+
+
+
+
+
+
+
diff --git a/views/html/users/logout.tpl b/views/html/users/logout.tpl
new file mode 100644
index 00000000..e69de29b
diff --git a/views/html/users/register.tpl b/views/html/users/register.tpl
new file mode 100644
index 00000000..29843569
--- /dev/null
+++ b/views/html/users/register.tpl
@@ -0,0 +1,90 @@
+=_('Users')?>
+
+=_('Registration')?>
+
+
+ &$settings) : ?>
+
+
+ $value) : ?>
+
+ getMessage();
+ break;
+ } ?>
+
+
+
+
+
+
+
+
+
+ =_('Username')?>:
+ />
+ =_('Prename')?>:
+ />
+ =_('Surname')?>:
+ />
+ =_('E‑mail address')?>:
+ />
+ =_('Password')?>:
+ />
+
+
+
diff --git a/views/html/users/user.tpl b/views/html/users/user.tpl
new file mode 100644
index 00000000..4be14ae5
--- /dev/null
+++ b/views/html/users/user.tpl
@@ -0,0 +1,35 @@
+=_('Users')?>
+ 0) : ?>
+
+ =_('Edit user')?>
+ =_('Delete user')?>
+
+
+=$user['username']?>
+
+ =sprintf(_('registered on %s'), $dateFormatter->format(new \DateTime($user['created'])))?>
+ =_('Name')?>: =$user['prename']?> =$user['surname']?>
+ =_('E‑mail address')?>: =$user['email']?>
+
+
+=_('Characters')?>
+
+
+=_('Roles')?>
+=$userroles?>
diff --git a/views/inlineerror.tpl b/views/inlineerror.tpl
new file mode 100644
index 00000000..87a2b222
--- /dev/null
+++ b/views/inlineerror.tpl
@@ -0,0 +1 @@
+Dieser Teil der Anwendung steht zur Zeit leider nicht zur Verfügung.
diff --git a/www/.htaccess b/www/.htaccess
new file mode 100644
index 00000000..63163a80
--- /dev/null
+++ b/www/.htaccess
@@ -0,0 +1,8 @@
+
+ RewriteEngine On
+
+ RewriteBase /
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteRule ^(.*)$ index.php?url=$1 [QSA,L,NE]
+
diff --git a/www/analytics/LEGALNOTICE b/www/analytics/LEGALNOTICE
new file mode 100644
index 00000000..e95df846
--- /dev/null
+++ b/www/analytics/LEGALNOTICE
@@ -0,0 +1,265 @@
+COPYRIGHT
+
+ Piwik - Open Source Web Analytics
+
+ The software package is:
+
+ Copyright (C) 2013 Matthieu Aubry
+
+ Individual contributions, components, and libraries are copyright
+ of their respective authors.
+
+
+SOFTWARE LICENSE
+
+ The free software license of Piwik is GNU General Public License v3
+ or later. A copy of GNU GPL v3 should have been included in this
+ software package in misc/gpl-3.0.txt.
+
+
+TRADEMARK
+
+ Piwik (TM) is an internationally registered trademark.
+
+ The software license does not grant any rights under trademark
+ law for use of the trademark. Refer to http://piwik.org/trademark/
+ for up-to-date trademark licensing information.
+
+*
+* The software license applies to both the aggregate software and
+* application-specific portions of the software.
+*
+* You may not remove this legal notice or modify the software
+* in such a way that misrepresents the origin of the software.
+*
+
+CREDITS
+
+ The software consists of contributions made by many individuals.
+ Major contributors are listed in http://piwik.org/the-piwik-team/.
+
+ For detailed contribution history, refer to the source, tickets,
+ patches, and Git revision history, available at
+ http://dev.piwik.org/trac/
+ https://github.com/piwik/piwik
+
+
+SEPARATELY LICENSED COMPONENTS AND LIBRARIES
+
+ The following components/libraries are distributed in this package,
+ and subject to their respective licenses.
+
+ Name: javascriptCode.tpl - tracking tag to embed in your web pages
+ Link: https://github.com/piwik/piwik/blob/master/core/Tracker/javascriptTag.tpl
+ License: Public Domain
+
+ Name: jquery.truncate
+ Link: https://github.com/piwik/piwik/blob/master/libs/jquery/truncate/
+ License: New BSD
+
+ Name: piwik.js - JavaScript tracker
+ Link: https://github.com/piwik/piwik/blob/master/js/piwik.js
+ License: New BSD
+
+ Name: PiwikTracker - server-side tracker (PHP)
+ Link: https://github.com/piwik/piwik/blob/master/libs/PiwikTracker/
+ License: New BSD
+
+ Name: UserAgentParser
+ Link: https://github.com/piwik/piwik/blob/master/libs/UserAgentParser/
+ License: New BSD
+
+
+THIRD-PARTY COMPONENTS AND LIBRARIES
+
+ The following components/libraries are redistributed in this package,
+ and subject to their respective licenses.
+
+ Name: jqPlot
+ Link: http://www.jqplot.com/
+ License: Dual-licensed: MIT or GPL v2
+
+ Name: jQuery
+ Link: http://jquery.com/
+ License: Dual-licensed: MIT or GPL
+ Notes:
+ - GPL version not explicitly stated in source but GPL v2 is in git
+ - includes Sizzle.js - multi-licensed: MIT, New BSD, or GPL [v2]
+
+ Name: jQuery UI
+ Link: http://jqueryui.com/
+ License: Dual-licensed: MIT or GPL
+ Notes:
+ - GPL version not explicitly stated in source but GPL v2 is in git
+
+ Name: jquery.history
+ Link: http://tkyk.github.com/jquery-history-plugin/
+ License: MIT
+
+ Name: jquery.scrollTo
+ Link: http://plugins.jquery.com/project/ScrollTo
+ License: Dual licensed: MIT or GPL
+
+ Name: jquery Tooltip
+ Link: http://bassistance.de/jquery-plugins/jquery-plugin-tooltip/
+ License: Dual licensed: MIT or GPL
+
+ Name: jquery placeholder
+ Link: http://mths.be/placeholder
+ License: Dual licensed: MIT or GPL
+
+ Name: jquery smartbanner
+ Link: https://github.com/jasny/jquery.smartbanner
+ License: Dual licensed: MIT
+
+ Name: json2.js
+ Link: http://json.org/
+ License: Public domain
+ Notes:
+ - reference implementation
+
+ Name: jshrink
+ Link: https://github.com/tedivm/jshrink
+ License: BSD-3-Clause
+
+ Name: sparkline
+ Link: https//sourceforge.net/projects/sparkline/
+ License: Dual-licensed: New BSD or GPL v2
+
+ Name: sprintf
+ Link: http://www.diveintojavascript.com/projects/javascript-sprintf
+ License: New BSD
+
+ Name: upgrade.php
+ Link: http://upgradephp.berlios.de/
+ License: Public domain
+
+ Name: Archive Tar
+ Link: http://pear.php.net/package/Archive_Tar
+ License: New BSD
+
+ Name: Event Dispatcher (and Notification)
+ Link: http://pear.php.net/package/Event_Dispatcher/
+ License: New BSD
+
+ Name: HTML Common2
+ Link: http://pear.php.net/package/HTML_Common2/
+ License: New BSD
+
+ Name: HTML QuickForm2
+ Link: http://pear.php.net/package/HTML_QuickForm2/
+ License: New BSD
+
+ Name: HTML QuickForm2_Renderer_Smarty
+ Link: http://www.phcomp.co.uk/tmp/Smarty.phps
+ License: New BSD
+
+ Name: MaxMindGeoIP
+ Link: http://dev.maxmind.com/geoip/downloadable#PHP-7
+ License: LGPL
+
+ Name: PclZip
+ Link: http://www.phpconcept.net/pclzip/
+ License: LGPL
+ Notes:
+ - GPL version not explicitly stated but tarball contains LGPL v2.1
+
+ Name: PEAR (base system)
+ Link: http://pear.php.net/package/PEAR
+ License: New BSD
+
+ Name: PhpSecInfo
+ Link: http://phpsec.org/projects/phpsecinfo/
+ License: New BSD
+
+ Name: RankChecker
+ Link: http://www.getrank.org/free-pagerank-script
+ License: GPL
+
+ Name: Twig
+ Link: http://twig.sensiolabs.org/
+ License: BSD
+
+ Name: TCPDF
+ Link: http://sourceforge.net/projects/tcpdf
+ License: LGPL v3 or later
+
+ Name: Zend Framework
+ Link: http://www.zendframework.com/
+ License: New BSD
+
+ Name: pChart 2.1.3
+ Link: http://www.pchart.net
+ License: GPL v3
+
+ Name: Chroma.js
+ Link: https://github.com/gka/chroma.js
+ License: GPL v3
+
+ Name: qTip2 - Pretty powerful tooltips
+ Link: http://craigsworks.com/projects/qtip2/
+ License: GPL
+
+ Name: Kartograph.js
+ Link: http://kartograph.org/
+ License: LGPL v3 or later
+
+ Name: Raphaël - JavaScript Vector Library
+ Link: http://raphaeljs.com/
+ License: MIT
+
+ Name: lessphp
+ Link: http://leafo.net/lessphp
+ License: GPL3/MIT
+
+ Name: Symfony Console Component
+ Link: https://github.com/symfony/Console
+ License: MIT
+
+
+THIRD-PARTY CONTENT
+
+ Name: FamFamFam icons - Mark James
+ Link: http://www.famfamfam.com/lab/icons/
+ License: CC BY 3.0
+
+ Name: Solar System icons - Dan Wiersema
+ Link: http://www.iconspedia.com/icon/neptune-4672.html
+ License: Free for non-commercial use
+ Notes:
+ - used in Piwik's ExampleUI plugin
+
+ Name: Wine project - tahoma.ttf font
+ Link: http://source.winehq.org/git/wine.git/blob_plain/HEAD:/fonts/tahoma.ttf
+ License: LGPL v2.1
+ Notes:
+ - used in ImageGraph plugin
+
+ Name: plugins/CorePluginsAdmin/images/themes.png
+ Link: https://www.iconfinder.com/icons/17022/colors_draw_paint_icon
+ License: Free for commercial use
+
+ Name: plugins/Feedback/angularjs/ratefeature/thumbs-down.png
+ Link: https://www.iconfinder.com/icons/216428/down_thumbs_icon
+ License: Creative Commons (Attribution-Share Alike 3.0 Unported)
+
+ Name: plugins/Feedback/angularjs/ratefeature/thumbs-up.png
+ Link: https://www.iconfinder.com/icons/216429/thumbs_up_icon
+ License: Creative Commons (Attribution-Share Alike 3.0 Unported)
+
+ Name: plugins/CorePluginsAdmin/images/plugins.png
+ Link: http://findicons.com/icon/94051/tools_wizard?id=396912
+ License: GNU/GPL
+
+ Name: plugins/Insights/images/idea.png
+ Link: https://www.iconfinder.com/icons/6074/brainstorm_bulb_idea_jabber_light_icon
+ License: GPL
+ By: Alessandro Rei - http://www.kde-look.org/usermanager/search.php?username=mentalrey
+
+Notes:
+- the "New BSD" license refers to either the "Modified BSD" and "Simplified BSD"
+ licenses (2- or 3-clause), which are GPL compatible.
+- the "MIT" license is also referred to as the "X11" license
+- icons for browsers, operating systems, browser plugins, search engines, and
+ and flags of countries are nominative use of third-party trademarks when
+ referring to the corresponding product or entity
diff --git a/www/analytics/README.md b/www/analytics/README.md
new file mode 100644
index 00000000..7894effd
--- /dev/null
+++ b/www/analytics/README.md
@@ -0,0 +1,92 @@
+# Piwik - piwik.org
+
+## Description
+
+Piwik is the leading Free/Libre open source Web Analytics platform.
+
+Piwik is a full featured PHP MySQL software program that you download and install on your own webserver.
+At the end of the five minute installation process you will be given a JavaScript code.
+Simply copy and paste this tag on websites you wish to track and access your analytics reports in real time.
+
+Piwik aims to be a Free software alternative to Google Analytics, and is already used on more than 1,000,000 websites.
+
+## Mission Statement
+
+> « To create, as a community, the leading international Free/Libre web analytics platform, providing access to all functionality through open components and open APIs. »
+
+Or in short:
+> « Liberate Web Analytics »
+
+## License
+
+Piwik is released under the GPL v3 (or later) license, see [misc/gpl-3.0.txt](misc/gpl-3.0.txt)
+
+## Requirements
+
+ * PHP 5.3.2 or greater
+ * MySQL 4.1 or greater, and either MySQLi or PDO library must be enabled
+ * Piwik is OS / server independent
+
+See http://piwik.org/docs/requirements/
+
+## Install
+
+ * Upload piwik to your webserver
+ * Point your browser to the directory
+ * Follow the steps
+ * Add the given javascript code to your pages
+ * (You may also generate fake data to experiment, by enabling the plugin VisitorGenerator)
+
+See http://piwik.org/docs/installation/
+
+If you do not have a server, consider our Piwik Hosting partner: http://piwik.org/hosting/
+
+## Changelog
+
+For the list of all tickets closed in the current and past releases, see http://piwik.org/changelog/
+
+## Participate!
+
+We believe in liberating Web Analytics, providing a free platform for simple and advanced analytics. Piwik was built by dozens of people like you,
+and we need your help to make Piwik better… Why not participate in a useful project today?
+
+You will find pointers on how you can participate in Piwik at http://piwik.org/get-involved/
+
+## Contact
+
+http://piwik.org
+
+hello@piwik.org
+
+About us: http://piwik.org/the-piwik-team/
+
+## More information
+
+What makes Piwik unique from the competition:
+
+ * Real time web analytics reports: in Piwik, reports are by default generated in real time.
+ For high traffic websites, you can choose the frequency for reports to be processed.
+
+ * You own your web analytics data: since Piwik is installed on your server, the data is stored in your own database and you can get all the statistics
+ using the powerful Piwik Analytics API.
+
+ * Piwik is a Free Software which can easily be configured to respect your visitors privacy.
+
+ * Modern, easy to use User Interface: you can fully customize your dashboard, drag and drop widgets and more.
+
+ * Piwik features are built inside plugins: you can add new features and remove the ones you don’t need.
+ You can build your own web analytics plugins or hire a consultant to have your custom feature built in Piwik
+
+ * Vibrant international Open community of more than 200,000 active users (tracking even more websites!)
+
+ * Advanced Web Analytics capabilities such as Ecommerce Tracking, Goal tracking, Campaign tracking,
+ Custom Variables, Email Reports, Custom Segment Editor, Geo Location, Real time maps, and more!
+
+Documentation and more info on http://piwik.org
+
+## Code Status
+The Piwik project uses an ever-expanding comprehensive set of thousands of unit tests and dozens of integration [tests](https://github.com/piwik/piwik/tree/master/tests),
+ running on the hosted distributed continuous integration platform Travis-CI.
+
+Build status (master branch) [](https://travis-ci.org/piwik/piwik) - Screenshot tests Build [](https://travis-ci.org/piwik/piwik-ui-tests)
+
diff --git a/www/analytics/composer.json b/www/analytics/composer.json
new file mode 100644
index 00000000..8c3fc620
--- /dev/null
+++ b/www/analytics/composer.json
@@ -0,0 +1,30 @@
+{
+ "name": "piwik/piwik",
+ "type": "application",
+ "description": "Open Source Real Time Web Analytics Platform",
+ "keywords": ["piwik","web","analytics"],
+ "homepage": "http://piwik.org",
+ "license": "GPL-3.0+",
+ "authors": [
+ {
+ "name": "The Piwik Team",
+ "email": "hello@piwik.org",
+ "homepage": "http://piwik.org/the-piwik-team/"
+ }
+ ],
+ "support": {
+ "forum": "http://forum.piwik.org/",
+ "issues": "http://dev.piwik.org/trac/roadmap",
+ "wiki": "http://dev.piwik.org/",
+ "source": "https://github.com/piwik/piwik"
+ },
+ "require": {
+ "php": ">=5.3.2",
+ "twig/twig": "1.*",
+ "leafo/lessphp": "~0.3",
+ "symfony/console": ">=v2.3.5",
+ "tedivm/jshrink": "v0.5.1",
+ "mustangostang/spyc": "0.5.*",
+ "piwik/device-detector": "*"
+ }
+}
diff --git a/www/analytics/composer.lock b/www/analytics/composer.lock
new file mode 100644
index 00000000..c31eafe5
--- /dev/null
+++ b/www/analytics/composer.lock
@@ -0,0 +1,310 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file"
+ ],
+ "hash": "69246584e6b57bfbc8d39799cd3b9213",
+ "packages": [
+ {
+ "name": "leafo/lessphp",
+ "version": "v0.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/leafo/lessphp.git",
+ "reference": "51f3f06f0fe78a722dabfd14578444bdd078d9de"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/leafo/lessphp/zipball/51f3f06f0fe78a722dabfd14578444bdd078d9de",
+ "reference": "51f3f06f0fe78a722dabfd14578444bdd078d9de",
+ "shasum": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.3-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "lessc.inc.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT",
+ "GPL-3.0"
+ ],
+ "authors": [
+ {
+ "name": "Leaf Corcoran",
+ "email": "leafot@gmail.com",
+ "homepage": "http://leafo.net"
+ }
+ ],
+ "description": "lessphp is a compiler for LESS written in PHP.",
+ "homepage": "http://leafo.net/lessphp/",
+ "time": "2013-08-09 17:09:19"
+ },
+ {
+ "name": "mustangostang/spyc",
+ "version": "0.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mustangostang/spyc.git",
+ "reference": "dc4785b4d7227fd9905e086d499fb8abfadf9977"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mustangostang/spyc/zipball/dc4785b4d7227fd9905e086d499fb8abfadf9977",
+ "reference": "dc4785b4d7227fd9905e086d499fb8abfadf9977",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.5.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "Spyc.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT License"
+ ],
+ "authors": [
+ {
+ "name": "mustangostang",
+ "email": "vlad.andersen@gmail.com"
+ }
+ ],
+ "description": "A simple YAML loader/dumper class for PHP",
+ "homepage": "https://github.com/mustangostang/spyc/",
+ "keywords": [
+ "spyc",
+ "yaml",
+ "yml"
+ ],
+ "time": "2013-02-21 10:52:01"
+ },
+ {
+ "name": "piwik/device-detector",
+ "version": "1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/piwik/device-detector.git",
+ "reference": "ea7c5d8b76def0d8345a4eba59c5f98ec0109de6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/piwik/device-detector/zipball/ea7c5d8b76def0d8345a4eba59c5f98ec0109de6",
+ "reference": "ea7c5d8b76def0d8345a4eba59c5f98ec0109de6",
+ "shasum": ""
+ },
+ "require": {
+ "mustangostang/spyc": "*",
+ "php": ">=5.3.1"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "DeviceDetector.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GPL-3.0+"
+ ],
+ "authors": [
+ {
+ "name": "The Piwik Team",
+ "email": "hello@piwik.org",
+ "homepage": "http://piwik.org/the-piwik-team/"
+ }
+ ],
+ "description": "The Universal Device Detection library, that parses User Agents and detects devices (desktop, tablet, mobile, tv, cars, console, etc.), and detects browsers, operating systems, devices, brands and models.",
+ "homepage": "http://piwik.org",
+ "keywords": [
+ "devicedetection",
+ "parser",
+ "useragent"
+ ],
+ "time": "2014-04-03 08:59:48"
+ },
+ {
+ "name": "symfony/console",
+ "version": "v2.4.3",
+ "target-dir": "Symfony/Component/Console",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/Console.git",
+ "reference": "ef20f1f58d7f693ee888353962bd2db336e3bbcb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/Console/zipball/ef20f1f58d7f693ee888353962bd2db336e3bbcb",
+ "reference": "ef20f1f58d7f693ee888353962bd2db336e3bbcb",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "symfony/event-dispatcher": "~2.1"
+ },
+ "suggest": {
+ "symfony/event-dispatcher": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.4-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Symfony\\Component\\Console\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Console Component",
+ "homepage": "http://symfony.com",
+ "time": "2014-03-01 17:35:04"
+ },
+ {
+ "name": "tedivm/jshrink",
+ "version": "v0.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/tedivm/JShrink.git",
+ "reference": "2d3f1a7d336ad54bdf2180732b806c768a791cbf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/tedivm/JShrink/zipball/2d3f1a7d336ad54bdf2180732b806c768a791cbf",
+ "reference": "2d3f1a7d336ad54bdf2180732b806c768a791cbf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "JShrink": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Robert Hafner",
+ "email": "tedivm@tedivm.com"
+ }
+ ],
+ "description": "Javascript Minifier built in PHP",
+ "homepage": "http://github.com/tedivm/JShrink",
+ "keywords": [
+ "javascript",
+ "minifier"
+ ],
+ "time": "2012-11-26 04:48:55"
+ },
+ {
+ "name": "twig/twig",
+ "version": "v1.15.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/fabpot/Twig.git",
+ "reference": "1fb5784662f438d7d96a541e305e28b812e2eeed"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/fabpot/Twig/zipball/1fb5784662f438d7d96a541e305e28b812e2eeed",
+ "reference": "1fb5784662f438d7d96a541e305e28b812e2eeed",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.2.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.15-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Twig_": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Armin Ronacher",
+ "email": "armin.ronacher@active-4.com",
+ "role": "Project Founder"
+ },
+ {
+ "name": "Twig Team",
+ "homepage": "https://github.com/fabpot/Twig/graphs/contributors",
+ "role": "Contributors"
+ }
+ ],
+ "description": "Twig, the flexible, fast, and secure template language for PHP",
+ "homepage": "http://twig.sensiolabs.org",
+ "keywords": [
+ "templating"
+ ],
+ "time": "2014-02-13 10:19:29"
+ }
+ ],
+ "packages-dev": [
+
+ ],
+ "aliases": [
+
+ ],
+ "minimum-stability": "stable",
+ "stability-flags": [
+
+ ],
+ "platform": {
+ "php": ">=5.3.2"
+ },
+ "platform-dev": [
+
+ ]
+}
diff --git a/www/analytics/config/.htaccess b/www/analytics/config/.htaccess
new file mode 100644
index 00000000..6cd2e134
--- /dev/null
+++ b/www/analytics/config/.htaccess
@@ -0,0 +1,13 @@
+
+
+Deny from all
+
+
+
+Deny from all
+
+
+
+Deny from all
+
+
diff --git a/www/analytics/config/global.ini.php b/www/analytics/config/global.ini.php
new file mode 100644
index 00000000..9d76c4be
--- /dev/null
+++ b/www/analytics/config/global.ini.php
@@ -0,0 +1,627 @@
+; DO NOT REMOVE THIS LINE
+; If you want to change some of these default values, the best practise is to override
+; them in your configuration file in config/config.ini.php. If you directly edit this file,
+; you will lose your changes when you upgrade Piwik.
+; For example if you want to override action_title_category_delimiter,
+; edit config/config.ini.php and add the following:
+; [General]
+; action_title_category_delimiter = "-"
+
+;--------
+; WARNING - YOU SHOULD NOT EDIT THIS FILE DIRECTLY - Edit config.ini.php instead.
+;--------
+
+[database]
+host =
+username =
+password =
+dbname =
+tables_prefix =
+port = 3306
+adapter = PDO_MYSQL
+type = InnoDB
+schema = Mysql
+
+; if charset is set to utf8, Piwik will ensure that it is storing its data using UTF8 charset.
+; it will add a sql query SET at each page view.
+; Piwik should work correctly without this setting.
+;charset = utf8
+
+[database_tests]
+host = localhost
+username = root
+password =
+dbname = piwik_tests
+tables_prefix = piwiktests_
+port = 3306
+adapter = PDO_MYSQL
+type = InnoDB
+schema = Mysql
+
+[log]
+; possible values for log: screen, database, file
+log_writers[] = screen
+
+; log level, everything logged w/ this level or one of greater severity
+; will be logged. everything else will be ignored. possible values are:
+; NONE, ERROR, WARN, INFO, DEBUG, VERBOSE
+log_level = WARN
+
+; if set to 1, only requests done in CLI mode (eg. the archive.php cron run) will be logged
+; NOTE: log_only_when_debug_parameter will also be checked for
+log_only_when_cli = 0
+
+; if set to 1, only requests with "&debug" parameter will be logged
+; NOTE: log_only_when_cli will also be checked for
+log_only_when_debug_parameter = 0
+
+; if configured to log in a file, log entries will be made to this file
+logger_file_path = tmp/logs/piwik.log
+
+
+[Debug]
+; if set to 1, the archiving process will always be triggered, even if the archive has already been computed
+; this is useful when making changes to the archiving code so we can force the archiving process
+always_archive_data_period = 0;
+always_archive_data_day = 0;
+; Force archiving Custom date range (without re-archiving sub-periods used to process this date range)
+always_archive_data_range = 0;
+
+; if set to 1, all the SQL queries will be recorded by the profiler
+; and a profiling summary will be printed at the end of the request
+; NOTE: you must also set [log] log_writers[] = "screen" to enable the profiler to print on screen
+enable_sql_profiler = 0
+
+; if set to 1, a Piwik tracking code will be included in the Piwik UI footer and will track visits, pages, etc. to idsite = 1
+; this is useful for Piwik developers as an easy way to create data in their local Piwik
+track_visits_inside_piwik_ui = 0
+
+; if set to 1, javascript files will be included individually and neither merged nor minified.
+; this option must be set to 1 when adding, removing or modifying javascript files
+disable_merged_assets = 0
+
+; If set to 1, all requests to piwik.php will be forced to be 'new visitors'
+tracker_always_new_visitor = 0
+
+; Allow automatic upgrades to Beta or RC releases
+allow_upgrades_to_beta = 0
+
+[DebugTests]
+; Set to 1 by default. If you set to 0, the standalone plugins (with their own git repositories)
+; will not be loaded when executing tests.
+enable_load_standalone_plugins_during_tests = 1
+
+[General]
+; the following settings control whether Unique Visitors will be processed for different period types.
+; year and range periods are disabled by default, to ensure optimal performance for high traffic Piwik instances
+; if you set it to 1 and want the Unique Visitors to be re-processed for reports in the past, drop all piwik_archive_* tables
+; it is recommended to always enable Unique Visitors processing for 'day' periods
+enable_processing_unique_visitors_day = 1
+enable_processing_unique_visitors_week = 1
+enable_processing_unique_visitors_month = 1
+enable_processing_unique_visitors_year = 0
+enable_processing_unique_visitors_range = 0
+
+; when set to 1, all requests to Piwik will return a maintenance message without connecting to the DB
+; this is useful when upgrading using the shell command, to prevent other users from accessing the UI while Upgrade is in progress
+maintenance_mode = 0
+
+; character used to automatically create categories in the Actions > Pages, Outlinks and Downloads reports
+; for example a URL like "example.com/blog/development/first-post" will create
+; the page first-post in the subcategory development which belongs to the blog category
+action_url_category_delimiter = /
+
+; similar to above, but this delimiter is only used for page titles in the Actions > Page titles report
+action_title_category_delimiter = /
+
+; the maximum url category depth to track. if this is set to 2, then a url such as
+; "example.com/blog/development/first-post" would be treated as "example.com/blog/development".
+; this setting is used mainly to limit the amount of data that is stored by Piwik.
+action_category_level_limit = 10
+
+; minimum number of websites to run autocompleter
+autocomplete_min_sites = 5
+
+; maximum number of websites showed in search results in autocompleter
+site_selector_max_sites = 15
+
+; if set to 1, shows sparklines (evolution graph) in 'All Websites' report (MultiSites plugin)
+show_multisites_sparklines = 1
+
+; number of websites to display per page in the All Websites dashboard
+all_websites_website_per_page = 50
+
+; if set to 0, the anonymous user will not be able to use the 'segments' parameter in the API request
+; this is useful to prevent full DB access to the anonymous user, or to limit performance usage
+anonymous_user_enable_use_segments_API = 1
+
+; if browser trigger archiving is disabled, API requests with a &segment= parameter will still trigger archiving.
+; You can force the browser archiving to be disabled in most cases by setting this setting to 1
+; The only time that the browser will still trigger archiving is when requesting a custom date range that is not pre-processed yet
+browser_archiving_disabled_enforce = 0
+
+; By default, users can create Segments which are to be processed in Real-time.
+; Setting this to 0 will force all newly created Custom Segments to be "Pre-processed (faster, requires archive.php cron)"
+; This can be useful if you want to prevent users from adding much load on the server.
+; Note: any existing Segment set to "processed in Real time", will still be set to Real-time.
+; this will only affect custom segments added or modified after this setting is changed.
+enable_create_realtime_segments = 1
+
+; this action name is used when the URL ends with a slash /
+; it is useful to have an actual string to write in the UI
+action_default_name = index
+
+; if you want all your users to use Piwik in only one language, disable the LanguagesManager
+; plugin, and set this default_language (users won't see the language drop down)
+default_language = en
+
+; default number of elements in the datatable
+datatable_default_limit = 10
+
+; default number of rows returned in API responses
+; this value is overwritten by the '# Rows to display' selector.
+; if set to -1, a click on 'Export as' will export all rows independently of the current '# Rows to display'.
+API_datatable_default_limit = 100
+
+; When period=range, below the datatables, when user clicks on "export", the data will be aggregate of the range.
+; Here you can specify the comma separated list of formats for which the data will be exported aggregated by day
+; (ie. there will be a new "date" column). For example set to: "rss,tsv,csv"
+datatable_export_range_as_day = "rss"
+
+; This setting is overriden in the UI, under "User Settings".
+; The date and period loaded by Piwik uses the defaults below. Possible values: yesterday, today.
+default_day = yesterday
+; Possible values: day, week, month, year.
+default_period = day
+
+; Time in seconds after which an archive will be computed again. This setting is used only for today's statistics.
+; Defaults to 10 seconds so that by default, Piwik provides real time reporting.
+; This setting is overriden in the UI, under "General Settings".
+; This setting is only used if it hasn't been overriden via the UI yet, or if enable_general_settings_admin=0
+time_before_today_archive_considered_outdated = 10
+
+; This setting is overriden in the UI, under "General Settings".
+; The default value is to allow browsers to trigger the Piwik archiving process.
+; This setting is only used if it hasn't been overriden via the UI yet, or if enable_general_settings_admin=0
+enable_browser_archiving_triggering = 1
+
+; By default Piwik runs OPTIMIZE TABLE SQL queries to free spaces after deleting some data.
+; If your Piwik tracks millions of pages, the OPTIMIZE TABLE queries might run for hours (seen in "SHOW FULL PROCESSLIST \g")
+; so you can disable these special queries here:
+enable_sql_optimize_queries = 1
+
+; MySQL minimum required version
+; note: timezone support added in 4.1.3
+minimum_mysql_version = 4.1
+
+; PostgreSQL minimum required version
+minimum_pgsql_version = 8.3
+
+; Minimum adviced memory limit in php.ini file (see memory_limit value)
+minimum_memory_limit = 128
+
+; Minimum memory limit enforced when archived via misc/cron/archive.php
+minimum_memory_limit_when_archiving = 768
+
+; Piwik will check that usernames and password have a minimum length, and will check that characters are "allowed"
+; This can be disabled, if for example you wish to import an existing User database in Piwik and your rules are less restrictive
+disable_checks_usernames_attributes = 0
+
+; Piwik will use the configured hash algorithm where possible.
+; For legacy data, fallback or non-security scenarios, we use md5.
+hash_algorithm = whirlpool
+
+; by default, Piwik uses PHP's built-in file-based session save handler with lock files.
+; For clusters, use dbtable.
+session_save_handler = files
+
+; If set to 1, Piwik will automatically redirect all http:// requests to https://
+; If SSL / https is not correctly configured on the server, this will break Piwik
+; If you set this to 1, and your SSL configuration breaks later on, you can always edit this back to 0
+; it is recommended for security reasons to always use Piwik over https
+force_ssl = 0
+
+; login cookie name
+login_cookie_name = piwik_auth
+
+; login cookie expiration (14 days)
+login_cookie_expire = 1209600
+
+; The path on the server in which the cookie will be available on.
+; Defaults to empty. See spec in http://curl.haxx.se/rfc/cookie_spec.html
+login_cookie_path =
+
+; email address that appears as a Sender in the password recovery email
+; if specified, {DOMAIN} will be replaced by the current Piwik domain
+login_password_recovery_email_address = "password-recovery@{DOMAIN}"
+; name that appears as a Sender in the password recovery email
+login_password_recovery_email_name = Piwik
+
+; By default when user logs out he is redirected to Piwik "homepage" usually the Login form.
+; Uncomment the next line to set a URL to redirect the user to after he logs out of Piwik.
+; login_logout_url = http://...
+
+; Set to 1 to disable the framebuster on standard Non-widgets pages (a click-jacking countermeasure).
+; Default is 0 (i.e., bust frames on all non Widget pages such as Login, API, Widgets, Email reports, etc.).
+enable_framed_pages = 0
+
+; Set to 1 to disable the framebuster on Admin pages (a click-jacking countermeasure).
+; Default is 0 (i.e., bust frames on the Settings forms).
+enable_framed_settings = 0
+
+; language cookie name for session
+language_cookie_name = piwik_lang
+
+; standard email address displayed when sending emails
+noreply_email_address = "noreply@{DOMAIN}"
+
+; feedback email address;
+; when testing, use your own email address or "nobody"
+feedback_email_address = "feedback@piwik.org"
+
+; during archiving, Piwik will limit the number of results recorded, for performance reasons
+; maximum number of rows for any of the Referrers tables (keywords, search engines, campaigns, etc.)
+datatable_archiving_maximum_rows_referrers = 1000
+; maximum number of rows for any of the Referrers subtable (search engines by keyword, keyword by campaign, etc.)
+datatable_archiving_maximum_rows_subtable_referrers = 50
+
+; maximum number of rows for the Custom Variables names report
+; Note: if the website is Ecommerce enabled, the two values below will be automatically set to 50000
+datatable_archiving_maximum_rows_custom_variables = 1000
+; maximum number of rows for the Custom Variables values reports
+datatable_archiving_maximum_rows_subtable_custom_variables = 1000
+
+; maximum number of rows for any of the Actions tables (pages, downloads, outlinks)
+datatable_archiving_maximum_rows_actions = 500
+; maximum number of rows for pages in categories (sub pages, when clicking on the + for a page category)
+; note: should not exceed the display limit in Piwik\Actions\Controller::ACTIONS_REPORT_ROWS_DISPLAY
+; because each subdirectory doesn't have paging at the bottom, so all data should be displayed if possible.
+datatable_archiving_maximum_rows_subtable_actions = 100
+
+; maximum number of rows for any of the Events tables (Categories, Actions, Names)
+datatable_archiving_maximum_rows_events = 500
+; maximum number of rows for sub-tables of the Events tables (eg. for the subtables Categories>Actions or Categories>Names).
+datatable_archiving_maximum_rows_subtable_events = 100
+
+; maximum number of rows for other tables (Providers, User settings configurations)
+datatable_archiving_maximum_rows_standard = 500
+
+; maximum number of rows to fetch from the database when archiving. if set to 0, no limit is used.
+; this can be used to speed up the archiving process, but is only useful if you're site has a large
+; amount of actions, referrers or custom variable name/value pairs.
+archiving_ranking_query_row_limit = 50000
+
+; maximum number of actions that is shown in the visitor log for each visitor
+visitor_log_maximum_actions_per_visit = 500
+
+; by default, the real time Live! widget will update every 5 seconds and refresh with new visits/actions/etc.
+; you can change the timeout so the widget refreshes more often, or not as frequently
+live_widget_refresh_after_seconds = 5
+
+; by default, the Live! real time visitor count widget will check to see how many visitors your
+; website received in the last 3 minutes. changing this value will change the number of minutes
+; the widget looks in.
+live_widget_visitor_count_last_minutes = 3
+
+; In "All Websites" dashboard, when looking at today's reports (or a date range including today),
+; the page will automatically refresh every 5 minutes. Set to 0 to disable automatic refresh
+multisites_refresh_after_seconds = 300
+
+; Set to 1 if you're using https on your Piwik server and Piwik can't detect it,
+; e.g., a reverse proxy using https-to-http, or a web server that doesn't
+; set the HTTPS environment variable.
+assume_secure_protocol = 0
+
+; List of proxy headers for client IP addresses
+;
+; CloudFlare (CF-Connecting-IP)
+;proxy_client_headers[] = HTTP_CF_CONNECTING_IP
+;
+; ISP proxy (Client-IP)
+;proxy_client_headers[] = HTTP_CLIENT_IP
+;
+; de facto standard (X-Forwarded-For)
+;proxy_client_headers[] = HTTP_X_FORWARDED_FOR
+
+; List of proxy headers for host IP addresses
+;
+; de facto standard (X-Forwarded-Host)
+;proxy_host_headers[] = HTTP_X_FORWARDED_HOST
+
+; List of proxy IP addresses (or IP address ranges) to skip (if present in the above headers).
+; Generally, only required if there's more than one proxy between the visitor and the backend web server.
+;
+; Examples:
+;proxy_ips[] = 204.93.240.*
+;proxy_ips[] = 204.93.177.0/24
+;proxy_ips[] = 199.27.128.0/21
+;proxy_ips[] = 173.245.48.0/20
+
+; Whether to enable trusted host checking. This can be disabled if you're running Piwik
+; on several URLs and do not wish to constantly edit the trusted host list.
+enable_trusted_host_check = 1
+
+; List of trusted hosts (eg domain or subdomain names) when generating absolute URLs.
+;
+; Examples:
+;trusted_hosts[] = example.com
+;trusted_hosts[] = stats.example.com
+
+; The release server is an essential part of the Piwik infrastructure/ecosystem
+; to provide the latest software version.
+latest_version_url = http://builds.piwik.org/latest.zip
+
+; The API server is an essential part of the Piwik infrastructure/ecosystem to
+; provide services to Piwik installations, e.g., getLatestVersion and
+; subscribeNewsletter.
+api_service_url = http://api.piwik.org
+
+; When the ImageGraph plugin is activated, report metadata have an additional entry : 'imageGraphUrl'.
+; This entry can be used to request a static graph for the requested report.
+; When requesting report metadata with $period=range, Piwik needs to translate it to multiple periods for evolution graphs.
+; eg. $period=range&date=previous10 becomes $period=day&date=previous10. Use this setting to override the $period value.
+graphs_default_period_to_plot_when_period_range = day
+
+; The Overlay plugin shows the Top X following pages, Top X downloads and Top X outlinks which followed
+; a view of the current page. The value X can be set here.
+overlay_following_pages_limit = 300
+
+; With this option, you can disable the framed mode of the Overlay plugin. Use it if your website contains a framebuster.
+overlay_disable_framed_mode = 0
+
+; By default we check whether the Custom logo is writable or not, before we display the Custom logo file uploader
+enable_custom_logo_check = 1
+
+; If php is running in a chroot environment, when trying to import CSV files with createTableFromCSVFile(),
+; Mysql will try to load the chrooted path (which is imcomplete). To prevent an error, here you can specify the
+; absolute path to the chroot environment. eg. '/path/to/piwik/chrooted/'
+absolute_chroot_path =
+
+; In some rare cases it may be useful to explicitely tell Piwik not to use LOAD DATA INFILE
+; This may for example be useful when doing Mysql AWS replication
+enable_load_data_infile = 1
+
+; By setting this option to 0, you can disable the Piwik marketplace. This is useful to prevent giving the Super user
+; the access to disk and install custom PHP code (Piwik plugins).
+enable_marketplace = 1
+
+; By setting this option to 0:
+; - links to Enable/Disable/Uninstall plugins will be hidden and disabled
+; - links to Uninstall themes will be disabled (but user can still enable/disable themes)
+; - as well as disabling plugins admin actions (such as "Upload new plugin"), setting this to 1 will have same effect as setting enable_marketplace=1
+enable_plugins_admin = 1
+
+; By setting this option to 0, you can prevent Super User from editing the Geolocation settings.
+enable_geolocation_admin = 1
+
+; By setting this option to 0, the old log data and old report data features will be hidden from the UI
+; Note: log purging and old data purging still occurs, just the Super User cannot change the settings.
+enable_delete_old_data_settings_admin = 1
+
+; By setting this option to 0, the following settings will be hidden and disabled from being set in the UI:
+; - "Archiving Settings"
+; - "Update settings"
+; - "Email server settings"
+enable_general_settings_admin = 1
+
+; By setting this option to 0, it will disable the "Auto update" feature
+enable_auto_update = 1
+
+; By setting this option to 0, no emails will be sent in case of an available core.
+; If set to 0 it also disables the "sent plugin update emails" feature in general and the related setting in the UI.
+enable_update_communication = 1
+
+[Tracker]
+; Piwik uses first party cookies by default. If set to 1,
+; the visit ID cookie will be set on the Piwik server domain as well
+; this is useful when you want to do cross websites analysis
+use_third_party_id_cookie = 0
+
+; If tracking does not work for you or you are stuck finding an issue, you might want to enable the tracker debug mode.
+; Once enabled (set to 1) messages will be logged to all loggers defined in "[log] log_writers" config.
+debug = 0
+
+; There is a feature in the Tracking API that lets you create new visit at any given time, for example if you know that a different user/customer is using
+; the app then you would want to tell Piwik to create a new visit (even though both users are using the same browser/computer).
+; To prevent abuse and easy creation of fake visits, this feature requires admin token_auth by default
+; If you wish to use this feature using the Javascript tracker, you can set the setting new_visit_api_requires_admin=0, and in Javascript write:
+; _paq.push(['appendToTrackingUrl', 'new_visit=1']);
+new_visit_api_requires_admin = 1
+
+; This setting should only be set to 1 in an intranet setting, where most users have the same configuration (browsers, OS)
+; and the same IP. If left to 0 in this setting, all visitors will be counted as one single visitor.
+trust_visitors_cookies = 0
+
+; name of the cookie used to store the visitor information
+; This is used only if use_third_party_id_cookie = 1
+cookie_name = _pk_uid
+
+; by default, the Piwik tracking cookie expires in 2 years
+; This is used only if use_third_party_id_cookie = 1
+cookie_expire = 63072000
+
+; The path on the server in which the cookie will be available on.
+; Defaults to empty. See spec in http://curl.haxx.se/rfc/cookie_spec.html
+; This is used for the Ignore cookie, and the third party cookie if use_third_party_id_cookie = 1
+cookie_path =
+
+; set to 0 if you want to stop tracking the visitors. Useful if you need to stop all the connections on the DB.
+record_statistics = 1
+
+; length of a visit in seconds. If a visitor comes back on the website visit_standard_length seconds
+; after his last page view, it will be recorded as a new visit
+visit_standard_length = 1800
+
+; The window to look back for a previous visit by this current visitor. Defaults to visit_standard_length.
+; If you are looking for higher accuracy of "returning visitors" metrics, you may set this value to 86400 or more.
+; This is especially useful when you use the Tracking API where tracking Returning Visitors often depends on this setting.
+; The value window_look_back_for_visitor is used only if it is set to greater than visit_standard_length
+window_look_back_for_visitor = 0
+
+; visitors that stay on the website and view only one page will be considered as time on site of 0 second
+default_time_one_page_visit = 0
+
+; if set to 1, Piwik attempts a "best guess" at the visitor's country of
+; origin when the preferred language tag omits region information.
+; The mapping is defined in core/DataFiles/LanguageToCountry.php,
+enable_language_to_country_guess = 1
+
+; When the misc/cron/archive.php cron hasn't been setup, we still need to regularly run some maintenance tasks.
+; Visits to the Tracker will try to trigger Scheduled Tasks (eg. scheduled PDF/HTML reports by email).
+; Scheduled tasks will only run if 'Enable Piwik Archiving from Browser' is enabled in the General Settings.
+; Tasks run once every hour maximum, they might not run every hour if traffic is low.
+; Set to 0 to disable Scheduled tasks completely.
+scheduled_tasks_min_interval = 3600
+
+; name of the cookie to ignore visits
+ignore_visits_cookie_name = piwik_ignore
+
+; Comma separated list of variable names that will be read to define a Campaign name, for example CPC campaign
+; Example: If a visitor first visits 'index.php?piwik_campaign=Adwords-CPC' then it will be counted as a campaign referrer named 'Adwords-CPC'
+; Includes by default the GA style campaign parameters
+campaign_var_name = "pk_campaign,piwik_campaign,utm_campaign,utm_source,utm_medium"
+
+; Comma separated list of variable names that will be read to track a Campaign Keyword
+; Example: If a visitor first visits 'index.php?piwik_campaign=Adwords-CPC&piwik_kwd=My killer keyword' ;
+; then it will be counted as a campaign referrer named 'Adwords-CPC' with the keyword 'My killer keyword'
+; Includes by default the GA style campaign keyword parameter utm_term
+campaign_keyword_var_name = "pk_kwd,piwik_kwd,pk_keyword,utm_term"
+
+; maximum length of a Page Title or a Page URL recorded in the log_action.name table
+page_maximum_length = 1024;
+
+; Tracker cache files are the simple caching layer for Tracking.
+; TTL: Time to live for cache files, in seconds. Default to 5 minutes.
+tracker_cache_file_ttl = 300
+
+; DO NOT USE THIS SETTING ON PUBLICLY AVAILABLE PIWIK SERVER
+; !!! Security risk: if set to 0, it would allow anyone to push data to Piwik with custom dates in the past/future and even with fake IPs!
+; When using the Tracking API, to override either the datetime and/or the visitor IP,
+; token_auth with an "admin" access is required. If you set this setting to 0, the token_auth will not be required anymore.
+; DO NOT USE THIS SETTING ON PUBLIC PIWIK SERVERS
+tracking_requests_require_authentication = 1
+
+[Segments]
+; Reports with segmentation in API requests are processed in real time.
+; On high traffic websites it is recommended to pre-process the data
+; so that the analytics reports are always fast to load.
+; You can define below the list of Segments strings
+; for which all reports should be Archived during the cron execution
+; All segment values MUST be URL encoded.
+;Segments[]="visitorType==new"
+;Segments[]="visitorType==returning"
+
+; If you define Custom Variables for your visitor, for example set the visit type
+;Segments[]="customVariableName1==VisitType;customVariableValue1==Customer"
+
+[Deletelogs]
+; delete_logs_enable - enable (1) or disable (0) delete log feature. Make sure that all archives for the given period have been processed (setup a cronjob!),
+; otherwise you may lose tracking data.
+; delete_logs_schedule_lowest_interval - lowest possible interval between two table deletes (in days, 1|7|30). Default: 7.
+; delete_logs_older_than - delete data older than XX (days). Default: 180
+delete_logs_enable = 0
+delete_logs_schedule_lowest_interval = 7
+delete_logs_older_than = 180
+delete_logs_max_rows_per_query = 100000
+enable_auto_database_size_estimate = 1
+
+[Deletereports]
+delete_reports_enable = 0
+delete_reports_older_than = 12
+delete_reports_keep_basic_metrics = 1
+delete_reports_keep_day_reports = 0
+delete_reports_keep_week_reports = 0
+delete_reports_keep_month_reports = 1
+delete_reports_keep_year_reports = 1
+delete_reports_keep_range_reports = 0
+delete_reports_keep_segment_reports = 0
+
+[mail]
+defaultHostnameIfEmpty = defaultHostnameIfEmpty.example.org ; default Email @hostname, if current host can't be read from system variables
+transport = ; smtp (using the configuration below) or empty (using built-in mail() function)
+port = ; optional; defaults to 25 when security is none or tls; 465 for ssl
+host = ; SMTP server address
+type = ; SMTP Auth type. By default: NONE. For example: LOGIN
+username = ; SMTP username
+password = ; SMTP password
+encryption = ; SMTP transport-layer encryption, either 'ssl', 'tls', or empty (i.e., none).
+
+[proxy]
+type = BASIC ; proxy type for outbound/outgoing connections; currently, only BASIC is supported
+host = ; Proxy host: the host name of your proxy server (mandatory)
+port = ; Proxy port: the port that the proxy server listens to. There is no standard default, but 80, 1080, 3128, and 8080 are popular
+username = ; Proxy username: optional; if specified, password is mandatory
+password = ; Proxy password: optional; if specified, username is mandatory
+
+[Plugins]
+Plugins[] = CorePluginsAdmin
+Plugins[] = CoreAdminHome
+Plugins[] = CoreHome
+Plugins[] = CoreVisualizations
+Plugins[] = Proxy
+Plugins[] = API
+Plugins[] = ExamplePlugin
+Plugins[] = Widgetize
+Plugins[] = Transitions
+Plugins[] = LanguagesManager
+Plugins[] = Actions
+Plugins[] = Dashboard
+Plugins[] = MultiSites
+Plugins[] = Referrers
+Plugins[] = UserSettings
+Plugins[] = Goals
+Plugins[] = SEO
+Plugins[] = Events
+Plugins[] = UserCountry
+Plugins[] = VisitsSummary
+Plugins[] = VisitFrequency
+Plugins[] = VisitTime
+Plugins[] = VisitorInterest
+Plugins[] = ExampleAPI
+Plugins[] = ExampleRssWidget
+Plugins[] = Provider
+Plugins[] = Feedback
+
+Plugins[] = Login
+Plugins[] = UsersManager
+Plugins[] = SitesManager
+Plugins[] = Installation
+Plugins[] = CoreUpdater
+Plugins[] = CoreConsole
+Plugins[] = ScheduledReports
+Plugins[] = UserCountryMap
+Plugins[] = Live
+Plugins[] = CustomVariables
+Plugins[] = PrivacyManager
+Plugins[] = ImageGraph
+Plugins[] = Annotations
+Plugins[] = MobileMessaging
+Plugins[] = Overlay
+Plugins[] = SegmentEditor
+Plugins[] = Insights
+
+Plugins[] = Morpheus
+
+[PluginsInstalled]
+PluginsInstalled[] = Login
+PluginsInstalled[] = CoreAdminHome
+PluginsInstalled[] = UsersManager
+PluginsInstalled[] = SitesManager
+PluginsInstalled[] = Installation
+
+[Plugins_Tracker]
+Plugins_Tracker[] = Provider
+Plugins_Tracker[] = Goals
+Plugins_Tracker[] = PrivacyManager
+Plugins_Tracker[] = UserCountry
+Plugins_Tracker[] = Login
+
+[APISettings]
+; Any key/value pair can be added in this section, they will be available via the REST call
+; index.php?module=API&method=API.getSettings
+; This can be used to expose values from Piwik, to control for example a Mobile app tracking
+SDK_batch_size = 10
+SDK_interval_value = 30
+
+; NOTE: do not directly edit this file! See notice at the top
+
diff --git a/www/analytics/config/manifest.inc.php b/www/analytics/config/manifest.inc.php
new file mode 100644
index 00000000..2a4eada2
--- /dev/null
+++ b/www/analytics/config/manifest.inc.php
@@ -0,0 +1,3200 @@
+ array("890", "69246584e6b57bfbc8d39799cd3b9213"),
+ "composer.lock" => array("10488", "8a98b2fd7cda2b841fe15aa1e72402a5"),
+ "config/global.ini.php" => array("28859", "5d3e559652bf2ebd390c2a4c0054b02b"),
+ "console" => array("924", "a4877c66060ee26f1edc476a5ee72776"),
+ "core/Access.php" => array("12704", "c9839d3aa44e8edc82acfbf1c2e9a4aa"),
+ "core/API/DataTableGenericFilter.php" => array("5060", "ab1da3c6e3e965a56f5d1274b4703a69"),
+ "core/API/DataTableManipulator/Flattener.php" => array("4096", "1ee3893fee29909ea33f6e4b767c99d5"),
+ "core/API/DataTableManipulator/LabelFilter.php" => array("5127", "92fffb40df331cd852a222cc482ea273"),
+ "core/API/DataTableManipulator.php" => array("6316", "45d5aabe63f1d50766fb036dfd9e5aa1"),
+ "core/API/DataTableManipulator/ReportTotalsCalculator.php" => array("7459", "dc3674946b59f691b4da26775ab575a9"),
+ "core/API/DocumentationGenerator.php" => array("10290", "2a0b1f027ca702048e7af16e238f89fa"),
+ "core/API/Proxy.php" => array("20309", "b447ae17730f347efd4c8407e2172c85"),
+ "core/API/Request.php" => array("14637", "3373eb0d7cf984a910a18e0335c1576a"),
+ "core/API/ResponseBuilder.php" => array("17712", "5b666de76bebe3f4163d106da1ef1610"),
+ "core/Archive/DataCollection.php" => array("11407", "64bcbd03c02f25cc0548ec9960459830"),
+ "core/Archive/DataTableFactory.php" => array("13364", "3fc26be6b543d69fcf5a5e24b071c89c"),
+ "core/Archive/Parameters.php" => array("1333", "4d8b5cee96ed8db3b9accb37e3be80b8"),
+ "core/Archive.php" => array("33032", "9102dc388920e1ce6962103d3afc5e01"),
+ "core/ArchiveProcessor/Loader.php" => array("6875", "d6dea12a41b609b1fb1a2cc52dc8ae8a"),
+ "core/ArchiveProcessor/Parameters.php" => array("4312", "fe51534b7e53509e371481a13295cbcd"),
+ "core/ArchiveProcessor.php" => array("18191", "e1266497acaac1c9f772965a123d832d"),
+ "core/ArchiveProcessor/PluginsArchiver.php" => array("5983", "aa8b4c422def253126cec71a6a7f4e26"),
+ "core/ArchiveProcessor/Rules.php" => array("11465", "6133c6cccad15b54b92e22b46ede06c9"),
+ "core/AssetManager.php" => array("11423", "97e469ef84fdd26bd050169ab869e8b3"),
+ "core/AssetManager/UIAssetCacheBuster.php" => array("1508", "4d376f8e099aaea9a2341528bf1db023"),
+ "core/AssetManager/UIAssetCatalog.php" => array("1340", "a225abb9e87ad2ee661f81f855d44872"),
+ "core/AssetManager/UIAssetCatalogSorter.php" => array("1512", "c6989c08fb3124904ced569774468488"),
+ "core/AssetManager/UIAssetFetcher/JScriptUIAssetFetcher.php" => array("2772", "8aa332089d60c0362bfabe121020448c"),
+ "core/AssetManager/UIAssetFetcher.php" => array("2247", "fb0cd6454f394c71adcbb3c19d897dfd"),
+ "core/AssetManager/UIAssetFetcher/StaticUIAssetFetcher.php" => array("729", "736a364eb1c3f2bef8d420364a17c6f4"),
+ "core/AssetManager/UIAssetFetcher/StylesheetUIAssetFetcher.php" => array("1915", "5476bd9d96ec0effeed0bd4696dadae6"),
+ "core/AssetManager/UIAsset/InMemoryUIAsset.php" => array("1090", "f26f85ec2046ec9d767f7d69434247a8"),
+ "core/AssetManager/UIAssetMerger/JScriptUIAssetMerger.php" => array("2466", "9098ce95776b28e6d48fab8dab09ea74"),
+ "core/AssetManager/UIAssetMerger.php" => array("4549", "9ac6a5d8b409076548b0dcc277985e29"),
+ "core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php" => array("4254", "d85777f9b6a2fffefbe3d329e450cc79"),
+ "core/AssetManager/UIAssetMinifier.php" => array("1727", "28d43428fd6ae8b1460c3e444e269b29"),
+ "core/AssetManager/UIAsset/OnDiskUIAsset.php" => array("2540", "f70f89fd35d15983aae8681e1facea6b"),
+ "core/AssetManager/UIAsset.php" => array("1198", "073b3a89bd5737ca6176b597ef16eea5"),
+ "core/Auth.php" => array("2915", "aeb757345dec1077cd0ba75774356962"),
+ "core/CacheFile.php" => array("5665", "985342d2fdae584019a7f4c1e955976a"),
+ "core/CliMulti/Output.php" => array("1063", "d00891b635aaf4d1dc2120ee39b5c5a6"),
+ "core/CliMulti.php" => array("8349", "265403a19c082fc4b0b4c74959a9208b"),
+ "core/CliMulti/Process.php" => array("4681", "3a6faa96f191b8d405f6a4972abd13b3"),
+ "core/CliMulti/RequestCommand.php" => array("2249", "c224d55ef5d6a3a55295cf35259feb96"),
+ "core/Common.php" => array("35753", "a8c976da84bc9ff1c54918992725eadc"),
+ "core/Config.php" => array("22997", "147ab7aa770b1fa5423e46871bc0278a"),
+ "core/Console.php" => array("4872", "c594ab40478f30fa294ea96484389439"),
+ "core/Cookie.php" => array("11300", "9f160dcf724d3f0f30ac6c50556ca88d"),
+ "core/CronArchive/FixedSiteIds.php" => array("1305", "7b0307e871cc50d74bbd00e8e26cccbd"),
+ "core/CronArchive.php" => array("47570", "18a2222278c77c7f4fba542005fa3f17"),
+ "core/CronArchive/SharedSiteIds.php" => array("4596", "0bc0bd027d24d9cb11f8cc99f73023bc"),
+ "core/DataAccess/ArchiveSelector.php" => array("14351", "98eaf72ed205b6261ca663d93a6693fe"),
+ "core/DataAccess/ArchiveTableCreator.php" => array("3320", "455700705f6f6c784f97dc2c84be1f7e"),
+ "core/DataAccess/ArchiveWriter.php" => array("9384", "3e3235f3a37fdd1e6a78e5624048dbf3"),
+ "core/DataAccess/LogAggregator.php" => array("41234", "7f733c60c036d166c31cea37567791e9"),
+ "core/DataArray.php" => array("16587", "ca482fd7e31e0f3479387c80ae746e88"),
+ "core/DataFiles/Countries.php" => array("7867", "32e4468ba434ffd83bc07d70ad7f4969"),
+ "core/DataFiles/Currencies.php" => array("8648", "4edf0cb07ef189624b4d14d682183d7a"),
+ "core/DataFiles/Languages.php" => array("7317", "7cbf38c6feae3f1dea58bc3d74a6e6a4"),
+ "core/DataFiles/LanguageToCountry.php" => array("2520", "d2de6f0a9b23560bc52c7b1726f1e5c7"),
+ "core/DataFiles/Providers.php" => array("1792", "bbb4dec4a616cd73ae6566feadc24154"),
+ "core/DataFiles/SearchEngines.php" => array("45747", "c4f37ecde5286617e51a445300502034"),
+ "core/DataFiles/Socials.php" => array("5467", "ff231864e2c740a1cac4c70311c09756"),
+ "core/DataTable/BaseFilter.php" => array("2042", "fce60bb7fa6dc0fe4f9ac0cc5cb0de69"),
+ "core/DataTable/Bridges.php" => array("562", "d0d252681214102d0f10a277bbddad57"),
+ "core/DataTable/DataTableInterface.php" => array("857", "19babd1eb37f5541bdd436b2eadbca5e"),
+ "core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php" => array("10060", "19245f7364829705ed645129187a26be"),
+ "core/DataTable/Filter/AddColumnsProcessedMetrics.php" => array("5444", "6cab57109eb9db0400a5d747bf88539e"),
+ "core/DataTable/Filter/AddSummaryRow.php" => array("1380", "072952c5aebbc41e28a032f3766b3e71"),
+ "core/DataTable/Filter/BeautifyRangeLabels.php" => array("6074", "ea93fc021a0a3f19c9a83111e63279d3"),
+ "core/DataTable/Filter/BeautifyTimeRangeLabels.php" => array("4548", "bcc614caf8c17085b9ac4b12e15701cd"),
+ "core/DataTable/Filter/CalculateEvolutionFilter.php" => array("5891", "fe1b274e742a054dbc701d905a5087f1"),
+ "core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php" => array("1012", "2132297b4a7073faf44f38eec883eb58"),
+ "core/DataTable/Filter/ColumnCallbackAddColumn.php" => array("2845", "ceb1d9be8b76d9869585dfe107c4a45a"),
+ "core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php" => array("5056", "6e5056fd84b5745733e65c628609e184"),
+ "core/DataTable/Filter/ColumnCallbackAddMetadata.php" => array("3021", "29eea90b9dbb6b5c3ce730bd5eb8ef29"),
+ "core/DataTable/Filter/ColumnCallbackDeleteRow.php" => array("2460", "8bf70a091da4f4e014ae393fbc999bc2"),
+ "core/DataTable/Filter/ColumnCallbackReplace.php" => array("4249", "fec351b71143e9298255c6e86a12ca6c"),
+ "core/DataTable/Filter/ColumnDelete.php" => array("5062", "dc7f5cd71a488bbc996fd38f1bf0bb30"),
+ "core/DataTable/Filter/ExcludeLowPopulation.php" => array("3379", "9847cc4a48fc0549546b51427f223457"),
+ "core/DataTable/Filter/GroupBy.php" => array("3472", "551e742f8d0c5e64e89b0660969fa6f5"),
+ "core/DataTable/Filter/Limit.php" => array("1892", "276f4352a6918be4c549d008d406c7c0"),
+ "core/DataTable/Filter/MetadataCallbackAddMetadata.php" => array("2651", "e0576cdfa84a99765f6e88960bb8e422"),
+ "core/DataTable/Filter/MetadataCallbackReplace.php" => array("2346", "be71bc6d2a6aca70a04404b42a1085f0"),
+ "core/DataTable/Filter/Pattern.php" => array("2933", "08e09a9da40a39a12d262fbc51d2753d"),
+ "core/DataTable/Filter/PatternRecursive.php" => array("2743", "12abb36c7bde6d4cf981f9d9a0684c9c"),
+ "core/DataTable/Filter/RangeCheck.php" => array("1607", "091a7675c8217cb03448b129b8daca5e"),
+ "core/DataTable/Filter/ReplaceColumnNames.php" => array("5749", "ff15fca13dcd7b5ed2f669f583233dc9"),
+ "core/DataTable/Filter/ReplaceSummaryRowLabel.php" => array("2089", "ada7b250bd2ea1079d3b10387763627f"),
+ "core/DataTable/Filter/SafeDecodeLabel.php" => array("1832", "4e46025ff3ae6b1fb1b8fae2db421785"),
+ "core/DataTable/Filter/Sort.php" => array("6432", "424750b4a5d7f94f4896c4705837cc53"),
+ "core/DataTable/Filter/Truncate.php" => array("4175", "7ea93e0bd0abd847c0e919207d1a1320"),
+ "core/DataTable/Manager.php" => array("4400", "c95164db72086a7c6479257aeef0d173"),
+ "core/DataTable/Map.php" => array("13011", "8dfdbc7dfe0f7992a83977005f0e2324"),
+ "core/DataTable.php" => array("57572", "d509d1932005f8dfb00c3a9f03ea78c9"),
+ "core/DataTable/Renderer/Console.php" => array("5028", "4b1b777fec6b5ae5ccd9c9ec2d523a0a"),
+ "core/DataTable/Renderer/Csv.php" => array("12195", "d127b1ac63e24895389b17cb43072752"),
+ "core/DataTable/Renderer/Html.php" => array("5839", "559f0fc629844984a9adf5edeb0b1507"),
+ "core/DataTable/Renderer/Json.php" => array("3671", "e4b3138a405fc1e129185150cee5b8ce"),
+ "core/DataTable/Renderer.php" => array("13396", "cfad13e54038358b5776d4cda01e98ee"),
+ "core/DataTable/Renderer/Php.php" => array("7732", "c603669a6b9945e711ba68b59c8b737d"),
+ "core/DataTable/Renderer/Rss.php" => array("6068", "fbb296b297c6d3ad4705196c587f76db"),
+ "core/DataTable/Renderer/Tsv.php" => array("701", "772cf9af832f601154357535a62a9d32"),
+ "core/DataTable/Renderer/Xml.php" => array("15715", "37c3a6d8f2b5965c720e2b6abca0c0b0"),
+ "core/DataTable/Row/DataTableSummaryRow.php" => array("1903", "8a25ba9516a43929261f157023402b49"),
+ "core/DataTable/Row.php" => array("21822", "91d9896e5123c829035a1011c524dd3a"),
+ "core/DataTable/Simple.php" => array("967", "5e662c2478307b1a9ca6b69294b99c0d"),
+ "core/DataTable/TableNotFoundException.php" => array("234", "74c2ecb43569238eb30d3f91a21941d8"),
+ "core/Date.php" => array("22031", "9a8a5696c6595e2ea04d5e5b2f17a61d"),
+ "core/Db/AdapterInterface.php" => array("1372", "e61f5273b376e6698f3a0a83e15896a6"),
+ "core/Db/Adapter/Mysqli.php" => array("4455", "8aef57991aae0aef6210a958569c2415"),
+ "core/Db/Adapter/Pdo/Mssql.php" => array("7582", "f5bbeeb60469cd98048933056286bcec"),
+ "core/Db/Adapter/Pdo/Mysql.php" => array("6172", "a1c2f38bf52784583abd6dbc7af639fe"),
+ "core/Db/Adapter/Pdo/Pgsql.php" => array("4769", "decde138f1566ea834a159b8694433f1"),
+ "core/Db/Adapter.php" => array("2807", "87a49f2522ddbec917fb1d7ec1456708"),
+ "core/Db/BatchInsert.php" => array("9296", "33039a060eaff7d7abaff65eaaa1c93b"),
+ "core/DbHelper.php" => array("3822", "a2b18b7df87431bcc7d9740557150412"),
+ "core/Db.php" => array("24578", "9f2e812e7a76db407a59a9f7495d1e14"),
+ "core/Db/SchemaInterface.php" => array("2112", "f55d75b05af2cb7de8133942faef36ca"),
+ "core/Db/Schema/Mysql.php" => array("19642", "9fe1d3d20a5c3a824fa13d7ae7e8da7c"),
+ "core/Db/Schema.php" => array("5616", "622f40c2f6a7a49e333f7309bb7e7111"),
+ "core/dispatch.php" => array("955", "6a9a4c5f99af08ed0048f8b2de1886ec"),
+ "core/Error.php" => array("7210", "5153b7ae689b464cce8b721ddb361d28"),
+ "core/EventDispatcher.php" => array("6188", "787ae06fb148af48dacf6fe2ab768586"),
+ "core/ExceptionHandler.php" => array("2053", "86c9c27bd7868af403d53c8a0b143fff"),
+ "core/Filechecks.php" => array("8786", "576db7c700047fbe3de33abd3a3e6dd3"),
+ "core/Filesystem.php" => array("10596", "fefaf74a3736cb39ee144e22a77a30ac"),
+ "core/FrontController.php" => array("20372", "3acd41cc80fdf61723cf0ec8f94c60b0"),
+ "core/Http.php" => array("29559", "4319b7bb5789c60dfb67b91f2c09520e"),
+ "core/IP.php" => array("14032", "c418038633d69c9c6acb2f512b08ae14"),
+ "core/Loader.php" => array("3251", "8a3541845131b8a209b60057b1a97568"),
+ "core/Log.php" => array("22487", "ae607b359689ee4539d1acade5dfe0bd"),
+ "core/Mail.php" => array("3051", "dd77f65726ccb1fb35125bc446fc7e9e"),
+ "core/Menu/MenuAbstract.php" => array("8516", "2380174f7029dad633a52319be1af913"),
+ "core/Menu/MenuAdmin.php" => array("3752", "d52a4c7296cb935b74122b42a802d8ab"),
+ "core/Menu/MenuMain.php" => array("2644", "d74adf1afca5dc8fc72e80b93b99a5c4"),
+ "core/Menu/MenuTop.php" => array("4237", "62983c9f66e4caf4812af80c9407b348"),
+ "core/MetricsFormatter.php" => array("8530", "aa60c76868df55960a3235d7e2f4b37f"),
+ "core/Metrics.php" => array("14842", "b1dbee19d075b83a3080a99909b0b380"),
+ "core/Nonce.php" => array("5260", "6908ea4c50150171fd6eed821a916a14"),
+ "core/Notification/Manager.php" => array("3903", "a23c790d484dc3ac76e19947efb6fd54"),
+ "core/Notification.php" => array("5732", "1cbe56fe6665c7206ca88739aad8832f"),
+ "core/Option.php" => array("6684", "3b110408402a646d6c7861dfd3eaed4e"),
+ "core/Period/Day.php" => array("2058", "dd8cc0376ce9235221bbc25c8730bc41"),
+ "core/Period/Month.php" => array("1630", "974b6b2604e1f9ef2a928ab4eaab58b7"),
+ "core/Period.php" => array("9748", "def7194a46591d12a1b66eb93a97a9cb"),
+ "core/Period/Range.php" => array("15936", "30c69c5eacb62cc6c173e58b95ca96dc"),
+ "core/Period/Week.php" => array("2544", "663151eb94b86e7a30404e49146088c2"),
+ "core/Period/Year.php" => array("1869", "da04fd59cdd94516ecb842c8ea00e5f2"),
+ "core/Piwik.php" => array("26583", "e8e87eb48bd0df5b155c9649e55cd457"),
+ "core/Plugin/API.php" => array("1108", "ee9a5d269cf33cde142c8c438322ace9"),
+ "core/Plugin/Archiver.php" => array("4319", "bd0c156de5ef58c75c3bb68cc79bc6f5"),
+ "core/Plugin/ConsoleCommand.php" => array("1805", "f1850b6f0d989d47b369afe18fb261f2"),
+ "core/Plugin/ControllerAdmin.php" => array("7991", "fe4081f2d6d58c5587b1b462afe20b0a"),
+ "core/Plugin/Controller.php" => array("38680", "7fcbba7a289fb5b1908046e7ef1a35ff"),
+ "core/Plugin/Dependency.php" => array("2890", "aaa67fe7929ebd4ca30a19a9635a94b8"),
+ "core/Plugin/Manager.php" => array("37639", "21ba8ef6db04c3800ea9074dafd4c7fe"),
+ "core/Plugin/MetadataLoader.php" => array("2553", "22ce1fad1069035d6692129b65cec244"),
+ "core/Plugin.php" => array("10373", "715f226ecb91745815cac730585df8d5"),
+ "core/Plugin/Settings.php" => array("12013", "ddb5052a732359804337c4eadee99bb4"),
+ "core/Plugin/ViewDataTable.php" => array("15746", "d6e134e0ae998383ea20d981106400a8"),
+ "core/Plugin/Visualization.php" => array("22404", "e0313135a50bbf1cd236a4de20606f81"),
+ "core/Profiler.php" => array("9578", "fb3a2b8d4abe4e848976ea842560f064"),
+ "core/ProxyHeaders.php" => array("2329", "2c2040497ebd44a257a7813b873ce8ae"),
+ "core/ProxyHttp.php" => array("9293", "a66d7477c1715daa43bfaf03848bc2d1"),
+ "core/QuickForm2.php" => array("3991", "38181ddffae0ddb03dded14f44ae8233"),
+ "core/RankingQuery.php" => array("12775", "86664cae23f6c9669606d1189d33d4af"),
+ "core/Registry.php" => array("1104", "5f93fbe06640eef9980ada45677c6ebe"),
+ "core/ReportRenderer/Csv.php" => array("3977", "5ec7fe33c5a71d6a0044446d2dc823bf"),
+ "core/ReportRenderer/Html.php" => array("5797", "b46530e59c537058a1cd5bebed7d5d9c"),
+ "core/ReportRenderer/Pdf.php" => array("20769", "1ace48a18c949c67516c3124db35cad7"),
+ "core/ReportRenderer.php" => array("7959", "bf5443a70c2f0c83ee4649139b3d25a2"),
+ "core/ScheduledTask.php" => array("5696", "83c2633ccf1c6d970827bb43bbe59578"),
+ "core/ScheduledTaskTimetable.php" => array("3398", "166f58a5c50c282c7c29d6f644ccc58a"),
+ "core/ScheduledTime/Daily.php" => array("1233", "d022648a5088a797a26348dc450fd774"),
+ "core/ScheduledTime/Hourly.php" => array("1300", "05f17149dbc4a4fbe3763097abc2526a"),
+ "core/ScheduledTime/Monthly.php" => array("4052", "8badd753f6c06f07ecac69f5e91bf16c"),
+ "core/ScheduledTime.php" => array("7625", "ab30399dcfca1ba1f3aa2d89c67e3cc8"),
+ "core/ScheduledTime/Weekly.php" => array("2032", "8de6f993989ee66ea786ad1ddc76baab"),
+ "core/SegmentExpression.php" => array("12147", "1c989bf36472614b2be51beef8f59eea"),
+ "core/Segment.php" => array("16496", "4a4795937a2c7726150e5d5d797a1f4c"),
+ "core/Session.php" => array("5408", "9c0b50a50516a2078ee3e043e5c5cdf8"),
+ "core/Session/SaveHandler/DbTable.php" => array("3400", "231cf14b800bbfbe688ced3fba0c743c"),
+ "core/Session/SessionNamespace.php" => array("656", "43fd5d53fdb1a60b41e93be8c8f80b44"),
+ "core/Settings/Manager.php" => array("4028", "42535abca90e8eb964a46c7289dde797"),
+ "core/SettingsPiwik.php" => array("12885", "a07fe55e36ea565d9bdb38c384345ce5"),
+ "core/SettingsServer.php" => array("6313", "2785b33d06035d586e892f2868ff57d0"),
+ "core/Settings/Setting.php" => array("6833", "82707639f441064423be496ecb229ede"),
+ "core/Settings/StorageInterface.php" => array("1615", "eacd97cea047d06e137d569e15af1179"),
+ "core/Settings/SystemSetting.php" => array("984", "821339fb538bc6fe35f6cc2ede502e8b"),
+ "core/Settings/UserSetting.php" => array("3449", "79cc353b6019a245fef3f0bacc6ec7e6"),
+ "core/Singleton.php" => array("1353", "ee0398b6ffb82242e498de630f73c091"),
+ "core/Site.php" => array("15122", "9ce12ed8d49fb31fbabf3380052cd199"),
+ "core/TaskScheduler.php" => array("6626", "4850a933550e4570430b73512cceab7a"),
+ "core/TCPDF.php" => array("1971", "a94bb257665eefecceadf7f753d7fc77"),
+ "core/testMinimumPhpVersion.php" => array("7398", "deada667cb4971e1296074cdc780d07f"),
+ "core/Theme.php" => array("4541", "d4de1d79c1cd622603ca70e5f0af91ae"),
+ "core/Timer.php" => array("1808", "39acf463b724b25bcce2583d7292d87f"),
+ "core/Tracker/ActionClickUrl.php" => array("1742", "61c02b31e21b6f9a401eb657fee4364e"),
+ "core/Tracker/ActionEvent.php" => array("2265", "336607e4d024a5f08c3be764f33c2460"),
+ "core/Tracker/ActionPageview.php" => array("2013", "366739709f90aa8d232511834f11414b"),
+ "core/Tracker/Action.php" => array("9811", "2f4aa70d96316e21d62aa56ce1cac5fb"),
+ "core/Tracker/ActionSiteSearch.php" => array("9254", "1b0f81d05b783b2bca998cd896e1ab8f"),
+ "core/Tracker/Cache.php" => array("6289", "94a28a7608f12b3e8acffe67c7acc211"),
+ "core/Tracker/Db/DbException.php" => array("271", "e705dd83b4a7498aa4f37ba24f7a82ca"),
+ "core/Tracker/Db/Mysqli.php" => array("7571", "63642f63c2364d44188c65e543d44a34"),
+ "core/Tracker/Db/Pdo/Mysql.php" => array("6810", "32af933dd0219d7f17234d61b6222f75"),
+ "core/Tracker/Db/Pdo/Pgsql.php" => array("3204", "d76f5782916317fa5d560baddf554877"),
+ "core/Tracker/Db.php" => array("6120", "f2fda7c7dfc83c28c5ab585ed47e26f0"),
+ "core/Tracker/GoalManager.php" => array("35981", "be9605a31afa46221ad36eb0a1dcb249"),
+ "core/Tracker/IgnoreCookie.php" => array("1762", "9c438161c21ee28ca5fceeb67d0884f8"),
+ "core/Tracker/PageUrl.php" => array("11076", "3c36bc8fdc8fcaf564f25a97ad1c766f"),
+ "core/Tracker.php" => array("28846", "9edc0aec5accc5d1f6c3a1bdea7fced1"),
+ "core/Tracker/Referrer.php" => array("11012", "0697bfd2c20a646fc6de922a7ec6e153"),
+ "core/Tracker/Request.php" => array("18612", "8796d52a20094467b8f3cb46b24ee4c6"),
+ "core/Tracker/TableLogAction.php" => array("9173", "0688a6b89a8eef0943bbb051dd5464b8"),
+ "core/Tracker/VisitExcluded.php" => array("8453", "d9179d7244a794b2a9fc471e53e0a96b"),
+ "core/Tracker/VisitInterface.php" => array("591", "44e61d7caeb0701b0a0cbd4351b8a4d5"),
+ "core/Tracker/VisitorNotFoundInDb.php" => array("237", "bb64791a33dfee4d64aff1f8495302ec"),
+ "core/Tracker/Visit.php" => array("42168", "193887df85ce090bcfb9f50bcf326422"),
+ "core/Translate/Filter/ByBaseTranslations.php" => array("1711", "0592c1a63f43e15c49b6f9579e80b0e2"),
+ "core/Translate/Filter/ByParameterCount.php" => array("2399", "1b054c3111be6abbbb393efcd80cdecf"),
+ "core/Translate/Filter/EmptyTranslations.php" => array("1140", "21a8ee941ba93b652f22043424ef0ca6"),
+ "core/Translate/Filter/EncodedEntities.php" => array("1028", "0fcf7b3c769742621beef97887b6f968"),
+ "core/Translate/Filter/FilterAbstract.php" => array("651", "385a5f448f1f38e14570a3e462c477b9"),
+ "core/Translate/Filter/UnnecassaryWhitespaces.php" => array("2408", "502ac2955cf7be7f655ddd5bc0c867b1"),
+ "core/Translate.php" => array("6857", "b18b68afe4900f9a9b42df364e4370ae"),
+ "core/Translate/Validate/CoreTranslations.php" => array("3269", "42f30ed563dbac7855e1558194bb28f6"),
+ "core/Translate/Validate/NoScripts.php" => array("1023", "d1db00f141ac004f0cb195a44d327468"),
+ "core/Translate/Validate/ValidateAbstract.php" => array("685", "5af8be0c67032bb6405c3dc6dd36a88f"),
+ "core/Translate/Writer.php" => array("9383", "2ffc837feb2e9e3c3b27f03a0d0cb931"),
+ "core/Twig.php" => array("10465", "f2cdbdb455ea6537b221905d9823d7bb"),
+ "core/Unzip/Gzip.php" => array("1588", "c37fe684918ef45cf5fceba0d90f2db8"),
+ "core/Unzip/PclZip.php" => array("2203", "a9b66ff795a00b9f9489231c40f00f38"),
+ "core/Unzip.php" => array("1246", "962c3818d46540dba5d0bdb1ff99d747"),
+ "core/Unzip/Tar.php" => array("1991", "aca63f78845e3a94b2c7c4afc65e0fa1"),
+ "core/Unzip/UncompressInterface.php" => array("789", "914626f0c5ff3750f2db9b618146a9fa"),
+ "core/Unzip/ZipArchive.php" => array("4446", "aa086fbaa27dc9653c49019022e912dc"),
+ "core/UpdateCheck.php" => array("3554", "78229bc91a7ab4d029e2c1a8f1285f11"),
+ "core/Updater.php" => array("11899", "1f59e2ee92b7cec575344d2c51fa4eae"),
+ "core/Updates/0.2.10.php" => array("2651", "876e9fe07b71a4fe9d76fd8fd174f0a6"),
+ "core/Updates/0.2.12.php" => array("932", "59f8221c661ccf08e94b5bf923d67c51"),
+ "core/Updates/0.2.13.php" => array("786", "349c538337653cb7c1b61603001ead79"),
+ "core/Updates/0.2.24.php" => array("978", "db0f1757f204103520d52a369d305406"),
+ "core/Updates/0.2.27.php" => array("2791", "4947fca28656acd2e1343514c7203ec3"),
+ "core/Updates/0.2.32.php" => array("1092", "0962b3c0781f2621e15825facc5f5ed8"),
+ "core/Updates/0.2.33.php" => array("1185", "a9d96ddf9036a5dd819da808ed875d68"),
+ "core/Updates/0.2.34.php" => array("592", "98ae3b028633eba1791d9b92c794d760"),
+ "core/Updates/0.2.35.php" => array("587", "a5c358cd52da528bcf9becb54172df8c"),
+ "core/Updates/0.2.37.php" => array("642", "ec8400f457c7a945713fe252947a9436"),
+ "core/Updates/0.4.1.php" => array("806", "e223d3c70ea7e3c1d804489a91853c7b"),
+ "core/Updates/0.4.2.php" => array("1156", "d37d6ddf7068db07377601fa6595ffb7"),
+ "core/Updates/0.4.4.php" => array("660", "c72894136443d613abb674baa7c11338"),
+ "core/Updates/0.4.php" => array("1167", "98af006ec25c0c0140e7f5ad9135794d"),
+ "core/Updates/0.5.4.php" => array("1958", "779d5f3a0d4d326f9e2f74d1c8cd128e"),
+ "core/Updates/0.5.5.php" => array("1293", "7f0033047ccc97f6d66d30390402070a"),
+ "core/Updates/0.5.php" => array("2096", "959cd8b36d92f8ef20ebc1ad78b1b28f"),
+ "core/Updates/0.6.2.php" => array("1126", "ac49a76d42ed5be509d3c7a40276ea6d"),
+ "core/Updates/0.6.3.php" => array("1279", "d2fb0180705d57144e3652f69ec3b166"),
+ "core/Updates/0.6-rc1.php" => array("4507", "9634cef986b658716b06e3a62c066adb"),
+ "core/Updates/0.7.php" => array("594", "6210a8eeb517c83d6d839e6b1864c13f"),
+ "core/Updates/0.9.1.php" => array("1454", "1c5185e07ca0bb2f08c93a86c65ed4b4"),
+ "core/Updates/1.10.1.php" => array("515", "9c1f18c6bed150d9088e4ffbd6ae85e0"),
+ "core/Updates/1.10.2-b1.php" => array("660", "6b8e21cc1f8082b01858940dee50fb92"),
+ "core/Updates/1.10.2-b2.php" => array("674", "64b57788df79cbc9ea1180b2ca2f4ced"),
+ "core/Updates/1.10-b4.php" => array("524", "da5f285f2e7de1a17ef8d39df8778fbf"),
+ "core/Updates/1.11-b1.php" => array("523", "e230d3911722fd98b6cbf349d34c664d"),
+ "core/Updates/1.12-b15.php" => array("452", "31134ebb6971b1459ca2f876d4892f22"),
+ "core/Updates/1.12-b16.php" => array("650", "7e4186377a518ddb166b1d55b8d25bac"),
+ "core/Updates/1.12-b1.php" => array("668", "bb6566428c9bf4382ff58cba3bc90091"),
+ "core/Updates/1.1.php" => array("971", "270bf13926c63efc04e6157d1e351ef8"),
+ "core/Updates/1.2.3.php" => array("1077", "f667e11902edc92fd98b335c236e2afe"),
+ "core/Updates/1.2.5-rc1.php" => array("864", "a36b86d356ce093a63177efcc56a8d3c"),
+ "core/Updates/1.2.5-rc7.php" => array("604", "8db19752e89143fe991cd9529d8be11d"),
+ "core/Updates/1.2-rc1.php" => array("6822", "346d520a049c2426fb2442e7f29347c3"),
+ "core/Updates/1.2-rc2.php" => array("434", "c92ff71c25dfb7eccec537abf745e1e1"),
+ "core/Updates/1.4-rc1.php" => array("805", "0691f7982446f93ddcc1047291c86001"),
+ "core/Updates/1.4-rc2.php" => array("1639", "c9f5619ad1c8a92545c73d548e44b58a"),
+ "core/Updates/1.5-b1.php" => array("2262", "ba785cbad196b247571781cadada94b6"),
+ "core/Updates/1.5-b2.php" => array("1087", "5f5e77f985c277d714b7281d7f965069"),
+ "core/Updates/1.5-b3.php" => array("2805", "3b4e6761ecdf28343cb3184223525f75"),
+ "core/Updates/1.5-b4.php" => array("571", "ad302b8cb925c0c10a417456fb502b07"),
+ "core/Updates/1.5-b5.php" => array("700", "96c669e08b45e60525fe1bb1fb943c9d"),
+ "core/Updates/1.5-rc6.php" => array("433", "db7f6f03adbb92928cb439c53d9d9ef9"),
+ "core/Updates/1.6-b1.php" => array("3151", "e5544d116d44ab8538b92eeeca4f3c58"),
+ "core/Updates/1.6-rc1.php" => array("429", "abc0632476540e4e8ef7e8a0c5d311f9"),
+ "core/Updates/1.7.2-rc5.php" => array("675", "c204aae2d7f7689d37f4091c37524268"),
+ "core/Updates/1.7.2-rc7.php" => array("1310", "111c0d0cb4f19766ff8e7635c387eb48"),
+ "core/Updates/1.7-b1.php" => array("801", "7d0bf59a66243ae5adb41e01b584a30a"),
+ "core/Updates/1.8.3-b1.php" => array("4214", "d2dab6e537e059c3f42be3b3fd00c1d2"),
+ "core/Updates/1.8.4-b1.php" => array("5602", "f6e09c6abb389ebb423530214fcb3732"),
+ "core/Updates/1.9.1-b2.php" => array("756", "926f80df9c4e1874aefe77d59a6ec56e"),
+ "core/Updates/1.9.3-b10.php" => array("522", "4b239e1966e6183b52fe8a56887e0480"),
+ "core/Updates/1.9.3-b3.php" => array("687", "effe8f40e5733e5a0244421735944f63"),
+ "core/Updates/1.9.3-b8.php" => array("730", "34ccb3d0357dbcd9a0d9df4030fdbf36"),
+ "core/Updates/1.9-b16.php" => array("1493", "07e70dd30d645aada68bfd71e65e9e49"),
+ "core/Updates/1.9-b19.php" => array("976", "a1f50808b7f7339bec7bb31c7423b120"),
+ "core/Updates/1.9-b9.php" => array("1440", "67363fcdc79574bda86993f3cea59405"),
+ "core/Updates/2.0.3-b7.php" => array("1816", "80902356a3532433940567151aaa5a82"),
+ "core/Updates/2.0.4-b5.php" => array("2589", "95f1d30f619a50ad99ad21d52713c949"),
+ "core/Updates/2.0.4-b7.php" => array("1773", "1bb19c5e4277994b1f414647def74cd6"),
+ "core/Updates/2.0.4-b8.php" => array("2124", "91e5b1108076279e5a3c8b0764887573"),
+ "core/Updates/2.0-a12.php" => array("1221", "c96c6665f2f05c26527968bb9391b01e"),
+ "core/Updates/2.0-a13.php" => array("2361", "bcc1c2d24b5e72bace075ac8075918fb"),
+ "core/Updates/2.0-a17.php" => array("1015", "c5d6321d2bbb8fd436c733a4213e3513"),
+ "core/Updates/2.0-a7.php" => array("872", "72f65ce2241de0e53e02e8f9a6911709"),
+ "core/Updates/2.0-b10.php" => array("404", "c10a63eb97efeb774f78b5efdbf8a866"),
+ "core/Updates/2.0-b13.php" => array("986", "897baf57c6104b512bb81ec186ed173a"),
+ "core/Updates/2.0-b3.php" => array("1122", "5f4d8995e425682b8e2feb9dc162e35a"),
+ "core/Updates/2.0-b9.php" => array("653", "8d3bd1b10367fac6d9ac351addd2e4da"),
+ "core/Updates/2.0-rc1.php" => array("431", "172c8341bd784da35b3b83b2c2f5b1f6"),
+ "core/Updates/2.1.1-b11.php" => array("5745", "acce7f7e6ff35875809478861353dbcb"),
+ "core/Updates/2.2.0-b15.php" => array("639", "c89858a73296cda3e66b2ded05ba6f4d"),
+ "core/Updates.php" => array("2803", "2deac460daaceeee0fa1fe9e479185bb"),
+ "core/UrlHelper.php" => array("18349", "dbd9d9cd951cfafa14e29bcc382502c5"),
+ "core/Url.php" => array("17731", "554508c27948cb1b29da65844b73b6aa"),
+ "core/Version.php" => array("345", "636e0ebdf450757751fa98b6ccca24c4"),
+ "core/ViewDataTable/Config.php" => array("20852", "41d12a2246ca76006b223105edcf5e4d"),
+ "core/ViewDataTable/Factory.php" => array("7098", "36920d013e8245b49cbf92ba2c0b500d"),
+ "core/ViewDataTable/Manager.php" => array("8846", "51e33664ee6ae517ed42648eddd31964"),
+ "core/ViewDataTable/RequestConfig.php" => array("8907", "343b0baae408f730c167129bf1ba2571"),
+ "core/ViewDataTable/Request.php" => array("4031", "d7b2c817853f519a8b2fda06e6211038"),
+ "core/View/OneClickDone.php" => array("2077", "bf64751dc4c8fa73f7bbc49b319308c6"),
+ "core/View.php" => array("12640", "47020ad4b4284ce91a83b1149a1190e5"),
+ "core/View/RenderTokenParser.php" => array("2051", "a8b5b4566507c29e1fdd09bfaeedd99e"),
+ "core/View/ReportsByDimension.php" => array("4192", "d72f400326ec14706f7f08f5386828c3"),
+ "core/View/UIControl.php" => array("4410", "d16a751a2063cac0b834ab63b448fdef"),
+ "core/View/ViewInterface.php" => array("403", "4a9732dd334255ba0ead94222d74f87f"),
+ "core/Visualization/Sparkline.php" => array("4576", "3533412a25076c86b0b2a2474a024e74"),
+ "core/WidgetsList.php" => array("7060", "dc525a4a4e618d56ae0563d22549bb08"),
+ "index.php" => array("1546", "15d01f358d90f66a19dcab6946679095"),
+ "js/index.php" => array("749", "a80b88022a2748952a62401871d025bd"),
+ "js/LICENSE.txt" => array("1549", "c1d16895887980c4494a95a7f6ee3671"),
+ "js/piwik.js" => array("125559", "9f58141514a1bb0001636373cbb8e068"),
+ "js/README.md" => array("2142", "a59319062bafe9657baac94e9fdb7995"),
+ "lang/am.json" => array("43298", "2e05b5fda32bdb397888910de8165463"),
+ "lang/ar.json" => array("116774", "108b69246990df2762bfddbe22696501"),
+ "lang/be.json" => array("133845", "d4791872ccafc77296fdc7f01c18935a"),
+ "lang/bg.json" => array("236400", "c618daca1fc95e32e57eefdcefcd6642"),
+ "lang/bn.json" => array("30389", "de30a94afc6c0cfd5e39805821dbcb8a"),
+ "lang/bs.json" => array("47846", "5f02521870cfe3146efee207e59bca4b"),
+ "lang/ca.json" => array("148886", "6219033d88bf58c96e5c09029708e38b"),
+ "lang/cs.json" => array("96631", "95e50796f835a36f0dd4a2f5b45ba362"),
+ "lang/cy.json" => array("33669", "3ae7125bad892632b4dbadbcde73e0eb"),
+ "lang/da.json" => array("175497", "3770805fc026e5100fd9ae140849514e"),
+ "lang/de.json" => array("192559", "021bc6cbdbce3eaae44017ef834d2f09"),
+ "lang/el.json" => array("293119", "f3a72418fd95b71ca8ec2ba7f833e0f2"),
+ "lang/en.json" => array("176161", "9c820d429d7a4cc0db23376a3166d9e2"),
+ "lang/es.json" => array("187985", "003cf7629a086625ba4277b9d1bdbe50"),
+ "lang/et.json" => array("89740", "7c339aa2dca5c02fcddd31ea37566a98"),
+ "lang/eu.json" => array("46504", "220e4c85c784d98f04b7915a07070dc0"),
+ "lang/fa.json" => array("185290", "3b5a3430bd600b2980f13418a2457884"),
+ "lang/fi.json" => array("171923", "92fabf05e4bc46c407dba69407077b53"),
+ "lang/fr.json" => array("191935", "169d8019b419009aaeee37c7f5ab2682"),
+ "lang/gl.json" => array("31239", "6e8e2ee3b00d9edb52d909ddf3765d0f"),
+ "lang/he.json" => array("66841", "5b3bded179f179a250499886a4dcb141"),
+ "lang/hi.json" => array("267566", "1243af856ce0f2de652a47777c01bdeb"),
+ "lang/hr.json" => array("44181", "d4d1f8a8210b743dfc74b2909c93e29d"),
+ "lang/hu.json" => array("88631", "5a393be54f20621ec373aae7952119b0"),
+ "lang/id.json" => array("160275", "8fe3288e6ab07886257964d2755af0d0"),
+ "lang/is.json" => array("44565", "d3e6d051c1bd28ec5677c50582e2249b"),
+ "lang/it.json" => array("188242", "f7547dcef63cce6d91627d6c001d22c1"),
+ "lang/ja.json" => array("146362", "f47bd751197cfc3d072f806d8deaaef2"),
+ "lang/ka.json" => array("133972", "d919209c916cd66daad1e233b97c24a6"),
+ "lang/ko.json" => array("159126", "7e91ce1f784680466763eb7b5a664c87"),
+ "lang/lt.json" => array("72812", "bacac0086478ec8d07f63229cd109c04"),
+ "lang/lv.json" => array("68729", "4073b4637d26b430110508ff59a800e8"),
+ "lang/nb.json" => array("79362", "b0da604e507b5c621b7e697ea1c6bf84"),
+ "lang/nl.json" => array("159749", "25663b03d177a31f8a8986dd8edcbde6"),
+ "lang/nn.json" => array("72987", "61ece0f6d31afbd53f42e840643613a8"),
+ "lang/pl.json" => array("105283", "96ea726f5a9c5ebf8b85f531069760eb"),
+ "lang/pt-br.json" => array("178705", "955707f471cbde34c6597fc06199469b"),
+ "lang/pt.json" => array("103399", "c8db5c35133612b7badeca45967a8cf8"),
+ "lang/README.md" => array("439", "7f5249109f73ae292bcd01eb23853738"),
+ "lang/ro.json" => array("107388", "4e16d637081c5f7d3bf887debbadee3d"),
+ "lang/ru.json" => array("236266", "3601f2fbf822f4e3eade4a99f25f0abb"),
+ "lang/sk.json" => array("72678", "82b11fbdab21b8bcb55665b48794ff42"),
+ "lang/sl.json" => array("71473", "8e2836b6c48a39847f029a08cc864768"),
+ "lang/sq.json" => array("108350", "abde20a3257f155aa6b6e88dd65830c9"),
+ "lang/sr.json" => array("171239", "b348d6ce2bb4f75a190b0e468e0b1584"),
+ "lang/sv.json" => array("179173", "ce59c0b2c6dab1f8180c6f87daffc90d"),
+ "lang/ta.json" => array("89885", "f861e7329aac8c31dc1f634d7bab8625"),
+ "lang/te.json" => array("45655", "41136b9370c8df0f8264d098e576a984"),
+ "lang/th.json" => array("161630", "ee834a50d83b3a356b0ee7069b80d6f2"),
+ "lang/tr.json" => array("67775", "d3059314807802e6e9c308c02c7a2b1f"),
+ "lang/uk.json" => array("101355", "305b6faaf47f8f52ff96070300759047"),
+ "lang/vi.json" => array("201534", "7a21f65bd54d7b500a4fbf1e6d4de09c"),
+ "lang/zh-cn.json" => array("149382", "3a6056925b838c9c17ec72ce97705958"),
+ "lang/zh-tw.json" => array("69883", "1b3dc84c6da058b41f3faf6f33052302"),
+ "LEGALNOTICE" => array("7322", "cec47f121512f46d75f04edc3821e0e9"),
+ "libs/angularjs/angular-animate.js" => array("73299", "f57eec6cfc24a9d7de89e7815c54b9cb"),
+ "libs/angularjs/angular-animate.min.js" => array("10506", "5927bc7044bf9f88c7fcef982cfc84e0"),
+ "libs/angularjs/angular-cookies.js" => array("5712", "8f359b0b2ccce92d6d0f27a275178646"),
+ "libs/angularjs/angular-cookies.min.js" => array("850", "f70eb186d69b8c34b9fc083fd95ad46e"),
+ "libs/angularjs/angular-csp.css" => array("346", "34f147527516ef16935f3499d13fdadf"),
+ "libs/angularjs/angular.js" => array("737843", "823656f04f0d28049733ea5472eaf885"),
+ "libs/angularjs/angular-loader.js" => array("14464", "dbd936b897ff5995f868309b1682973e"),
+ "libs/angularjs/angular-loader.min.js" => array("1505", "540937ba021f2ba2d65679de81cfc225"),
+ "libs/angularjs/angular.min.js" => array("101076", "f13ec1ca50778530d05f5e7d55964873"),
+ "libs/angularjs/angular-mocks.js" => array("67709", "b64cf0b49047bda8058ffebcb4b969ba"),
+ "libs/angularjs/angular-resource.js" => array("23494", "e567b1825d04f7d3c8c4932bafe45c92"),
+ "libs/angularjs/angular-resource.min.js" => array("3297", "52f14aa58396ba6f06c873048ac757ad"),
+ "libs/angularjs/angular-route.js" => array("32505", "8eaf3177d26ff41eadaa0bb325cc84cc"),
+ "libs/angularjs/angular-route.min.js" => array("3885", "39e7751707e2fb3108f6148c957bb726"),
+ "libs/angularjs/angular-sanitize.js" => array("20736", "bccd4bf0adf6318da2e84e87eea451ef"),
+ "libs/angularjs/angular-sanitize.min.js" => array("4246", "57046863351fc02e0058321e20691254"),
+ "libs/angularjs/angular-scenario.js" => array("1080031", "b4202d55bb0ef488e710d0ec0b718f7b"),
+ "libs/angularjs/angular-touch.js" => array("20706", "f1c23f96e05f1cf0991be9f0ac5f44d8"),
+ "libs/angularjs/angular-touch.min.js" => array("3205", "27edc5cdcdb0cc9da0a8e1bc6510cb9b"),
+ "libs/angularjs/errors.json" => array("5385", "4de0bf0d0e45b08c96275179d1e45445"),
+ "libs/angularjs/LICENSE" => array("1098", "0547d38c79782d84bb4df3c036a0bcc0"),
+ "libs/angularjs/version.json" => array("102", "2ca74bf29fbf307a7f626d922bb20696"),
+ "libs/angularjs/version.txt" => array("6", "88979175ec60695fcd6e3c97243cdecd"),
+ "libs/Archive_Tar/Tar.php" => array("66956", "501bf628481dce9ded472e4ae24d1c7a"),
+ "libs/html5shiv/html5shiv.js" => array("2427", "11af8654413ddf8b0fe00c3395787e77"),
+ "libs/HTML/Common2.php" => array("15005", "a9c9b708cd3f2691192171bb78177db3"),
+ "libs/HTML/QuickForm2/Container/Fieldset.php" => array("3009", "c01d23505028494f202da1e7c092c285"),
+ "libs/HTML/QuickForm2/Container/Group.php" => array("10690", "af6598a96ab2505936156cf3c82eb231"),
+ "libs/HTML/QuickForm2/Container.php" => array("15568", "907e655970384bc7761709412e756b25"),
+ "libs/HTML/QuickForm2/Controller/Action/Back.php" => array("3041", "23865bbfba7359c376c51c8cfc947310"),
+ "libs/HTML/QuickForm2/Controller/Action/Direct.php" => array("2906", "94748939d6be141ebeb7f44c92c52717"),
+ "libs/HTML/QuickForm2/Controller/Action/Display.php" => array("4938", "b934c86efb099ae30adaaf2c3157f41b"),
+ "libs/HTML/QuickForm2/Controller/Action/Jump.php" => array("7694", "79e07bc733b2a9548b6e43470d970f79"),
+ "libs/HTML/QuickForm2/Controller/Action/Next.php" => array("3492", "6d503bf9c4a33c6c2fa4e6cdd653c10b"),
+ "libs/HTML/QuickForm2/Controller/Action.php" => array("2820", "6feaf02ad3852182db2be80bcdd39245"),
+ "libs/HTML/QuickForm2/Controller/Action/Submit.php" => array("3070", "3408028e3f5cc33f837a89bbd48759dd"),
+ "libs/HTML/QuickForm2/Controller/DefaultAction.php" => array("4312", "25469caf28ac5e6b1a7a38acd83a3e2f"),
+ "libs/HTML/QuickForm2/Controller/Page.php" => array("8568", "2bc1a11f7ab068f70841630c8313aaa3"),
+ "libs/HTML/QuickForm2/Controller.php" => array("16291", "9a233f661695b278e42656e088c455a2"),
+ "libs/HTML/QuickForm2/Controller/SessionContainer.php" => array("6377", "f2fe7d2b18792c9177192ac151b2696a"),
+ "libs/HTML/QuickForm2/DataSource/Array.php" => array("3541", "03534063e0146c04b3cd3f9f2c0c654c"),
+ "libs/HTML/QuickForm2/DataSource.php" => array("2683", "65331d40fb6a97680442255b29f9d81f"),
+ "libs/HTML/QuickForm2/DataSource/Session.php" => array("3179", "ff60e85f7c3cee6f7b5ee1af86d61198"),
+ "libs/HTML/QuickForm2/DataSource/Submit.php" => array("3075", "7a4ec6f260af2b63eb2957e2ff0b74ba"),
+ "libs/HTML/QuickForm2/DataSource/SuperGlobal.php" => array("6087", "135315d3a791d4666404750cc627bacf"),
+ "libs/HTML/QuickForm2/Element/Button.php" => array("5179", "59e46ad40d28c606928d77d0c135adef"),
+ "libs/HTML/QuickForm2/Element/Date.php" => array("35658", "93d5fd008c523739663e61d6d5f6acaf"),
+ "libs/HTML/QuickForm2/Element/InputButton.php" => array("3363", "9c1cf6f4a218fe171640b0e11b44ee2c"),
+ "libs/HTML/QuickForm2/Element/InputCheckable.php" => array("5638", "2ac2c2b81fdcd2fce8cd4c58b178a474"),
+ "libs/HTML/QuickForm2/Element/InputCheckbox.php" => array("3740", "f9351dba835f3e06a09cc5a5994ddfed"),
+ "libs/HTML/QuickForm2/Element/InputFile.php" => array("12294", "4f0ae38065fefb2267394efe7322db27"),
+ "libs/HTML/QuickForm2/Element/InputHidden.php" => array("3049", "d15383783799c661b04c47b831af7b1a"),
+ "libs/HTML/QuickForm2/Element/InputImage.php" => array("5721", "d3f9f6d831482752dcabed9631c5db4a"),
+ "libs/HTML/QuickForm2/Element/InputPassword.php" => array("2771", "248ee12b9cf08708e56a6e95d08374b1"),
+ "libs/HTML/QuickForm2/Element/Input.php" => array("3846", "78f8d93e73d3526a42177ca0c79b1b92"),
+ "libs/HTML/QuickForm2/Element/InputRadio.php" => array("2714", "d0d7bb6033e27a63bf24059f7981388b"),
+ "libs/HTML/QuickForm2/Element/InputReset.php" => array("3368", "5a024664c05f7100d64c4f3da8948c44"),
+ "libs/HTML/QuickForm2/Element/InputSubmit.php" => array("4018", "8c65288419b635f579e5cfe3f80897ad"),
+ "libs/HTML/QuickForm2/Element/InputText.php" => array("2561", "a4422cb34668d6201db9f86297f84e01"),
+ "libs/HTML/QuickForm2/Element.php" => array("4547", "3b7e83d416635f885cde255031c23061"),
+ "libs/HTML/QuickForm2/Element/Select.php" => array("19653", "a301198923f2a48397c028d7e0d6b016"),
+ "libs/HTML/QuickForm2/Element/Static.php" => array("4653", "3507961ac37bfcc4b50f9fba986da73b"),
+ "libs/HTML/QuickForm2/Element/Textarea.php" => array("3817", "a494dd182508598a7034de948f7a9b8a"),
+ "libs/HTML/QuickForm2/Exception.php" => array("3832", "97bd3d2daab0f1adee753a8b60da3224"),
+ "libs/HTML/QuickForm2/Factory.php" => array("10234", "e6570f82222a308a5e3292555dac07db"),
+ "libs/HTML/QuickForm2/JavascriptBuilder.php" => array("4132", "6efa38420b43753762d2ed5e202d94a1"),
+ "libs/HTML/QuickForm2/Loader.php" => array("5033", "50ac48c8f880a82c5cbaf4de45a01818"),
+ "libs/HTML/QuickForm2/Node.php" => array("20904", "21ea89366d3f68617bf77cc407bf22b1"),
+ "libs/HTML/QuickForm2.php" => array("7407", "aa4307cba861837825a3ded27a0b6b0b"),
+ "libs/HTML/QuickForm2/Renderer/Array.php" => array("12034", "add33fbd588592ec5946b8201686ccc3"),
+ "libs/HTML/QuickForm2/Renderer/Default.php" => array("22480", "7dd6af9bcecf2a883a951605db183fbd"),
+ "libs/HTML/QuickForm2/Renderer.php" => array("13052", "b63f79f5185291c1f1ccbe290593ca2d"),
+ "libs/HTML/QuickForm2/Renderer/Plugin.php" => array("2698", "868b936844b79bbb4e9a0db084830ebb"),
+ "libs/HTML/QuickForm2/Renderer/Proxy.php" => array("9190", "59eb89991cabcda6509d3445a2d63eb1"),
+ "libs/HTML/QuickForm2/Renderer/Smarty.php" => array("11535", "a9587fa6fe27be5c97538bc8acaa35a6"),
+ "libs/HTML/QuickForm2/Rule/Callback.php" => array("6779", "536ccc9641568a038ef354350f63541c"),
+ "libs/HTML/QuickForm2/Rule/Compare.php" => array("8708", "7d1a7c78eb33648a5a89acf5eb6adee3"),
+ "libs/HTML/QuickForm2/Rule/Each.php" => array("5371", "ae1d806fbcc145a71901c3b1253c8347"),
+ "libs/HTML/QuickForm2/Rule/Empty.php" => array("3393", "79be971ced97cecdd2e1d1378cbcddb7"),
+ "libs/HTML/QuickForm2/Rule/Length.php" => array("9237", "977498e22d978636d6b703a83a64da44"),
+ "libs/HTML/QuickForm2/Rule/MaxFileSize.php" => array("4880", "3517b0cec4010c556aba1656f8db8cf8"),
+ "libs/HTML/QuickForm2/Rule/MimeType.php" => array("4841", "45b5784084abb9f7d338b8eb303650dc"),
+ "libs/HTML/QuickForm2/Rule/Nonempty.php" => array("5821", "04addd2b94427be3954d807e289f05d0"),
+ "libs/HTML/QuickForm2/Rule/NotCallback.php" => array("3176", "e63a66af2231d91bca713458a4731825"),
+ "libs/HTML/QuickForm2/Rule/NotRegex.php" => array("4139", "7bcfef75d06d6e772566b8a5806f4ae4"),
+ "libs/HTML/QuickForm2/Rule.php" => array("10991", "3dec84bf1d9ab983608bce52553cd822"),
+ "libs/HTML/QuickForm2/Rule/Regex.php" => array("5158", "842b30f0687b7d65c2c940bbfc44008e"),
+ "libs/HTML/QuickForm2/Rule/Required.php" => array("3555", "4c440ff4e187faf10e307b04e774011b"),
+ "libs/javascript/json2.js" => array("3377", "ba3293970e13b03a2ea92f5b6b5bf544"),
+ "libs/javascript/sprintf.js" => array("3795", "f8659e7549fb9e482d4f8145399f421c"),
+ "libs/jqplot/build_minified_script.sh" => array("1434", "b5fe6cddbb5e8bf8f440a380f3e7e5a9"),
+ "libs/jqplot/excanvas.min.js" => array("19351", "5e2fefd5c782233c12383cca3b19e935"),
+ "libs/jqplot/gpl-2.0.txt" => array("15112", "8ef64b86db8e0e63606284cf36b643be"),
+ "libs/jqplot/jqplot.axisLabelRenderer.js" => array("3275", "61248e4baf0752ee2e89cf303c52a95e"),
+ "libs/jqplot/jqplot.axisTickRenderer.js" => array("6570", "f7bdc4053619d39dd6ad9564de13b779"),
+ "libs/jqplot/jqplot.canvasGridRenderer.js" => array("19588", "5d6c6588a60d6a298747d095fa11c662"),
+ "libs/jqplot/jqplot.core.js" => array("175127", "57d2753e6e1a1aedb78e4c1ed7e2f48b"),
+ "libs/jqplot/jqplot-custom.min.js" => array("191785", "ad2a8daa551040c7c6061ade311f91b0"),
+ "libs/jqplot/jqplot.divTitleRenderer.js" => array("4174", "1ae09b12907cc772b2137ce60364b8a0"),
+ "libs/jqplot/jqplot.linearAxisRenderer.js" => array("45487", "187b69d695443da0b2135a5e46d7b292"),
+ "libs/jqplot/jqplot.linePattern.js" => array("4772", "1091be3885aaa2a2d9ed8171b85aed80"),
+ "libs/jqplot/jqplot.lineRenderer.js" => array("52298", "e29243f2d8d9fd062487f9ee61419a94"),
+ "libs/jqplot/jqplot.markerRenderer.js" => array("9045", "3c2a6c18390499ca28663acda6c787cb"),
+ "libs/jqplot/jqplot.shadowRenderer.js" => array("5769", "0557c0911bd30b61fdf6ae3e12b5943e"),
+ "libs/jqplot/jqplot.shapeRenderer.js" => array("6369", "335e972fd8f402b55350f2f4877f78de"),
+ "libs/jqplot/jqplot.sprintf.js" => array("14391", "c677c8120d186d393c67a439ffab99db"),
+ "libs/jqplot/jqplot.tableLegendRenderer.js" => array("13478", "9e82bd2c17228d0cf3fe922eb2827395"),
+ "libs/jqplot/jqplot.themeEngine.js" => array("31016", "3ca6a4eece29fd93244a57f17ec2cd4f"),
+ "libs/jqplot/MIT-LICENSE.txt" => array("1082", "fc0fc2aa2423fd4397d40a7d7ffafbe4"),
+ "libs/jqplot/plugins/jqplot.barRenderer.js" => array("34737", "cabdfba746026a2f4eac7bbfbb81ec0f"),
+ "libs/jqplot/plugins/jqplot.canvasAxisTickRenderer.js" => array("9843", "3f8a0bbc13e1793f6fc9a123c9bf3b98"),
+ "libs/jqplot/plugins/jqplot.canvasTextRenderer.js" => array("24372", "58a963d919b36061685c46a75f6a4a50"),
+ "libs/jqplot/plugins/jqplot.categoryAxisRenderer.js" => array("28571", "77d0a561f20e0b889e88758449ba7180"),
+ "libs/jqplot/plugins/jqplot.pieRenderer.js" => array("35551", "f78c034a0bae5eb0310392b5632907ec"),
+ "libs/jquery/gpl-2.0.txt" => array("15099", "2c1778696d3ba68569a0352e709ae6b7"),
+ "libs/jquery/gpl-3.0.txt" => array("35147", "d32239bcb673463ab874e80d47fae504"),
+ "libs/jquery/images/down_arrow.png" => array("2898", "6a5d9f1bd953608817ebf4904d4591bc"),
+ "libs/jquery/images/scroller.png" => array("3329", "899d80589a386f767cb07b33cc28c216"),
+ "libs/jquery/images/slide.png" => array("2831", "ef737b4f1d6b594656bca9e99b7f5968"),
+ "libs/jquery/images/up_arrow.png" => array("2881", "bf4bbd999b2027d836e00fde27a46b39"),
+ "libs/jquery/jquery.browser.js" => array("619", "236a3b854b2eeae01c8640ad82bb4178"),
+ "libs/jquery/jquery.history.js" => array("5592", "acd0667f090e0bf6e759bb86db1d674f"),
+ "libs/jquery/jquery.js" => array("96380", "52d16e147b5346147d0f3269cd4d0f80"),
+ "libs/jquery/jquery.jscrollpane.js" => array("63508", "08450c17ce2a83ab43176f80253d6f99"),
+ "libs/jquery/jquery.mousewheel.js" => array("3846", "f77bd9ca0396c7a8672f536884b1e1aa"),
+ "libs/jquery/jquery.placeholder.js" => array("4515", "6e5b889042b348bdee267ceecbe4159d"),
+ "libs/jquery/jquery.scrollTo.js" => array("2434", "c4dff68594e0fdb05b48aac9a90c0a19"),
+ "libs/jquery/jquery.smartbanner.js" => array("9921", "4e24cb04b88e2e0a2c013ab589ef5e2f"),
+ "libs/jquery/jquery.truncate.js" => array("2452", "cc3fcfc30d8a4496e891469ee20fbd77"),
+ "libs/jquery/jquery-ui.js" => array("228546", "ea464a34403c8772f3bc6ff2f5355f4b"),
+ "libs/jquery/LICENSE-sizzle.txt" => array("17635", "418d0239a1435dae6b5c2e919a75f8c9"),
+ "libs/jquery/MIT-LICENSE-history.txt" => array("1109", "242685841d67aa0bbc837735b177709f"),
+ "libs/jquery/MIT-LICENSE-jquery.txt" => array("1074", "13a84fe33922678518c49596de032d92"),
+ "libs/jquery/MIT-LICENSE-jqueryui.txt" => array("1311", "bac9338b4387621f0cea7720ace0450c"),
+ "libs/jquery/MIT-LICENSE-placeholder.txt" => array("1075", "0146cb31436f780624be47ce08eec616"),
+ "libs/jquery/MIT-LICENSE-scrollto.txt" => array("1120", "4a5d0d8578e331f5172fe5d0d1470fc4"),
+ "libs/jquery/MIT-LICENSE-smartbanner.txt" => array("1071", "a23932b5f367ad0272456270829b52a7"),
+ "libs/jquery/mwheelIntent.js" => array("2249", "71fe6a97b5e149ae08829a586fe7e7ad"),
+ "libs/jquery/stylesheets/jquery.jscrollpane.css" => array("1400", "ead9005a6e67449768db45b42e3f9b89"),
+ "libs/jquery/stylesheets/jquery.smartbanner.css" => array("3901", "94fb09ced6636405d47b5ec6c1ed23c1"),
+ "libs/jquery/stylesheets/scroll.less" => array("2066", "ab33e88a6d4360052d87685173dbf07a"),
+ "libs/jquery/themes/base/images/ui-anim_basic_16x16.gif" => array("1553", "03ce3dcc84af110e9da8699a841e5200"),
+ "libs/jquery/themes/base/images/ui-bg_flat_0_aaaaaa_40x100.png" => array("180", "2a44fbdb7360c60122bcf6dcef0387d8"),
+ "libs/jquery/themes/base/images/ui-bg_flat_75_ffffff_40x100.png" => array("178", "8692e6efddf882acbff144c38ea7dfdf"),
+ "libs/jquery/themes/base/images/ui-bg_glass_55_fbf9ee_1x400.png" => array("120", "f8f4558e0b92ff2cd6136781533902ec"),
+ "libs/jquery/themes/base/images/ui-bg_glass_65_ffffff_1x400.png" => array("105", "e5a8f32e28fd5c27bf0fed33c8a8b9b5"),
+ "libs/jquery/themes/base/images/ui-bg_glass_75_dadada_1x400.png" => array("111", "c12c6510dad3ebfa64c8a30e959a2469"),
+ "libs/jquery/themes/base/images/ui-bg_glass_75_e6e6e6_1x400.png" => array("110", "f4254356c2a8c9a383205ef2c4de22c4"),
+ "libs/jquery/themes/base/images/ui-bg_glass_95_fef1ec_1x400.png" => array("119", "5a3be2d8fff8324d59aec3df7b0a0c83"),
+ "libs/jquery/themes/base/images/ui-bg_highlight-soft_75_cccccc_1x100.png" => array("101", "72c593d16e998952cd8d798fee33c6f3"),
+ "libs/jquery/themes/base/images/ui-icons_222222_256x240.png" => array("4369", "9129e086dc488d8bcaf808510bc646ba"),
+ "libs/jquery/themes/base/images/ui-icons_2e83ff_256x240.png" => array("4369", "25162bf857a8eb83ea932a58436e1049"),
+ "libs/jquery/themes/base/images/ui-icons_454545_256x240.png" => array("4369", "771099482bdc1571ece41073b1752596"),
+ "libs/jquery/themes/base/images/ui-icons_888888_256x240.png" => array("4369", "faf6f5dc44e713178784c1fb053990aa"),
+ "libs/jquery/themes/base/images/ui-icons_cd0a0a_256x240.png" => array("4369", "5d8808d43cefca6f6781a5316d176632"),
+ "libs/jquery/themes/base/jquery-ui.css" => array("25699", "76b945b2f5246c4cbe31857afd481ceb"),
+ "libs/MaxMindGeoIP/geoipcity.inc" => array("6973", "d191ea9f911a9cf3bc98a07faadf511d"),
+ "libs/MaxMindGeoIP/geoip.inc" => array("31447", "26f4d94b6c394dcbb6abf57df5acbccf"),
+ "libs/MaxMindGeoIP/geoipregionvars.php" => array("95763", "1635d7024c6a8b2502ed6f60659c9a1f"),
+ "libs/pChart2.1.3/change.log" => array("11838", "47d20227c6b22d5425e0bc60475084ad"),
+ "libs/pChart2.1.3/class/pData.class.php" => array("30575", "3e35fc351f143b70e8959fb65f6bb522"),
+ "libs/pChart2.1.3/class/pDraw.class.php" => array("319989", "f27d42bc9fa34966f4483546254c312c"),
+ "libs/pChart2.1.3/class/pImage.class.php" => array("19960", "abbaf39d5fb456ccec682c0f0814d8a9"),
+ "libs/pChart2.1.3/class/pPie.class.php" => array("65564", "4f2ed8e8c89bd3646c4289e5f2547b7b"),
+ "libs/pChart2.1.3/GPLv3.txt" => array("35148", "8f0e2cd40e05189ec81232da84bd6e1a"),
+ "libs/pChart2.1.3/readme.txt" => array("5971", "8e04cbe332d419f3177b2d9300b15e1a"),
+ "libs/PclZip/lgpl-2.1.txt" => array("26530", "4fbd65380cdd255951079008b364516c"),
+ "libs/PclZip/pclzip.lib.php" => array("196363", "968cb96854866df0370e6fd5523fa05a"),
+ "libs/PEAR5.php" => array("1087", "1a8f67d58009372a6cbcddd638b128cf"),
+ "libs/PEAR/Exception.php" => array("14006", "424a61a67dbd5f9f3ed5fc3be2b9ac54"),
+ "libs/PEAR/FixPHP5PEARWarnings.php" => array("152", "ff5f4e5d365b916ea63225840bc0b71a"),
+ "libs/PEAR/LICENSE" => array("1477", "45b44486d8090de17b2a8b4211fab247"),
+ "libs/PEAR.php" => array("33932", "f9f83fb6efef354ec16765ffe17d2ae4"),
+ "libs/PiwikTracker/LICENSE.txt" => array("1505", "7bbcab51f5db7fee7e6864a639ff56ab"),
+ "libs/PiwikTracker/PiwikTracker.php" => array("56782", "2a84b2de3492460089a52baad3b1fcd6"),
+ "libs/README.md" => array("1713", "fad15836001bfea82cdc4f362febe4fb"),
+ "libs/sparkline/CHANGES" => array("648", "b6d213a7ad5d1f2c6e3eac38d48c1f8a"),
+ "libs/sparkline/DESIGN" => array("648", "a9e2a29ce386fb408ab2eaa02a86c2ed"),
+ "libs/sparkline/gpl-2.0.txt" => array("18092", "b234ee4d69f5fce4486a80fdaf4a4263"),
+ "libs/sparkline/lib/Object.php" => array("3951", "095eae57154bed92a78e3d951e0135ef"),
+ "libs/sparkline/lib/Sparkline_Bar.php" => array("6834", "c5aa452cf5698ca7baca128aa02ab2f8"),
+ "libs/sparkline/lib/Sparkline_Line.php" => array("11026", "9cc756ac498e10ae7291d0a252ef3bf6"),
+ "libs/sparkline/lib/Sparkline.php" => array("16623", "31777f846d96077e1b32cba2f1874138"),
+ "libs/sparkline/LICENSE-BSD.txt" => array("1505", "51639a73ddb4999a16fc9249eb445acc"),
+ "libs/sparkline/README" => array("1043", "aa954952640a7c645151f63905e9692a"),
+ "libs/tcpdf/2dbarcodes.php" => array("7785", "ab7d01f44bbea0b839c6b2c2f362f4cb"),
+ "libs/tcpdf/barcodes.php" => array("59791", "9f0f79a92e025acfd5aef0c09b0ea736"),
+ "libs/tcpdf/composer.json" => array("933", "8681cdc1c3eea06625bf0c827f4a353e"),
+ "libs/tcpdf/config/lang/eng.php" => array("1200", "cc64ed556dacc69a43777204d08c952b"),
+ "libs/tcpdf/config/tcpdf_config_alt.php" => array("5320", "43c5e852ab49252b52871512ef6bd3db"),
+ "libs/tcpdf/config/tcpdf_config.php" => array("5202", "f2ca50b70b473fa4f0fd002e34970761"),
+ "libs/tcpdf/fonts/almohanad.ctg.z" => array("2780", "f2a06979cd7c5262b773f1c5257f93f8"),
+ "libs/tcpdf/fonts/almohanad.php" => array("14074", "3cf9d431468593f9d4eba1795631943b"),
+ "libs/tcpdf/fonts/almohanad.z" => array("121292", "80a4fbacf654e77c487bbcefad390dfb"),
+ "libs/tcpdf/fonts/dejavusans.ctg.z" => array("10120", "b693c24b2880b5d2e5f7c7ecd31213dc"),
+ "libs/tcpdf/fonts/dejavusans.php" => array("52352", "f38c6f5c629707b6b1197c60a68a77ed"),
+ "libs/tcpdf/fonts/dejavusans.z" => array("361229", "df214f9763ae5fcab34b85aab01d49d9"),
+ "libs/tcpdf/fonts/helveticabi.php" => array("2589", "c22fdc8941f2956e0930b20105870468"),
+ "libs/tcpdf/fonts/helveticab.php" => array("2580", "3daad3713df02c15beebd09ceecacacd"),
+ "libs/tcpdf/fonts/helveticai.php" => array("2584", "e0a7f23376f50de631db93814aff2e35"),
+ "libs/tcpdf/fonts/helvetica.php" => array("2575", "2a315fa2593161154c319788f0ef2127"),
+ "libs/tcpdf/fonts/hysmyeongjostdmedium.php" => array("1829", "51f6fe162641de3714866950d5eff4e8"),
+ "libs/tcpdf/fonts/kozgopromedium.php" => array("3577", "2c5e8a67d1a805aae9842bbad59a873f"),
+ "libs/tcpdf/fonts/kozminproregular.php" => array("3454", "78fdf805f1cea6cd01912192821ec734"),
+ "libs/tcpdf/fonts/msungstdlight.php" => array("1550", "c940b153fb6c5b3498efa181881b5b6c"),
+ "libs/tcpdf/fonts/stsongstdlight.php" => array("1627", "eb85dc872664c0769e9fab1b7540b4d5"),
+ "libs/tcpdf/fonts/symbol.php" => array("2555", "20e28c8b386ddbb38ead777f717d7c44"),
+ "libs/tcpdf/fonts/timesbi.php" => array("2580", "a5f3fbbef1831fe0bcd060edb6e5010b"),
+ "libs/tcpdf/fonts/timesb.php" => array("2577", "ad485022027867116de0bf6c25b1854a"),
+ "libs/tcpdf/fonts/timesi.php" => array("2575", "8fd8e9a11cca513a4da0f25ff1a24149"),
+ "libs/tcpdf/fonts/times.php" => array("2572", "a75033315ee90464410b47cc27ce9ff0"),
+ "libs/tcpdf/gpl.txt" => array("35147", "d32239bcb673463ab874e80d47fae504"),
+ "libs/tcpdf/htmlcolors.php" => array("5499", "fe132ea6a41cd787b4c2ff18716625b5"),
+ "libs/tcpdf/include/sRGB.icc" => array("3048", "060e79448f1454582be37b3de490da2f"),
+ "libs/tcpdf/include/tcpdf_colors.php" => array("14661", "f782f4cb468e8a5e956ca0759530e132"),
+ "libs/tcpdf/include/tcpdf_filters.php" => array("14677", "e175abe0e5c8661e08834014abead451"),
+ "libs/tcpdf/include/tcpdf_font_data.php" => array("313432", "8f83bbc144d70505672f82679546c72d"),
+ "libs/tcpdf/include/tcpdf_fonts.php" => array("95129", "70425e7872fbabb524b934ec474b0880"),
+ "libs/tcpdf/include/tcpdf_images.php" => array("11170", "b1c6baba7c286f48d813df899350c8db"),
+ "libs/tcpdf/include/tcpdf_static.php" => array("106691", "fab9356ece2e9d51e831dd00da64324b"),
+ "libs/tcpdf/lgpl-3.0.txt" => array("7651", "e6a600fd5e1d9cbde2d983680233ad02"),
+ "libs/tcpdf/LICENSE.TXT" => array("43636", "5c87b66a5358ebcc495b03e0afcd342c"),
+ "libs/tcpdf/pdf417.php" => array("53738", "130560d35265c9666440dbf5086f7846"),
+ "libs/tcpdf/qrcode.php" => array("80058", "59adfba524c4445f850a92380eb2a17f"),
+ "libs/tcpdf/README.TXT" => array("5396", "46bcf50a79dc0d5f82b4c5c3df95f504"),
+ "libs/tcpdf/spotcolors.php" => array("2153", "4b3c09efafad1e9c28021fca16f52147"),
+ "libs/tcpdf/tcpdf_autoconfig.php" => array("6918", "1df4c5b1b14c31160da910ed8e390e91"),
+ "libs/tcpdf/tcpdf.crt" => array("2290", "c137aab97f7e06a6038589968e10d976"),
+ "libs/tcpdf/tcpdf.fdf" => array("1286", "96f873c30a6f6a0f884d713d185d5bd8"),
+ "libs/tcpdf/tcpdf_import.php" => array("3327", "6bb88a8a3d69511d1bf9e7af12ab5f47"),
+ "libs/tcpdf/tcpdf.p12" => array("1749", "7e078148a1ab66ca05041915f7013204"),
+ "libs/tcpdf/tcpdf_parser.php" => array("26986", "d7fcc07fbea917f9ae4e07aad9b86fd3"),
+ "libs/tcpdf/tcpdf.php" => array("883754", "5c02ba6d7a3dba39c140ca7aa25d22f0"),
+ "libs/tcpdf/unicode_data.php" => array("227828", "2ec28c23ec6ad9f71f8b822a0ce0a1bf"),
+ "libs/upgradephp/README" => array("373", "f0a0f26fd38b1e01be19766c11654564"),
+ "libs/upgradephp/upgrade.php" => array("16164", "4cc4006f9ae5e23b73e69c51b64cd99c"),
+ "libs/UserAgentParser/README.md" => array("972", "34ff27b5748bcd32dd255595cb386ece"),
+ "libs/UserAgentParser/UserAgentParser.php" => array("28445", "974d5ff712e0331175d543d797e821fd"),
+ "libs/UserAgentParser/UserAgentParser.test.php" => array("4690", "7ec960da6d502b46706f6fa3602a72f4"),
+ "libs/Zend/Cache/Backend/Apc.php" => array("11108", "db51f9156bd866fa2ffcb7ee3f8bfd77"),
+ "libs/Zend/Cache/Backend/BlackHole.php" => array("7461", "0058fa2c88b65f6c3aa022534aa1f86e"),
+ "libs/Zend/Cache/Backend/ExtendedInterface.php" => array("4035", "ac5c1f66c2eb9a542dd144a9c1ea8b53"),
+ "libs/Zend/Cache/Backend/File.php" => array("34776", "e1e689dc52f044dd4193509124576261"),
+ "libs/Zend/Cache/Backend/Interface.php" => array("3904", "99e8ce8e5cb9d4a2f5ef9a5497ee6bb8"),
+ "libs/Zend/Cache/Backend/Libmemcached.php" => array("16282", "00af5472742bd210a2a50324434ae91e"),
+ "libs/Zend/Cache/Backend/Memcached.php" => array("17894", "1ca58d3fafd0b6cd543e83d5f0ba3b85"),
+ "libs/Zend/Cache/Backend.php" => array("7904", "5e2f9dfa4b59950217a2f3fdc4bd2c60"),
+ "libs/Zend/Cache/Backend/Sqlite.php" => array("22998", "507c9052187140bd66041d718534e40c"),
+ "libs/Zend/Cache/Backend/Static.php" => array("19549", "c440ac44e75c3787f9b1ea19be1fd34d"),
+ "libs/Zend/Cache/Backend/Test.php" => array("11830", "c9857254695014074cc6d120cbb08ac1"),
+ "libs/Zend/Cache/Backend/TwoLevels.php" => array("19721", "a0f3d47a82ca81aed51b7a29d86e7353"),
+ "libs/Zend/Cache/Backend/WinCache.php" => array("10912", "bd2f8a26e9063a25c498aae14cccf613"),
+ "libs/Zend/Cache/Backend/Xcache.php" => array("7349", "cf198559e45316e0d9f62b206ef3312e"),
+ "libs/Zend/Cache/Backend/ZendPlatform.php" => array("11985", "1e397d0389a1e704657271a58c498dbf"),
+ "libs/Zend/Cache/Backend/ZendServer/Disk.php" => array("2962", "10d8e804fc1e8d268a702437e43aef62"),
+ "libs/Zend/Cache/Backend/ZendServer.php" => array("6504", "7ca2debde7c4913b281606a4504a3800"),
+ "libs/Zend/Cache/Backend/ZendServer/ShMem.php" => array("2925", "d4710ceb0fef64004047ba2a13f1bdb0"),
+ "libs/Zend/Cache/Core.php" => array("25933", "58a9232671767047d6bd40a26353d2e9"),
+ "libs/Zend/Cache/Exception.php" => array("1070", "959583afad956ae0539248ff79eb904b"),
+ "libs/Zend/Cache/Frontend/Capture.php" => array("2448", "a0a0fea8115ea64b6725b8e514b963c4"),
+ "libs/Zend/Cache/Frontend/Class.php" => array("7681", "ec321e41fa5dfb0ba53220d75c9f52f4"),
+ "libs/Zend/Cache/Frontend/File.php" => array("6943", "410eaa222a5a89ae11d79e0d1c4d8493"),
+ "libs/Zend/Cache/Frontend/Function.php" => array("6068", "d6c647fc22012bad41ba19f53c16414f"),
+ "libs/Zend/Cache/Frontend/Output.php" => array("3462", "57b0fefac338ebea55c859d965eb8e8f"),
+ "libs/Zend/Cache/Frontend/Page.php" => array("14366", "16200bf6fe055e0722f2b2478820a7e0"),
+ "libs/Zend/Cache/Manager.php" => array("9603", "653dac865fe4e38a23c364b383ff6fd1"),
+ "libs/Zend/Cache.php" => array("9726", "e750f91ff55b5dd0dee7421a185445c8"),
+ "libs/Zend/Config/Exception.php" => array("1093", "76092808cc3b56cd09cd9643d2fce0d7"),
+ "libs/Zend/Config/Ini.php" => array("10885", "54bca1c1151a1f8202c9ad0973bc9a3a"),
+ "libs/Zend/Config/Json.php" => array("8250", "3876a5476bf7c40b4821cd00fc082902"),
+ "libs/Zend/Config.php" => array("12969", "a031082ad853bb15c8dff89bfb0268a4"),
+ "libs/Zend/Config/Writer/Array.php" => array("1635", "db20bc7ffd5eda5394b82a9542f1f4a8"),
+ "libs/Zend/Config/Writer/FileAbstract.php" => array("3487", "8a29ec44e8144727021c7ec399db180e"),
+ "libs/Zend/Config/Writer/Ini.php" => array("5749", "d8ddcc8c365acf30150106d05226761c"),
+ "libs/Zend/Config/Writer/Json.php" => array("3030", "2f68be8aeaed2e9cbffce4fe8bc54249"),
+ "libs/Zend/Config/Writer.php" => array("2477", "821b961c378d009c50cbe5f9a275d3fc"),
+ "libs/Zend/Config/Writer/Xml.php" => array("4149", "3498b638e9a2d694aff8b40723310cbb"),
+ "libs/Zend/Config/Writer/Yaml.php" => array("4151", "b600f000e238dffa088d965c95bc2f70"),
+ "libs/Zend/Config/Xml.php" => array("11406", "4ac395cd38fe1e623d61f677cef9169e"),
+ "libs/Zend/Config/Yaml.php" => array("12535", "e6cdff175bfac2f33e1c6e605ae848bb"),
+ "libs/Zend/Db/Adapter/Abstract.php" => array("41521", "8815cdbfe075893a3551713e262816dc"),
+ "libs/Zend/Db/Adapter/Db2/Exception.php" => array("1413", "0e8ed8be745de00e92bfb3525b351cb5"),
+ "libs/Zend/Db/Adapter/Db2.php" => array("27603", "0e3132abd56e493b76df721eb37ad1ce"),
+ "libs/Zend/Db/Adapter/Exception.php" => array("1595", "51665355378ff8414795d85ccfef5c2e"),
+ "libs/Zend/Db/Adapter/Mysqli/Exception.php" => array("1192", "9e9e5636095fc47fd3e5c2a75cabd90f"),
+ "libs/Zend/Db/Adapter/Mysqli.php" => array("17419", "f9813544420dcd4d92a78e3f8cd3a49d"),
+ "libs/Zend/Db/Adapter/Oracle/Exception.php" => array("1994", "6c5110c3729078357759f489dba8ed0c"),
+ "libs/Zend/Db/Adapter/Oracle.php" => array("21700", "2b190daba528c06706d7a44651e85b32"),
+ "libs/Zend/Db/Adapter/Pdo/Abstract.php" => array("11914", "9d9ff4dc2c4968da967e580487c5c3c8"),
+ "libs/Zend/Db/Adapter/Pdo/Ibm/Db2.php" => array("7569", "60d45091ab06357533c30c0dd68fc854"),
+ "libs/Zend/Db/Adapter/Pdo/Ibm/Ids.php" => array("9392", "316050a3025e2b8a7b7a198a087861b3"),
+ "libs/Zend/Db/Adapter/Pdo/Ibm.php" => array("11884", "466784fa6d0821fcda67e872c10ecc3c"),
+ "libs/Zend/Db/Adapter/Pdo/Mssql.php" => array("14262", "bda19408a8162fc3abf1bfa68d22cabf"),
+ "libs/Zend/Db/Adapter/Pdo/Mysql.php" => array("9330", "5f412003fd6977f9749879adbd17da3b"),
+ "libs/Zend/Db/Adapter/Pdo/Oci.php" => array("13957", "550360b2439f751626e554b3d9e66ec2"),
+ "libs/Zend/Db/Adapter/Pdo/Pgsql.php" => array("12271", "8487c4baaee4df2190cabb28bf8b3b67"),
+ "libs/Zend/Db/Adapter/Pdo/Sqlite.php" => array("10122", "59609edf7a71e971aaf3b27ceb33e2e9"),
+ "libs/Zend/Db/Adapter/Sqlsrv/Exception.php" => array("2035", "f325fe5cf94848bb6323f9a3bce94730"),
+ "libs/Zend/Db/Adapter/Sqlsrv.php" => array("22053", "dc0db212f96ab188071b60797e6e52d9"),
+ "libs/Zend/Db/Exception.php" => array("1077", "d54398825f7c3fad909a84ae58715e41"),
+ "libs/Zend/Db/Expr.php" => array("2608", "dd587f06121cb5e88ad392f7f1b3d2b3"),
+ "libs/Zend/Db.php" => array("10143", "d4a194aaa867c6a819425c7c1d686ab0"),
+ "libs/Zend/Db/Profiler/Exception.php" => array("1151", "f256021ad53646cb54a2e92aa8603c01"),
+ "libs/Zend/Db/Profiler/Firebug.php" => array("4890", "d8abc94dc862905d569bfe04f5a8960a"),
+ "libs/Zend/Db/Profiler.php" => array("14204", "f1661ce5c33c88c0cedb625ab83fbe0d"),
+ "libs/Zend/Db/Profiler/Query.php" => array("4985", "57794c09e0ba954c16472616c927e7a7"),
+ "libs/Zend/Db/Select/Exception.php" => array("1139", "983119e8c9188c1233a7317c249f577c"),
+ "libs/Zend/Db/Select.php" => array("44421", "8818e5ffd4070e93d1e14b84fcfb903f"),
+ "libs/Zend/Db/Statement/Db2/Exception.php" => array("1526", "33acfe812ee187965341d5cfd62fae09"),
+ "libs/Zend/Db/Statement/Db2.php" => array("10236", "b6a6033a5ca3afbe64f5917cb743b618"),
+ "libs/Zend/Db/Statement/Exception.php" => array("1572", "d0d988d59ac560916273d2918e279091"),
+ "libs/Zend/Db/Statement/Interface.php" => array("6625", "d0689b067d31691e6b3ade50be1a5835"),
+ "libs/Zend/Db/Statement/Mysqli/Exception.php" => array("1165", "998d0c773e745cb6a6dcf6d7c7edcc37"),
+ "libs/Zend/Db/Statement/Mysqli.php" => array("10859", "3281b0909307260f8485576aa786fbf6"),
+ "libs/Zend/Db/Statement/Oracle/Exception.php" => array("1899", "847f3a25ac2890812075350510b2cf31"),
+ "libs/Zend/Db/Statement/Oracle.php" => array("17263", "1bf918c7b7c276847d1afe2e45d8c67e"),
+ "libs/Zend/Db/Statement/Pdo/Ibm.php" => array("3397", "88515f1af79d21f220e020df57460974"),
+ "libs/Zend/Db/Statement/Pdo/Oci.php" => array("3031", "31e7984762ddc13679cb9bec8a56247b"),
+ "libs/Zend/Db/Statement/Pdo.php" => array("14364", "fa99e7d7f3d1d28929e78d6c76a8b5f4"),
+ "libs/Zend/Db/Statement.php" => array("14085", "e746df61336a9296964e6839fabd54b2"),
+ "libs/Zend/Db/Statement/Sqlsrv/Exception.php" => array("1957", "9a19266373fa09bdf7859194169a35c8"),
+ "libs/Zend/Db/Statement/Sqlsrv.php" => array("12348", "71d0197118360b6aeec7d45580864c53"),
+ "libs/Zend/Db/Table/Abstract.php" => array("50573", "a1dd4a222db071be104566cd06ad1f1b"),
+ "libs/Zend/Db/Table/Definition.php" => array("3241", "3995bddf9cb81b4567337b175cd7c13c"),
+ "libs/Zend/Db/Table/Exception.php" => array("1135", "6a0d57ab372184670c8e79710784c533"),
+ "libs/Zend/Db/Table.php" => array("2754", "504c6665d867bb043273aeee466381fb"),
+ "libs/Zend/Db/Table/Row/Abstract.php" => array("39920", "bc3e1b05140c1ffd0185c987c61b433e"),
+ "libs/Zend/Db/Table/Row/Exception.php" => array("1157", "f0d6c6ff51ecc3cd091109f78c3bf12a"),
+ "libs/Zend/Db/Table/Row.php" => array("1308", "1be229fa2c6fa297b0a2d56051a0a9b6"),
+ "libs/Zend/Db/Table/Rowset/Abstract.php" => array("11117", "010b900fae45fb59301be979c5338e3f"),
+ "libs/Zend/Db/Table/Rowset/Exception.php" => array("1159", "23e8752ebbfa48e036f63b1bf1a92918"),
+ "libs/Zend/Db/Table/Rowset.php" => array("1327", "80f842e48117b43e2ab0eb810a479fde"),
+ "libs/Zend/Db/Table/Select/Exception.php" => array("1158", "30805dc268e86338b44700afa0a6c537"),
+ "libs/Zend/Db/Table/Select.php" => array("6673", "522b3005d9a684590778faf24b017bf0"),
+ "libs/Zend/Exception.php" => array("2555", "496025ea0e253c9d0b75a0fbf8f355b2"),
+ "libs/Zend/LICENSE.txt" => array("1548", "81fc08e70d11a8ecb317b16a97bfbcc3"),
+ "libs/Zend/Mail/Exception.php" => array("1090", "b9f0d25d50c78939d660fc0f06793f98"),
+ "libs/Zend/Mail/Message/File.php" => array("2638", "1318de5056a1121ba8820dd3d31c5aa3"),
+ "libs/Zend/Mail/Message/Interface.php" => array("1572", "1290f359b6cd4b186048a673a447a8f1"),
+ "libs/Zend/Mail/Message.php" => array("3308", "7d12b766dd42dadd47649f9d8f101297"),
+ "libs/Zend/Mail/Part/File.php" => array("6163", "d6ad1139df9e878af0f62de25f486b54"),
+ "libs/Zend/Mail/Part/Interface.php" => array("4221", "97f42b9ccbc229328cb7fcc85f5a9d9d"),
+ "libs/Zend/Mail/Part.php" => array("14365", "1a030ff9c9bcab54142b88c39d019197"),
+ "libs/Zend/Mail.php" => array("32415", "1539511cc9d93db522bffdd11d9cbe99"),
+ "libs/Zend/Mail/Protocol/Abstract.php" => array("11300", "c4f17b922a216b7647093e72a46ebcd7"),
+ "libs/Zend/Mail/Protocol/Exception.php" => array("1157", "906550551820e61592e7d3612d0c8d65"),
+ "libs/Zend/Mail/Protocol/Imap.php" => array("27668", "a26f41950c29af2fb9ef999101fa79e9"),
+ "libs/Zend/Mail/Protocol/Pop3.php" => array("13024", "56bce77ea137ac0a7f93cfbb8b6f71f4"),
+ "libs/Zend/Mail/Protocol/Smtp/Auth/Crammd5.php" => array("3163", "7aeeca7c701a27751646950b822de583"),
+ "libs/Zend/Mail/Protocol/Smtp/Auth/Login.php" => array("2522", "9b779d5cca265a5bd637e4649c7012d2"),
+ "libs/Zend/Mail/Protocol/Smtp/Auth/Plain.php" => array("2471", "b7fda475bebbebb35d5cae2cb7f15e84"),
+ "libs/Zend/Mail/Protocol/Smtp.php" => array("11894", "65482fffb922640d19b2a490e2c6d2be"),
+ "libs/Zend/Mail/Storage/Abstract.php" => array("9320", "5c5f202001e8fe4bece29aaf85d47006"),
+ "libs/Zend/Mail/Storage/Exception.php" => array("1159", "36c0e03007ce490970cc2b9eceed34b1"),
+ "libs/Zend/Mail/Storage/Folder/Interface.php" => array("1866", "c42d375159c8c7e2f52336ae21898815"),
+ "libs/Zend/Mail/Storage/Folder/Maildir.php" => array("8942", "a8aa29833af22a57b32547d14be91490"),
+ "libs/Zend/Mail/Storage/Folder/Mbox.php" => array("8744", "fc5db27edf62c75a54ed49842c7172cb"),
+ "libs/Zend/Mail/Storage/Folder.php" => array("5739", "9f67663c26984532c6bb36cd1c03bc1e"),
+ "libs/Zend/Mail/Storage/Imap.php" => array("21921", "cd05cb2e160632fc939bc8bb6c307496"),
+ "libs/Zend/Mail/Storage/Maildir.php" => array("14507", "2bcbe4a98a75574f9216a1201b82ce38"),
+ "libs/Zend/Mail/Storage/Mbox.php" => array("13272", "c773cb8f8b42ad3cdc687bcdacfd8ab5"),
+ "libs/Zend/Mail/Storage.php" => array("1389", "f6dfb86357dd6c45e9ff8a4c71ed9a3e"),
+ "libs/Zend/Mail/Storage/Pop3.php" => array("9807", "3f4973116e37a78ef33a152cfa4aafa9"),
+ "libs/Zend/Mail/Storage/Writable/Interface.php" => array("3802", "0a3c13e8799e46a27cb9d7dddfef8702"),
+ "libs/Zend/Mail/Storage/Writable/Maildir.php" => array("39879", "04ef151a97b29640ce1fdac54f0d4d85"),
+ "libs/Zend/Mail/Transport/Abstract.php" => array("10410", "0087be943456216a8cb418472df971b6"),
+ "libs/Zend/Mail/Transport/Exception.php" => array("1165", "3bea49e543c5158eccc7879b4a9d70ff"),
+ "libs/Zend/Mail/Transport/File.php" => array("3797", "aa5d88139cb4462fbc3a71323e1e5154"),
+ "libs/Zend/Mail/Transport/Sendmail.php" => array("6540", "e4b0c2b97a61a0877e0f5dd848b7be93"),
+ "libs/Zend/Mail/Transport/Smtp.php" => array("6229", "5ceaaa08b1f9d9d7f2e2103834026009"),
+ "libs/Zend/Mime/Decode.php" => array("8805", "b9d2cbd7e7e6d9318579b5fac81ff74d"),
+ "libs/Zend/Mime/Exception.php" => array("1085", "b8189c831996b86dfd924aa5413caf24"),
+ "libs/Zend/Mime/Message.php" => array("8518", "7612d04ebd0dd380502366ed2cd8a20d"),
+ "libs/Zend/Mime/Part.php" => array("6671", "a2ca88579585efada8b1bdbcb9339094"),
+ "libs/Zend/Mime.php" => array("12927", "0059c7eef064f1225af33d1ddc0e8cef"),
+ "libs/Zend/Registry.php" => array("6155", "cebfa4d6d7658af3167d521ffdc09153"),
+ "libs/Zend/Session/Abstract.php" => array("6051", "f46f1047a6f5b37b2180cfe18f9f37a2"),
+ "libs/Zend/Session/Exception.php" => array("2313", "5c87f32e2d6c61f0937481ff6da38d05"),
+ "libs/Zend/Session/Namespace.php" => array("16860", "1351d7294b06b262d8fa63f88fed5503"),
+ "libs/Zend/Session.php" => array("27322", "02d23cef6fbabae08268d017defdb0bc"),
+ "libs/Zend/Session/SaveHandler/DbTable.php" => array("17926", "97ed5c81d24270470450211991de3c12"),
+ "libs/Zend/Session/SaveHandler/Exception.php" => array("1174", "b49e92bc8cd9c7e29afb7a5182da8325"),
+ "libs/Zend/Session/SaveHandler/Interface.php" => array("2056", "f27f2b6f050869a7f7aef5a5385a8e05"),
+ "libs/Zend/Session/Validator/Abstract.php" => array("2111", "38cda6376c7519170579ea0a012364c1"),
+ "libs/Zend/Session/Validator/HttpUserAgent.php" => array("1991", "52ae6c4e4afcb62b1a935c4b807beb22"),
+ "libs/Zend/Session/Validator/Interface.php" => array("1594", "bbbb5dfd24df96e4d8d115bc62aa1b7c"),
+ "libs/Zend/Validate/Abstract.php" => array("12334", "1feb4fc3aa9245d7c7d9eb40a98cf1f3"),
+ "libs/Zend/Validate/Alnum.php" => array("4121", "9a4536c9051c7c80319594e4dc872bb6"),
+ "libs/Zend/Validate/Alpha.php" => array("4027", "d34f2e3a78c21b707e37af8d9819e53d"),
+ "libs/Zend/Validate/Between.php" => array("5698", "bc105fb1a1c1522b85a48ff5612205f6"),
+ "libs/Zend/Validate/Callback.php" => array("4479", "89b01c7bdd1aabfa80700314ddc47e2d"),
+ "libs/Zend/Validate/Ccnum.php" => array("3143", "b55b29546253067178aa79cecb347dab"),
+ "libs/Zend/Validate/CreditCard.php" => array("10109", "3358f5b5d31aabf5ce6ba35a53b0bba9"),
+ "libs/Zend/Validate/Date.php" => array("7713", "c3b1ea92bb219a5651307e9d9c2907e2"),
+ "libs/Zend/Validate/Digits.php" => array("2637", "a7a1ed17245d8e42493d6ed1c4801b94"),
+ "libs/Zend/Validate/EmailAddress.php" => array("17338", "8e481bb7208b0eaf6176a74ef7342a45"),
+ "libs/Zend/Validate/Exception.php" => array("1099", "c4a0c36f38e3345b65edba863b16635a"),
+ "libs/Zend/Validate/File/Count.php" => array("8002", "e243f53362e28bdec644aa9a98344f59"),
+ "libs/Zend/Validate/File/Crc32.php" => array("4927", "9c63207b87e8bb54f62bd927dbbcf1e1"),
+ "libs/Zend/Validate/File/ExcludeExtension.php" => array("2957", "f959a30f6813b0212b5593b07223bc06"),
+ "libs/Zend/Validate/File/ExcludeMimeType.php" => array("3592", "3749ff0f8954c5a553660663dbf1399d"),
+ "libs/Zend/Validate/File/Exists.php" => array("5785", "eb5fb5df3d52fee96eaf45a6c3fc5d3c"),
+ "libs/Zend/Validate/File/Extension.php" => array("6012", "3d680a7541dbd03d9dba46f8d8c76322"),
+ "libs/Zend/Validate/File/FilesSize.php" => array("5330", "494fa7561432a0eb8f0bf1b8adf89045"),
+ "libs/Zend/Validate/File/Hash.php" => array("5472", "ce5218d5a498c422f2cb55c37dd12616"),
+ "libs/Zend/Validate/File/ImageSize.php" => array("11171", "743921b7ac4ce28a4a0d54e1bd4d2de0"),
+ "libs/Zend/Validate/File/IsCompressed.php" => array("4663", "a1c9c4a90166ff846733dd44060a1026"),
+ "libs/Zend/Validate/File/IsImage.php" => array("5287", "17212ae2c053b562d262186dbdbde8af"),
+ "libs/Zend/Validate/File/Md5.php" => array("5104", "484d03c5852a9fc51710da516939c2b8"),
+ "libs/Zend/Validate/File/MimeType.php" => array("11534", "1ced67edf9732895c766b9edbd95e718"),
+ "libs/Zend/Validate/File/NotExists.php" => array("2521", "95cd7265a2e92d853a0f3f832a430408"),
+ "libs/Zend/Validate/File/Sha1.php" => array("4959", "a73df076e93830182509e351111c8632"),
+ "libs/Zend/Validate/File/Size.php" => array("11213", "8fae681d72b8aeb28745050b188ac274"),
+ "libs/Zend/Validate/File/Upload.php" => array("7857", "596ce8ec37c74472f326f317ab59f886"),
+ "libs/Zend/Validate/File/WordCount.php" => array("3115", "d1de816d4952a5141ed78e1b278f5156"),
+ "libs/Zend/Validate/Float.php" => array("3521", "296ea1a209fce97d748e914522404688"),
+ "libs/Zend/Validate/GreaterThan.php" => array("2899", "02b85da332cf6f1a815e5ee24aa6b6bd"),
+ "libs/Zend/Validate/Hex.php" => array("2037", "8bfb961b57b882a0719d54ccc885add7"),
+ "libs/Zend/Validate/Hostname/Biz.php" => array("223195", "ffa0c236e122b45995cff4ee4964e595"),
+ "libs/Zend/Validate/Hostname/Cn.php" => array("168071", "55fa16bdeecbebf10df472ede8681606"),
+ "libs/Zend/Validate/Hostname/Com.php" => array("13661", "1b25658d4741fdcb5702e0fdc64f7e0e"),
+ "libs/Zend/Validate/Hostname/Jp.php" => array("55551", "0fe384871528b21e2a981bbf1d0de767"),
+ "libs/Zend/Validate/Hostname.php" => array("35735", "cb6fff7c0769efb452f7e04ba185e54a"),
+ "libs/Zend/Validate/Iban.php" => array("7303", "2725d29f25df052fe2a0b5b28464960b"),
+ "libs/Zend/Validate/Identical.php" => array("4094", "3c325081db3380872533a48ffb4e54f3"),
+ "libs/Zend/Validate/InArray.php" => array("5291", "2a1f8aab4b1d7e39da319972c0fcbba5"),
+ "libs/Zend/Validate/Interface.php" => array("1926", "f406d5497efbd054be505a4fdce3fab3"),
+ "libs/Zend/Validate/Int.php" => array("4020", "c3c5ea3796107150a78d1c92eff0c899"),
+ "libs/Zend/Validate/Ip.php" => array("5511", "06007f3e8595f35b51403ab2bc2f825e"),
+ "libs/Zend/Validate/Isbn.php" => array("7716", "51f8056b95dfb18359628b0715b9d166"),
+ "libs/Zend/Validate/LessThan.php" => array("2869", "0840ec582e239eb647a7435d5a7eca2d"),
+ "libs/Zend/Validate/NotEmpty.php" => array("8138", "9190fda962b420724260002bc466e7d8"),
+ "libs/Zend/Validate.php" => array("8509", "88ef476e71d54440da1123a10f59ceea"),
+ "libs/Zend/Validate/PostCode.php" => array("6018", "1cffbba8fc10e1b3780bb4c6e5550956"),
+ "libs/Zend/Validate/Regex.php" => array("4021", "eb2c6b2377b924a3ebcd1c708802caf7"),
+ "libs/Zend/Validate/StringLength.php" => array("6633", "9c0f8e241452e13eb5a19c3994e74d11"),
+ "libs/Zend/Version.php" => array("2558", "5d840e09ef8ece6d15e8bfb786af0faf"),
+ "misc/cron/archive.php" => array("1427", "17d3d028e0063eddbd2171a971937e96"),
+ "misc/cron/archive.sh" => array("3411", "310b24292e5968a2b27fc81cc2d0133b"),
+ "misc/cron/updatetoken.php" => array("1227", "40be39dd7be96bf3cf07a25321646a53"),
+ "misc/gpl-3.0.txt" => array("35147", "d32239bcb673463ab874e80d47fae504"),
+ "misc/How to install Piwik.html" => array("281", "f4a48dcdad08996699fa52876fe2e59f"),
+ "misc/log-analytics/import_logs.py" => array("63576", "d27a2bb0e1aeb6c8ef28ee2140eac2f6"),
+ "misc/log-analytics/README.md" => array("12850", "81e24593a103725475033d73a7911308"),
+ "misc/others/api_internal_call.php" => array("925", "f0cadfd1d505c9190c5b799116329c27"),
+ "misc/others/api_rest_call.php" => array("926", "ee4a436279d3588350998d23a4457331"),
+ "misc/others/cli-script-bootstrap.php" => array("1197", "c9f9cc36be2fa58324405d675f6cdb21"),
+ "misc/others/download-count.txt" => array("563", "fa8ababaa1c23873c85b576807012997"),
+ "misc/others/ExamplePiwikTracker.php" => array("650", "fef8fb6167d19edfe9050da691d3e80d"),
+ "misc/others/geoipUpdateRows.php" => array("8375", "f35ef4770cebf00f9cf6b733aef3b14a"),
+ "misc/others/iframeWidget.htm" => array("613", "3c0f965d8cbc05e01e431f2a060fa3e5"),
+ "misc/others/iframeWidget_localhost.php" => array("2156", "d0ab5ceb17e0010599d67442a6e113f7"),
+ "misc/others/phpstorm-codestyles/Piwik_codestyle.xml" => array("3585", "18b7d31e7283b1db8e778fdf7ab18721"),
+ "misc/others/phpstorm-codestyles/README.md" => array("984", "948c9dfd9918312ffe9dab44eb454054"),
+ "misc/others/stress.sh" => array("293", "d1964246f8080b3e2101610fad483c8c"),
+ "misc/others/test_cookies_GenerateHundredsWebsitesAndVisits.php" => array("1055", "0dee3fddccfe0ab8aa440f1a9f4d7827"),
+ "misc/others/test_generateLotsVisitsWebsites.php" => array("10290", "2e61928d6e310ddf6ab7a61964850fdb"),
+ "misc/others/tracker_simpleImageTracker.php" => array("1060", "a9436b017e7ad30d59cff5b87e715308"),
+ "misc/others/uninstall-delete-piwik-directory.php" => array("1140", "8bdcdad35ced4e49b82cb9b809eebabe"),
+ "misc/others/widget_example_lastvisits.html" => array("456", "faacc1f323fede8966eabd56cbacc362"),
+ "misc/proxy-hide-piwik-url/piwik.php" => array("2763", "742ba5f11208ea5e08dc90d8055bc6c8"),
+ "misc/proxy-hide-piwik-url/README.md" => array("3234", "dd49db51608aa49d3b490053a98f0d68"),
+ "misc/user/.gitkeep" => array("0", "d41d8cd98f00b204e9800998ecf8427e"),
+ "misc/user/index.html" => array("167", "bcef3676ee6eefa6968695ef954db78f"),
+ "piwik.js" => array("23183", "6e7bb550f0b87029593c66404f477088"),
+ "piwik.php" => array("5150", "30817ddf6963d6ef026ec587b8c262c7"),
+ "plugins/Actions/Actions.php" => array("47067", "8fd3c026d5bda12e6e35d4582fece05e"),
+ "plugins/Actions/API.php" => array("25131", "eb9695dccffefdb8b0be655caf4c9c7e"),
+ "plugins/Actions/Archiver.php" => array("22986", "f340a0c2888b7ad69baaa017ae85978f"),
+ "plugins/Actions/ArchivingHelper.php" => array("23888", "e2a415e828d32a1ba8f07137df8cf60d"),
+ "plugins/Actions/Controller.php" => array("3508", "d2b0b84b804e880aee1d80a68e8bdd03"),
+ "plugins/Actions/javascripts/actionsDataTable.js" => array("13340", "00a944a54e01638152a462776094fecf"),
+ "plugins/Actions/stylesheets/dataTableActions.less" => array("80", "6ea9a28abb3fdf5d59b75af48dbde129"),
+ "plugins/Actions/templates/indexSiteSearch.twig" => array("598", "f8495db5767bbb21576a5690e9b92435"),
+ "plugins/Annotations/AnnotationList.php" => array("15708", "0507b807d118f763ff4012e34ffca9ae"),
+ "plugins/Annotations/Annotations.php" => array("1134", "fa376e6beeda62967ac35bf96f42486f"),
+ "plugins/Annotations/API.php" => array("14544", "83c95cab8ef9e4862d31cab0dfa21c55"),
+ "plugins/Annotations/Controller.php" => array("8319", "42bb469975f6bc567f0bc78f3362b08d"),
+ "plugins/Annotations/javascripts/annotations.js" => array("22584", "b14eadbeac0cfacb80e9858140da762f"),
+ "plugins/Annotations/stylesheets/annotations.less" => array("3505", "f311fd592023253c5a386753b70eec9f"),
+ "plugins/Annotations/templates/_annotationList.twig" => array("1281", "6305cb8e31ab433b40e2fde4ae22ecf2"),
+ "plugins/Annotations/templates/_annotation.twig" => array("2543", "2e4cd965a46c6c3ed241be7ef6253006"),
+ "plugins/Annotations/templates/getAnnotationManager.twig" => array("1133", "b330fa07ac6c11095ed0c165f3a51446"),
+ "plugins/Annotations/templates/getEvolutionIcons.twig" => array("859", "fb82d9e0fe92f4e31701a902173f17a1"),
+ "plugins/Annotations/templates/saveAnnotation.twig" => array("45", "5ad07d0a082d1cb3b199874b80285ea0"),
+ "plugins/API/API.php" => array("29238", "2b460cf97b6ab918db29b21d2effae1b"),
+ "plugins/API/Controller.php" => array("4818", "4d7a6fc1ec88da6888efa79a784387af"),
+ "plugins/API/ProcessedReport.php" => array("30939", "247cb79b399231721079044292d3686e"),
+ "plugins/API/RowEvolution.php" => array("19547", "2f282c72293c1b915683ba7e88033bec"),
+ "plugins/API/stylesheets/listAllAPI.less" => array("724", "c18396c6a93e74893483fbdcd7ff3fb6"),
+ "plugins/API/templates/listAllAPI.twig" => array("1049", "4adf1392c2bd87a6aac4b578dbcd65b6"),
+ "plugins/CoreAdminHome/API.php" => array("8164", "96c8e23e9390b352a77e4242f79665df"),
+ "plugins/CoreAdminHome/Controller.php" => array("12138", "5ff36d4263102d2c0e2e576b56354dfe"),
+ "plugins/CoreAdminHome/CoreAdminHome.php" => array("4902", "36802b98ee443d60935a58497c19306d"),
+ "plugins/CoreAdminHome/CustomLogo.php" => array("6329", "c8d6e7f8a505489bb4eb6a2e31c2ca05"),
+ "plugins/CoreAdminHome/javascripts/generalSettings.js" => array("4863", "b219e00ad3f5544e7b5e37b0effb3463"),
+ "plugins/CoreAdminHome/javascripts/jsTrackingGenerator.js" => array("12282", "291cc9b609d1b3f40ff1b4084043b3a8"),
+ "plugins/CoreAdminHome/javascripts/pluginSettings.js" => array("2358", "4a5e5984b547f409e8bd3a73542e961c"),
+ "plugins/CoreAdminHome/stylesheets/generalSettings.less" => array("2793", "fc401910d4ff9fa8b7be809f0468f717"),
+ "plugins/CoreAdminHome/stylesheets/jsTrackingGenerator.css" => array("1405", "9265daa10ff275ccf75dcfcd2926b9a9"),
+ "plugins/CoreAdminHome/stylesheets/menu.less" => array("2149", "4f32e2ba236cf2ea0798676b670050d3"),
+ "plugins/CoreAdminHome/stylesheets/pluginSettings.less" => array("520", "4e1b32c3e7108e45055051cfbdede938"),
+ "plugins/CoreAdminHome/templates/generalSettings.twig" => array("17731", "16a31e5ccac7ff372d952e492d037d90"),
+ "plugins/CoreAdminHome/templates/_menu.twig" => array("840", "5ee52c14bfe1e41fa07db26bd0f49e90"),
+ "plugins/CoreAdminHome/templates/optOut.twig" => array("1122", "4b2985cac29da2d123a4c780d368fa50"),
+ "plugins/CoreAdminHome/templates/pluginSettings.twig" => array("7608", "52ae5d20cd1ad28f616fe08ecf3047ad"),
+ "plugins/CoreAdminHome/templates/trackingCodeGenerator.twig" => array("13443", "34381ec263e148ee5991913c51a1bd01"),
+ "plugins/CoreConsole/Commands/CodeCoverage.php" => array("3188", "675e82de2141b1caadedbf2f91a59529"),
+ "plugins/CoreConsole/Commands/CoreArchiver.php" => array("4245", "37f3acf3ee8df8d4973f192a56227cb5"),
+ "plugins/CoreConsole/Commands/GenerateApi.php" => array("1863", "0998c1fcfd9fe56c82bc22812a45bb60"),
+ "plugins/CoreConsole/Commands/GenerateCommand.php" => array("2898", "d4180aa10d123f70c33ea2385d760cb0"),
+ "plugins/CoreConsole/Commands/GenerateController.php" => array("1958", "93b4d27330e51e13c497052ca3f5854a"),
+ "plugins/CoreConsole/Commands/GeneratePluginBase.php" => array("4542", "6f359b866d9df97303139fbf955bd30d"),
+ "plugins/CoreConsole/Commands/GeneratePlugin.php" => array("7264", "786dcfdc680750902ddea34ee0325aa8"),
+ "plugins/CoreConsole/Commands/GenerateSettings.php" => array("1935", "d760e64d6b9f41d48431d7cfff2247da"),
+ "plugins/CoreConsole/Commands/GenerateTest.php" => array("6086", "dab2b156fe2548658562090e686c9b88"),
+ "plugins/CoreConsole/Commands/GenerateVisualizationPlugin.php" => array("3556", "653ebdfc17dccde2d643134d7c708f1c"),
+ "plugins/CoreConsole/Commands/GitCommit.php" => array("4456", "dc6ef207850ca10950f694d8cd9b4cc0"),
+ "plugins/CoreConsole/Commands/GitPull.php" => array("1592", "69f2c84488535f2944a8b3c61619da35"),
+ "plugins/CoreConsole/Commands/GitPush.php" => array("1186", "774591997e01ba6d6a898e447048b6a7"),
+ "plugins/CoreConsole/Commands/ManagePlugin.php" => array("2372", "d2926cda27fee604b517b113b2ad83ad"),
+ "plugins/CoreConsole/Commands/ManageTestFiles.php" => array("1812", "a86ae0b8137c618dcf1af27fa18ba68b"),
+ "plugins/CoreConsole/Commands/RunTests.php" => array("3057", "b3a65e6e74b23b67d682edbc9e6679b8"),
+ "plugins/CoreConsole/Commands/RunUITests.php" => array("2647", "538924f987e82a8c4ae48851404ad03f"),
+ "plugins/CoreConsole/Commands/SetupFixture.php" => array("6080", "2b1aff33dd7ffda5737cb83f9998ac8d"),
+ "plugins/CoreConsole/Commands/SyncUITestScreenshots.php" => array("3016", "595086d2646fe3e3b15d2e7e904cb948"),
+ "plugins/CoreConsole/Commands/WatchLog.php" => array("855", "092903712bd6a244a1199eeab7458b50"),
+ "plugins/CoreHome/angularjs/anchorLinkFix.js" => array("2634", "96f55c02ee23e9e4b2c8f88e9c05e2b8"),
+ "plugins/CoreHome/angularjs/common/directives/autocomplete-matched.js" => array("1355", "9e42e502d560b4a4d140db85b9ac485e"),
+ "plugins/CoreHome/angularjs/common/directives/autocomplete-matched_spec.js" => array("1515", "ac1cb173a0b49e64d18f8f7bd2e49c34"),
+ "plugins/CoreHome/angularjs/common/directives/dialog.js" => array("1197", "343ca648f67762baada217cb848d0564"),
+ "plugins/CoreHome/angularjs/common/directives/directive.js" => array("174", "35c24d7f8602201a1a86fccd1c2ced63"),
+ "plugins/CoreHome/angularjs/common/directives/focus-anywhere-but-here.js" => array("1257", "a54b382326b1724ecfe08b75d9bda271"),
+ "plugins/CoreHome/angularjs/common/directives/focusif.js" => array("727", "3e050dd7229a73fc4c850acba33f215d"),
+ "plugins/CoreHome/angularjs/common/directives/ignore-click.js" => array("606", "7c44f9411918577878d2786d29149709"),
+ "plugins/CoreHome/angularjs/common/directives/onenter.js" => array("737", "c4da43d5d72695e6accaa72635e0b619"),
+ "plugins/CoreHome/angularjs/common/filters/evolution.js" => array("1067", "f03049c52419158e37b3b7485ad9ec97"),
+ "plugins/CoreHome/angularjs/common/filters/filter.js" => array("170", "e3cb8ba6c47023ea0eb11b00da89d752"),
+ "plugins/CoreHome/angularjs/common/filters/startfrom.js" => array("319", "b0d892fbbba5a5c59c0ebfe9cccc2ff4"),
+ "plugins/CoreHome/angularjs/common/filters/startfrom_spec.js" => array("997", "28e65c932ef636402a5436ce9036747b"),
+ "plugins/CoreHome/angularjs/common/filters/translate.js" => array("518", "4b3608c3d3925c2de1c812ca4c7095b7"),
+ "plugins/CoreHome/angularjs/common/services/piwik-api.js" => array("4931", "1f26832cd0e4667a9550f140d9b63f2d"),
+ "plugins/CoreHome/angularjs/common/services/piwik.js" => array("289", "7f8e1209a339cbb3df5c8b1c67608ff5"),
+ "plugins/CoreHome/angularjs/common/services/piwik_spec.js" => array("958", "918d81d86edb8ac04ea9fd4b2b3233d2"),
+ "plugins/CoreHome/angularjs/common/services/service.js" => array("172", "7df34c228b662caedc891189287d26bc"),
+ "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline-directive.js" => array("2117", "f53eb6919e53ee6085b20af35e75a071"),
+ "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline.html" => array("968", "0a3bb46fea31257f0c3c254e5686a2d4"),
+ "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline.less" => array("763", "74046d60c1f671857b7583fad14cb2b5"),
+ "plugins/CoreHome/angularjs/enrichedheadline/help.png" => array("350", "a2442fd403f530897728f540aa374c70"),
+ "plugins/CoreHome/angularjs/piwikAppConfig.js" => array("335", "286c144553a0f1389f804a4f2931c27a"),
+ "plugins/CoreHome/angularjs/piwikApp.js" => array("321", "8b891e5568091f5bef5566fe2a8440f3"),
+ "plugins/CoreHome/angularjs/siteselector/siteselector-controller.js" => array("1826", "a297e0adb8c651e9c49b975f72290e17"),
+ "plugins/CoreHome/angularjs/siteselector/siteselector-directive.js" => array("2676", "3c6cb7efaefd524922328842205f15ed"),
+ "plugins/CoreHome/angularjs/siteselector/siteselector.html" => array("3030", "2a1df7bc671183fb140a04132a0a6a6e"),
+ "plugins/CoreHome/angularjs/siteselector/siteselector.less" => array("3527", "7741715b7b9ccbdfd9c44f92e9080eff"),
+ "plugins/CoreHome/angularjs/siteselector/siteselector-model.js" => array("1909", "9ef295ef4cec80096d9c24705411618b"),
+ "plugins/CoreHome/Controller.php" => array("8189", "5cc9b5304f98cd5dd4fe96c28665a7eb"),
+ "plugins/CoreHome/CoreHome.php" => array("10068", "d32fe2afae1943efe49db6276a33c164"),
+ "plugins/CoreHome/DataTableRowAction/MultiRowEvolution.php" => array("2134", "09919d9abed888e1a1ad0149cf241ea9"),
+ "plugins/CoreHome/DataTableRowAction/RowEvolution.php" => array("11730", "6dbc8fcb21eb457148022097a445d778"),
+ "plugins/CoreHome/images/bg_header.jpg" => array("9097", "0af504cfba0e9a1df7024e5e8d37ace9"),
+ "plugins/CoreHome/images/bullet1.gif" => array("52", "4ea97a49fbc122f369c0509a1e312d36"),
+ "plugins/CoreHome/images/bullet2.gif" => array("52", "e105364ac53547afc6453aac69c4e43a"),
+ "plugins/CoreHome/images/favicon.ico" => array("17947", "9f21326e0c543b50bbe7022b44d97615"),
+ "plugins/CoreHome/images/googleplay.png" => array("16550", "94203701766674b6ea54cce20b5e4a7e"),
+ "plugins/CoreHome/images/more_date.gif" => array("56", "0f97ae90441e7bf074981d7854f7bdb7"),
+ "plugins/CoreHome/images/more_period.gif" => array("53", "0741dc6f4a2e767ac60c619e85142555"),
+ "plugins/CoreHome/images/more.png" => array("1045", "91d6a597dc70d86071f6aa333cce20aa"),
+ "plugins/CoreHome/images/promo_splash.png" => array("12070", "98b50af9fcfe08214b630aa5c598848e"),
+ "plugins/CoreHome/images/reset_search.png" => array("1021", "7e761f3444bf4edd4cd1779801c963bd"),
+ "plugins/CoreHome/images/search.png" => array("136", "2fae5ccecd05ad37bd228e9490875ad4"),
+ "plugins/CoreHome/javascripts/broadcast.js" => array("22763", "4c3cd06055cafa714fd2e2efd4ff82ba"),
+ "plugins/CoreHome/javascripts/calendar.js" => array("22538", "b87e51fbf1cad8c856337656bca22a8b"),
+ "plugins/CoreHome/javascripts/color_manager.js" => array("10985", "c22a21e71febe410b3e62de68ce31359"),
+ "plugins/CoreHome/javascripts/corehome.js" => array("5684", "30d487fa1f07c5b3907886359d4b168a"),
+ "plugins/CoreHome/javascripts/dataTable.js" => array("67710", "03047b604c67c04845a34b7a1ff3074c"),
+ "plugins/CoreHome/javascripts/dataTable_rowactions.js" => array("12810", "c98799009416f594610fb887bd7414f9"),
+ "plugins/CoreHome/javascripts/donate.js" => array("5832", "b1519aaa05ab20ff55e6515f73f22840"),
+ "plugins/CoreHome/javascripts/menu_init.js" => array("445", "d11ca5e76d64fcb588095037bb3a9e4f"),
+ "plugins/CoreHome/javascripts/menu.js" => array("3808", "0c2b7400db1cd4144b9820d8a71d6dbf"),
+ "plugins/CoreHome/javascripts/notification.js" => array("6438", "409154616a282cc31d394f15b1207ee9"),
+ "plugins/CoreHome/javascripts/notification_parser.js" => array("824", "4860da380ee3224ae5628967055c7285"),
+ "plugins/CoreHome/javascripts/popover.js" => array("8640", "805e980248f8043a1df32b011d677c13"),
+ "plugins/CoreHome/javascripts/promo.js" => array("541", "d55cfc743448fc782c3a084ca5205619"),
+ "plugins/CoreHome/javascripts/require.js" => array("1297", "7f170c5276e38599d334b6e4c167ceb7"),
+ "plugins/CoreHome/javascripts/sparkline.js" => array("3304", "47ab94274d11f6bdbf840aad41129f0e"),
+ "plugins/CoreHome/javascripts/top_controls.js" => array("704", "5a27c5eeaa6e8e0c0682c170128be44d"),
+ "plugins/CoreHome/javascripts/uiControl.js" => array("3663", "b0d3706d547d7f102d39c17b000ea894"),
+ "plugins/CoreHome/stylesheets/cloud.less" => array("874", "7f28097a511022a86beb4735cb3eb877"),
+ "plugins/CoreHome/stylesheets/color_manager.css" => array("42", "40709b59fb63c5e7fcdcab9230c1dbaa"),
+ "plugins/CoreHome/stylesheets/coreHome.less" => array("4274", "966a251617979d9103c999cb5a3c56e8"),
+ "plugins/CoreHome/stylesheets/dataTable/_dataTable.less" => array("10157", "7fe43c52bc99e776a9e2b7de3db419f4"),
+ "plugins/CoreHome/stylesheets/dataTable.less" => array("366", "b5a4df46b9d7f8e1193f54d3aad665c8"),
+ "plugins/CoreHome/stylesheets/dataTable/_limitSelection.less" => array("1306", "68d63775a50badf3b9ac1992997d1c9b"),
+ "plugins/CoreHome/stylesheets/dataTable/_reportDocumentation.less" => array("1182", "2423f8ebb34d4a3fd818d580b176ca45"),
+ "plugins/CoreHome/stylesheets/dataTable/_rowActions.less" => array("656", "afe520a206b30502fa9e4c3812efa605"),
+ "plugins/CoreHome/stylesheets/dataTable/_subDataTable.less" => array("860", "e5b7fb01c2a7682310d5e9121960d6fa"),
+ "plugins/CoreHome/stylesheets/dataTable/_tableConfiguration.less" => array("1499", "5c63c6d3ff0fc046514ee80f572158a4"),
+ "plugins/CoreHome/stylesheets/_donate.less" => array("2251", "8a37ac490210fd01f9439e821d2f5aeb"),
+ "plugins/CoreHome/stylesheets/jqplotColors.less" => array("2449", "cee067a8b15cf5a393fdde65c03e0d03"),
+ "plugins/CoreHome/stylesheets/jquery.ui.autocomplete.css" => array("1200", "518cf68de85e435653447e9102316661"),
+ "plugins/CoreHome/stylesheets/menu.less" => array("2935", "de31a81958b769f798adaea6673f6fe8"),
+ "plugins/CoreHome/stylesheets/notification.less" => array("1612", "a5400eef0a05d0159e5675726ef8e2f3"),
+ "plugins/CoreHome/stylesheets/promo.less" => array("1222", "10e9dd09f69cd77f2d2b35ae86a99b7b"),
+ "plugins/CoreHome/stylesheets/sparklineColors.less" => array("522", "a8fe19271fefc2f1829b9e271838467a"),
+ "plugins/CoreHome/templates/checkForUpdates.twig" => array("45", "15faa60f49f1c4c281c76bc2a7e00ce7"),
+ "plugins/CoreHome/templates/_dataTableCell.twig" => array("2957", "d6b282e985fee6e9f1b9524c3d5b8346"),
+ "plugins/CoreHome/templates/_dataTableFooter.twig" => array("8068", "cbe13cdda27009f397de576cbab1e37d"),
+ "plugins/CoreHome/templates/_dataTableHead.twig" => array("821", "f39052d9ca22525d016796aa27662900"),
+ "plugins/CoreHome/templates/_dataTableJS.twig" => array("159", "7233ccf9c682cdb1744b43ccb9ca84d7"),
+ "plugins/CoreHome/templates/_dataTable.twig" => array("2016", "0e4018ea821c7e713d8f62c7e1bd31f1"),
+ "plugins/CoreHome/templates/_donate.twig" => array("2986", "2dc12be6edbf01dcb96bca7caf65a9d2"),
+ "plugins/CoreHome/templates/getDefaultIndexView.twig" => array("291", "a63dbf0283c9695118f5403b9cea9c9e"),
+ "plugins/CoreHome/templates/getDonateForm.twig" => array("38", "ceda9c8c6ba889dbac8b55ca7e61e030"),
+ "plugins/CoreHome/templates/getMultiRowEvolutionPopover.twig" => array("1907", "2777f9e84b4d06496a779b8581566038"),
+ "plugins/CoreHome/templates/getPromoVideo.twig" => array("1379", "f40ed16a053efaa6df2c682c3e620b41"),
+ "plugins/CoreHome/templates/getRowEvolutionPopover.twig" => array("1685", "d9be8e51169aef72e1e6acf3d93b1d82"),
+ "plugins/CoreHome/templates/_headerMessage.twig" => array("2915", "83a77be74cbcedac5eff74e80e33be49"),
+ "plugins/CoreHome/templates/_indexContent.twig" => array("567", "936cb35de43d98985a05715734b634d6"),
+ "plugins/CoreHome/templates/_javaScriptDisabled.twig" => array("134", "9f7e7b75b3dd7928b0d7db27802cec9e"),
+ "plugins/CoreHome/templates/_logo.twig" => array("607", "d14a3ee156c537331868b5cfe52034b9"),
+ "plugins/CoreHome/templates/_menu.twig" => array("952", "499786b48fe52cdc248d4ecb45a7d7dd"),
+ "plugins/CoreHome/templates/_notifications.twig" => array("333", "974e20fdb4837324b61cbfb052d3b911"),
+ "plugins/CoreHome/templates/_periodSelect.twig" => array("1794", "71ebf4a07f218ccd3a6b8e71bd9ad9a4"),
+ "plugins/CoreHome/templates/ReportRenderer/_htmlReportBody.twig" => array("3773", "5e44bacceb55616bd659c7c6b7883fe6"),
+ "plugins/CoreHome/templates/ReportRenderer/_htmlReportFooter.twig" => array("15", "4d214dd44eaca17a5b8c100a0cee7ebc"),
+ "plugins/CoreHome/templates/ReportRenderer/_htmlReportHeader.twig" => array("1165", "f16ab5bc3e57cb8df3a4d5c101f506b2"),
+ "plugins/CoreHome/templates/ReportsByDimension/_reportsByDimension.twig" => array("1112", "7c22d58e39b426bd649d88d1199aa749"),
+ "plugins/CoreHome/templates/_singleReport.twig" => array("61", "3ed4c965676469386ba1e6142f55f049"),
+ "plugins/CoreHome/templates/_siteSelectHeader.twig" => array("234", "9b4fa86af9fec5ea3755b4dde733efa6"),
+ "plugins/CoreHome/templates/_topBarHelloMenu.twig" => array("1027", "f7dfa6faaa6a6ccc28f5f00e16874a0b"),
+ "plugins/CoreHome/templates/_topBarTopMenu.twig" => array("763", "ab004040fabe91dff6bc8752ddbc8ef0"),
+ "plugins/CoreHome/templates/_topBar.twig" => array("200", "9987986288e7ce9072205fb329f9fc87"),
+ "plugins/CoreHome/templates/_topScreen.twig" => array("109", "6fae551319f6efff81bf460ebad78b66"),
+ "plugins/CoreHome/templates/_uiControl.twig" => array("327", "35b5eef8d6bb93196947469ab721d0d6"),
+ "plugins/CoreHome/templates/_warningInvalidHost.twig" => array("915", "ef59e3a2d2828b269f2e9dd41238602d"),
+ "plugins/CorePluginsAdmin/Controller.php" => array("17319", "a0cecaa6e6c468d9a598d2e5ded186b8"),
+ "plugins/CorePluginsAdmin/CorePluginsAdmin.php" => array("4817", "4b53a6ef27725c8d61723351162f3156"),
+ "plugins/CorePluginsAdmin/images/plugins.png" => array("14076", "44b89bd30206dd317c032a8595e1ddeb"),
+ "plugins/CorePluginsAdmin/images/rating_important.png" => array("673", "0e63393e13ce89a4920684b4b06f68ee"),
+ "plugins/CorePluginsAdmin/images/themes.png" => array("79517", "67eb6beb264c9181c79246fc6f255c04"),
+ "plugins/CorePluginsAdmin/javascripts/pluginDetail.js" => array("2270", "5122b5212d9bd47b215b8899fcc448a2"),
+ "plugins/CorePluginsAdmin/javascripts/pluginExtend.js" => array("756", "4177e6ed911cce4e0c2fdf257f0b538f"),
+ "plugins/CorePluginsAdmin/javascripts/pluginOverview.js" => array("900", "a7221dc1b624beb54936fce19faf8fcf"),
+ "plugins/CorePluginsAdmin/javascripts/plugins.js" => array("2974", "e9b57f6e7a1fe31bf46672c7af2001a7"),
+ "plugins/CorePluginsAdmin/MarketplaceApiClient.php" => array("5380", "32b4ee5cc03f93a5cb0b437023025628"),
+ "plugins/CorePluginsAdmin/MarketplaceApiException.php" => array("258", "504b17a7c04736eac83d9d532c4666d9"),
+ "plugins/CorePluginsAdmin/Marketplace.php" => array("5717", "9ee5d691a3e9b28404b7caddfafe6fb3"),
+ "plugins/CorePluginsAdmin/PluginInstallerException.php" => array("286", "6f252dd00be63fc7c60035c042bfa17e"),
+ "plugins/CorePluginsAdmin/PluginInstaller.php" => array("9509", "6b462a3b03902bcfeeebab6491e6cfca"),
+ "plugins/CorePluginsAdmin/stylesheets/marketplace.less" => array("5941", "0d9e782e0476bed3714d79572289e3fb"),
+ "plugins/CorePluginsAdmin/stylesheets/plugins_admin.less" => array("910", "c3f90c42b657a52f81e4de1f774acfad"),
+ "plugins/CorePluginsAdmin/templates/browsePluginsActions.twig" => array("787", "ec50c06b24b52ac8a77e0473f35ecdf8"),
+ "plugins/CorePluginsAdmin/templates/browsePlugins.twig" => array("1569", "807f2a4521e8c2fa60ecb096c7733aae"),
+ "plugins/CorePluginsAdmin/templates/browseThemes.twig" => array("1355", "ace812045eef7acf7f999493badb3154"),
+ "plugins/CorePluginsAdmin/templates/extend.twig" => array("3852", "d29aebabcbd868d40e6e13703436d65e"),
+ "plugins/CorePluginsAdmin/templates/installPlugin.twig" => array("1593", "678e6e40b503dfce996bd1638140dae2"),
+ "plugins/CorePluginsAdmin/templates/macros.twig" => array("13807", "c53678062e9729c889b4f0c964e3eed6"),
+ "plugins/CorePluginsAdmin/templates/pluginDetails.twig" => array("10614", "425ca997711e3a742a8c05b9a7547a40"),
+ "plugins/CorePluginsAdmin/templates/pluginMetadata.twig" => array("579", "573e9a85cc47e6581d4ea2c27e21f271"),
+ "plugins/CorePluginsAdmin/templates/pluginOverview.twig" => array("1373", "f63e81873d0894f1fe510860e0bb72ce"),
+ "plugins/CorePluginsAdmin/templates/plugins.twig" => array("985", "614227399bb790846563585476ab7293"),
+ "plugins/CorePluginsAdmin/templates/safemode.twig" => array("4851", "b7bc45d170dd3d6906251f8f66c24fd4"),
+ "plugins/CorePluginsAdmin/templates/themeOverview.twig" => array("1432", "7a7fe7a864cacf63b83afcb9ad1ae51a"),
+ "plugins/CorePluginsAdmin/templates/themes.twig" => array("1165", "ab2cb33c411d1b72aae3b2810982e603"),
+ "plugins/CorePluginsAdmin/templates/updatePlugin.twig" => array("1431", "509baa9e82b722e0518e31f45c85678c"),
+ "plugins/CorePluginsAdmin/templates/uploadPlugin.twig" => array("1640", "f61cd131dda39221cf364b82f03d727e"),
+ "plugins/CorePluginsAdmin/UpdateCommunication.php" => array("6203", "cd40906be1bc9082e7ae01ebe55b4434"),
+ "plugins/CoreUpdater/Commands/Update.php" => array("2038", "f713523751a5850ea31df6068d813a4f"),
+ "plugins/CoreUpdater/Controller.php" => array("15315", "baa77e239df7fe6679394684ef0db52f"),
+ "plugins/CoreUpdater/CoreUpdater.php" => array("5380", "7e2b3bc913e03712d30aef6eecf7af00"),
+ "plugins/CoreUpdater/javascripts/updateLayout.js" => array("246", "97c81a1a8c66bb5ddb6567591c623387"),
+ "plugins/CoreUpdater/NoUpdatesFoundException.php" => array("243", "4c4002fd3bdb636949915467b95d9607"),
+ "plugins/CoreUpdater/stylesheets/updateLayout.css" => array("564", "9fd4b49281584e2af9d964fd9e8a754a"),
+ "plugins/CoreUpdater/templates/layout.twig" => array("1774", "4764b278f8e7d90fdab9523feef448a1"),
+ "plugins/CoreUpdater/templates/newVersionAvailable.twig" => array("1667", "dc764e7c76232bca2fac9626e7127b6c"),
+ "plugins/CoreUpdater/templates/oneClickResults.twig" => array("768", "fd13410beecb2907de3e0121379fc0a2"),
+ "plugins/CoreUpdater/templates/runUpdaterAndExit_done_cli.twig" => array("1632", "34eaa2c5de8a88c4b320e5f8d4c0df54"),
+ "plugins/CoreUpdater/templates/runUpdaterAndExit_done.twig" => array("3017", "e28b48da5e46a69f714e03d6e0ad845b"),
+ "plugins/CoreUpdater/templates/runUpdaterAndExit_welcome_cli.twig" => array("1329", "b84d36f1cac2070b4f82efc39955e8da"),
+ "plugins/CoreUpdater/templates/runUpdaterAndExit_welcome.twig" => array("4394", "ec2fafcabb5b9ff34f63873f710dc259"),
+ "plugins/CoreUpdater/UpdateCommunication.php" => array("4260", "44ea4f46e1a34360f2b8d2b1779d1bf0"),
+ "plugins/CoreVisualizations/CoreVisualizations.php" => array("3098", "59fc60592977b80fecb9a22cb789a339"),
+ "plugins/CoreVisualizations/javascripts/jqplotBarGraph.js" => array("2624", "8663e49a42d16fcd7faaf628b88c2cbc"),
+ "plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js" => array("4990", "0555513c693cdcc529d45b82044646c0"),
+ "plugins/CoreVisualizations/javascripts/jqplot.js" => array("37901", "e06b89f2c320761abc576e24f0d73057"),
+ "plugins/CoreVisualizations/javascripts/jqplotPieGraph.js" => array("2675", "f51532d8a5c7ff32da50b032c31c5584"),
+ "plugins/CoreVisualizations/javascripts/seriesPicker.js" => array("13503", "834304040b3260bb6f708cc5f43d7547"),
+ "plugins/CoreVisualizations/JqplotDataGenerator/Chart.php" => array("3191", "b7b441685a7b019bb9c29b8478be0538"),
+ "plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php" => array("7403", "be2979d456835753937f5c10890270cc"),
+ "plugins/CoreVisualizations/JqplotDataGenerator.php" => array("4865", "b53f8d32bfe0d79da637d502863170ce"),
+ "plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less" => array("537", "d54bd4990eb168a209dea17adfccc1df"),
+ "plugins/CoreVisualizations/stylesheets/jqplot.css" => array("4847", "03cf5f3e336655a1b5e43b7d7d35bc3a"),
+ "plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig" => array("2641", "e28eee7fdc78defe23ba01c368f153b4"),
+ "plugins/CoreVisualizations/templates/_dataTableViz_jqplotGraph.twig" => array("149", "ee20fc44c0c435c5dc4b876a42b5632d"),
+ "plugins/CoreVisualizations/templates/_dataTableViz_tagCloud.twig" => array("839", "b01f096bf415c6b90b405e5be27938dd"),
+ "plugins/CoreVisualizations/Visualizations/Cloud/Config.php" => array("848", "096f5191662d35f16818ec8262976315"),
+ "plugins/CoreVisualizations/Visualizations/Cloud.php" => array("5071", "c7e1b761725380954a056fd1b523a0aa"),
+ "plugins/CoreVisualizations/Visualizations/Graph/Config.php" => array("3359", "44e5bfb03fdc0a72f58f8a50e34f6e15"),
+ "plugins/CoreVisualizations/Visualizations/Graph.php" => array("5311", "f72e91bb0e8c6a7dd7870a330f51b028"),
+ "plugins/CoreVisualizations/Visualizations/HtmlTable/AllColumns.php" => array("2206", "891d8f79c7fa4509dbe51fd21d87759d"),
+ "plugins/CoreVisualizations/Visualizations/HtmlTable/Config.php" => array("3363", "22c395e75a04d580abd824cba212d43d"),
+ "plugins/CoreVisualizations/Visualizations/HtmlTable.php" => array("2170", "4281101774e7592f03d22abb332e760d"),
+ "plugins/CoreVisualizations/Visualizations/HtmlTable/RequestConfig.php" => array("1559", "1fa011dffcbb16625833555774fec70e"),
+ "plugins/CoreVisualizations/Visualizations/JqplotGraph/Bar.php" => array("1054", "d94682d59ff312d8cb3632b5f1bb34a0"),
+ "plugins/CoreVisualizations/Visualizations/JqplotGraph/Config.php" => array("1808", "c070eb1e52624df57e477ef395a1d8b8"),
+ "plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution/Config.php" => array("1146", "6d32a05c97c3ed61fcf07a4934772099"),
+ "plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution.php" => array("6285", "273e819c224b072f98acbd38db00dcd2"),
+ "plugins/CoreVisualizations/Visualizations/JqplotGraph.php" => array("1415", "802e838dd17eb03d047bf80cdf75507a"),
+ "plugins/CoreVisualizations/Visualizations/JqplotGraph/Pie.php" => array("1526", "a63ef246318b7af4a3f930b8ba9e1f95"),
+ "plugins/CoreVisualizations/Visualizations/Sparkline.php" => array("3542", "4901c3281e7997d351f27bcd868aabc9"),
+ "plugins/CustomVariables/API.php" => array("3729", "a14f0b41fa2c9d429f4bcb1b79604a89"),
+ "plugins/CustomVariables/Archiver.php" => array("8023", "67584cad7e95fcea09249f9a98d79dae"),
+ "plugins/CustomVariables/Commands/Info.php" => array("2337", "7ad2373927c02fc97c9dd0b718918b43"),
+ "plugins/CustomVariables/Commands/SetNumberOfCustomVariables.php" => array("7048", "7988044609e3aec8acb12b5adb9add94"),
+ "plugins/CustomVariables/Controller.php" => array("734", "4660b5e7cda48db92afab520d689c7bc"),
+ "plugins/CustomVariables/CustomVariables.php" => array("8766", "c0dfd6ad3cef8186ca6512524c71a604"),
+ "plugins/CustomVariables/Model.php" => array("4890", "ab27c1ca66ad3015d05f5fcda3ef660f"),
+ "plugins/Dashboard/API.php" => array("3932", "d87fdaf7673503e6014957c961d16189"),
+ "plugins/Dashboard/Controller.php" => array("10134", "f867dfa6d4e7c74293bfb9e44abf247b"),
+ "plugins/Dashboard/DashboardManagerControl.php" => array("1610", "fb1b28942d7990b8bbe90406cf800830"),
+ "plugins/Dashboard/Dashboard.php" => array("9928", "ffdb68de39c57e26b77d7ac9ecaf08c8"),
+ "plugins/Dashboard/DashboardSettingsControlBase.php" => array("671", "4979cb16799468c2e48306fc69b51a9d"),
+ "plugins/Dashboard/javascripts/dashboard.js" => array("10758", "b790e5f955248dec1129ef302cd268e3"),
+ "plugins/Dashboard/javascripts/dashboardObject.js" => array("18451", "dd99d1afae535b93176693343d7496b0"),
+ "plugins/Dashboard/javascripts/dashboardWidget.js" => array("11715", "60dc1080ad458f829e2ecca912fd2a34"),
+ "plugins/Dashboard/javascripts/widgetMenu.js" => array("16065", "45348093e269dbbfaa759ad98965ecc9"),
+ "plugins/Dashboard/stylesheets/dashboard.less" => array("8508", "65b1b29e4898fb49936a9c02bbcfc1fc"),
+ "plugins/Dashboard/stylesheets/standalone.css" => array("1163", "c05b2508df30267e2272e8998fd441d8"),
+ "plugins/Dashboard/templates/_dashboardSettings.twig" => array("800", "2c5218bdfc8b5500d438590af1babacd"),
+ "plugins/Dashboard/templates/embeddedIndex.twig" => array("4843", "6ee72f07ff096808072bd4667298bf90"),
+ "plugins/Dashboard/templates/_header.twig" => array("581", "5d695bcc4f1f82c41acb586eeac3d191"),
+ "plugins/Dashboard/templates/index.twig" => array("775", "f9cfac7175ab189a36954a9a6021796c"),
+ "plugins/Dashboard/templates/_widgetFactoryTemplate.twig" => array("963", "a2a1af34eef792aaa743d97e6d0f15e4"),
+ "plugins/DBStats/API.php" => array("9746", "7fc743a372e25e41bf222d6aaab68162"),
+ "plugins/DBStats/Controller.php" => array("5044", "d4efb7c5a12a4c87209c848e36766d75"),
+ "plugins/DBStats/DBStats.php" => array("15822", "98f174296aea7b597ff7b508944e3377"),
+ "plugins/DBStats/.gitignore" => array("33", "478d4f2ee44c0d87d241ed2565482e1c"),
+ "plugins/DBStats/lang/am.json" => array("544", "7baa1ff89068b48d9d30244ee87c00cd"),
+ "plugins/DBStats/lang/ar.json" => array("1131", "56ba42e94f87e55fe5350d50eb1be05f"),
+ "plugins/DBStats/lang/be.json" => array("1093", "d46871abcfc087252143e630b0658a21"),
+ "plugins/DBStats/lang/bg.json" => array("1470", "4529c23a8802c448433bee85ca847bbe"),
+ "plugins/DBStats/lang/bn.json" => array("61", "82e91890b454800e8d058c5f53f2e5d0"),
+ "plugins/DBStats/lang/bs.json" => array("178", "f35b90a6e1ec759920d7b1e1eb536308"),
+ "plugins/DBStats/lang/ca.json" => array("1129", "25c425a101f8093240cbc5499c9343e5"),
+ "plugins/DBStats/lang/cs.json" => array("1008", "cffa24240285345e52e5ef6e9d1204bc"),
+ "plugins/DBStats/lang/da.json" => array("1025", "12d7024ec084a9be18e42dfbeade0690"),
+ "plugins/DBStats/lang/de.json" => array("1063", "db7842b901098a4f140678bdedebf630"),
+ "plugins/DBStats/lang/el.json" => array("1672", "dc03df11ce42ed0f466fccd2cf512cbe"),
+ "plugins/DBStats/lang/en.json" => array("971", "19ece5a2cac5a31c426fd8bb21435e89"),
+ "plugins/DBStats/lang/es.json" => array("1120", "68b351e3419418a1da6084016a5a05ac"),
+ "plugins/DBStats/lang/et.json" => array("1043", "f0cfc6c767c2659465ec0c0d909c2502"),
+ "plugins/DBStats/lang/eu.json" => array("607", "ba6f32b3c0e57fb6ccaedc4ba30b64e2"),
+ "plugins/DBStats/lang/fa.json" => array("1512", "c2dae8fe3dfb7b109afa25ccdfedd69e"),
+ "plugins/DBStats/lang/fi.json" => array("971", "9fdf9bb616f21541583c6d7cbe2b98c6"),
+ "plugins/DBStats/lang/fr.json" => array("1183", "9cbacf1e08fdc1f5addf27e3ed1b723f"),
+ "plugins/DBStats/lang/he.json" => array("867", "dfa4fc8df95dd71cc99bc14ad034d229"),
+ "plugins/DBStats/lang/hi.json" => array("1844", "46f80ba8f44e7333f082f2f35e595b40"),
+ "plugins/DBStats/lang/hu.json" => array("780", "fcbd2ce72e8155f459fe5cb090f09a31"),
+ "plugins/DBStats/lang/id.json" => array("1034", "b9e8adfb0c460cd383755fb9f5a81987"),
+ "plugins/DBStats/lang/is.json" => array("480", "9610b7003f73148183d44ac3f6cc7ded"),
+ "plugins/DBStats/lang/it.json" => array("1080", "3b7b0b7c2eb09c2c57d748f11c2a78bc"),
+ "plugins/DBStats/lang/ja.json" => array("1206", "6a36a174f6cca7cc6d16e95f99f88814"),
+ "plugins/DBStats/lang/ka.json" => array("1486", "17d027bb3b07c7b4f6e84b138f32a668"),
+ "plugins/DBStats/lang/ko.json" => array("1082", "259dbf55c70e0f2dac918896e2a104fc"),
+ "plugins/DBStats/lang/lt.json" => array("683", "f2dd194a13234292af5c78d4f918e948"),
+ "plugins/DBStats/lang/lv.json" => array("667", "e22bc702ad6799f314090447d3890c4d"),
+ "plugins/DBStats/lang/nb.json" => array("770", "6e9614e188669135143894149cf6afb8"),
+ "plugins/DBStats/lang/nl.json" => array("1034", "b75930ea35d558b9b31239dabb98615b"),
+ "plugins/DBStats/lang/nn.json" => array("1010", "bc65370bea4018a4c4a89f471170316c"),
+ "plugins/DBStats/lang/pl.json" => array("741", "351e6de4ab5a458b65790e7f61374607"),
+ "plugins/DBStats/lang/pt-br.json" => array("1098", "35a2292924438fdabc003199c9ce0927"),
+ "plugins/DBStats/lang/pt.json" => array("707", "b9f87b78d97a52b9815fa071ec0f1fdd"),
+ "plugins/DBStats/lang/ro.json" => array("1071", "58ff2ea72c28ee743b598ff434ca1135"),
+ "plugins/DBStats/lang/ru.json" => array("1521", "56c5e96f586510b1c3d29fcd89f80cfb"),
+ "plugins/DBStats/lang/sk.json" => array("486", "1df64facbdd26e49116c87e6629c71e6"),
+ "plugins/DBStats/lang/sl.json" => array("576", "e1e0d54c1bf14659683595826509e709"),
+ "plugins/DBStats/lang/sq.json" => array("796", "3d358248703abc04f0609902883fdbcb"),
+ "plugins/DBStats/lang/sr.json" => array("1013", "abb86e964d3e0fa91e301d6d2ea6d9fc"),
+ "plugins/DBStats/lang/sv.json" => array("1037", "b8bda69de93baab3d956a23d3e8a03bc"),
+ "plugins/DBStats/lang/ta.json" => array("993", "e1d3c2a837fa6be11f1d5668dc4d812a"),
+ "plugins/DBStats/lang/te.json" => array("125", "10cb7b33f9cab70f0cd8f12b752063df"),
+ "plugins/DBStats/lang/th.json" => array("1383", "efaa040a75b0b20ef860d27218d7a784"),
+ "plugins/DBStats/lang/tr.json" => array("651", "848ac6471a4e74f96d213e3488dae3dc"),
+ "plugins/DBStats/lang/uk.json" => array("978", "56d1978d76916aea7480b4414003ea41"),
+ "plugins/DBStats/lang/vi.json" => array("1245", "336115958d203a6e6b72beebc7278b4a"),
+ "plugins/DBStats/lang/zh-cn.json" => array("943", "ccfe32f44e5d3cdd8febf164e3a239f5"),
+ "plugins/DBStats/lang/zh-tw.json" => array("673", "760492ac770163bc1cf019e96ae1d464"),
+ "plugins/DBStats/MySQLMetadataDataAccess.php" => array("2279", "d6b533086e17fb9fe1eb988c4e030744"),
+ "plugins/DBStats/MySQLMetadataProvider.php" => array("12336", "7d8c570b43a07269a54627220ba473fe"),
+ "plugins/DBStats/stylesheets/dbStatsTable.less" => array("233", "4324953bbb513a4e0aa2d932d2447e5a"),
+ "plugins/DBStats/templates/index.twig" => array("4282", "3790e00ea5964ec59aa5b014b7a9d957"),
+ "plugins/DevicesDetection/API.php" => array("5930", "1d311bf37c78c7ffd3d6a55523f53e57"),
+ "plugins/DevicesDetection/Archiver.php" => array("2780", "9f07edb22c5f4ce1240d8d742193895a"),
+ "plugins/DevicesDetection/Controller.php" => array("5541", "ded6665d42e88f384cd5e45574600a52"),
+ "plugins/DevicesDetection/DevicesDetection.php" => array("14416", "3065fdd010d298d6ad65ef38625335ae"),
+ "plugins/DevicesDetection/functions.php" => array("6788", "f239b1d1cb96dfc6e4ff2ed2ef676d25"),
+ "plugins/DevicesDetection/images/brand/Acer.ico" => array("673", "5453e4cc0e9fddd4aac446d0b10d4e36"),
+ "plugins/DevicesDetection/images/brand/Alcatel.ico" => array("577", "df5ccd4326721199d02870aab0e225c3"),
+ "plugins/DevicesDetection/images/brand/Apple.ico" => array("1179", "3b58ada0634f0a1687a44c15e4e1936f"),
+ "plugins/DevicesDetection/images/brand/Archos.ico" => array("205", "c3fc2d2e6b64fa163a7d17603f247ea7"),
+ "plugins/DevicesDetection/images/brand/Asus.ico" => array("1016", "1107fad067ea7b3551081c56b9f525cf"),
+ "plugins/DevicesDetection/images/brand/Audiovox.ico" => array("807", "5e684babc5c21bf7a8255a0ad0f9545a"),
+ "plugins/DevicesDetection/images/brand/Avvio.ico" => array("964", "c4a27544c3af4093265a0cbb45cd5b31"),
+ "plugins/DevicesDetection/images/brand/BangOlufsen.ico" => array("3692", "e062a24b4ed0af1fe513053eb2e70750"),
+ "plugins/DevicesDetection/images/brand/Becker.ico" => array("519", "6c5ed512b9de01a0d60d4efcc78b1ea0"),
+ "plugins/DevicesDetection/images/brand/Beetel.ico" => array("1645", "034453672fc6b71827ea0126248843b8"),
+ "plugins/DevicesDetection/images/brand/BenQ.ico" => array("846", "70f0361f99a39dcdde7e1731364e19c9"),
+ "plugins/DevicesDetection/images/brand/Cat.ico" => array("809", "2b5844049936b7e33220ba80accfabfb"),
+ "plugins/DevicesDetection/images/brand/CnM.ico" => array("421", "6a3a2122bcab660df2d010087914b7b7"),
+ "plugins/DevicesDetection/images/brand/Compal.ico" => array("432", "a7190dfd9e3b833a17bd9576b2fc47a0"),
+ "plugins/DevicesDetection/images/brand/CreNova.ico" => array("3142", "b34cd9a39800b0e0109ab291368e3177"),
+ "plugins/DevicesDetection/images/brand/Cricket.ico" => array("1483", "6b3f14fc16f8b9e61cbf4eaf4af9efbf"),
+ "plugins/DevicesDetection/images/brand/Dell.ico" => array("886", "10a5c1a530dc6a0758dec93fea8a08ff"),
+ "plugins/DevicesDetection/images/brand/Denver.ico" => array("552", "6efa83cf0f661e769272515970bcbc97"),
+ "plugins/DevicesDetection/images/brand/DMM.ico" => array("3623", "3a154f04b274d2e67ac9298ca2df08fd"),
+ "plugins/DevicesDetection/images/brand/DoCoMo.ico" => array("636", "28df0a65fbcb99b0e2ca93c6cdcc8e38"),
+ "plugins/DevicesDetection/images/brand/Ericsson.ico" => array("684", "de92f235c5d09fa7b1140538058c8bcf"),
+ "plugins/DevicesDetection/images/brand/eTouch.ico" => array("889", "779c7c6654749cffea8363ed1c6ee971"),
+ "plugins/DevicesDetection/images/brand/Fly.ico" => array("572", "d608aa4b0b9a861c77501c744aa4c275"),
+ "plugins/DevicesDetection/images/brand/Gemini.ico" => array("323", "07def536ff813416b91b196660b1f4a2"),
+ "plugins/DevicesDetection/images/brand/Google.ico" => array("863", "ad67a48c2dc917325f9cb38a88f8a8a3"),
+ "plugins/DevicesDetection/images/brand/Gradiente.ico" => array("1012", "25ed337d67bf8d6fdb5012b331aadd05"),
+ "plugins/DevicesDetection/images/brand/Grundig.ico" => array("3029", "afae28b8f05bae1e8c476fc3cdb6b5ab"),
+ "plugins/DevicesDetection/images/brand/Haier.ico" => array("957", "23b40417f5adfe2e0e0a263f9d70456d"),
+ "plugins/DevicesDetection/images/brand/HP.ico" => array("936", "ae90fea570472f65946c4f2b71a2aac7"),
+ "plugins/DevicesDetection/images/brand/HTC.ico" => array("1161", "b0b6419c96392cbaa646129a9f609f51"),
+ "plugins/DevicesDetection/images/brand/Huawei.ico" => array("1022", "cf2cca917f1b7655f69ebf439d464823"),
+ "plugins/DevicesDetection/images/brand/Humax.ico" => array("3010", "cb8aee651c6434c0ff6630e609808db9"),
+ "plugins/DevicesDetection/images/brand/Ikea.ico" => array("3291", "56fc079b565f603be4bea0faa90dfb71"),
+ "plugins/DevicesDetection/images/brand/i-mobile.ico" => array("615", "15a5cf09f8e2ad6d60956ae07086b50c"),
+ "plugins/DevicesDetection/images/brand/INQ.ico" => array("1059", "7ece6e5474c9ab6fdcda278c7e341b81"),
+ "plugins/DevicesDetection/images/brand/Intek.ico" => array("3121", "fa198786b40dc3cae6c36d9bbe481fa8"),
+ "plugins/DevicesDetection/images/brand/Inverto.ico" => array("3292", "f80dab0f0c81f444c2ea6cbf2fd833ad"),
+ "plugins/DevicesDetection/images/brand/Jolla.ico" => array("494", "940ce2510cdc18ca0e5a87a0d34e8b90"),
+ "plugins/DevicesDetection/images/brand/Karbonn.ico" => array("1042", "59d3efb46e18658f4ee0c780ce175dd7"),
+ "plugins/DevicesDetection/images/brand/KDDI.ico" => array("473", "d0f2c629657b13fc0fe671eb9221e9b2"),
+ "plugins/DevicesDetection/images/brand/Kindle.ico" => array("720", "bf3c48f92a4ca2beedb7485ff7eae609"),
+ "plugins/DevicesDetection/images/brand/Kyocera.ico" => array("639", "ca13b59af47f69c5ac6917cef9512add"),
+ "plugins/DevicesDetection/images/brand/Lanix.ico" => array("437", "fb1e54c478ce9d908a08e4a0b1005c5c"),
+ "plugins/DevicesDetection/images/brand/Lenovo.ico" => array("237", "42abac25970f9750ec395cefffa30b9d"),
+ "plugins/DevicesDetection/images/brand/LG.ico" => array("1510", "79a2c6d36a1ba70423162b4c1fe46ec2"),
+ "plugins/DevicesDetection/images/brand/LGUPlus.ico" => array("1081", "337b3f1e7368a44b7c40dd8e53940445"),
+ "plugins/DevicesDetection/images/brand/Loewe.ico" => array("2938", "e5ec4483d155267a2944dadad9d9235e"),
+ "plugins/DevicesDetection/images/brand/Manta_Multimedia.ico" => array("800", "cf2f49d840f9492ebcaf53e9b8ed8944"),
+ "plugins/DevicesDetection/images/brand/MediaTek.ico" => array("2976", "19e087b87519151688299d3f9366a3e3"),
+ "plugins/DevicesDetection/images/brand/Medion.ico" => array("3161", "be609b90e6df4e4121bb656575f256b1"),
+ "plugins/DevicesDetection/images/brand/Metz.ico" => array("3244", "4cb9c9047e80672b5cc5f48b6e0696de"),
+ "plugins/DevicesDetection/images/brand/MicroMax.ico" => array("1531", "db3505d0efd7517bf2820de499a08c88"),
+ "plugins/DevicesDetection/images/brand/Microsoft.ico" => array("285", "8a7346d4ef508c9e55ac4eb0933a58ff"),
+ "plugins/DevicesDetection/images/brand/Mio.ico" => array("753", "9f53b557757e285f1de1fe7b67434406"),
+ "plugins/DevicesDetection/images/brand/Mitsubishi.ico" => array("342", "a0d050b3c27c242d11a1a58f30e58ff9"),
+ "plugins/DevicesDetection/images/brand/Motorola.ico" => array("465", "0561bdea0b5842d6c9bc0301bde19b0f"),
+ "plugins/DevicesDetection/images/brand/MyPhone.ico" => array("933", "84cd21bc7363dda6be3a1181be22de6b"),
+ "plugins/DevicesDetection/images/brand/NEC.ico" => array("450", "a3ae0709656decaaf958595aed950f4d"),
+ "plugins/DevicesDetection/images/brand/Nexian.ico" => array("2041", "4001957b806ce17dcaca59b9b852be8a"),
+ "plugins/DevicesDetection/images/brand/NGM.ico" => array("1298", "3cbe7ef2372c4074ee2734aa3a58b328"),
+ "plugins/DevicesDetection/images/brand/Nintendo.ico" => array("740", "7bdf9ff565d9cc45a08824a6bade6f47"),
+ "plugins/DevicesDetection/images/brand/Nokia.ico" => array("1283", "ae41cc8ec06c6e81d9f5afadf6097f5b"),
+ "plugins/DevicesDetection/images/brand/O2.ico" => array("768", "36b1b15cc750a45b5c2b6edc80594bbd"),
+ "plugins/DevicesDetection/images/brand/Onda.ico" => array("732", "2a8e1c31d12c2cf27c1931a13fd178df"),
+ "plugins/DevicesDetection/images/brand/OPPO.ico" => array("870", "4128f2c75414aa765adc5866bb627635"),
+ "plugins/DevicesDetection/images/brand/Orange.ico" => array("461", "21a6df281c10a240cbc134f79ea0d6eb"),
+ "plugins/DevicesDetection/images/brand/Panasonic.ico" => array("3649", "71da2a84a1fdf04abd130562699a9ab4"),
+ "plugins/DevicesDetection/images/brand/Pantech.ico" => array("605", "c9f16f7630211e6db026ea36919e46d0"),
+ "plugins/DevicesDetection/images/brand/PEAQ.ico" => array("3060", "3f0244e520fccb81e4acfffa330d7aab"),
+ "plugins/DevicesDetection/images/brand/Philips.ico" => array("3749", "2213ce1a4b99c4af0a53dff146aca457"),
+ "plugins/DevicesDetection/images/brand/Polaroid.ico" => array("737", "c6703ba6a2d008aa2197df33370e124c"),
+ "plugins/DevicesDetection/images/brand/PolyPad.ico" => array("1381", "35f641a4463636392646a476a4ffbfbd"),
+ "plugins/DevicesDetection/images/brand/RIM.ico" => array("705", "e5660baaa9484cddcd779e16672d48ce"),
+ "plugins/DevicesDetection/images/brand/Sagem.ico" => array("694", "6d0664111d655a222cf7ec86cebd7750"),
+ "plugins/DevicesDetection/images/brand/Samsung.ico" => array("3095", "988bb1039ac4b49d767dcfbdff86172a"),
+ "plugins/DevicesDetection/images/brand/Sanyo.ico" => array("639", "ca13b59af47f69c5ac6917cef9512add"),
+ "plugins/DevicesDetection/images/brand/Sega.ico" => array("706", "d73c2098167d02eb81c7b83cfe5787a2"),
+ "plugins/DevicesDetection/images/brand/Selevision.ico" => array("3497", "c920706882f4d553619e275f0d336423"),
+ "plugins/DevicesDetection/images/brand/Sharp.ico" => array("403", "ce78aee244f948bba92bfbf91397448b"),
+ "plugins/DevicesDetection/images/brand/Siemens.ico" => array("395", "a82f9c7c51665f04cad1a2a4a69bc590"),
+ "plugins/DevicesDetection/images/brand/Smart.ico" => array("3419", "04bc938b9b436ac140922f399767fd9f"),
+ "plugins/DevicesDetection/images/brand/Softbank.ico" => array("381", "8c42b583b4e1b05900b06698a65678c5"),
+ "plugins/DevicesDetection/images/brand/Sony_Ericsson.ico" => array("628", "7c8230f2d8dc34c6c5761aa9db204588"),
+ "plugins/DevicesDetection/images/brand/Sony.ico" => array("3525", "181229784d53d42d3699d3fc0f562ddd"),
+ "plugins/DevicesDetection/images/brand/Spice.ico" => array("556", "1d61255e7c137b6214eee3f3a7059027"),
+ "plugins/DevicesDetection/images/brand/TCL.ico" => array("2927", "207121f24b1b59c9e3e2ae43ad420f97"),
+ "plugins/DevicesDetection/images/brand/TechniSat.ico" => array("3347", "08762844360b5220aeb01e23dd6ea410"),
+ "plugins/DevicesDetection/images/brand/TechnoTrend.ico" => array("3500", "65748765610b09c759b739f1a97dc8ca"),
+ "plugins/DevicesDetection/images/brand/Telefunken.ico" => array("3651", "677bdb0ce07503bf11c437896148332d"),
+ "plugins/DevicesDetection/images/brand/Telit.ico" => array("527", "419e9ae7ac193023e1b0082fd96bc5df"),
+ "plugins/DevicesDetection/images/brand/Thomson.ico" => array("2974", "bff62a739264a6a81f8447a1aaf1d955"),
+ "plugins/DevicesDetection/images/brand/TiPhone.ico" => array("1179", "d5e6eeafbbde1409d878c6b17a6bba9e"),
+ "plugins/DevicesDetection/images/brand/T-Mobile.ico" => array("499", "d7869b7ce833ed1d9db2f85dfb0140b5"),
+ "plugins/DevicesDetection/images/brand/Toshiba.ico" => array("248", "f96539816d5b1d433bd5e8c4f1222970"),
+ "plugins/DevicesDetection/images/brand/unknown.ico" => array("1077", "32104fb21cf3f82b68d30bee64972ea7"),
+ "plugins/DevicesDetection/images/brand/Vertu.ico" => array("387", "f87d83b0a636bb325aefddcf436d50c4"),
+ "plugins/DevicesDetection/images/brand/Vestel.ico" => array("3096", "33dcd35a24a21eb0f660b26fcc41bca1"),
+ "plugins/DevicesDetection/images/brand/Videocon.ico" => array("617", "c9948a26325d7e73f4eb287e1475cb82"),
+ "plugins/DevicesDetection/images/brand/Videoweb.ico" => array("3129", "ca8e2df12c63f4799f69f74e141319e2"),
+ "plugins/DevicesDetection/images/brand/ViewSonic.ico" => array("605", "537cb66bf7d13d8be07efefc178af164"),
+ "plugins/DevicesDetection/images/brand/Voxtel.ico" => array("222", "93267b0d0b38e7b47f2dccb7bd791171"),
+ "plugins/DevicesDetection/images/brand/Xiaomi.ico" => array("492", "fe0ef8c5a55aeb5e33067b051ca87fd7"),
+ "plugins/DevicesDetection/images/brand/Yuandao.ico" => array("639", "866dddeae2afafd1a86a28b3621962f3"),
+ "plugins/DevicesDetection/images/brand/Zonda.ico" => array("371", "43bfb6a3cfca4edd0bf27bbf21e5baed"),
+ "plugins/DevicesDetection/images/brand/ZTE.ico" => array("555", "1f124412f72d709497dd3f4ad50b0761"),
+ "plugins/DevicesDetection/images/screens/camera.png" => array("644", "f948997fff235b4bbdece324d9054969"),
+ "plugins/DevicesDetection/images/screens/carbrowser.png" => array("3218", "fcbb6fa6a2c617418df8b17299d3d7fb"),
+ "plugins/DevicesDetection/images/screens/computer.png" => array("550", "305f4f50137fe4e909ad4aa4f61d52a8"),
+ "plugins/DevicesDetection/images/screens/console.gif" => array("617", "d8f2cae9e8e7723241c6c862f12c7511"),
+ "plugins/DevicesDetection/images/screens/dual.gif" => array("1082", "7903e070f4c4aa7933030e547e78073e"),
+ "plugins/DevicesDetection/images/screens/mobile.gif" => array("324", "04942ef60dd75380cfd682314e87e857"),
+ "plugins/DevicesDetection/images/screens/normal.gif" => array("1088", "b0c3de8704745e58c67844e54282ee42"),
+ "plugins/DevicesDetection/images/screens/smartphone.png" => array("547", "ca9c84628d2fb0bd58362d4689ab38f2"),
+ "plugins/DevicesDetection/images/screens/tablet.png" => array("602", "13529400c1e0d81762f08efac9f865d8"),
+ "plugins/DevicesDetection/images/screens/tv.png" => array("644", "415f9d8ae19f77c0203fd82de985ab2a"),
+ "plugins/DevicesDetection/images/screens/unknown.gif" => array("80", "c4a7f0e333a6079fdb0b6595e11bca74"),
+ "plugins/DevicesDetection/images/screens/wide.gif" => array("1025", "c0104958e6fb23668d0406fd4d89095e"),
+ "plugins/DevicesDetection/lang/bg.json" => array("1918", "4efec5ddc313eff04a301805586e13f2"),
+ "plugins/DevicesDetection/lang/cs.json" => array("340", "9cbf609028387879af0c6a54c9fd67f1"),
+ "plugins/DevicesDetection/lang/da.json" => array("1309", "09ba8d9edf2c0bf40a4b78c2df0d9174"),
+ "plugins/DevicesDetection/lang/de.json" => array("1334", "14ab1307ce18bb680bc954764bdd8e2f"),
+ "plugins/DevicesDetection/lang/el.json" => array("2121", "71ba9e2ac13f5592daae2e6f8085361d"),
+ "plugins/DevicesDetection/lang/en.json" => array("1305", "e1cc97069255c3ed10eb9c96ab1faab9"),
+ "plugins/DevicesDetection/lang/es.json" => array("1487", "38413c85a6b8d00fb099c450bc215493"),
+ "plugins/DevicesDetection/lang/et.json" => array("1326", "925cd20b8c5560958c1c5af28fdabf03"),
+ "plugins/DevicesDetection/lang/fa.json" => array("1191", "f9021aacbeef48009222e6157bed9fb2"),
+ "plugins/DevicesDetection/lang/fi.json" => array("1360", "d35fa82abe7de2b9d6ff3b6ecb40aa90"),
+ "plugins/DevicesDetection/lang/fr.json" => array("1616", "304b6674bbda463695f47ef601130345"),
+ "plugins/DevicesDetection/lang/it.json" => array("1405", "ea2a06c896ed564b2d8fb3b3b128abf3"),
+ "plugins/DevicesDetection/lang/ja.json" => array("1757", "b8c584f9b316aa5be5694ea2a9452baf"),
+ "plugins/DevicesDetection/lang/nb.json" => array("359", "bd7d02fa1b1c53285f948b31d7aae4a6"),
+ "plugins/DevicesDetection/lang/nl.json" => array("1251", "b0ae612fa5e5c16ae19201e610934f68"),
+ "plugins/DevicesDetection/lang/pt-br.json" => array("937", "a3eb60325ac25d48584eecfb771b89e6"),
+ "plugins/DevicesDetection/lang/ro.json" => array("1003", "676591f9dc497e4117264d56690a5647"),
+ "plugins/DevicesDetection/lang/ru.json" => array("1510", "ea3b2ebb6e5c0cf0ea1944c56116a539"),
+ "plugins/DevicesDetection/lang/sr.json" => array("550", "b31ae00fd98c9b050823db27790b2628"),
+ "plugins/DevicesDetection/lang/sv.json" => array("1325", "9c96fbcb18791021e36d2ca4868830ba"),
+ "plugins/DevicesDetection/templates/detection.twig" => array("3599", "12494f5f5bff7b5f031e040c0f44f3df"),
+ "plugins/DevicesDetection/templates/index.twig" => array("632", "31c98fab8308315ac38c8c0d7800e105"),
+ "plugins/DevicesDetection/templates/list.twig" => array("174", "bfda35ec5629ff523fea8502339daa58"),
+ "plugins/DevicesDetection/Updates/1.14.php" => array("780", "e405cf2a34f8299ab31ff7f27e7cda9f"),
+ "plugins/Events/API.php" => array("7610", "1f1edc6d625743c1301eb05f28b94534"),
+ "plugins/Events/Archiver.php" => array("9467", "1058da454ca46bdcbf2751605eb434ef"),
+ "plugins/Events/Controller.php" => array("2819", "2744d5a2eaa6532b02fdae411518d6cc"),
+ "plugins/Events/Events.php" => array("11572", "eb5998643543849b6a86625296dc6f37"),
+ "plugins/Events/lang/bg.json" => array("342", "b6ca024b4dc8d44227d9b09687522b20"),
+ "plugins/Events/lang/da.json" => array("257", "53898474762bee31b2bce9efa4218033"),
+ "plugins/Events/lang/de.json" => array("846", "e7ffc157518edd6f563a036a33ceea6b"),
+ "plugins/Events/lang/el.json" => array("332", "f8fa308217f7fea4bad1b5cd1fda8dae"),
+ "plugins/Events/lang/en.json" => array("1388", "ccfc90445d2f2d1b2e9a03276c96e076"),
+ "plugins/Events/lang/es.json" => array("852", "02df421dd69675296c0f084dae5c3d0a"),
+ "plugins/Events/lang/et.json" => array("262", "b6f500811defac532922653d562467c6"),
+ "plugins/Events/lang/fa.json" => array("291", "b11cba57d7f4654e63e864509c018bcf"),
+ "plugins/Events/lang/fi.json" => array("262", "d00802eb689dac69d2609cc6289017f2"),
+ "plugins/Events/lang/fr.json" => array("279", "39724ce65da5324898709bd365ceddee"),
+ "plugins/Events/lang/it.json" => array("818", "6a9e72466790e1e84a20281cf188a543"),
+ "plugins/Events/lang/ja.json" => array("300", "8b531ac5ac179f64c711cbfa3ca7b692"),
+ "plugins/Events/lang/nl.json" => array("271", "6a01d8a33c0234eee5d06c2da015e23e"),
+ "plugins/Events/lang/pt-br.json" => array("208", "292c740643d6491e0fe066ac5198345d"),
+ "plugins/Events/lang/ro.json" => array("921", "59e6c3b7b2527ec51a61fdc08b8d4754"),
+ "plugins/Events/lang/sr.json" => array("529", "993200724d25f114e3e500eab2459df8"),
+ "plugins/Events/lang/sv.json" => array("258", "c3d44c14aa30eebcad1133d6ae1a7b2c"),
+ "plugins/Events/lang/ta.json" => array("355", "b0e9b20f277a78aee6dc90005ec0c3f3"),
+ "plugins/Events/lang/vi.json" => array("254", "63d3e8849dd421c075b182f3db5b3d3e"),
+ "plugins/Events/lang/zh-cn.json" => array("204", "f15db4c7236481d992551e7964b1cc79"),
+ "plugins/Events/plugin.json" => array("139", "b5998447ccf2ee34f37ebe94be0aa845"),
+ "plugins/Events/templates/index.twig" => array("27", "a1080e4dc13f29008f29ac2d2e5dc8a8"),
+ "plugins/ExampleAPI/API.php" => array("4049", "2bda550f0e37806d7d8910789eee8199"),
+ "plugins/ExampleAPI/ExampleAPI.php" => array("325", "cef3615976cd2876afa2d2fc77acc5c1"),
+ "plugins/ExampleAPI/plugin.json" => array("448", "1a13a70c7ea7281490ed4af6a7221064"),
+ "plugins/ExampleCommand/Commands/HelloWorld.php" => array("1074", "70069208b271812bbf7e8faca036e7af"),
+ "plugins/ExampleCommand/plugin.json" => array("115", "cb68e3b4adc3511c16067f2cf02c5922"),
+ "plugins/ExamplePlugin/API.php" => array("815", "8bfc9244633eabb0c4f7f32e9b4a3fcf"),
+ "plugins/ExamplePlugin/Controller.php" => array("484", "f998699c283e1b20006c9afa8092f4ad"),
+ "plugins/ExamplePlugin/ExamplePlugin.php" => array("587", "7fedf67b0a08fb7fe1f8d71372ce6e91"),
+ "plugins/ExamplePlugin/.gitignore" => array("20", "4a9a2f0c455651c6b04479aafba04568"),
+ "plugins/ExamplePlugin/javascripts/plugin.js" => array("480", "eb32c805acb3e4af9370afd515a6dd1d"),
+ "plugins/ExamplePlugin/plugin.json" => array("208", "89a9672ed7f49b867ed88133a69120f4"),
+ "plugins/ExamplePlugin/README.md" => array("207", "48e2fb45e071d5d991d2b6243d9e8ff4"),
+ "plugins/ExamplePlugin/screenshots/.gitkeep" => array("0", "d41d8cd98f00b204e9800998ecf8427e"),
+ "plugins/ExamplePlugin/templates/index.twig" => array("77", "16efbdb92b36269be3801782f230f7d6"),
+ "plugins/ExamplePlugin/.travis.yml" => array("1056", "3a37a0ec586442058d923e1d7d640f43"),
+ "plugins/ExampleRssWidget/Controller.php" => array("1192", "090efd2ade281d8b58713d703b2beeda"),
+ "plugins/ExampleRssWidget/ExampleRssWidget.php" => array("949", "75f2273eb309a058b35e8c04cf3bfc45"),
+ "plugins/ExampleRssWidget/plugin.json" => array("449", "5ff4fed5b604abbae9e887aa0b45cb6a"),
+ "plugins/ExampleRssWidget/RssRenderer.php" => array("2078", "e2101bb2155cf6cc36ec064b2ca93b9e"),
+ "plugins/ExampleRssWidget/stylesheets/rss.less" => array("468", "9890a5a97e8e0be796c1b993b6991218"),
+ "plugins/ExampleSettingsPlugin/plugin.json" => array("163", "2672083085a6a297afd769beef9107a2"),
+ "plugins/ExampleSettingsPlugin/Settings.php" => array("5641", "a65a623faf6b325eb9109bb480eff723"),
+ "plugins/ExampleTheme/plugin.json" => array("158", "7bd2577897bda6846b7c457e059766f4"),
+ "plugins/ExampleTheme/README.md" => array("208", "1e90083ee059ed260d3cc73e3bbff133"),
+ "plugins/ExampleTheme/stylesheets/theme.less" => array("0", "d41d8cd98f00b204e9800998ecf8427e"),
+ "plugins/ExampleUI/API.php" => array("3032", "c4622a9b0fa70d56d9eef72dbf22b546"),
+ "plugins/ExampleUI/Controller.php" => array("7325", "4255650a64a9cf4f3b41f98fd101dff5"),
+ "plugins/ExampleUI/ExampleUI.php" => array("1707", "f5ce7ad0cc8af387cc373ce590105a00"),
+ "plugins/ExampleUI/images/icons-planet/earth.png" => array("11823", "82a4f58618b5194ba7d50600115d36da"),
+ "plugins/ExampleUI/images/icons-planet/jupiter.png" => array("10686", "08c6c466b8068c18e0c1859115515a5d"),
+ "plugins/ExampleUI/images/icons-planet/LICENSE" => array("112", "5275944112cc13e4e74b2de749497988"),
+ "plugins/ExampleUI/images/icons-planet/mars.png" => array("9837", "5714bd49c465b187d60e64f64ca4a566"),
+ "plugins/ExampleUI/images/icons-planet/mercury.png" => array("10322", "a83fe0221f91cd7581947323ebfdff44"),
+ "plugins/ExampleUI/images/icons-planet/neptune.png" => array("9770", "207ad3426696544e7fc301f82f81ce40"),
+ "plugins/ExampleUI/images/icons-planet/saturn.png" => array("10561", "9514535a2fde794c6dac3e3382fcf517"),
+ "plugins/ExampleUI/images/icons-planet/uranus.png" => array("10358", "9ff6281138f4ebb8ca5b8b3b54b9d9b0"),
+ "plugins/ExampleUI/images/icons-planet/venus.png" => array("10557", "1aa0425158220a53f36537f83d266231"),
+ "plugins/ExampleUI/plugin.json" => array("502", "fdd7feb05198028a198c8cedae0ce6b8"),
+ "plugins/ExampleUI/templates/evolutiongraph.twig" => array("90", "541ef7126cfe01ac071d053d720d2ccd"),
+ "plugins/ExampleUI/templates/notifications.twig" => array("447", "9525341678a5441f789cd446cbbdba56"),
+ "plugins/ExampleUI/templates/sparklines.twig" => array("237", "31e796d35929d2839fe62b5d2f3460f6"),
+ "plugins/ExampleVisualization/ExampleVisualization.php" => array("631", "44f36c70c57d48b86a5342b7ee2e3010"),
+ "plugins/ExampleVisualization/images/table.png" => array("151", "327ee0e75605ab865796053f2c0aebf1"),
+ "plugins/ExampleVisualization/plugin.json" => array("410", "6879c05e6a2b969cfac1eb24b53e5e79"),
+ "plugins/ExampleVisualization/README.md" => array("238", "1353685fc485ed3691c8ce507d530411"),
+ "plugins/ExampleVisualization/SimpleTable.php" => array("2515", "c5dcc6edbdb74ed20ffe9d36f6846c22"),
+ "plugins/ExampleVisualization/templates/simpleTable.twig" => array("829", "abccf18a3c8bfb921a53f65cb339282a"),
+ "plugins/Feedback/angularjs/ratefeature/icon_license" => array("268", "0a278d7bd9ac7a0022c8adfdf07418cd"),
+ "plugins/Feedback/angularjs/ratefeature/ratefeature-controller.js" => array("633", "4dd44a1936c30a3cfc157ed7c3fd2653"),
+ "plugins/Feedback/angularjs/ratefeature/ratefeature-directive.js" => array("542", "4376ff3f0cf1d5c78a92e8900fa14161"),
+ "plugins/Feedback/angularjs/ratefeature/ratefeature.html" => array("1554", "f61eee9f73fada95471dec5c5980b6b4"),
+ "plugins/Feedback/angularjs/ratefeature/ratefeature.less" => array("358", "51dbbd1fab358ef455a06d53697bffc7"),
+ "plugins/Feedback/angularjs/ratefeature/ratefeature-model.js" => array("543", "3807890aedaff7895eb3f3c9f1f00015"),
+ "plugins/Feedback/angularjs/ratefeature/thumbs-down.png" => array("2188", "38c37611f821aa2671d60dc843345d12"),
+ "plugins/Feedback/angularjs/ratefeature/thumbs-up.png" => array("2202", "9befe01114b4a5801808e8899b5010a7"),
+ "plugins/Feedback/API.php" => array("3265", "09a9d79dc7bbc8010239f81887d0de38"),
+ "plugins/Feedback/Controller.php" => array("623", "89de65e12271361e2e2aee5c59641288"),
+ "plugins/Feedback/Feedback.php" => array("2185", "73bd60a41e1b6221493799866a47291b"),
+ "plugins/Feedback/images/facebook.png" => array("302", "9ae558673b40a7f3eeaad17b281ba4f8"),
+ "plugins/Feedback/images/github.png" => array("361", "9417fe2b0664c8d8f23b1765728c46c1"),
+ "plugins/Feedback/images/linkedin.png" => array("336", "26bebcbbef85af866892f6aa7c49e079"),
+ "plugins/Feedback/images/newsletter.png" => array("444", "740cc523cc992a4e9c089d7dc589b3eb"),
+ "plugins/Feedback/images/twitter.png" => array("376", "fcac98bcbf056e136c7b06557d7bcb98"),
+ "plugins/Feedback/stylesheets/feedback.less" => array("1539", "c0c45446610709fef33f18bcf4f6ef14"),
+ "plugins/Feedback/templates/index.twig" => array("5746", "e963b3fcd9eb4f753c2d944c174def7b"),
+ "plugins/Goals/API.php" => array("24370", "539367dfc9fbf1b43bf84839ee86eb58"),
+ "plugins/Goals/Archiver.php" => array("15665", "d6178a9fdfbaf1c96ae1fed57ba6a683"),
+ "plugins/Goals/Controller.php" => array("21788", "377dae6591eab934b51afaa0fab43aa9"),
+ "plugins/Goals/Goals.php" => array("31816", "0ea5f9311ee7c1d5ef051dd14f81e357"),
+ "plugins/Goals/javascripts/goalsForm.js" => array("6201", "acbd9a7ee2de6fd6e5a2f2b6698ece5a"),
+ "plugins/Goals/stylesheets/goals.css" => array("460", "6b076df7c73aa99431820279517ea9cd"),
+ "plugins/Goals/templates/_addEditGoal.twig" => array("4456", "980da0a6237f27c853fd9c57a3db1380"),
+ "plugins/Goals/templates/addNewGoal.twig" => array("432", "530107fc4f4a5fb8ea5744592e3c3646"),
+ "plugins/Goals/templates/_formAddGoal.twig" => array("5011", "af7bdec1ae036d7dc363cbb0977fc850"),
+ "plugins/Goals/templates/getGoalReportView.twig" => array("3030", "395e8c171b9268be5e4e7ccf24e7c6be"),
+ "plugins/Goals/templates/getOverviewView.twig" => array("1863", "afb8bced90b1de94b24546bd5ae21604"),
+ "plugins/Goals/templates/_listGoalEdit.twig" => array("2736", "6c0d563edf0c1971097e0c41d5516a9c"),
+ "plugins/Goals/templates/_listTopDimension.twig" => array("615", "86df6f4ecfa56b02c72075f7beace1d5"),
+ "plugins/Goals/templates/_titleAndEvolutionGraph.twig" => array("3785", "ae6e728ff64176a617f69d426834e6ba"),
+ "plugins/Goals/Visualizations/Goals.php" => array("11535", "13ea4bfab83f1f683db27a8f62d453f8"),
+ "plugins/ImageGraph/API.php" => array("22505", "b5f4b78e2b6b1e4eb667cd5601124edf"),
+ "plugins/ImageGraph/Controller.php" => array("2596", "028bc0f95cddb8fd42a72007ee8677d4"),
+ "plugins/ImageGraph/fonts/tahoma.ttf" => array("94740", "fabde5f388432a4a2bd40bc07ac53971"),
+ "plugins/ImageGraph/ImageGraph.php" => array("6041", "e604bd6a45f1e5c165319f0108826fbf"),
+ "plugins/ImageGraph/StaticGraph/Evolution.php" => array("576", "75d6bc92ee26a9153a19479fc26e81d6"),
+ "plugins/ImageGraph/StaticGraph/Exception.php" => array("1513", "b2e05ae76bb47496c259bbbb23632b1c"),
+ "plugins/ImageGraph/StaticGraph/GridGraph.php" => array("19969", "3beb0b57cb2837fac11c5f53be6605d3"),
+ "plugins/ImageGraph/StaticGraph/HorizontalBar.php" => array("7097", "a0b09ce105f15b874f7f867a71a80888"),
+ "plugins/ImageGraph/StaticGraph.php" => array("9417", "9d2e8b5b979bed66c7db0bbd64c87152"),
+ "plugins/ImageGraph/StaticGraph/Pie3D.php" => array("464", "f432e2a17d9c16fb86909bc596bacf44"),
+ "plugins/ImageGraph/StaticGraph/PieGraph.php" => array("4227", "d61366790d5b83979b81633631479e50"),
+ "plugins/ImageGraph/StaticGraph/Pie.php" => array("463", "4ffc2f6fbc909875f01df6159472d6c5"),
+ "plugins/ImageGraph/StaticGraph/VerticalBar.php" => array("700", "fa2f2c41aa965694711d69b94269f0c2"),
+ "plugins/ImageGraph/templates/index.twig" => array("148", "29658e5544cd076021ea898993b8506d"),
+ "plugins/ImageGraph/templates/testAllSizes.twig" => array("2557", "2eca39df92b75a1c67933d928d8815e9"),
+ "plugins/Insights/API.php" => array("13847", "788dd0b6a9e6d3ead74f319a706b94db"),
+ "plugins/Insights/Controller.php" => array("2184", "2f3d6d7f84ecddfb5b75a1ad780ace40"),
+ "plugins/Insights/DataTable/Filter/ExcludeLowValue.php" => array("1607", "bde83b71854b84f55894e754cc36b43f"),
+ "plugins/Insights/DataTable/Filter/Insight.php" => array("3876", "d5b9908e494619123f6c61d1e2a9aeb0"),
+ "plugins/Insights/DataTable/Filter/Limit.php" => array("1427", "896f5aaa0f1203918f09bcd96152c713"),
+ "plugins/Insights/DataTable/Filter/MinGrowth.php" => array("1489", "f385e31013c7e27a8b178ce5cf193aed"),
+ "plugins/Insights/DataTable/Filter/OrderBy.php" => array("1984", "fa2aa2e46620f2d92fa2aac30960efa1"),
+ "plugins/Insights/images/idea.png" => array("364", "bc677415eb4fb941a85c568b6aa656ba"),
+ "plugins/Insights/InsightReport.php" => array("11233", "e74af746ab91d381bb04cb1cf5c9506a"),
+ "plugins/Insights/Insights.php" => array("1412", "00a6ff865dc2ed2728dd8dd58438784b"),
+ "plugins/Insights/javascripts/insightsDataTable.js" => array("3882", "559f938d2e4a3e48ceb0e2827c2a33c3"),
+ "plugins/Insights/Model.php" => array("3366", "ca38127d95af08cb9f7e7aa1744f396c"),
+ "plugins/Insights/plugin.json" => array("284", "15013f4f566ee639b3d4566468b46fe8"),
+ "plugins/Insights/stylesheets/insightVisualization.less" => array("619", "5cb33ec3db0c5d33d3d7714ec80be638"),
+ "plugins/Insights/templates/cannotDisplayReport.twig" => array("103", "02d257f7747a1f7734235a56faa900ba"),
+ "plugins/Insights/templates/insightControls.twig" => array("3190", "a1b43257232d5e18e746f3839b8e17be"),
+ "plugins/Insights/templates/insightsOverviewWidget.twig" => array("314", "19c200a37664de6dda1a858e2e133df4"),
+ "plugins/Insights/templates/insightVisualization.twig" => array("1360", "7036f0548b2901935552b9929bf4bd6d"),
+ "plugins/Insights/templates/moversAndShakersOverviewWidget.twig" => array("672", "26093e2b2575a0b1b058d8b15e30f02e"),
+ "plugins/Insights/templates/overviewWidget.twig" => array("1270", "b420d52e333ea4ca167b897de920a029"),
+ "plugins/Insights/templates/table_header.twig" => array("440", "6f5ad76f20c6fa8536612ffe632a8d99"),
+ "plugins/Insights/templates/table_row.twig" => array("1497", "955a553498046e53131a3320fab79a6f"),
+ "plugins/Insights/Visualizations/Insight.php" => array("3714", "028a760f1f2c2fd291f0609881aac743"),
+ "plugins/Insights/Visualizations/Insight/RequestConfig.php" => array("1254", "e62e3a81b32acf3e8f834ff688d9e94f"),
+ "plugins/Installation/Controller.php" => array("37029", "f9b9cc3440e4b8ebf9a4ba4f5eb5ebe0"),
+ "plugins/Installation/FormDatabaseSetup.php" => array("11591", "fb9e70d02e5a8201d1455ea6b9cedd13"),
+ "plugins/Installation/FormFirstWebsiteSetup.php" => array("3369", "e3e7a92773c26cedb6d7b3f9801cf7a3"),
+ "plugins/Installation/FormGeneralSetup.php" => array("4342", "74f162e30c4601c9bc2d06fa0a26590f"),
+ "plugins/Installation/Installation.php" => array("3249", "3ad87571d8d4abdc72779452f3b2bb9e"),
+ "plugins/Installation/javascripts/installation.js" => array("307", "4c37587c9e1d490726acb45f723ca7b9"),
+ "plugins/Installation/ServerFilesGenerator.php" => array("4673", "64c8e3186155de098d2b7f47cf2f6fee"),
+ "plugins/Installation/stylesheets/installation.css" => array("3791", "821b94028ac1602a34fce93061d3a7ca"),
+ "plugins/Installation/stylesheets/systemCheckPage.less" => array("778", "cae7f2f24da86ea2b691ebfed87b8e4c"),
+ "plugins/Installation/templates/_allSteps.twig" => array("392", "82b5478f80593011dbcf3ecf0af61302"),
+ "plugins/Installation/templates/databaseCheck.twig" => array("1462", "0ec6aa932fdbbb9fc9a21c498f9d38c4"),
+ "plugins/Installation/templates/databaseSetup.twig" => array("453", "126314587ffced4a2251edfdf9805169"),
+ "plugins/Installation/templates/finished.twig" => array("828", "98ccc1a91a49856f891a514a434df74c"),
+ "plugins/Installation/templates/firstWebsiteSetup.twig" => array("803", "c069ebc5ad0dcd2f032de51e12718437"),
+ "plugins/Installation/templates/generalSetup.twig" => array("450", "bfbb3e56dfd595a6085034ece031ce5c"),
+ "plugins/Installation/templates/_integrityDetails.twig" => array("1157", "26f9f8e6a60611f972182b9188cb40ce"),
+ "plugins/Installation/templates/layout.twig" => array("2369", "171a8bd7aa74aa4e0993c68c6ce69937"),
+ "plugins/Installation/templates/reuseTables.twig" => array("2884", "11a9acbfc09adb2bdeb2b37c9c810fe0"),
+ "plugins/Installation/templates/_systemCheckLegend.twig" => array("783", "868726c336470173b390efd117db3eb8"),
+ "plugins/Installation/templates/systemCheckPage.twig" => array("959", "2e4074ddeff70cf07b68a34500647a42"),
+ "plugins/Installation/templates/_systemCheckSection.twig" => array("13651", "3ae35c6efa7d38f24dc0fc0206988e91"),
+ "plugins/Installation/templates/systemCheck.twig" => array("654", "4ca83235ac1786803d690125beb943d5"),
+ "plugins/Installation/templates/tablesCreation.twig" => array("2445", "0713ca27f416427d8dde18759284e5fa"),
+ "plugins/Installation/templates/trackingCode.twig" => array("678", "e13755a135a80b77df5960ca92b59174"),
+ "plugins/Installation/templates/welcome.twig" => array("1341", "c04fe82942a7b099e7d35c32f2752b33"),
+ "plugins/Installation/View.php" => array("1591", "7433b9692f499c2c3f8804cd8095c788"),
+ "plugins/LanguagesManager/API.php" => array("9715", "bf712c41d7a1171976490570ab3dac04"),
+ "plugins/LanguagesManager/Commands/CreatePull.php" => array("8019", "da7be9f183aed2f38461d989a9841ac8"),
+ "plugins/LanguagesManager/Commands/FetchFromOTrance.php" => array("6166", "19f36b874c56ad9767ce0a3378d9351d"),
+ "plugins/LanguagesManager/Commands/LanguageCodes.php" => array("1063", "283814548ee804e448d6f1e0e5b90739"),
+ "plugins/LanguagesManager/Commands/LanguageNames.php" => array("1071", "c7df597dd84037238cba1369489ad396"),
+ "plugins/LanguagesManager/Commands/PluginsWithTranslations.php" => array("1192", "27e732d20ad5343ad54c2671c7f469bc"),
+ "plugins/LanguagesManager/Commands/SetTranslations.php" => array("3990", "0197fbf7ed6a56ec8e20edd236487cd0"),
+ "plugins/LanguagesManager/Commands/Update.php" => array("5748", "e71235b082bad8b9c1f67b941d4ece99"),
+ "plugins/LanguagesManager/Controller.php" => array("813", "ccfc729a22e4398f1caffd2b4ee32d4b"),
+ "plugins/LanguagesManager/javascripts/languageSelector.js" => array("2545", "8cad615cf92b73f4c3401419957c2abf"),
+ "plugins/LanguagesManager/LanguagesManager.php" => array("6527", "522f063e16bec7dd87d380fca5095f18"),
+ "plugins/LanguagesManager/templates/getLanguagesSelector.twig" => array("1015", "61337194fa9393f34a99726587c6939e"),
+ "plugins/LeftMenu/plugin.json" => array("128", "af274a9b50394d8054b01dd4508cfcf1"),
+ "plugins/LeftMenu/stylesheets/theme.less" => array("3139", "3e1b68f368095012788e07bce8e111bd"),
+ "plugins/Live/API.php" => array("29944", "4aa60c7abc780bb6dfa2ecb0385e6e66"),
+ "plugins/Live/Controller.php" => array("10331", "d0abb0c4882fd59af19da0968dc063b5"),
+ "plugins/Live/images/avatar_frame.png" => array("5375", "b9a974286bc6de507e88ffd8ad40a8dd"),
+ "plugins/Live/images/file0.png" => array("593", "d3bd9b53340629a9ba00ff9d3c295995"),
+ "plugins/Live/images/file1.png" => array("637", "c1844ec7c1a8acf7573a54f78bf3565a"),
+ "plugins/Live/images/file2.png" => array("642", "c441ff9c29af20f36c4a2939184b1e7e"),
+ "plugins/Live/images/file3.png" => array("674", "d76acf5c11f14b310877bae8bb22a57c"),
+ "plugins/Live/images/file4.png" => array("611", "ac9684501547cf526bbe19f69ef7e1bf"),
+ "plugins/Live/images/file5.png" => array("668", "fe6c43a812e1f10025e3413562fcaac3"),
+ "plugins/Live/images/file6.png" => array("659", "6ec8688f0470d55ffcc6077241e0fbf8"),
+ "plugins/Live/images/file7.png" => array("681", "f9678a677fb759b43d9dc66a573bff89"),
+ "plugins/Live/images/file8.png" => array("642", "c164fa32f73b6f67012b0fde60d0e756"),
+ "plugins/Live/images/file9.png" => array("569", "0551f3605e761068b1b1169c188cb45c"),
+ "plugins/Live/images/paperclip.png" => array("10924", "aa65189568a2cb4c9d08af6f46926ae0"),
+ "plugins/Live/images/pause_disabled.gif" => array("619", "5c68e96a0ce8eac7b75fff5735dc7a3e"),
+ "plugins/Live/images/pause.gif" => array("669", "534b3892f5bda663651f86356df69a8d"),
+ "plugins/Live/images/play_disabled.gif" => array("407", "3c1374c5bfc73f12d50a97e5017a120f"),
+ "plugins/Live/images/play.gif" => array("666", "10ab3e64171780613434062a50378714"),
+ "plugins/Live/images/returningVisitor.gif" => array("995", "dbdd14d7d5528f2c74ec8508c99aacfc"),
+ "plugins/Live/images/unknown_avatar.jpg" => array("13984", "8151db2b0f9b45ba92f9b81c2791df94"),
+ "plugins/Live/images/visitor_profile_background.jpg" => array("11060", "b7eaca3d0097fe8f27d6625214092fd5"),
+ "plugins/Live/images/visitor_profile_close.png" => array("4734", "e12231490573aa1f37d7eed6eb6867f0"),
+ "plugins/Live/images/visitor_profile_gradient.png" => array("2840", "377a58f8be3b30690845aa71b0db0a5b"),
+ "plugins/Live/images/visitorProfileLaunch.png" => array("661", "60ac00d9db615450c8d6090521f30330"),
+ "plugins/Live/javascripts/live.js" => array("8926", "7208e2076c000a03a5f9ada452d8c428"),
+ "plugins/Live/javascripts/visitorLog.js" => array("3150", "5d8488563053c8dd0248bdbba49aa59d"),
+ "plugins/Live/javascripts/visitorProfile.js" => array("10635", "6d6476652f681ef87d8eda4eecffb2e1"),
+ "plugins/Live/Live.php" => array("2559", "3b4cbcdd287bc9bdc96c5927a15a2a4f"),
+ "plugins/Live/stylesheets/live.less" => array("3761", "78a7b21ca37b37518c66eda4d07c18d7"),
+ "plugins/Live/stylesheets/visitor_profile.less" => array("9904", "86b092ab364a0e298e8edbfa0c6418c1"),
+ "plugins/Live/templates/_actionsList.twig" => array("7713", "d8c003ff4ef3b36a2f9ce6426faa09ed"),
+ "plugins/Live/templates/ajaxTotalVisitors.twig" => array("41", "5c57d90d62eab1c2c11969f14ff6faaa"),
+ "plugins/Live/templates/_dataTableViz_visitorLog.twig" => array("11165", "f10f764079fc95b18bee9964e505bbd5"),
+ "plugins/Live/templates/getLastVisitsStart.twig" => array("9637", "66723cfc37872398f74bcebc0170fa3a"),
+ "plugins/Live/templates/getSimpleLastVisitCount.twig" => array("1501", "ff589f5ab71d3364bbce2b50ac635304"),
+ "plugins/Live/templates/getSingleVisitSummary.twig" => array("3644", "a6d27ef3106ba799ee77ab6c7e52901b"),
+ "plugins/Live/templates/getVisitList.twig" => array("1401", "21f5605f7c91c89b27ec184d71e89a34"),
+ "plugins/Live/templates/getVisitorProfilePopup.twig" => array("10894", "ae422171a4dc85bae4c491b8530a04c7"),
+ "plugins/Live/templates/index.twig" => array("1728", "6de08f5ddb5d6802ab97258cafbf7d13"),
+ "plugins/Live/templates/indexVisitorLog.twig" => array("170", "8e65a17376e8d870da1fdb791bdb019b"),
+ "plugins/Live/templates/_totalVisitors.twig" => array("1193", "cb5f29dc2c34e531f7e8469feb201a32"),
+ "plugins/Live/VisitorLog.php" => array("4032", "95e464cd76d6842f656e0b4f7bb5e9d8"),
+ "plugins/Live/Visitor.php" => array("35950", "df9ec109f4a08c6bc5b7c33a1b544f73"),
+ "plugins/Login/Auth.php" => array("3935", "449818805ed2d144bfcd22d4e881318f"),
+ "plugins/Login/Controller.php" => array("14593", "3ad0b3aefe6b7aec78622079aa965691"),
+ "plugins/Login/FormLogin.php" => array("1283", "97995c1ab8eb36c1c4a899f5f87a4e23"),
+ "plugins/Login/FormResetPassword.php" => array("1247", "13d7b72379ff19943051061fa0e17f4b"),
+ "plugins/Login/javascripts/login.js" => array("3457", "ddf60c9aab3769010b4e022369337dfb"),
+ "plugins/Login/Login.php" => array("4566", "48ff16be33aab8f87ad9fa99c615ab71"),
+ "plugins/Login/stylesheets/login.css" => array("3564", "df89b474360b0e1c59f306df7c76255c"),
+ "plugins/Login/templates/login.twig" => array("7493", "7c6eebc0b8825c41214dba7647238660"),
+ "plugins/Login/templates/resetPassword.twig" => array("389", "751937bee878c24f3b72890273548069"),
+ "plugins/MobileMessaging/APIException.php" => array("261", "9f8b71ac2a11ea82638ff785a1368ecc"),
+ "plugins/MobileMessaging/API.php" => array("12198", "36de01e312125b59016d8f2f2ad3aa58"),
+ "plugins/MobileMessaging/Controller.php" => array("2675", "9b26ad0184726bc42e630ebe95b2a05d"),
+ "plugins/MobileMessaging/CountryCallingCodes.php" => array("7207", "0f3e6aa1f434d4c93ed4dc899ec751bf"),
+ "plugins/MobileMessaging/GSMCharset.php" => array("2978", "1a7692e6ddd18ff57f146e9c064db4c9"),
+ "plugins/MobileMessaging/images/Clockwork.png" => array("3585", "c0ae966302ddca87a6cc55abf0617efe"),
+ "plugins/MobileMessaging/images/phone.png" => array("568", "c294efbf17ecc6c3384e1cbff1a69521"),
+ "plugins/MobileMessaging/javascripts/MobileMessagingSettings.js" => array("10525", "eedfe8a5ae54d77c74590698e108b258"),
+ "plugins/MobileMessaging/MobileMessaging.php" => array("8963", "08ac0b55c51fb2e617173ce07c0ed673"),
+ "plugins/MobileMessaging/ReportRenderer/ReportRendererException.php" => array("1433", "864c4f04d4bc18daa94d59927fc87ca8"),
+ "plugins/MobileMessaging/ReportRenderer/Sms.php" => array("4337", "8c5cd9fb9e9e88ac7490db381ac414c3"),
+ "plugins/MobileMessaging/SMSProvider/Clockwork.php" => array("2850", "5de85a92828a6fb7ab107fd1821efb22"),
+ "plugins/MobileMessaging/SMSProvider.php" => array("6529", "4265464da0ce3dd71daccd3365f8b5d3"),
+ "plugins/MobileMessaging/SMSProvider/StubbedProvider.php" => array("591", "ec1c6ab90be8c4ee259ee51012a116af"),
+ "plugins/MobileMessaging/stylesheets/MobileMessagingSettings.less" => array("248", "b8f6c5c9dc4d2376def8d30a436f2f2d"),
+ "plugins/MobileMessaging/templates/index.twig" => array("8828", "ff291f3400b4d3e6548d7d72254fcaeb"),
+ "plugins/MobileMessaging/templates/reportParametersScheduledReports.twig" => array("2220", "facbb81972351c7bf9552bb6d23ad6a6"),
+ "plugins/MobileMessaging/templates/SMSReport.twig" => array("2081", "8757ecc10c18b0db36031f0ef659756a"),
+ "plugins/Morpheus/images/add.png" => array("1261", "56daa2c3de8f7091e4a796940a395399"),
+ "plugins/Morpheus/images/annotations.png" => array("1069", "d7778841e7e56de10f4d594e56f315c8"),
+ "plugins/Morpheus/images/annotations_starred.png" => array("248", "c55f6bd966fee694d42996d73de74e0e"),
+ "plugins/Morpheus/images/bullet.png" => array("963", "efd0e064fa9fc92187d976b08d707855"),
+ "plugins/Morpheus/images/calendar.gif" => array("1260", "92079affe9d57713492ba8fc8991ed15"),
+ "plugins/Morpheus/images/chart_bar.png" => array("968", "ff2414d707510b4188795cd7ba73b092"),
+ "plugins/Morpheus/images/chart_line_edit.png" => array("421", "d7f696960ffe8f17fedefcc42415adf4"),
+ "plugins/Morpheus/images/chart_pie.png" => array("1223", "cf703b3878e59ffde83f5cbe6d7571b3"),
+ "plugins/Morpheus/images/cities.png" => array("1038", "7eaa39f8e0021507b8685e3e0b2c87e9"),
+ "plugins/Morpheus/images/close.png" => array("1122", "9276313cd6bbff312cf65222b970c1d3"),
+ "plugins/Morpheus/images/configure-highlight.png" => array("356", "b7e641dd5732e92000dfedcacf0fb42d"),
+ "plugins/Morpheus/images/configure.png" => array("1210", "2241fa7107cc901a362a54b372d9e5d4"),
+ "plugins/Morpheus/images/datepicker_arr_l.png" => array("963", "e770866fb6c0b6dc9318bb635cb8b25c"),
+ "plugins/Morpheus/images/datepicker_arr_r.png" => array("968", "324f7f5e7b2d60ac543d574fec0638fe"),
+ "plugins/Morpheus/images/export.png" => array("1182", "4ab0af6ccaf4cbe06da8e2f19d611893"),
+ "plugins/Morpheus/images/forms-sprite.png" => array("1758", "c87c3f0ae1eeb5b9f928d76a019afbea"),
+ "plugins/Morpheus/images/goal.png" => array("1042", "13fcf0fc4f2f5c7cbeb3b954be2302cb"),
+ "plugins/Morpheus/images/help.png" => array("1297", "2030eb18bab29a957d84f6ab578f04a1"),
+ "plugins/Morpheus/images/ico_delete.png" => array("1306", "fd41409c74d29bab6be2a1f1def2f1e3"),
+ "plugins/Morpheus/images/ico_edit.png" => array("1202", "67cfadf5265b4d2c506f3db2e1ca367a"),
+ "plugins/Morpheus/images/icon-calendar.gif" => array("1260", "7acac555dc283dfb65de9f7f524e662b"),
+ "plugins/Morpheus/images/image.png" => array("1139", "70283dccd506faa0a0a4580d822a4609"),
+ "plugins/Morpheus/images/info.png" => array("1278", "6df252a916c3df5b269732e76ecfa50e"),
+ "plugins/Morpheus/images/link.gif" => array("1147", "6098367a5cd6a3dd93c56aced55925bf"),
+ "plugins/Morpheus/images/loading-blue.gif" => array("723", "23f0762fea3d694b579522524bd5628f"),
+ "plugins/Morpheus/images/logo-header.png" => array("2215", "9695149e76d3080dcffd78eb45cd3735"),
+ "plugins/Morpheus/images/logo.png" => array("3902", "d11828697d06b1117c49413d77a6e9ff"),
+ "plugins/Morpheus/images/logo.svg" => array("2290", "d19186837572bb5994e4d6e2b463348b"),
+ "plugins/Morpheus/images/maximise.png" => array("1053", "b98f8497b9b242cac626128ffd4d5ef7"),
+ "plugins/Morpheus/images/minimise.png" => array("968", "2ad0c819d2416043d07be478ac9a9156"),
+ "plugins/Morpheus/images/pause_disabled.gif" => array("1141", "5dcb5e11d359f2a4e180fd7b5a3a5dfd"),
+ "plugins/Morpheus/images/pause.gif" => array("1142", "defa62045c5e21609cc6b36de1d299ef"),
+ "plugins/Morpheus/images/play_disabled.gif" => array("1155", "dd7c8730e4be8128138ee68aebd0a5b2"),
+ "plugins/Morpheus/images/play.gif" => array("1184", "8db25ba7dc1701ed2c93a483fa25ef88"),
+ "plugins/Morpheus/images/refresh.png" => array("1312", "1672c3502fac047c53f2e74797fe124b"),
+ "plugins/Morpheus/images/regions.png" => array("1265", "1fdee0b6664804d6525dfbe12931b922"),
+ "plugins/Morpheus/images/search_ico.png" => array("1227", "e2da6bc860b189996bdafef30bb2e3cc"),
+ "plugins/Morpheus/images/segment-users.png" => array("1270", "95b3e6b1ed4c79c826f59da6100a27e1"),
+ "plugins/Morpheus/images/sortasc_dark.png" => array("92", "96b06c701e6ef3645c39206045821495"),
+ "plugins/Morpheus/images/sortasc.png" => array("964", "736682039b3ce45e6565e3f871f71057"),
+ "plugins/Morpheus/images/sortdesc_dark.png" => array("98", "dbc223ec613d5bc863e897cf3b525335"),
+ "plugins/Morpheus/images/sortdesc.png" => array("980", "968ded7fbfb0c38d1883788c07f090a4"),
+ "plugins/Morpheus/images/sort_subtable_desc.png" => array("980", "bc2a98535cbcd85c256c534627f4fffc"),
+ "plugins/Morpheus/images/table_more.png" => array("1157", "720c588117a9dc781d40a42e90c40466"),
+ "plugins/Morpheus/images/table.png" => array("1056", "375eae11704a2d2e749cdefcb26f2a39"),
+ "plugins/Morpheus/images/tagcloud.png" => array("1098", "1c9b9a43cef5e807087cf3f69544c9ef"),
+ "plugins/Morpheus/images/zoom-out-disabled.png" => array("1297", "81d56e2c732e3ed4bc1a1c7d94632e7b"),
+ "plugins/Morpheus/images/zoom-out.png" => array("1300", "b598e49632becfb91970083f8a9ff62e"),
+ "plugins/Morpheus/javascripts/jquery.icheck.min.js" => array("4005", "a31ce1654416358e8d933cbf79b5ffbd"),
+ "plugins/Morpheus/javascripts/morpheus.js" => array("617", "ad7fbd24380028485a5e0758e4b66e81"),
+ "plugins/Morpheus/plugin.json" => array("468", "e7be1bd0537250d805421d36c880232a"),
+ "plugins/Morpheus/stylesheets/admin.less" => array("2273", "103712f53f7978b39f5898f4581815b5"),
+ "plugins/Morpheus/stylesheets/charts.less" => array("3046", "c72c436df07f2c5282afa95e13722d0e"),
+ "plugins/Morpheus/stylesheets/colors.less" => array("1454", "6fa45b1e4c87373f790dec9d4822645d"),
+ "plugins/Morpheus/stylesheets/components.less" => array("8430", "66b71131d92624e3a3d5cd654e29bb3b"),
+ "plugins/Morpheus/stylesheets/forms.less" => array("6410", "df34cc9255b55f23c7eb82d90abe06f5"),
+ "plugins/Morpheus/stylesheets/map.less" => array("1824", "fd90d3d1127a9ad8b8c71b611f202904"),
+ "plugins/Morpheus/stylesheets/mixins.less" => array("2884", "b62ca2d8a1c8c1867a60520243a417c2"),
+ "plugins/Morpheus/stylesheets/popups.less" => array("1122", "24fd3106538b5a84d77c516bb647651c"),
+ "plugins/Morpheus/stylesheets/theme.less" => array("19045", "b9c3f6250cd4d85602a319464731c47d"),
+ "plugins/Morpheus/stylesheets/tooltip.less" => array("725", "8801596d6ea42a576a486f82ffe41e29"),
+ "plugins/Morpheus/stylesheets/typography.less" => array("2196", "582c812de3aa430bb86b54b7d82d548f"),
+ "plugins/MultiSites/angularjs/dashboard/dashboard-controller.js" => array("955", "7cdb911b5a86c789d1545d7c16d90084"),
+ "plugins/MultiSites/angularjs/dashboard/dashboard-directive.js" => array("1097", "231dadf20d40d8e78622e9b97621c026"),
+ "plugins/MultiSites/angularjs/dashboard/dashboard-filter.js" => array("1962", "aa3286a874ef6b3aa0f982a815feb533"),
+ "plugins/MultiSites/angularjs/dashboard/dashboard.html" => array("6925", "6b97a82b739ea3c1309caf7ce274982e"),
+ "plugins/MultiSites/angularjs/dashboard/dashboard.less" => array("2810", "f9d750db11d5a06dc3cc60f9fcd0a5e8"),
+ "plugins/MultiSites/angularjs/dashboard/dashboard-model.js" => array("8474", "8ae117224a8c797c54f7fe8383503368"),
+ "plugins/MultiSites/angularjs/site/site-directive.js" => array("1984", "4944d752571c5e40cf6717bc74ef83cf"),
+ "plugins/MultiSites/angularjs/site/site.html" => array("2225", "e2ce56b83fd0599b73ffb2c64c8f6d55"),
+ "plugins/MultiSites/API.php" => array("20412", "11973574d15341554d0bec191dfea6e4"),
+ "plugins/MultiSites/Controller.php" => array("2577", "b68eb4db8925d0a5f3a6a445c76c7c4d"),
+ "plugins/MultiSites/images/arrow_asc.gif" => array("120", "ff5921e3047f33fa2ddc2b85c960133c"),
+ "plugins/MultiSites/images/arrow_desc.gif" => array("130", "38ab9e8c27b8ce2d1a6b6f9753206f4b"),
+ "plugins/MultiSites/images/arrow_down_green.png" => array("221", "9b81c5a2fb3f3b979e5d941a7b51d0e5"),
+ "plugins/MultiSites/images/arrow_down.png" => array("234", "e28552b85bf45e547a6fa5a387c8a062"),
+ "plugins/MultiSites/images/arrow_up.png" => array("222", "d5642aff98a988d93f317894b665eeb2"),
+ "plugins/MultiSites/images/arrow_up_red.png" => array("248", "906746c2060a32f4aa0ee204b60fe027"),
+ "plugins/MultiSites/images/door_in.png" => array("693", "e20ba15525185c16acfbf043e7b4a9cd"),
+ "plugins/MultiSites/images/link.gif" => array("75", "b8de0b2b517e1999b32353209be4e976"),
+ "plugins/MultiSites/images/loading-blue.gif" => array("1849", "483d45d0beb0b5547988926b795f8190"),
+ "plugins/MultiSites/images/stop.png" => array("307", "031e5dba74ad3b375096806ff1662402"),
+ "plugins/MultiSites/MultiSites.php" => array("4567", "09b8e3130ea25bbb6eaa46058c854f2d"),
+ "plugins/MultiSites/templates/getSitesInfo.twig" => array("791", "93ca3b820a17e74883bd777766da5660"),
+ "plugins/Overlay/API.php" => array("4545", "5bdb88b44970b0286c3c6476115f7422"),
+ "plugins/Overlay/client/client.css" => array("2403", "b89a7f8790c46666d3f1f4bd324b0842"),
+ "plugins/Overlay/client/client.js" => array("7986", "c91fc2d4c5af7f909a3189cb4e6bd10b"),
+ "plugins/Overlay/client/close.png" => array("655", "42492684e24356a4081134894eabeb9e"),
+ "plugins/Overlay/client/followingpages.js" => array("20057", "017a346250b30fe408b154388439dac4"),
+ "plugins/Overlay/client/linktags.eps" => array("460096", "3b89babacdf68b23c14e37619af9c6a7"),
+ "plugins/Overlay/client/linktags_lessshadow.png" => array("6353", "80e04f62a5efa4ec128efb40b0670d37"),
+ "plugins/Overlay/client/linktags_noshadow.png" => array("5355", "1471510499dfc32c978ace341095f0aa"),
+ "plugins/Overlay/client/linktags.png" => array("6489", "1e7ee586288b9aa1d1ecb3b6cc402cae"),
+ "plugins/Overlay/client/linktags.psd" => array("38518", "0ad1ccd72db63437365e1d2225213afe"),
+ "plugins/Overlay/client/loading.gif" => array("723", "6ce8f9a2c650cf90261acfc98b2edf90"),
+ "plugins/Overlay/client/translations.js" => array("756", "b25f65e35f2091e51f3846f58f0b0843"),
+ "plugins/Overlay/client/urlnormalizer.js" => array("5688", "4c43dc9ca32de3fcf5f6c9fb3a93082f"),
+ "plugins/Overlay/Controller.php" => array("8120", "0145004a547bfc7983686f765040b2c9"),
+ "plugins/Overlay/images/info.png" => array("778", "3750c701d2ec35a45d289b9b9c1a0667"),
+ "plugins/Overlay/images/overlay_icon_hover.png" => array("360", "7021a0169999242feade5c21859185b1"),
+ "plugins/Overlay/images/overlay_icon.png" => array("359", "dfabdc7dd24cad1b55101fa5dc77a2ad"),
+ "plugins/Overlay/javascripts/Overlay_Helper.js" => array("952", "76159386fdc4544d42d58af61051f655"),
+ "plugins/Overlay/javascripts/Piwik_Overlay.js" => array("8715", "0757e757ec98579e800c54557633f78b"),
+ "plugins/Overlay/javascripts/rowaction.js" => array("1904", "47d98c45ed01ec168770ea77fa4d200b"),
+ "plugins/Overlay/Overlay.php" => array("1239", "5f268e56246335ca2ab65fb295d537a4"),
+ "plugins/Overlay/stylesheets/overlay.css" => array("2736", "c45eed3e933565c3737d3500d642fa53"),
+ "plugins/Overlay/stylesheets/showErrorWrongDomain.css" => array("225", "c816e5c78ca1e4b4cffd6a94defcd42d"),
+ "plugins/Overlay/templates/index_noframe.twig" => array("651", "c52979a6cf4b4271976930fee8d75c9c"),
+ "plugins/Overlay/templates/index.twig" => array("2884", "39ccaab561b54a8b08d2928a111df52d"),
+ "plugins/Overlay/templates/notifyParentIframe.twig" => array("408", "511ff3cd9499c53ca4c83bf12ca4a2d5"),
+ "plugins/Overlay/templates/renderSidebar.twig" => array("872", "2692f857cb9ed8c4e57e42e32f9e9d79"),
+ "plugins/Overlay/templates/showErrorWrongDomain.twig" => array("434", "5ea0fec6f13ac36e82316bd98faffa17"),
+ "plugins/PrivacyManager/Config.php" => array("3733", "cb83704c0bef57bfd13926749ff933c0"),
+ "plugins/PrivacyManager/Controller.php" => array("12791", "f42e2f56e22820fc57379e2ca5445728"),
+ "plugins/PrivacyManager/DoNotTrackHeaderChecker.php" => array("2463", "f8225b87bffb42e0f2e37677b3887991"),
+ "plugins/PrivacyManager/IPAnonymizer.php" => array("2588", "b1d6687046c4138308f8906e29f5aa27"),
+ "plugins/PrivacyManager/javascripts/privacySettings.js" => array("7303", "6bc9a333ab44580364f515dabaff87d5"),
+ "plugins/PrivacyManager/LogDataPurger.php" => array("11350", "1615d14d06032ab180b20ec90d07475e"),
+ "plugins/PrivacyManager/PrivacyManager.php" => array("17475", "20d15ebced957a28e59abfda6bb85c9b"),
+ "plugins/PrivacyManager/ReportsPurger.php" => array("14516", "f5927d9196cb3efb7804c537b243a0f4"),
+ "plugins/PrivacyManager/templates/getDatabaseSize.twig" => array("394", "b20b279686f6084c9b6d1e054711991c"),
+ "plugins/PrivacyManager/templates/privacySettings.twig" => array("20783", "b213cd581d7f612668eded5b4daf9875"),
+ "plugins/Provider/API.php" => array("1282", "02ed45d84f2acf2d31d1ec3dfa657715"),
+ "plugins/Provider/Archiver.php" => array("885", "12721c47c4a162253ef42338f86d6407"),
+ "plugins/Provider/Controller.php" => array("442", "074ac889d8bbd8521336f33cafc5980b"),
+ "plugins/Provider/functions.php" => array("1874", "fcce854c96b6a99d906cb348833c6f79"),
+ "plugins/Provider/Provider.php" => array("7890", "bcf48128ceabc04b1295ba16cffbaf20"),
+ "plugins/Proxy/Controller.php" => array("3809", "92af78c5373bb29f60fa3dbbdc6e96de"),
+ "plugins/Proxy/Proxy.php" => array("748", "7fa5c2c560d621ea057cb3258f6ed0e4"),
+ "plugins/Referrers/API.php" => array("21979", "18add5717656db81281bf2aab318f4bd"),
+ "plugins/Referrers/Archiver.php" => array("11276", "312f7099cd0f95a633a20a20d9e0e7a3"),
+ "plugins/Referrers/Controller.php" => array("21890", "3d8ee0bbab081d220d5501ba2535e9a6"),
+ "plugins/Referrers/functions.php" => array("6981", "5bb99c15c9912d20d94aa399831c3cc3"),
+ "plugins/Referrers/images/searchEngines/1.cz.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"),
+ "plugins/Referrers/images/searchEngines/abcsok.no.png" => array("734", "46c308c614869c6a7b6dea023df9fb7a"),
+ "plugins/Referrers/images/searchEngines/alexa.com.png" => array("878", "dea186e4049c87f56d90aa473cc58e65"),
+ "plugins/Referrers/images/searchEngines/all.by.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"),
+ "plugins/Referrers/images/searchEngines/apollo7.de.png" => array("477", "abcd4567b53f0fe2bdcca311c2a187bd"),
+ "plugins/Referrers/images/searchEngines/apollo.lv.png" => array("664", "bfa565028e74747d1e23c08c552cf66d"),
+ "plugins/Referrers/images/searchEngines/arama.com.png" => array("736", "cba21479a2a41f47c032f9b5b64f78f8"),
+ "plugins/Referrers/images/searchEngines/ariadna.elmundo.es.png" => array("272", "3914156463a1c2b30ab7cdb016daa05d"),
+ "plugins/Referrers/images/searchEngines/arianna.libero.it.png" => array("763", "c1a56d1d62a9688628a7535c7a1fc43e"),
+ "plugins/Referrers/images/searchEngines/ask.com.png" => array("587", "abe650b5fec9261b153ca4ec220f7957"),
+ "plugins/Referrers/images/searchEngines/bg.setooz.com.png" => array("493", "edaf9d5fc1de89a88a7962e0d9b2bb16"),
+ "plugins/Referrers/images/searchEngines/bing.com.png" => array("590", "b0e1af67d03e24178a6d36188ba5fca5"),
+ "plugins/Referrers/images/searchEngines/blekko.com.png" => array("174", "9d2a93c74a1a5e12be0b3e86738d9ca5"),
+ "plugins/Referrers/images/searchEngines/blogsearch.google.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"),
+ "plugins/Referrers/images/searchEngines/blogs.icerocket.com.png" => array("461", "79157675a65ad90c9a142b49f23e39aa"),
+ "plugins/Referrers/images/searchEngines/buscador.terra.es.png" => array("869", "63d72ea6bbcc6d19955873b50b5426d2"),
+ "plugins/Referrers/images/searchEngines/busca.orange.es.png" => array("430", "da209f011cf118792af6682db448e0cc"),
+ "plugins/Referrers/images/searchEngines/busca.uol.com.br.png" => array("701", "b7108a19b9c249b774f6a5777580cf18"),
+ "plugins/Referrers/images/searchEngines/cgi.search.biglobe.ne.jp.png" => array("716", "806a4ed72d78c7b84472567d28589894"),
+ "plugins/Referrers/images/searchEngines/claro-search.com.png" => array("564", "302c71d5eb2d7216713dc6bec51d51d4"),
+ "plugins/Referrers/images/searchEngines/daemon-search.com.png" => array("718", "c14a1c8b35e78a1d6661f7e25e370f9f"),
+ "plugins/Referrers/images/searchEngines/digg.com.png" => array("465", "1755793491662ad23c71eb1623706dc2"),
+ "plugins/Referrers/images/searchEngines/dir.gigablast.com.png" => array("846", "e81d15ca1d53cf6a929fefa476ed144d"),
+ "plugins/Referrers/images/searchEngines/dmoz.org.png" => array("732", "ede1566f596031434b7dc6f7444bd371"),
+ "plugins/Referrers/images/searchEngines/duckduckgo.com.png" => array("900", "52012c79bfab9cc5001892030f0a9fc3"),
+ "plugins/Referrers/images/searchEngines/ecosia.org.png" => array("667", "2a93403cf58fb4ad1f7393abca387180"),
+ "plugins/Referrers/images/searchEngines/encrypted.google.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"),
+ "plugins/Referrers/images/searchEngines/eo.st.png" => array("445", "aa76f4c0c414201782d015ee304643d0"),
+ "plugins/Referrers/images/searchEngines/forestle.org.png" => array("814", "3849505c44dd9af3564964b2c7f72c24"),
+ "plugins/Referrers/images/searchEngines/fr.dir.com.png" => array("735", "a08719a48edcf34246e545276a9537b6"),
+ "plugins/Referrers/images/searchEngines/friendfeed.com.png" => array("524", "91638ac895b524e32425e742cfd98dd0"),
+ "plugins/Referrers/images/searchEngines/fr.wedoo.com.png" => array("490", "b6a644b5c63af673421a7ee33f63e154"),
+ "plugins/Referrers/images/searchEngines/gais.cs.ccu.edu.tw.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"),
+ "plugins/Referrers/images/searchEngines/geona.net.png" => array("3072", "4396514318dc0485426e50686a887997"),
+ "plugins/Referrers/images/searchEngines/go.mail.ru.png" => array("409", "a4e16117bb905b547a4607836503f858"),
+ "plugins/Referrers/images/searchEngines/google.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"),
+ "plugins/Referrers/images/searchEngines/googlesyndicatedsearch.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"),
+ "plugins/Referrers/images/searchEngines/holmes.ge.png" => array("774", "89027cbd84500007fb09c4ed1a2c8273"),
+ "plugins/Referrers/images/searchEngines/images.google.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"),
+ "plugins/Referrers/images/searchEngines/images.search.yahoo.com.png" => array("538", "9c23aa1c6d5ad4fdea3738632364cbb2"),
+ "plugins/Referrers/images/searchEngines/images.yandex.ru.png" => array("497", "0ff0e2ee9e0b08fbfba2eaa9f4612848"),
+ "plugins/Referrers/images/searchEngines/infospace.com.png" => array("940", "c269cb5b0658fe1679db465613a7c406"),
+ "plugins/Referrers/images/searchEngines/iwon.ask.com.png" => array("587", "abe650b5fec9261b153ca4ec220f7957"),
+ "plugins/Referrers/images/searchEngines/ixquick.com.png" => array("613", "203d372a14add8806719d1fe499210e4"),
+ "plugins/Referrers/images/searchEngines/junglekey.com.png" => array("580", "498a589e8b6b01a45be9b5b090897c03"),
+ "plugins/Referrers/images/searchEngines/jyxo.1188.cz.png" => array("401", "f1845d795944ceb17f0d2490e771331d"),
+ "plugins/Referrers/images/searchEngines/ko.search.need2find.com.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"),
+ "plugins/Referrers/images/searchEngines/lo.st.png" => array("828", "cebd310f1c9e95a2afab3699e8877e4a"),
+ "plugins/Referrers/images/searchEngines/maps.google.com.png" => array("1132", "b95321cca7e7e4745c9478060113984e"),
+ "plugins/Referrers/images/searchEngines/metager2.de.png" => array("556", "147f78f79ab041ce6131f310a94ad772"),
+ "plugins/Referrers/images/searchEngines/meta.rrzn.uni-hannover.de.png" => array("281", "c851ac18187f08ac7f5860f31f5a8231"),
+ "plugins/Referrers/images/searchEngines/meta.ua.png" => array("873", "190caab492691e94df900c74f9445f85"),
+ "plugins/Referrers/images/searchEngines/news.google.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"),
+ "plugins/Referrers/images/searchEngines/nigma.ru.png" => array("673", "41743688398e2ee8a5e30bb32f74ad34"),
+ "plugins/Referrers/images/searchEngines/nova.rambler.ru.png" => array("765", "f74b4c361dfae2e7a1b4116ad7b28925"),
+ "plugins/Referrers/images/searchEngines/online.no.png" => array("591", "58e1432fce37400564a576212cadf667"),
+ "plugins/Referrers/images/searchEngines/otsing.delfi.ee.png" => array("543", "46e62fb3ebddb902b5b426dda74bdc07"),
+ "plugins/Referrers/images/searchEngines/pesquisa.clix.pt.png" => array("838", "97772b088367714116f98eb907facf1e"),
+ "plugins/Referrers/images/searchEngines/pesquisa.sapo.pt.png" => array("718", "34578b89393fc073f9dc6b8020834624"),
+ "plugins/Referrers/images/searchEngines/plusnetwork.com.png" => array("719", "e8b97277b41273e1d5c356b4d43b722f"),
+ "plugins/Referrers/images/searchEngines/poisk.ru.png" => array("275", "9ceb066fcbdf350f7c62afda4de91779"),
+ "plugins/Referrers/images/searchEngines/p.zhongsou.com.png" => array("869", "c26a7cf8255c86ff68507a9e4dff651a"),
+ "plugins/Referrers/images/searchEngines/recherche.francite.com.png" => array("706", "ea7d6f697a21a5ebad227bca27228ad5"),
+ "plugins/Referrers/images/searchEngines/rechercher.aliceadsl.fr.png" => array("755", "c2adde9c48b4f209ec0b5517d8ebb986"),
+ "plugins/Referrers/images/searchEngines/req.hit-parade.com.png" => array("275", "fcba5ae6bd7ad81283454b6307e65f10"),
+ "plugins/Referrers/images/searchEngines/ricerca.virgilio.it.png" => array("333", "f2e51cb3e90152cd7acd6db08912d404"),
+ "plugins/Referrers/images/searchEngines/rpmfind.net.png" => array("867", "9a85c9538342f8612b52c7932a7e1484"),
+ "plugins/Referrers/images/searchEngines/s1.metacrawler.de.png" => array("368", "7be2b165fae10c54b6b28b2b69be07d6"),
+ "plugins/Referrers/images/searchEngines/scholar.google.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"),
+ "plugins/Referrers/images/searchEngines/scour.com.png" => array("706", "21e8a06160647a9d9406c745c65327a6"),
+ "plugins/Referrers/images/searchEngines/searchalot.com.png" => array("369", "6dad79f26644dfc7640af20743994c82"),
+ "plugins/Referrers/images/searchEngines/search.aol.com.png" => array("713", "52bf4d61fe18c1e8564c75709cfee36a"),
+ "plugins/Referrers/images/searchEngines/searchatlas.centrum.cz.png" => array("716", "fac3ddbc5e34c229aac77d4297d09e8b"),
+ "plugins/Referrers/images/searchEngines/search.babylon.com.png" => array("930", "5db9449cdc8ab7964896f62c26bd5799"),
+ "plugins/Referrers/images/searchEngines/search.bluewin.ch.png" => array("349", "4ce3ac28b1d3446d71c8db2335f79506"),
+ "plugins/Referrers/images/searchEngines/search.centrum.cz.png" => array("599", "4a90a6fd8cd5a587ee3f887fb2e91a87"),
+ "plugins/Referrers/images/searchEngines/search.comcast.net.png" => array("839", "642a06a86184d81af3a95a149f3b3dbd"),
+ "plugins/Referrers/images/searchEngines/search.conduit.com.png" => array("617", "359aa81dbcc48d70537a37d661bf8dbd"),
+ "plugins/Referrers/images/searchEngines/search.daum.net.png" => array("800", "fc91668b8deb23b4b7290dceb75f2428"),
+ "plugins/Referrers/images/searchEngines/search.earthlink.net.png" => array("564", "889aec3d7357d1c7cbb6d52225644907"),
+ "plugins/Referrers/images/searchEngines/search.excite.it.png" => array("323", "5c915ccd6526168e81de15c7fefb050a"),
+ "plugins/Referrers/images/searchEngines/search.freecause.com.png" => array("951", "834f2c111eed35eaf997913c20ab56ff"),
+ "plugins/Referrers/images/searchEngines/search.free.fr.png" => array("881", "02f2297d212c26bd136a99fbc5488170"),
+ "plugins/Referrers/images/searchEngines/search.goo.ne.jp.png" => array("730", "8d650ba94af30886600ca236f9fbb443"),
+ "plugins/Referrers/images/searchEngines/search.imesh.com.png" => array("644", "2dd75a109986a2337893e3b0d0a621a1"),
+ "plugins/Referrers/images/searchEngines/search.ke.voila.fr.png" => array("331", "5a2240c18b117cf38cc00914d01ee34e"),
+ "plugins/Referrers/images/searchEngines/search.lycos.com.png" => array("367", "ad1b661599c12fdab08d92a5d35c3fa1"),
+ "plugins/Referrers/images/searchEngines/search.nate.com.png" => array("670", "a9c88ec2867945d90b9f68f927d1e2db"),
+ "plugins/Referrers/images/searchEngines/search.naver.com.png" => array("317", "5de3a5f618088b08fa92181ae4063b70"),
+ "plugins/Referrers/images/searchEngines/search.nifty.com.png" => array("565", "f38b9e99892cf2fdf146a8f6e2731858"),
+ "plugins/Referrers/images/searchEngines/search.peoplepc.com.png" => array("832", "67f88efd19919d2292405319f6d5092d"),
+ "plugins/Referrers/images/searchEngines/search.qip.ru.png" => array("556", "8bc854ef9a247905ed29938e69d2cea0"),
+ "plugins/Referrers/images/searchEngines/search.rr.com.png" => array("794", "3e9a94cd34825c4246eb666338b22e6b"),
+ "plugins/Referrers/images/searchEngines/searchservice.myspace.com.png" => array("610", "96ecdce6052dd3a535f95ed92c056bc2"),
+ "plugins/Referrers/images/searchEngines/search.seznam.cz.png" => array("553", "4b4f7b4eec38531fe662f678682041f8"),
+ "plugins/Referrers/images/searchEngines/search.smartaddressbar.com.png" => array("624", "13ce4ccf571cd2f09553318b719948b2"),
+ "plugins/Referrers/images/searchEngines/search.snap.do.png" => array("570", "efb27bff7c57883d2b7c9f1d96a4a65f"),
+ "plugins/Referrers/images/searchEngines/search.softonic.com.png" => array("716", "155c0a14700a0f57a40da2f4b43228a1"),
+ "plugins/Referrers/images/searchEngines/search.tiscali.it.png" => array("548", "f9fea4cbf8bc6f5748c7b1e8e5317b0e"),
+ "plugins/Referrers/images/searchEngines/search.winamp.com.png" => array("753", "3210bbc6d253913396ca23a76ef7ae51"),
+ "plugins/Referrers/images/searchEngines/search.www.ee.png" => array("625", "d0b310f299b5fe2f13f925b8a7fd8e66"),
+ "plugins/Referrers/images/searchEngines/search.yahoo.com.png" => array("522", "76ac9b6fda30c632b0c4258d72072680"),
+ "plugins/Referrers/images/searchEngines/search.yam.com.png" => array("186", "5585e8456efc56e4ccd870fce71c349a"),
+ "plugins/Referrers/images/searchEngines/search.yippy.com.png" => array("654", "348c872e7f49261c025074587210c494"),
+ "plugins/Referrers/images/searchEngines/sm.aport.ru.png" => array("469", "956d1ffb844dcd9c22fc0f82dd02f6d8"),
+ "plugins/Referrers/images/searchEngines/smart.delfi.lv.png" => array("543", "46e62fb3ebddb902b5b426dda74bdc07"),
+ "plugins/Referrers/images/searchEngines/so.360.cn.png" => array("480", "083a258a36d5571cbb3cdbca50231f89"),
+ "plugins/Referrers/images/searchEngines/startgoogle.startpagina.nl.png" => array("801", "f60a50c67088bd1387631ff895df1b9a"),
+ "plugins/Referrers/images/searchEngines/start.iplay.com.png" => array("293", "96909e49478da43937ced8de17bb7b43"),
+ "plugins/Referrers/images/searchEngines/suche.freenet.de.png" => array("719", "4fb7eeb8dbd2eefd6bb5572d87ca4be9"),
+ "plugins/Referrers/images/searchEngines/suche.info.png" => array("888", "9273755eccf191f5d6b332b43f316219"),
+ "plugins/Referrers/images/searchEngines/suche.t-online.de.png" => array("470", "c0e4b19d5d61b6d97befdb347ae53260"),
+ "plugins/Referrers/images/searchEngines/suche.web.de.png" => array("350", "bbfdec13aa9bf83b184327ad28ec98bd"),
+ "plugins/Referrers/images/searchEngines/surfcanyon.com.png" => array("686", "8c68cb01ee94ae8b2dce7f4f7b22d962"),
+ "plugins/Referrers/images/searchEngines/szukaj.onet.pl.png" => array("796", "23f87874093150c9cc5a3d72c7713aae"),
+ "plugins/Referrers/images/searchEngines/szukaj.wp.pl.png" => array("672", "a14bb3c9901c8158d7051ed50b40a6df"),
+ "plugins/Referrers/images/searchEngines/technorati.com.png" => array("567", "23fbfff1215e8bf0270b529849200214"),
+ "plugins/Referrers/images/searchEngines/translate.google.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"),
+ "plugins/Referrers/images/searchEngines/video.google.com.png" => array("545", "22c5e4db03b94c9ba501f522d8011a53"),
+ "plugins/Referrers/images/searchEngines/web.canoe.ca.png" => array("721", "7645213daa2213dc1b1361a42c11700c"),
+ "plugins/Referrers/images/searchEngines/websearch.cs.com.png" => array("806", "daf75065560d60f37511023d8b2a4212"),
+ "plugins/Referrers/images/searchEngines/websearch.rakuten.co.jp.png" => array("513", "e32baedde4b2a9b508336e2560a7b8ec"),
+ "plugins/Referrers/images/searchEngines/web.volny.cz.png" => array("619", "99d3a4f4a17741a150a388a3341c23e9"),
+ "plugins/Referrers/images/searchEngines/www.123people.com.png" => array("724", "a8ea0943911abdfd7978bae93da9fe16"),
+ "plugins/Referrers/images/searchEngines/www.1881.no.png" => array("528", "f5b12a0935ac022931e4285c9e6050bc"),
+ "plugins/Referrers/images/searchEngines/www1.dastelefonbuch.de.png" => array("556", "3579471b7521c789e0b60fcd5d956f86"),
+ "plugins/Referrers/images/searchEngines/www2.austronaut.at.png" => array("498", "bd9f63b819bfef1659dde292561e7ad6"),
+ "plugins/Referrers/images/searchEngines/www2.inbox.com.png" => array("569", "5b62059714bd57204e28fc5572b89fde"),
+ "plugins/Referrers/images/searchEngines/www3.zoek.nl.png" => array("1038", "bc78e6bf962a4e633fbb78dd5bb00a08"),
+ "plugins/Referrers/images/searchEngines/www.abacho.de.png" => array("545", "9f0d64f72999c8069842987b8713fac6"),
+ "plugins/Referrers/images/searchEngines/www.acoon.de.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"),
+ "plugins/Referrers/images/searchEngines/www.allesklar.de.png" => array("328", "d104c475b308bc29ca17d41dd852aa85"),
+ "plugins/Referrers/images/searchEngines/www.alltheweb.com.png" => array("458", "26f469c6d9701c54ccfed58d4188c6dc"),
+ "plugins/Referrers/images/searchEngines/www.altavista.com.png" => array("627", "06323034af3945170fbdcd2884ad0197"),
+ "plugins/Referrers/images/searchEngines/www.arcor.de.png" => array("924", "9b4af83fded3c2b4f29ca2c19de9d014"),
+ "plugins/Referrers/images/searchEngines/www.baidu.com.png" => array("725", "65946f31c95143d6f316b471f8a5f5eb"),
+ "plugins/Referrers/images/searchEngines/www.blogdigger.com.png" => array("863", "2a0c218c33c5e169e18a538d2b1d7aa6"),
+ "plugins/Referrers/images/searchEngines/www.blogpulse.com.png" => array("725", "6d46916da44302a96770d0379068bac7"),
+ "plugins/Referrers/images/searchEngines/www.charter.net.png" => array("861", "5a4105275865e5dce2c41afdb95f34ae"),
+ "plugins/Referrers/images/searchEngines/www.crawler.com.png" => array("392", "d9aba7a98b84a5f0eb74f5f9f26d9eca"),
+ "plugins/Referrers/images/searchEngines/www.cuil.com.png" => array("708", "17e2ef0a8adf65ddc02942c8b260b2a1"),
+ "plugins/Referrers/images/searchEngines/www.dasoertliche.de.png" => array("767", "6001417565edb464b5848a4b1ed4f3a4"),
+ "plugins/Referrers/images/searchEngines/www.eniro.se.png" => array("758", "3e28e3ac9105723fc9902af3b259a0bd"),
+ "plugins/Referrers/images/searchEngines/www.eurip.com.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"),
+ "plugins/Referrers/images/searchEngines/www.euroseek.com.png" => array("841", "1be6aefacc4275a656ab0dc82bd197cf"),
+ "plugins/Referrers/images/searchEngines/www.everyclick.com.png" => array("843", "b89c7db338a93d5a420ddbdea148501e"),
+ "plugins/Referrers/images/searchEngines/www.exalead.fr.png" => array("880", "6cb4ba9a2fda31f0063972e6d608412e"),
+ "plugins/Referrers/images/searchEngines/www.facebook.com.png" => array("349", "34811a0b31ca7dd2934cc02cffbcdc95"),
+ "plugins/Referrers/images/searchEngines/www.fastbrowsersearch.com.png" => array("965", "d1616dfa7bf9d20634b52857f1c91177"),
+ "plugins/Referrers/images/searchEngines/www.fireball.de.png" => array("275", "7831b2d7ebbbacacf8ae272bb407e599"),
+ "plugins/Referrers/images/searchEngines/www.firstsfind.com.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"),
+ "plugins/Referrers/images/searchEngines/www.fixsuche.de.png" => array("975", "4701f16ae042e18187b99ad23da4aef0"),
+ "plugins/Referrers/images/searchEngines/www.flix.de.png" => array("518", "86af8975ae1e5c87c91332155b294dc6"),
+ "plugins/Referrers/images/searchEngines/www.gigablast.com.png" => array("846", "e81d15ca1d53cf6a929fefa476ed144d"),
+ "plugins/Referrers/images/searchEngines/www.gnadenmeer.de.png" => array("895", "8e70fc1ecfb90fbb4d0be558c79c56c5"),
+ "plugins/Referrers/images/searchEngines/www.gomeo.com.png" => array("432", "8b06ecc277f5b2a0aa4ab46b19f2ca72"),
+ "plugins/Referrers/images/searchEngines/www.google.interia.pl.png" => array("690", "fa07f267bcb77246a0cf5e76a77b5c77"),
+ "plugins/Referrers/images/searchEngines/www.goyellow.de.png" => array("778", "d410a511b82055b8817585031292756c"),
+ "plugins/Referrers/images/searchEngines/www.gulesider.no.png" => array("575", "0eae2709e4afbe459883bcb690dbdeef"),
+ "plugins/Referrers/images/searchEngines/www.highbeam.com.png" => array("705", "d6fd2b1bd62893f37451bc0fe4d8b0a0"),
+ "plugins/Referrers/images/searchEngines/www.hooseek.com.png" => array("548", "610bbf7fd42486596a7edf046c0aa3ba"),
+ "plugins/Referrers/images/searchEngines/www.hotbot.com.png" => array("275", "7831b2d7ebbbacacf8ae272bb407e599"),
+ "plugins/Referrers/images/searchEngines/www.icq.com.png" => array("692", "08c5c23ac6d124387547282977787515"),
+ "plugins/Referrers/images/searchEngines/www.ilse.nl.png" => array("854", "7321baaf2ff095c525fb1a27dd8a3256"),
+ "plugins/Referrers/images/searchEngines/www.jungle-spider.de.png" => array("867", "6c675278c93d7f5145c116fd12b83076"),
+ "plugins/Referrers/images/searchEngines/www.kataweb.it.png" => array("273", "b9f2ffb235b4459c6e2b6f3f8c811b1c"),
+ "plugins/Referrers/images/searchEngines/www.kvasir.no.png" => array("395", "2be12aaef3172e501bb5c2960c4bde26"),
+ "plugins/Referrers/images/searchEngines/www.latne.lv.png" => array("756", "89197fc28e878fb017d1fe0f2be63b26"),
+ "plugins/Referrers/images/searchEngines/www.looksmart.com.png" => array("423", "c63999bc972ee4181b3aed5506727684"),
+ "plugins/Referrers/images/searchEngines/www.maailm.com.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"),
+ "plugins/Referrers/images/searchEngines/www.mamma.com.png" => array("931", "fbd287ad4cb8ff307ddfde1a61ba6b18"),
+ "plugins/Referrers/images/searchEngines/www.meinestadt.de.png" => array("545", "985a57518000ffa9ee74433b697be26b"),
+ "plugins/Referrers/images/searchEngines/www.mister-wong.com.png" => array("1065", "90af7f0ef98dd4a44a59298e6bd11287"),
+ "plugins/Referrers/images/searchEngines/www.monstercrawler.com.png" => array("992", "8d60a75253a6679cd88589e1f527eb31"),
+ "plugins/Referrers/images/searchEngines/www.mozbot.fr.png" => array("539", "a5454d5f9612b9be0592c46caaaf5a0f"),
+ "plugins/Referrers/images/searchEngines/www.mysearch.com.png" => array("754", "0ce5f3e478b44af9c0203820730d0782"),
+ "plugins/Referrers/images/searchEngines/www.najdi.si.png" => array("746", "d1419b2cbe8dc0a2685d71b284b2ff7f"),
+ "plugins/Referrers/images/searchEngines/www.neti.ee.png" => array("874", "a5b8bc123991996558acdae38e2f7edf"),
+ "plugins/Referrers/images/searchEngines/www.paperball.de.png" => array("275", "7831b2d7ebbbacacf8ae272bb407e599"),
+ "plugins/Referrers/images/searchEngines/www.picsearch.com.png" => array("661", "7fd004f30bee9ed392c3dfdd36bc7cce"),
+ "plugins/Referrers/images/searchEngines/www.plazoo.com.png" => array("471", "ce3c7526febd2d33468292a2cc18fd05"),
+ "plugins/Referrers/images/searchEngines/www.qualigo.at.png" => array("627", "c793911b3efed6122c975023c6c86369"),
+ "plugins/Referrers/images/searchEngines/www.searchcanvas.com.png" => array("867", "a148aa4f2ebd3a9d627b156c261b11f1"),
+ "plugins/Referrers/images/searchEngines/www.search.ch.png" => array("344", "cbf2de2cac7cf39897ed488301b88687"),
+ "plugins/Referrers/images/searchEngines/www.search.com.png" => array("517", "7675624d816df97773003fd968d3fb92"),
+ "plugins/Referrers/images/searchEngines/www.searchy.co.uk.png" => array("656", "e6018e106ad0190fff95a73bb3bb31d9"),
+ "plugins/Referrers/images/searchEngines/www.sharelook.fr.png" => array("433", "85be47a9b158874e7831982d2872c4f3"),
+ "plugins/Referrers/images/searchEngines/www.skynet.be.png" => array("855", "9561e59ff5068d31f7a3a95bbed6bb8f"),
+ "plugins/Referrers/images/searchEngines/www.sogou.com.png" => array("636", "babba0fbf8f3dc3f8e332a9bc0e0b616"),
+ "plugins/Referrers/images/searchEngines/www.soso.com.png" => array("895", "4cb24e3d5b5b4f434e9e77b78283fb6e"),
+ "plugins/Referrers/images/searchEngines/www.startsiden.no.png" => array("285", "4d50ad01809bb118e0e14b690c2898c2"),
+ "plugins/Referrers/images/searchEngines/www.suchmaschine.com.png" => array("605", "fec00799f617313dcec453d470bcc1ec"),
+ "plugins/Referrers/images/searchEngines/www.suchnase.de.png" => array("289", "c0f4d4e1a6d633a2de161d847e51ae38"),
+ "plugins/Referrers/images/searchEngines/www.talimba.com.png" => array("735", "a99e8625fb9bddd6a8bdcac8a8dfe492"),
+ "plugins/Referrers/images/searchEngines/www.talktalk.co.uk.png" => array("457", "26dd7805cb12703447280f3988a36662"),
+ "plugins/Referrers/images/searchEngines/www.teoma.com.png" => array("564", "6feee990d3cd058e302adac8e750428b"),
+ "plugins/Referrers/images/searchEngines/www.tixuma.de.png" => array("933", "ac483ba65b637e352bd9b7edf11e462e"),
+ "plugins/Referrers/images/searchEngines/www.toile.com.png" => array("868", "7e46a21790c2ca55a8adacaa3ecb860f"),
+ "plugins/Referrers/images/searchEngines/www.toolbarhome.com.png" => array("617", "d39570e56b848781f614ae70f8ccd8a9"),
+ "plugins/Referrers/images/searchEngines/www.trouvez.com.png" => array("498", "cfcae76baa6008dbdad8d65736ca9c07"),
+ "plugins/Referrers/images/searchEngines/www.trovarapido.com.png" => array("540", "25010fae9beae3454550b78cd1c62cca"),
+ "plugins/Referrers/images/searchEngines/www.trusted-search.com.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"),
+ "plugins/Referrers/images/searchEngines/www.twingly.com.png" => array("459", "2898c7f2d9f5b014299da9f9a6b3bef1"),
+ "plugins/Referrers/images/searchEngines/www.url.org.png" => array("584", "ac66fdc45b162af2e579875b516f151f"),
+ "plugins/Referrers/images/searchEngines/www.vinden.nl.png" => array("550", "9b0acbf516f9070d9f8644ae48e1e756"),
+ "plugins/Referrers/images/searchEngines/www.vindex.nl.png" => array("627", "0b2831d17790a99df8644b335baa923f"),
+ "plugins/Referrers/images/searchEngines/www.walhello.info.png" => array("811", "efa838f67dd8595a651e800500c72057"),
+ "plugins/Referrers/images/searchEngines/www.web.nl.png" => array("736", "64629e968a100f3d52bbd645f3eeeabd"),
+ "plugins/Referrers/images/searchEngines/www.weborama.fr.png" => array("978", "c56c4180238352c79c39105ca60c8d41"),
+ "plugins/Referrers/images/searchEngines/www.websearch.com.png" => array("830", "b7275242d17e492422677fd66e4fe38a"),
+ "plugins/Referrers/images/searchEngines/www.witch.de.png" => array("384", "3471981c242407d47d9786ee4b9dea4a"),
+ "plugins/Referrers/images/searchEngines/www.x-recherche.com.png" => array("546", "1f424da61da3eb848ecef393a6bccfc5"),
+ "plugins/Referrers/images/searchEngines/www.yasni.de.png" => array("747", "01c5182fc922793a5b408411b53a3c6e"),
+ "plugins/Referrers/images/searchEngines/www.yatedo.com.png" => array("647", "aa0d9cbbe207eaa5bcab22dee20185ce"),
+ "plugins/Referrers/images/searchEngines/www.yougoo.fr.png" => array("731", "d616bbdaae629c1268580b8191ce03de"),
+ "plugins/Referrers/images/searchEngines/www.zapmeta.com.png" => array("220", "1707e427049d54e0046fdbe8d6b90250"),
+ "plugins/Referrers/images/searchEngines/www.zoeken.nl.png" => array("598", "d26fe06a02e1b7e33e0f519af929d9a6"),
+ "plugins/Referrers/images/searchEngines/www.zoznam.sk.png" => array("349", "b690e483bd1a89acd54f0f21531453f6"),
+ "plugins/Referrers/images/searchEngines/xx.gif" => array("55", "a7bc48d335263fe532d80efb25afb2d5"),
+ "plugins/Referrers/images/searchEngines/xx.png" => array("265", "34652cdc25d70bba01ce9ac01d929d3b"),
+ "plugins/Referrers/images/searchEngines/yandex.ru.png" => array("497", "0ff0e2ee9e0b08fbfba2eaa9f4612848"),
+ "plugins/Referrers/images/searchEngines/yellowmap.de.png" => array("750", "4a18331804df464c10cc285c86ba9db4"),
+ "plugins/Referrers/images/searchEngines/zoohoo.cz.png" => array("305", "bf13a4db674a29b03b39763e4f525d80"),
+ "plugins/Referrers/images/socials/badoo.com.png" => array("1080", "ed2fa1b5028f7c6d07f2914a275ab33d"),
+ "plugins/Referrers/images/socials/bebo.com.png" => array("978", "668d898d7c6e0f46cce4c6770f06c238"),
+ "plugins/Referrers/images/socials/blackplanet.com.png" => array("432", "9ba7571d00f4a9aa65272ffd9ac7b538"),
+ "plugins/Referrers/images/socials/buzznet.com.png" => array("462", "3b3e4ee1fe990041080a93bff2d34f98"),
+ "plugins/Referrers/images/socials/classmates.com.png" => array("861", "fc6ae2a786dba146d8279e269e39bb48"),
+ "plugins/Referrers/images/socials/douban.com.png" => array("530", "be1b7fb618ab9ea1eeb04e15b4428b1a"),
+ "plugins/Referrers/images/socials/facebook.com.png" => array("300", "cadba1be0b6e8d4e3ef1755f3cee55ed"),
+ "plugins/Referrers/images/socials/flickr.com.png" => array("1120", "4bf2b10ac1b80eac5f749ea65ff808b0"),
+ "plugins/Referrers/images/socials/flixster.com.png" => array("908", "2011245e7a16ab25fc4f0c5d21487f6a"),
+ "plugins/Referrers/images/socials/fotolog.com.png" => array("184", "54ff02becc80358d072fbee8152d6543"),
+ "plugins/Referrers/images/socials/foursquare.com.png" => array("1026", "250fef7ff00c447df3bfad80c444473b"),
+ "plugins/Referrers/images/socials/friendsreunited.com.png" => array("1113", "840685b0402d54f176e99b954711b965"),
+ "plugins/Referrers/images/socials/friendster.com.png" => array("589", "225ac089a0b2a2f0398876e869139682"),
+ "plugins/Referrers/images/socials/gaiaonline.com.png" => array("1593", "c127b6a77505784b3f299e00913bbb10"),
+ "plugins/Referrers/images/socials/geni.com.png" => array("947", "ca77efda7cb46b3abcd32fb8cbd8e2c0"),
+ "plugins/Referrers/images/socials/github.com.png" => array("399", "0d8bf5bcf113db4141c7fbbdf54fccd9"),
+ "plugins/Referrers/images/socials/habbo.com.png" => array("1090", "6f9272e8ba7cd8499a07f747c2c323fb"),
+ "plugins/Referrers/images/socials/hi5.com.png" => array("282", "c20e494873e7ba853fa18b746dbc5586"),
+ "plugins/Referrers/images/socials/hyves.nl.png" => array("1183", "c5282f8e0520c5c9cff8598ae87135f5"),
+ "plugins/Referrers/images/socials/identi.ca.png" => array("1166", "16d4be83fe740a61e52920de197f62d3"),
+ "plugins/Referrers/images/socials/last.fm.png" => array("1265", "f61e7d729ac616ddd41b67aafe834cba"),
+ "plugins/Referrers/images/socials/linkedin.com.png" => array("694", "a91e0f79ef848cf6819b0461f5d11aaf"),
+ "plugins/Referrers/images/socials/livejournal.ru.png" => array("984", "a7f82911e90475d70f512dbbdd450cbc"),
+ "plugins/Referrers/images/socials/login.live.com.png" => array("1581", "44579a577af6d00245b3b02af4d848bb"),
+ "plugins/Referrers/images/socials/login.tagged.com.png" => array("286", "0d53cd2fa01a0861f302783249909960"),
+ "plugins/Referrers/images/socials/meinvz.net.png" => array("554", "f8d3351ef56c470712ec5c31e452e695"),
+ "plugins/Referrers/images/socials/mixi.jp.png" => array("592", "15330f429050fe69d2625885e89fae8f"),
+ "plugins/Referrers/images/socials/moikrug.ru.png" => array("1206", "c38b40ddd099378a3a2b04e2924eb8b9"),
+ "plugins/Referrers/images/socials/multiply.com.png" => array("586", "3f41adad9c975e2ec481bb5ee4ae97bb"),
+ "plugins/Referrers/images/socials/myheritage.com.png" => array("1138", "ebaf4513d68f0095d7040bee1df08be7"),
+ "plugins/Referrers/images/socials/mylife.ru.png" => array("465", "283214f37f52abc61adade0c2677bf12"),
+ "plugins/Referrers/images/socials/my.mail.ru.png" => array("750", "b593c60c47256140c22e8bb0f1a52c91"),
+ "plugins/Referrers/images/socials/myspace.com.png" => array("280", "1cc0e9fcd7d381a43d2d4759b3d75392"),
+ "plugins/Referrers/images/socials/myyearbook.com.png" => array("741", "5d63ebc165436f50a064ce8f03f69521"),
+ "plugins/Referrers/images/socials/netlog.com.png" => array("636", "fb047fbf0fd1c25d74504d4b53365bce"),
+ "plugins/Referrers/images/socials/news.ycombinator.com.png" => array("1166", "b95c1fafa6166e6c337f199b9b711baa"),
+ "plugins/Referrers/images/socials/nk.pl.png" => array("628", "136217dd7a6d7008e8196632274c9423"),
+ "plugins/Referrers/images/socials/odnoklassniki.ru.png" => array("583", "4b5a37acc6a433d465603edefcb0df9e"),
+ "plugins/Referrers/images/socials/orkut.com.png" => array("496", "a3002f600988602451adb45ae9d2292b"),
+ "plugins/Referrers/images/socials/pinterest.com.png" => array("3369", "12ea288c608f89d455c6ed69131e4e8f"),
+ "plugins/Referrers/images/socials/plaxo.com.png" => array("754", "cbc9763598e8e6ff132dd0ebdcc2402a"),
+ "plugins/Referrers/images/socials/qzone.qq.com.png" => array("604", "5d336e06dcf629dce2e2887d717ee62a"),
+ "plugins/Referrers/images/socials/reddit.com.png" => array("1166", "1a961c1f4b8085054090a5926d3f0366"),
+ "plugins/Referrers/images/socials/renren.com.png" => array("784", "aaa11f9bd4b58333f501136b4f82b59b"),
+ "plugins/Referrers/images/socials/ru.netlog.com.png" => array("1252", "fbafccfe19543363c449bae7e2f76279"),
+ "plugins/Referrers/images/socials/skyrock.com.png" => array("527", "12cf60ee8e93a8936fc8d8de92262568"),
+ "plugins/Referrers/images/socials/sonico.com.png" => array("1292", "113cb411c36df6dc36432ee0d5d62f5e"),
+ "plugins/Referrers/images/socials/sourceforge.net.png" => array("1166", "5bc6c9e7f0c467874a8d435dca140518"),
+ "plugins/Referrers/images/socials/stackoverflow.com.png" => array("1166", "dd2d4e47871aea283ea2beb07280db0a"),
+ "plugins/Referrers/images/socials/studivz.net.png" => array("1222", "9fd0e7479c96e750d2efbf5068598ec2"),
+ "plugins/Referrers/images/socials/stumbleupon.com.png" => array("584", "e12262acf466e1fc00e6f8549c00390b"),
+ "plugins/Referrers/images/socials/tagged.com.png" => array("282", "c20e494873e7ba853fa18b746dbc5586"),
+ "plugins/Referrers/images/socials/taringa.net.png" => array("716", "3b60deeeec0b87e127ae16c231e1e87f"),
+ "plugins/Referrers/images/socials/tuenti.com.png" => array("902", "07880a7cdcea70ad2be60129e56a754d"),
+ "plugins/Referrers/images/socials/tumblr.com.png" => array("443", "b0931c0a06b72e6c140982710c75495f"),
+ "plugins/Referrers/images/socials/twitter.com.png" => array("892", "0b4042cb3686dd166042066e5d586ab8"),
+ "plugins/Referrers/images/socials/url.google.com.png" => array("1231", "1e684d1e6c018b2aff40930c597ceaf9"),
+ "plugins/Referrers/images/socials/viadeo.com.png" => array("710", "09e5fa949178167278c39149c44cba21"),
+ "plugins/Referrers/images/socials/vimeo.com.png" => array("704", "c928a20a0b8bd5eb8134147536fc0c7b"),
+ "plugins/Referrers/images/socials/vk.com.png" => array("370", "247bccf89561fe2aaa7d722152497c08"),
+ "plugins/Referrers/images/socials/vkontakte.ru.png" => array("576", "094907c8a8d2120213855506e0f6ed08"),
+ "plugins/Referrers/images/socials/vkrugudruzei.ru.png" => array("788", "29aa370e0983aef3ffffea74e43d8cbc"),
+ "plugins/Referrers/images/socials/wayn.com.png" => array("648", "ebddcc58a0414031b2b82622a198fb8c"),
+ "plugins/Referrers/images/socials/weeworld.com.png" => array("492", "5f3332f20321d7e0c24ee57579a18822"),
+ "plugins/Referrers/images/socials/weibo.com.png" => array("1179", "c3eff4eb95677e26c555446bbd1c54fa"),
+ "plugins/Referrers/images/socials/xanga.com.png" => array("265", "f2820fe33cb5bafbc9f3a5205235a958"),
+ "plugins/Referrers/images/socials/xing.com.png" => array("675", "3764aea0fd7c26bf8bf8e95958d94fff"),
+ "plugins/Referrers/images/socials/xx.png" => array("265", "34652cdc25d70bba01ce9ac01d929d3b"),
+ "plugins/Referrers/images/socials/youtube.com.png" => array("695", "81125e13b69e308800a98d9752e3f48b"),
+ "plugins/Referrers/Referrers.php" => array("29947", "909f867aa6cc8daf48218c512f3c48de"),
+ "plugins/Referrers/templates/getSearchEnginesAndKeywords.twig" => array("264", "0dff654fb12896c6e24d6579f099c244"),
+ "plugins/Referrers/templates/index.twig" => array("6007", "644a0b2555e61f18dabca2e7239b800f"),
+ "plugins/Referrers/templates/indexWebsites.twig" => array("252", "bf3a7aa50faea341e0603b53ff14e4d5"),
+ "plugins/ScheduledReports/API.php" => array("36820", "527f23578d1ff32edacf572ebef32fb0"),
+ "plugins/ScheduledReports/config/tcpdf_config.php" => array("6402", "923e6b4e2b61bbc44fb7882d911e438c"),
+ "plugins/ScheduledReports/Controller.php" => array("3576", "10ce18981e80bb5f89eb9532805f46cb"),
+ "plugins/ScheduledReports/javascripts/pdf.js" => array("7001", "f42863d0418f199dcef8af7e46d1c0bc"),
+ "plugins/ScheduledReports/ScheduledReports.php" => array("25874", "523d4cdbf0cc7407353b5958875acb9e"),
+ "plugins/ScheduledReports/templates/_addReport.twig" => array("8636", "00ecae5fe8b8aa647bcd0efa248e2bc0"),
+ "plugins/ScheduledReports/templates/index.twig" => array("1937", "0aaa4e1d71cbcb85794f920f6530e953"),
+ "plugins/ScheduledReports/templates/_listReports.twig" => array("4267", "b554291be9228ef8cb7d250da9449288"),
+ "plugins/ScheduledReports/templates/reportParametersScheduledReports.twig" => array("3848", "4b32be0f5bb38a54ac06b276a962aae6"),
+ "plugins/SegmentEditor/API.php" => array("10874", "52a80f940f2bd60881732aa28d7def09"),
+ "plugins/SegmentEditor/Controller.php" => array("386", "f565883b4e48442253c328ab00016755"),
+ "plugins/SegmentEditor/images/ajax-loader.gif" => array("847", "30d8e72bfdae694b1938658e1b087df0"),
+ "plugins/SegmentEditor/images/bg-inverted-corners.png" => array("968", "b441529c920b6ce5ca36d62b08a73fa2"),
+ "plugins/SegmentEditor/images/bg-segment-search.png" => array("1068", "938234a77114aff5541b1b9a47acf1f9"),
+ "plugins/SegmentEditor/images/bg-select.png" => array("1035", "a4f45d333fbcbadfb3c3bf42d45bed0e"),
+ "plugins/SegmentEditor/images/close_btn.png" => array("928", "58c02c2bf48632f89a571f8065dfedfa"),
+ "plugins/SegmentEditor/images/close.png" => array("288", "122f9ccffeba88ccdc3eb2ef41b518bc"),
+ "plugins/SegmentEditor/images/dashboard_h_bg_hover.png" => array("378", "41c5d393f8c12cd2cc727fada94b154a"),
+ "plugins/SegmentEditor/images/icon-users.png" => array("1728", "89e68113ed647295901322eea82ff8ed"),
+ "plugins/SegmentEditor/images/reset_search.png" => array("1021", "7e761f3444bf4edd4cd1779801c963bd"),
+ "plugins/SegmentEditor/images/search_btn.png" => array("2825", "2f11b8a2a361aa4eb350d33048ea56c9"),
+ "plugins/SegmentEditor/images/segment-close.png" => array("1302", "28e429768ba7f35ea4c48930848c2a26"),
+ "plugins/SegmentEditor/images/segment-move.png" => array("1447", "5c8a0111446ce9eaf70cd0cb82211ded"),
+ "plugins/SegmentEditor/javascripts/Segmentation.js" => array("50044", "f8db88f49b88e433bb74ad004b81b2e1"),
+ "plugins/SegmentEditor/Model.php" => array("2610", "b8de12b8dcb6d85d01ce48c2c7d35f4d"),
+ "plugins/SegmentEditor/SegmentEditor.php" => array("3196", "88b61079a22b9489913274251b4b0539"),
+ "plugins/SegmentEditor/SegmentSelectorControl.php" => array("4496", "9f540610044cf02578bfdfbcf3d1edff"),
+ "plugins/SegmentEditor/stylesheets/segmentation.less" => array("14770", "3bccb8665591b5904b8fd1641aae90a2"),
+ "plugins/SegmentEditor/templates/_segmentSelector.twig" => array("9000", "46fd47d6e427123362e9f39dd2c58099"),
+ "plugins/SEO/API.php" => array("3604", "29f8089a50cfe5e5796c097813a92341"),
+ "plugins/SEO/Controller.php" => array("1195", "93cc25b095aa6bfe89d91088ee620964"),
+ "plugins/SEO/images/majesticseo.png" => array("674", "a771319c2aa22f4b4744f76e59ec5fb3"),
+ "plugins/SEO/images/whois.png" => array("928", "ef67a4e9689efda71625a2ef894fb700"),
+ "plugins/SEO/javascripts/rank.js" => array("802", "8a49f8635a4d501d73868dcfc415e4e5"),
+ "plugins/SEO/MajesticClient.php" => array("3132", "2973973ceba8173cab44fd0cac09f869"),
+ "plugins/SEO/RankChecker.php" => array("10231", "72e8c9795cb7a537ff80bee9e5890d77"),
+ "plugins/SEO/SEO.php" => array("1194", "bb40b45a7b5ef22684fc1370fd5553ba"),
+ "plugins/SEO/templates/getRank.twig" => array("2900", "a686edd3171736d456f4ec6062206819"),
+ "plugins/SitesManager/API.php" => array("55565", "1d1299fa3efe085855b42f4134a8b0fc"),
+ "plugins/SitesManager/Controller.php" => array("6317", "bac508220402a58b5c67442b641d8c09"),
+ "plugins/SitesManager/javascripts/SitesManager.js" => array("22790", "153a9bd5ec22c2f98791989235a5289d"),
+ "plugins/SitesManager/SitesManager.php" => array("7094", "4e0830e699db4d59d697efac994a8858"),
+ "plugins/SitesManager/stylesheets/SitesManager.less" => array("1059", "2c47402c996f5783383b5dd9bdb41048"),
+ "plugins/SitesManager/templates/displayJavascriptCode.twig" => array("121", "ab4d91574f554b6e4ff05cae26373902"),
+ "plugins/SitesManager/templates/_displayJavascriptCode.twig" => array("731", "cc93bf9bffdbd5efc0ff98a0bee83e86"),
+ "plugins/SitesManager/templates/index.twig" => array("19864", "71b4a9079d4e90951e1b83571f72a64f"),
+ "plugins/Transitions/API.php" => array("25342", "1d0bf029a35fa8cad6444271571d8388"),
+ "plugins/Transitions/Controller.php" => array("3778", "f926b1496c2db36deb6f7f2ee1d48e36"),
+ "plugins/Transitions/images/transitions_icon_hover.png" => array("647", "2ec41fe6df63da14f7c187b067b94cc9"),
+ "plugins/Transitions/images/transitions_icon.png" => array("643", "fbf901428436a1ba3bbcc5c3081b6f24"),
+ "plugins/Transitions/javascripts/transitions.js" => array("56161", "97aee28fdcf5a08f9ca8cc9e744c3eb8"),
+ "plugins/Transitions/stylesheets/_transitionColors.less" => array("1677", "4c6eb4b6425faabe6d2eb4e1821904ef"),
+ "plugins/Transitions/stylesheets/transitions.less" => array("3702", "b58c35379c79ce4060216479e0310c2f"),
+ "plugins/Transitions/templates/renderPopover.twig" => array("2576", "81bba58121f174ab9edf66c6560f1815"),
+ "plugins/Transitions/Transitions.php" => array("1125", "efe0685071d15cd7ce16c94116ed71bc"),
+ "plugins/UserCountry/API.php" => array("8863", "a79d65ab6a040442be62b43d687e01cb"),
+ "plugins/UserCountry/Archiver.php" => array("6221", "191beb3c68d3dddc8a3407f0e4c33594"),
+ "plugins/UserCountry/Controller.php" => array("14875", "3b186fb085a8147c30078f722f7bccfc"),
+ "plugins/UserCountry/functions.php" => array("5232", "de661ba6505b4d27286e6e8543295d65"),
+ "plugins/UserCountry/GeoIPAutoUpdater.php" => array("23171", "102290b02300a18c821aa1b46ef294d5"),
+ "plugins/UserCountry/images/flags/a1.png" => array("290", "f1c31042bade52bcf13d8bd778fcf37c"),
+ "plugins/UserCountry/images/flags/a2.png" => array("290", "f1c31042bade52bcf13d8bd778fcf37c"),
+ "plugins/UserCountry/images/flags/ac.png" => array("545", "373b8f3d6c92338b228a160e2a0d87fc"),
+ "plugins/UserCountry/images/flags/ad.png" => array("454", "e06c9805db8406f1dbdcaa3e1aabeb46"),
+ "plugins/UserCountry/images/flags/ae.png" => array("277", "96fb796bddf2d631ea759ca8c33f5ad4"),
+ "plugins/UserCountry/images/flags/af.png" => array("420", "ce511852782ff21c1b47a1669fb2a60b"),
+ "plugins/UserCountry/images/flags/ag.png" => array("456", "d85c0b66cf5b68d85b99d90ef87b62a6"),
+ "plugins/UserCountry/images/flags/ai.png" => array("516", "2e32ad81c92ea64e94d9841deeb9a3a9"),
+ "plugins/UserCountry/images/flags/al.png" => array("434", "6657f61f8ce5e361bc7d9028fae5f3fd"),
+ "plugins/UserCountry/images/flags/am.png" => array("332", "b2df641a3610e13f5c20fbbe34deef40"),
+ "plugins/UserCountry/images/flags/an.png" => array("365", "f13b1a70f214b13a633180def6e10950"),
+ "plugins/UserCountry/images/flags/ao.png" => array("395", "d2e36307da318dfa6ad8f4b4724152bd"),
+ "plugins/UserCountry/images/flags/ap.png" => array("290", "f1c31042bade52bcf13d8bd778fcf37c"),
+ "plugins/UserCountry/images/flags/aq.png" => array("376", "8558085a2c8fc10b891b7f0156e74e4d"),
+ "plugins/UserCountry/images/flags/ar.png" => array("367", "3262694d9da0651e4ba89f320f861045"),
+ "plugins/UserCountry/images/flags/as.png" => array("540", "04702221a1b3bd67f53f9cd7046db824"),
+ "plugins/UserCountry/images/flags/at.png" => array("290", "b046bc17a7da41bc1a2ee0b6dd08f5b1"),
+ "plugins/UserCountry/images/flags/au.png" => array("580", "a30514266c8337b5eba8b8273f75069a"),
+ "plugins/UserCountry/images/flags/aw.png" => array("393", "9fd3abfd00e9f343df80fdfcfacb54d8"),
+ "plugins/UserCountry/images/flags/ax.png" => array("480", "e1a53e9e759cb932145827601707e00a"),
+ "plugins/UserCountry/images/flags/az.png" => array("423", "4e3840fb6e5dbcba28b60634a7be9ec7"),
+ "plugins/UserCountry/images/flags/ba.png" => array("471", "48a30e57ae529784a396e1823f05efa5"),
+ "plugins/UserCountry/images/flags/bb.png" => array("403", "4faf161b0978a2a11774ced111c607d2"),
+ "plugins/UserCountry/images/flags/bd.png" => array("372", "6728d331503b3d1c305f685ff51a9b89"),
+ "plugins/UserCountry/images/flags/be.png" => array("294", "57fe6dca1d2136916139c69fdeef3a87"),
+ "plugins/UserCountry/images/flags/bf.png" => array("341", "740751253fbcad427d7b45078589cb5e"),
+ "plugins/UserCountry/images/flags/bg.png" => array("320", "90e45cf12b431458b940a73f7b9fcbc1"),
+ "plugins/UserCountry/images/flags/bh.png" => array("345", "f2adc039c2c9a247a4e632d7477b2ee4"),
+ "plugins/UserCountry/images/flags/bi.png" => array("566", "38a6aef240d5c49fac5a09677e085a33"),
+ "plugins/UserCountry/images/flags/bj.png" => array("311", "53dde6e55a419bd214c9eae169674d5b"),
+ "plugins/UserCountry/images/flags/bl.png" => array("369", "d9062600b752c4fea4f3561920154724"),
+ "plugins/UserCountry/images/flags/bm.png" => array("499", "8e6f49c8a8c9fb066ee497583c3258aa"),
+ "plugins/UserCountry/images/flags/bn.png" => array("502", "d5b2aa73a6996fe49a5f06f344048bed"),
+ "plugins/UserCountry/images/flags/bo.png" => array("341", "1af0cf8f742a60e8389dc99d3f5399bb"),
+ "plugins/UserCountry/images/flags/bq.png" => array("310", "ab1c7bbbd651992aa563ce983942f33a"),
+ "plugins/UserCountry/images/flags/br.png" => array("486", "8692d9a84ae751a1fe469a1a4f010b11"),
+ "plugins/UserCountry/images/flags/bs.png" => array("391", "d5f8d687941e6bb02668e83df22df0f4"),
+ "plugins/UserCountry/images/flags/bt.png" => array("471", "a465c0295b6c7acf73064f5bae4301ef"),
+ "plugins/UserCountry/images/flags/bu.png" => array("336", "d28c3b10b62ca4be38f92763c39af558"),
+ "plugins/UserCountry/images/flags/bv.png" => array("397", "6935c8100f0abefa6d95aa82306d4b16"),
+ "plugins/UserCountry/images/flags/bw.png" => array("327", "da36c5267bc44e5bb6cc42e39bfe50e0"),
+ "plugins/UserCountry/images/flags/by.png" => array("382", "d463ffed4445b3430d3b6fea26f22d63"),
+ "plugins/UserCountry/images/flags/bz.png" => array("476", "c7c1ad98b142f8613c32333555f02387"),
+ "plugins/UserCountry/images/flags/ca.png" => array("471", "73a7d298286803c509103e35e3e00c95"),
+ "plugins/UserCountry/images/flags/cat.png" => array("353", "bc71723d17f7f996855b4829a8d7c525"),
+ "plugins/UserCountry/images/flags/cc.png" => array("496", "78b4cbd583030bcba8aca726764ded1d"),
+ "plugins/UserCountry/images/flags/cd.png" => array("477", "3ba2370dec6811f52f8cc63006091a5a"),
+ "plugins/UserCountry/images/flags/cf.png" => array("456", "b918f09c201b31356cf0e30867486d37"),
+ "plugins/UserCountry/images/flags/cg.png" => array("380", "7bd1bcc513aac082fa6f6292b72edc0c"),
+ "plugins/UserCountry/images/flags/ch.png" => array("239", "5c062086332c658050ee206a95bc0529"),
+ "plugins/UserCountry/images/flags/ci.png" => array("306", "f23877742bb6680fd23e4c9105f8f3fa"),
+ "plugins/UserCountry/images/flags/ck.png" => array("495", "ea6f14f288c3ce733d1d83c6498d23a9"),
+ "plugins/UserCountry/images/flags/cl.png" => array("324", "a2bf641aa64e7abcefae3850894cb297"),
+ "plugins/UserCountry/images/flags/cm.png" => array("347", "c512b727f3c9f61511193c44f755fb84"),
+ "plugins/UserCountry/images/flags/cn.png" => array("349", "3e1c715cc9abdd7090532108f8219c33"),
+ "plugins/UserCountry/images/flags/co.png" => array("330", "35eafa1a7a61403bb1c3dc4c568fb058"),
+ "plugins/UserCountry/images/flags/cp.png" => array("369", "d9062600b752c4fea4f3561920154724"),
+ "plugins/UserCountry/images/flags/cr.png" => array("349", "21abdfe041c3718220bf0bf0abdaa432"),
+ "plugins/UserCountry/images/flags/cs.png" => array("321", "cd0e549ac62fab737357ab35c8494813"),
+ "plugins/UserCountry/images/flags/cu.png" => array("445", "96210e07d6e39e4a6bdf3c1a9dd7d3a6"),
+ "plugins/UserCountry/images/flags/cv.png" => array("441", "0d3f95379c078fd9ba13bc699b4f6bd0"),
+ "plugins/UserCountry/images/flags/cw.png" => array("308", "dd7f242eaabedb2beb99976d6a5c8dad"),
+ "plugins/UserCountry/images/flags/cx.png" => array("498", "f4489b88940e95b95df0516d9f5214b5"),
+ "plugins/UserCountry/images/flags/cy.png" => array("337", "f77e40cda1c65710b084e160377b9630"),
+ "plugins/UserCountry/images/flags/cz.png" => array("367", "cef49be4d099506a5498b98e08e77df4"),
+ "plugins/UserCountry/images/flags/de.png" => array("364", "659cdf8a9a4996dcb084df3e3f442fd4"),
+ "plugins/UserCountry/images/flags/dg.png" => array("658", "38afe5a0e9817027e1f1615028aca521"),
+ "plugins/UserCountry/images/flags/dj.png" => array("430", "a77093af69aa5141d24e4a07a6ea10af"),
+ "plugins/UserCountry/images/flags/dk.png" => array("352", "ac122dcf0c9d72093852a94df3f69001"),
+ "plugins/UserCountry/images/flags/dm.png" => array("508", "ede464c70c3965917c3bc08212e053c1"),
+ "plugins/UserCountry/images/flags/do.png" => array("368", "9de12f2fe8df051d0c6a461e8a6157d6"),
+ "plugins/UserCountry/images/flags/dz.png" => array("454", "a6d211fa9bff373bb1f7bf3480a1dd89"),
+ "plugins/UserCountry/images/flags/ea.png" => array("344", "2115a1177252059ffb47e8a5477dcd59"),
+ "plugins/UserCountry/images/flags/ec.png" => array("355", "a190e98f2d2d451ec8d893fe69f786f3"),
+ "plugins/UserCountry/images/flags/ee.png" => array("297", "d732e3bb710f9592f5c86c29c34ceeb8"),
+ "plugins/UserCountry/images/flags/eg.png" => array("348", "c18ffe9ace7b61e83a506faab3b41384"),
+ "plugins/UserCountry/images/flags/eh.png" => array("388", "2e31790bf2567befe1161e154009610f"),
+ "plugins/UserCountry/images/flags/er.png" => array("497", "6c7fbe82caa633621f911246ef9be03a"),
+ "plugins/UserCountry/images/flags/es.png" => array("344", "2115a1177252059ffb47e8a5477dcd59"),
+ "plugins/UserCountry/images/flags/et.png" => array("445", "d96a0ec31d7a3ead5ee9c357cfce053a"),
+ "plugins/UserCountry/images/flags/eu.png" => array("418", "fe5555fc4c82b7ac5a6a521464247122"),
+ "plugins/UserCountry/images/flags/fi.png" => array("368", "37777cbdf0781e0563757e48958e51e9"),
+ "plugins/UserCountry/images/flags/fj.png" => array("517", "1e6bf4a4e40b2b3d63781b1da19bedae"),
+ "plugins/UserCountry/images/flags/fk.png" => array("526", "0d646049eae5abbb52668c2894ee7ab7"),
+ "plugins/UserCountry/images/flags/fm.png" => array("409", "cdacefe7bf02449fdb58f280d1d9e55e"),
+ "plugins/UserCountry/images/flags/fo.png" => array("377", "42b46f18e7a9a33cd9096eba9a753625"),
+ "plugins/UserCountry/images/flags/fr.png" => array("369", "d9062600b752c4fea4f3561920154724"),
+ "plugins/UserCountry/images/flags/fx.png" => array("369", "d9062600b752c4fea4f3561920154724"),
+ "plugins/UserCountry/images/flags/ga.png" => array("342", "93a562701e165bf2d756e9278bc7d356"),
+ "plugins/UserCountry/images/flags/gb.png" => array("545", "373b8f3d6c92338b228a160e2a0d87fc"),
+ "plugins/UserCountry/images/flags/gd.png" => array("461", "917ce5e00553e6c2e179faa9548fa305"),
+ "plugins/UserCountry/images/flags/ge.png" => array("493", "da1fb67e7e2e295dd318cb0195bccf7e"),
+ "plugins/UserCountry/images/flags/gf.png" => array("369", "d9062600b752c4fea4f3561920154724"),
+ "plugins/UserCountry/images/flags/gg.png" => array("524", "519dbe9562e66af76134b2e1656bf7e7"),
+ "plugins/UserCountry/images/flags/gh.png" => array("336", "159033244c7f70c3ff47e8aab4c8b4b6"),
+ "plugins/UserCountry/images/flags/gi.png" => array("369", "d26ba11a107a358926d39a54f543c9f6"),
+ "plugins/UserCountry/images/flags/gl.png" => array("351", "ef0137e3eb36a2a2006e970a36530e36"),
+ "plugins/UserCountry/images/flags/gm.png" => array("363", "030330ba9fd1daf8c4ac38ea29451129"),
+ "plugins/UserCountry/images/flags/gn.png" => array("319", "a492e6802b6ad9ac04016bf90133d5fb"),
+ "plugins/UserCountry/images/flags/gp.png" => array("353", "84b6bb6ae1eae63fdaa0c080ce2ece8a"),
+ "plugins/UserCountry/images/flags/gq.png" => array("405", "9631ca35fc10a2bc0aaef06a25a0974b"),
+ "plugins/UserCountry/images/flags/gr.png" => array("391", "3410bc2892d3337e99c16e385dbc93ce"),
+ "plugins/UserCountry/images/flags/gs.png" => array("522", "660137680d6eecdd60d83ec520b3634c"),
+ "plugins/UserCountry/images/flags/gt.png" => array("333", "e362d2900d8948a98c1a978f149fd059"),
+ "plugins/UserCountry/images/flags/gu.png" => array("384", "ef393701f40bf95b9719cc964422ae54"),
+ "plugins/UserCountry/images/flags/gw.png" => array("346", "519905115aa28bdbdd23d093f3f8063f"),
+ "plugins/UserCountry/images/flags/gy.png" => array("521", "378b1e8c706951ad98055560d0909b92"),
+ "plugins/UserCountry/images/flags/hk.png" => array("393", "214af14f93dd43f8ce2801923a175bf1"),
+ "plugins/UserCountry/images/flags/hm.png" => array("580", "a30514266c8337b5eba8b8273f75069a"),
+ "plugins/UserCountry/images/flags/hn.png" => array("411", "f28d848f8e4907e6d2b6dfd553799e25"),
+ "plugins/UserCountry/images/flags/hr.png" => array("386", "8dad95795dd99d9f424434b1f1ea9514"),
+ "plugins/UserCountry/images/flags/ht.png" => array("327", "d5567c83055cd19b25bb72936a83c25c"),
+ "plugins/UserCountry/images/flags/hu.png" => array("293", "62260c58e5fa2b7c7a6ce2da699ef5a9"),
+ "plugins/UserCountry/images/flags/ic.png" => array("378", "e88eea0f0079926867fdd0f1bf9c7b93"),
+ "plugins/UserCountry/images/flags/id.png" => array("301", "716db907475765a2620dbb0f6642297e"),
+ "plugins/UserCountry/images/flags/ie.png" => array("333", "3ffe523ffe959e15d72ef743503d3f2f"),
+ "plugins/UserCountry/images/flags/il.png" => array("326", "e8873cb08623e3f481ccf77a690d35ff"),
+ "plugins/UserCountry/images/flags/im.png" => array("320", "771463291d57daf357ae6357c96ede8c"),
+ "plugins/UserCountry/images/flags/in.png" => array("377", "775d8be8bbb271e0864a7c1cab2b0999"),
+ "plugins/UserCountry/images/flags/io.png" => array("575", "2ec8170fa7e61e3c210fe934c9b227d8"),
+ "plugins/UserCountry/images/flags/iq.png" => array("403", "f53e799a3adfd26999067b47efe7228f"),
+ "plugins/UserCountry/images/flags/ir.png" => array("398", "37ce6f51ba93110101e0c98382d5d537"),
+ "plugins/UserCountry/images/flags/is.png" => array("410", "8b8c4469ceaea80d82fd7157b2afca5c"),
+ "plugins/UserCountry/images/flags/it.png" => array("283", "39df341cd39d68db5d66b15030c26cb2"),
+ "plugins/UserCountry/images/flags/je.png" => array("475", "ca59d84a34953e6a3c5109732c4cbe96"),
+ "plugins/UserCountry/images/flags/jm.png" => array("508", "4278b6211095c5fe7fc5ef4ff94ebbfa"),
+ "plugins/UserCountry/images/flags/jo.png" => array("353", "447e0a243bacbf21a93abadc217530b4"),
+ "plugins/UserCountry/images/flags/jp.png" => array("307", "a25e6ad4f410e17091a1098f4455b9bf"),
+ "plugins/UserCountry/images/flags/ke.png" => array("435", "d7b94971ba7293909b27c84c715f08d2"),
+ "plugins/UserCountry/images/flags/kg.png" => array("354", "2fc98fb293749508ba3bc5d478e885ce"),
+ "plugins/UserCountry/images/flags/kh.png" => array("422", "b30a25ac2f5ecfa88a9fc5a7e2ee389c"),
+ "plugins/UserCountry/images/flags/ki.png" => array("551", "6bb6b40309a404b95276ac8c1409b177"),
+ "plugins/UserCountry/images/flags/km.png" => array("456", "65b5f51c6bd37c49a3c5598df077c92e"),
+ "plugins/UserCountry/images/flags/kn.png" => array("480", "8f6f9a3f041f10a64406e812a86a0604"),
+ "plugins/UserCountry/images/flags/kp.png" => array("424", "e4d4914b068ec1d9a5aa30c27ce5dffa"),
+ "plugins/UserCountry/images/flags/kr.png" => array("507", "7fe839454cadda986f93835b311fafe8"),
+ "plugins/UserCountry/images/flags/kw.png" => array("351", "57273db3fee5cbed627a9fe4137d1800"),
+ "plugins/UserCountry/images/flags/ky.png" => array("532", "d7de64a8dbb7e92c75f57b152ab22a0e"),
+ "plugins/UserCountry/images/flags/kz.png" => array("459", "f392017f7085082d391fa219dc198c93"),
+ "plugins/UserCountry/images/flags/la.png" => array("415", "1f881c45b18afdd50e58aec5cbcc5dec"),
+ "plugins/UserCountry/images/flags/lb.png" => array("393", "b310dae884660aeda0ab764b748f3601"),
+ "plugins/UserCountry/images/flags/lc.png" => array("462", "abe2a1f2dc0082a4efcb074ae34bd316"),
+ "plugins/UserCountry/images/flags/li.png" => array("399", "9f60f2b3ac0e3fd8ed97b9995171dfb1"),
+ "plugins/UserCountry/images/flags/lk.png" => array("464", "b7b781bc919c6f07591ba4e5c4c82038"),
+ "plugins/UserCountry/images/flags/lr.png" => array("365", "45f7777dd613ff68e50b12ffe7d72993"),
+ "plugins/UserCountry/images/flags/ls.png" => array("500", "df909606b1ba95c418a0d16df7b85ba6"),
+ "plugins/UserCountry/images/flags/lt.png" => array("345", "f59a5d82915ad225f66d3db1536a5c48"),
+ "plugins/UserCountry/images/flags/lu.png" => array("338", "697bc4d6e01977c8e6cd07ad30ea3015"),
+ "plugins/UserCountry/images/flags/lv.png" => array("339", "066cd44f363225e08bdd3a55e8b28a56"),
+ "plugins/UserCountry/images/flags/ly.png" => array("277", "e830f941ef36e6d5d2cf6a7d04995a36"),
+ "plugins/UserCountry/images/flags/ma.png" => array("293", "e1de39a33dcb622a58bb4f0f8804f46b"),
+ "plugins/UserCountry/images/flags/mc.png" => array("254", "fc73ac65437d6546b67e9fb3032eb7c5"),
+ "plugins/UserCountry/images/flags/md.png" => array("404", "5096eaf480a14028af90c15a950e5b5c"),
+ "plugins/UserCountry/images/flags/me.png" => array("394", "7dcb9e23a649b7a476a51e3654d9c40a"),
+ "plugins/UserCountry/images/flags/mf.png" => array("369", "d9062600b752c4fea4f3561920154724"),
+ "plugins/UserCountry/images/flags/mg.png" => array("313", "87207a6990f372ffe13da8dbcde69dcb"),
+ "plugins/UserCountry/images/flags/mh.png" => array("518", "83a9baf30759566bc98a2756fa79645c"),
+ "plugins/UserCountry/images/flags/mk.png" => array("449", "9666fff48fb95187610cb71eab755e22"),
+ "plugins/UserCountry/images/flags/ml.png" => array("322", "d88965b3532e2c354919d05c1aafcc08"),
+ "plugins/UserCountry/images/flags/mm.png" => array("336", "d28c3b10b62ca4be38f92763c39af558"),
+ "plugins/UserCountry/images/flags/mn.png" => array("343", "c5f1b63d69c8224535abcf9acd240fe0"),
+ "plugins/UserCountry/images/flags/mo.png" => array("456", "f7de32c7fc01e95776d3179663f3e1ae"),
+ "plugins/UserCountry/images/flags/mp.png" => array("481", "ab89359dc8bddea8c3c76782ef02664f"),
+ "plugins/UserCountry/images/flags/mq.png" => array("541", "02bb4f3d37eaa907d0573d84abb2da0a"),
+ "plugins/UserCountry/images/flags/mr.png" => array("408", "521673db6268518284cc26e2b2e8d74f"),
+ "plugins/UserCountry/images/flags/ms.png" => array("497", "fa296f8bfb2e4ac54f96d11682e6ac52"),
+ "plugins/UserCountry/images/flags/mt.png" => array("296", "e2f1112757abd443f4ed22875535b37a"),
+ "plugins/UserCountry/images/flags/mu.png" => array("360", "87c04283d8ee559893ebda052e7b233e"),
+ "plugins/UserCountry/images/flags/mv.png" => array("391", "c32cf906304a45879ed373134010327b"),
+ "plugins/UserCountry/images/flags/mw.png" => array("365", "50d75d8b8809943a60a175545b6946de"),
+ "plugins/UserCountry/images/flags/mx.png" => array("424", "562a94f110a3488776f48563a8c25370"),
+ "plugins/UserCountry/images/flags/my.png" => array("464", "d24895e58d70a5c93dc9c05708a4c8c6"),
+ "plugins/UserCountry/images/flags/mz.png" => array("439", "710edfb3da21e88c1eaa8ea0fb664989"),
+ "plugins/UserCountry/images/flags/na.png" => array("559", "9fe8cd53bc6aa19d550d01abf5ec89b3"),
+ "plugins/UserCountry/images/flags/nc.png" => array("470", "5cbd7427200b4094f237bdd45d373bb0"),
+ "plugins/UserCountry/images/flags/ne.png" => array("393", "debda1a3728c9c4e31e9d6a100ebefc6"),
+ "plugins/UserCountry/images/flags/nf.png" => array("474", "730d26f59272f66b437923915ad22251"),
+ "plugins/UserCountry/images/flags/ng.png" => array("341", "b89c573ee8ba57bc8425320ca64afd1f"),
+ "plugins/UserCountry/images/flags/ni.png" => array("372", "08126d0ea55a7e49f9c07368cf0dc98d"),
+ "plugins/UserCountry/images/flags/nl.png" => array("310", "ab1c7bbbd651992aa563ce983942f33a"),
+ "plugins/UserCountry/images/flags/no.png" => array("397", "6935c8100f0abefa6d95aa82306d4b16"),
+ "plugins/UserCountry/images/flags/np.png" => array("349", "9c2e7fc45dd7e74893f232eef70f7bc0"),
+ "plugins/UserCountry/images/flags/nr.png" => array("391", "b4010849f42c30b54afc100f8788126a"),
+ "plugins/UserCountry/images/flags/nt.png" => array("198", "d9d0f882ce22e2d253ad8ba0cf6c852b"),
+ "plugins/UserCountry/images/flags/nu.png" => array("468", "25aa9e4913ad5ae9e60336f646fb689d"),
+ "plugins/UserCountry/images/flags/nz.png" => array("529", "e05cbdd067b2c7e80d34c14869d229db"),
+ "plugins/UserCountry/images/flags/o1.png" => array("290", "f1c31042bade52bcf13d8bd778fcf37c"),
+ "plugins/UserCountry/images/flags/om.png" => array("339", "c994c0cebaafb1c5fa87320823425908"),
+ "plugins/UserCountry/images/flags/pa.png" => array("390", "5e4e0de6fceba0ff1aba0f514e68f91d"),
+ "plugins/UserCountry/images/flags/pe.png" => array("264", "9a8ca1b7553c317c93e9265349c64ec3"),
+ "plugins/UserCountry/images/flags/pf.png" => array("379", "5299b7a40ba88d1b68fc40c1aa26d969"),
+ "plugins/UserCountry/images/flags/pg.png" => array("438", "3cc130dec98fd482f3866aa2433cfd4a"),
+ "plugins/UserCountry/images/flags/ph.png" => array("416", "21506e0e83093563757005b2abe41de3"),
+ "plugins/UserCountry/images/flags/pk.png" => array("448", "d528a749bc9e65e8d4f4cc14d8972ee3"),
+ "plugins/UserCountry/images/flags/pl.png" => array("243", "8a37cec158680c7540f46d570d033bc7"),
+ "plugins/UserCountry/images/flags/pm.png" => array("572", "48491aa92b33d90b79203cc6bfa3d335"),
+ "plugins/UserCountry/images/flags/pn.png" => array("547", "817dbdd6d096f075a3c9441228e7c189"),
+ "plugins/UserCountry/images/flags/pr.png" => array("445", "39b1a64d719e96bd8ec3e3552adc4ab8"),
+ "plugins/UserCountry/images/flags/ps.png" => array("348", "efc702c7067b0844aa0de8415fc32522"),
+ "plugins/UserCountry/images/flags/pt.png" => array("407", "3f346b92635e99725cd1599750e3707f"),
+ "plugins/UserCountry/images/flags/pw.png" => array("424", "f8898bb2f627f26b00908b2a092d5676"),
+ "plugins/UserCountry/images/flags/py.png" => array("344", "316d698d7a73710f1a4029b139affa5f"),
+ "plugins/UserCountry/images/flags/qa.png" => array("343", "8bf82817ab9c6bbe8b8efddf0e2d073a"),
+ "plugins/UserCountry/images/flags/re.png" => array("369", "d9062600b752c4fea4f3561920154724"),
+ "plugins/UserCountry/images/flags/ro.png" => array("333", "1339430b14e6eb3223afb99b380327b7"),
+ "plugins/UserCountry/images/flags/rs.png" => array("376", "919a2922411c55b7a937d3022ac5e8d8"),
+ "plugins/UserCountry/images/flags/ru.png" => array("299", "ba00aed092784334418dbe95d845c16e"),
+ "plugins/UserCountry/images/flags/rw.png" => array("382", "65a180a4f224a0ecdb81f82635f7d4c0"),
+ "plugins/UserCountry/images/flags/sa.png" => array("428", "3fb73b67cd560459084d0518665e4dce"),
+ "plugins/UserCountry/images/flags/sb.png" => array("520", "4b59f117a420d114a1da826ab5a5ae83"),
+ "plugins/UserCountry/images/flags/sc.png" => array("481", "dc3491160c1d96eebcf51e879ff87431"),
+ "plugins/UserCountry/images/flags/sd.png" => array("364", "d657c4d6ef980b8165291dbe2c946718"),
+ "plugins/UserCountry/images/flags/se.png" => array("389", "8aa93fc2143c4a2c4bdeb394e97eddd6"),
+ "plugins/UserCountry/images/flags/sf.png" => array("368", "37777cbdf0781e0563757e48958e51e9"),
+ "plugins/UserCountry/images/flags/sg.png" => array("350", "bf8d88d0c82a753cb0f2251ca1da737b"),
+ "plugins/UserCountry/images/flags/sh.png" => array("524", "2c4eecfe866d08445d02161b91dc968c"),
+ "plugins/UserCountry/images/flags/si.png" => array("383", "32086e3fb9c161a52ae27126941f8b05"),
+ "plugins/UserCountry/images/flags/sj.png" => array("397", "6935c8100f0abefa6d95aa82306d4b16"),
+ "plugins/UserCountry/images/flags/sk.png" => array("439", "35dc3d0a7ecd995a4886073854273ce8"),
+ "plugins/UserCountry/images/flags/sl.png" => array("321", "b826798d48c8bbca0a233d5a5a1c805c"),
+ "plugins/UserCountry/images/flags/sm.png" => array("396", "5cecf1a8c7b1e7cc701ac895f1d2339e"),
+ "plugins/UserCountry/images/flags/sn.png" => array("356", "753dd84a311206fc06e7a5ea9bef91e7"),
+ "plugins/UserCountry/images/flags/so.png" => array("376", "62708904359cd14179be31f35c0cb825"),
+ "plugins/UserCountry/images/flags/sr.png" => array("370", "a9a1c0ef23dd3a874616b8a0c7ce0b28"),
+ "plugins/UserCountry/images/flags/ss.png" => array("422", "5058328bd522fd528778ebab10a68277"),
+ "plugins/UserCountry/images/flags/st.png" => array("429", "5de079956c1e527db8f21c12f0f3b7ba"),
+ "plugins/UserCountry/images/flags/su.png" => array("273", "c363974eb7f9a50a58b1132ad6b1235f"),
+ "plugins/UserCountry/images/flags/sv.png" => array("373", "7e76cb3e4ddd2acdc6a74dbbbeb083d9"),
+ "plugins/UserCountry/images/flags/sx.png" => array("416", "2e2d4e66d402c89252773c492b81db2a"),
+ "plugins/UserCountry/images/flags/sy.png" => array("322", "38b241abe7b34e15560dd826e32b8c6a"),
+ "plugins/UserCountry/images/flags/sz.png" => array("508", "df08b36f98d56d9848249f9f23458251"),
+ "plugins/UserCountry/images/flags/ta.png" => array("512", "cdb7b60ea75c5f97aa16c6b0b1d6f40a"),
+ "plugins/UserCountry/images/flags/tc.png" => array("509", "bde929f2a84e0d654780a8192fcb09ca"),
+ "plugins/UserCountry/images/flags/td.png" => array("378", "e844177adfb2b257380fae3cafcaadae"),
+ "plugins/UserCountry/images/flags/tf.png" => array("399", "6e1989551034dab8f010ef273b992316"),
+ "plugins/UserCountry/images/flags/tg.png" => array("410", "14d78aff10308b333b6499ac483ef2da"),
+ "plugins/UserCountry/images/flags/th.png" => array("327", "75e627f1a1103f0061a89444fd967099"),
+ "plugins/UserCountry/images/flags/ti.png" => array("811", "fc18b7c184a4bbceba248fb586add8c0"),
+ "plugins/UserCountry/images/flags/tj.png" => array("367", "04aa8a0ca380ce6fe8e1d0eafcbe6070"),
+ "plugins/UserCountry/images/flags/tk.png" => array("522", "22268e11d7d4067b7d87fb79292bb307"),
+ "plugins/UserCountry/images/flags/tl.png" => array("395", "a2ed00b82bac63017de36d0c9223d32f"),
+ "plugins/UserCountry/images/flags/tm.png" => array("456", "76e44b117033521dcb3071c7f58b5f29"),
+ "plugins/UserCountry/images/flags/tn.png" => array("367", "9d838d48a0a2bf001d4c6d3cd9925c96"),
+ "plugins/UserCountry/images/flags/to.png" => array("302", "d310830a17d23bd04709535f136c265d"),
+ "plugins/UserCountry/images/flags/tp.png" => array("395", "a2ed00b82bac63017de36d0c9223d32f"),
+ "plugins/UserCountry/images/flags/tr.png" => array("366", "66ead8c0309aeba400db1b52e5e826f9"),
+ "plugins/UserCountry/images/flags/tt.png" => array("486", "17688174ffff0fc1377fe0003db3b4bd"),
+ "plugins/UserCountry/images/flags/tv.png" => array("443", "34d00fe909063cb40ebcb4b2da2c6a26"),
+ "plugins/UserCountry/images/flags/tw.png" => array("330", "672f72f068987c462bf8fe49421fb2f3"),
+ "plugins/UserCountry/images/flags/tz.png" => array("514", "e94f7371cc5773322b28f676e12ec258"),
+ "plugins/UserCountry/images/flags/ua.png" => array("304", "db4d976dddb471cef4948dbe872fd810"),
+ "plugins/UserCountry/images/flags/ug.png" => array("388", "3fde945762cea9cad16e0e1511b4d98b"),
+ "plugins/UserCountry/images/flags/uk.png" => array("545", "373b8f3d6c92338b228a160e2a0d87fc"),
+ "plugins/UserCountry/images/flags/um.png" => array("455", "fb0a3a027a3cd591b92d726b182fe6af"),
+ "plugins/UserCountry/images/flags/us.png" => array("492", "36eefb4c632941c12b48071111485b4b"),
+ "plugins/UserCountry/images/flags/uy.png" => array("411", "aafef31fbd168ea85768d604efe65f2f"),
+ "plugins/UserCountry/images/flags/uz.png" => array("411", "237681a54efe6934209aefbfec30b397"),
+ "plugins/UserCountry/images/flags/va.png" => array("419", "060fa2a1d8c86e0789fda81efc292cbf"),
+ "plugins/UserCountry/images/flags/vc.png" => array("412", "302707497da91992ac19638c67a79cf3"),
+ "plugins/UserCountry/images/flags/ve.png" => array("412", "59de75a614781f76617dc4cb1e755878"),
+ "plugins/UserCountry/images/flags/vg.png" => array("510", "dccf0db1a1faf55688624d5339313c3a"),
+ "plugins/UserCountry/images/flags/vi.png" => array("523", "741e37e202f4049d36ade5a4200148c6"),
+ "plugins/UserCountry/images/flags/vn.png" => array("324", "d083243d22c9099070dbe6335dfb22aa"),
+ "plugins/UserCountry/images/flags/vu.png" => array("450", "4f1124454a864d07d675bb196eddb934"),
+ "plugins/UserCountry/images/flags/wf.png" => array("438", "849d8c881f1f72bc9a5f3332dc817e39"),
+ "plugins/UserCountry/images/flags/ws.png" => array("352", "5c38a0321507c42dad39bdcfab373d3a"),
+ "plugins/UserCountry/images/flags/xx.png" => array("290", "f1c31042bade52bcf13d8bd778fcf37c"),
+ "plugins/UserCountry/images/flags/ye.png" => array("302", "9ad11f5fb4821eaea4215ee278d3d8c3"),
+ "plugins/UserCountry/images/flags/yt.png" => array("456", "95aa4f9d04f3f08ce8f5cc722c1f7c95"),
+ "plugins/UserCountry/images/flags/yu.png" => array("321", "cd0e549ac62fab737357ab35c8494813"),
+ "plugins/UserCountry/images/flags/za.png" => array("523", "38986661e79041f7388529a46da2d3c3"),
+ "plugins/UserCountry/images/flags/zm.png" => array("359", "96b272c89f9775f2a8aca88497d660ae"),
+ "plugins/UserCountry/images/flags/zr.png" => array("380", "7bd1bcc513aac082fa6f6292b72edc0c"),
+ "plugins/UserCountry/images/flags/zw.png" => array("462", "29cc6d7fa4fb68eb909da5b1ee142bdd"),
+ "plugins/UserCountry/javascripts/userCountry.js" => array("8251", "cba3f6cb36fe9e75404d48a96ec2349b"),
+ "plugins/UserCountry/LocationProvider/Default.php" => array("3251", "31708eccdddce97672027891f9a6888b"),
+ "plugins/UserCountry/LocationProvider/GeoIp/Pecl.php" => array("11193", "e705086e0d6a9ed88a0571e58c7ee291"),
+ "plugins/UserCountry/LocationProvider/GeoIp.php" => array("8983", "f2c5d51c4d954ba83a6b79f52142e270"),
+ "plugins/UserCountry/LocationProvider/GeoIp/Php.php" => array("13353", "524c43039566f77e0bb99b97d0d89408"),
+ "plugins/UserCountry/LocationProvider/GeoIp/ServerBased.php" => array("10066", "cd4ab8bff2cfc6a1de0b660622771679"),
+ "plugins/UserCountry/LocationProvider.php" => array("16287", "a884a71eb8a2c99dd5e856d0c0120710"),
+ "plugins/UserCountryMap/Controller.php" => array("11481", "c1083ef590a08229977f908487188697"),
+ "plugins/UserCountryMap/images/cities.png" => array("267", "05566bd830e4a3225fa746eafc2534b1"),
+ "plugins/UserCountryMap/images/realtimemap-loading.gif" => array("308", "a41ca826560fe6eaeb46dd69b6d9dba2"),
+ "plugins/UserCountryMap/images/regions.png" => array("296", "98c1643253f8198b91297c5db80be6b0"),
+ "plugins/UserCountryMap/images/zoom-out-disabled.png" => array("270", "153e42ba1158ec475aa0e7e0f67acaac"),
+ "plugins/UserCountryMap/javascripts/realtime-map.js" => array("27190", "de5fbfd35a558cc047e3491a7f4f2e08"),
+ "plugins/UserCountryMap/javascripts/vendor/chroma.min.js" => array("25434", "33d7d9ebf37530751174e8ac63d83c95"),
+ "plugins/UserCountryMap/javascripts/vendor/jquery.qtip.min.js" => array("24441", "9b50d81ac4cb3194777a05429abb39d3"),
+ "plugins/UserCountryMap/javascripts/vendor/kartograph.min.js" => array("67544", "457051a2a34be85bbc921d0fdcb94351"),
+ "plugins/UserCountryMap/javascripts/vendor/raphael.min.js" => array("90648", "3af49700d08ae8f43d613218eec1f754"),
+ "plugins/UserCountryMap/javascripts/visitor-map.js" => array("71971", "d9d908d5c59527221ccd4098fcbcea48"),
+ "plugins/UserCountryMap/stylesheets/map.css" => array("1504", "843c9412e98b9da0ece4b8533017ddd8"),
+ "plugins/UserCountryMap/stylesheets/realtime-map.less" => array("3192", "c367e7663058456db63ffdcac5af6fea"),
+ "plugins/UserCountryMap/stylesheets/visitor-map.less" => array("4529", "659ac501b37cd5b2ee9dbdf78829f8ed"),
+ "plugins/UserCountryMap/svg/AFG.svg" => array("23731", "2093e033509062e7928f7917fa7a33c5"),
+ "plugins/UserCountryMap/svg/AF.svg" => array("40727", "4bb303e7ee6eb2b29caec82db8843123"),
+ "plugins/UserCountryMap/svg/AGO.svg" => array("11968", "820cf71105c9c3c1339ba3740ccc9ea8"),
+ "plugins/UserCountryMap/svg/ALB.svg" => array("9932", "7f854f3d2059296108821e9d533b05ad"),
+ "plugins/UserCountryMap/svg/ARE.svg" => array("8336", "ac56cc1c2abe3a47961db80b6db176c1"),
+ "plugins/UserCountryMap/svg/ARG.svg" => array("15590", "5d311fe12916296a82a30e1016acec0f"),
+ "plugins/UserCountryMap/svg/ARM.svg" => array("9259", "65b3fb425f03a1f1f0d259c6dd549bc7"),
+ "plugins/UserCountryMap/svg/AS.svg" => array("49832", "4ce9cb0f2027fea500199bde3c74734f"),
+ "plugins/UserCountryMap/svg/AUS.svg" => array("8193", "b2324da92c1f67f2a8f013abf3f06581"),
+ "plugins/UserCountryMap/svg/AUT.svg" => array("11567", "00d3a4679c9b265ea6fb5205d7951ce8"),
+ "plugins/UserCountryMap/svg/AZE.svg" => array("26750", "76ff6e1729843054d172f3d528c09965"),
+ "plugins/UserCountryMap/svg/BDI.svg" => array("9071", "42ea3eecafdfc12bbe1a2129ed18ec4e"),
+ "plugins/UserCountryMap/svg/BEL.svg" => array("12982", "3a99b3abe49b13c8dc7edb92f9e13ebb"),
+ "plugins/UserCountryMap/svg/BEN.svg" => array("7495", "842badf56f297049dfa63243cfac1494"),
+ "plugins/UserCountryMap/svg/BFA.svg" => array("22092", "837025e1f04060a6ca78ea1407ccfc30"),
+ "plugins/UserCountryMap/svg/BGD.svg" => array("16829", "16d661876d9b416ba731822312ce15bc"),
+ "plugins/UserCountryMap/svg/BGR.svg" => array("18695", "bba6aae17a231dfed22122ca7a66467a"),
+ "plugins/UserCountryMap/svg/BIH.svg" => array("12239", "60d8be06db5b01d1e15a798815ee281d"),
+ "plugins/UserCountryMap/svg/BLR.svg" => array("10822", "a396566e089711c3e3a5c2649bcfe447"),
+ "plugins/UserCountryMap/svg/BLZ.svg" => array("5017", "277dcaba5f6388fcbc1101806038fd78"),
+ "plugins/UserCountryMap/svg/BOL.svg" => array("11485", "a4f0080f779ce2088988872a33aa0028"),
+ "plugins/UserCountryMap/svg/BRA.svg" => array("20787", "96be5f2df617bdfa33b7c8ca354db113"),
+ "plugins/UserCountryMap/svg/BRB.svg" => array("3422", "1672e26d0267fce35d916e07d3f068d7"),
+ "plugins/UserCountryMap/svg/BRN.svg" => array("3571", "fa632d5303e8b920d76a268060f80705"),
+ "plugins/UserCountryMap/svg/BTN.svg" => array("25375", "785161e8ec1cc331c189fcd0789ec2dc"),
+ "plugins/UserCountryMap/svg/BWA.svg" => array("6630", "b3ac298a6413a11ab6e4f7a5baa28cf2"),
+ "plugins/UserCountryMap/svg/CAF.svg" => array("12600", "30c7e170a0ad49ee29e4a3e08fb9bea7"),
+ "plugins/UserCountryMap/svg/CAN.svg" => array("37420", "0630624406a2d42ab07398f6af67e407"),
+ "plugins/UserCountryMap/svg/CHE.svg" => array("18235", "4ac55015c22ce6c864c29a34d191084c"),
+ "plugins/UserCountryMap/svg/CHL.svg" => array("17090", "23cec4aa7438083af5caa3aa5d9d4550"),
+ "plugins/UserCountryMap/svg/CHN.svg" => array("35787", "fcf1dd8e8daf9c633413e0d20d6fcbdc"),
+ "plugins/UserCountryMap/svg/CIV.svg" => array("14584", "e39a2e8252c854aa92455355a4e21116"),
+ "plugins/UserCountryMap/svg/CMR.svg" => array("10175", "3a5ceb16302b5de7eea867abe8d0c57d"),
+ "plugins/UserCountryMap/svg/COD.svg" => array("13916", "eb8cbda4671accad54902624f7958e9a"),
+ "plugins/UserCountryMap/svg/COG.svg" => array("11594", "607ed58ad290b5c55b61916863da2bf6"),
+ "plugins/UserCountryMap/svg/COL.svg" => array("21538", "75a311f5331d14612fbaf2a64272cc6f"),
+ "plugins/UserCountryMap/svg/CRI.svg" => array("8063", "260aba99939b461dd3832b3705a5077f"),
+ "plugins/UserCountryMap/svg/CUB.svg" => array("10311", "b0042681c42ea286f4edae229579cb79"),
+ "plugins/UserCountryMap/svg/CYP.svg" => array("4941", "ba56757a6aecef1e9d2bf6c495308b1c"),
+ "plugins/UserCountryMap/svg/CZE.svg" => array("12588", "a8c33fd6d40a496d588be46fd124f93e"),
+ "plugins/UserCountryMap/svg/DEU.svg" => array("22903", "f8e7e84b8ff3e8676c6f2734b07a41b1"),
+ "plugins/UserCountryMap/svg/DJI.svg" => array("4116", "79feff33fda4eb002a7bcfe3d4ee7cf8"),
+ "plugins/UserCountryMap/svg/DMA.svg" => array("3482", "de751b6b8dd58c266b0f53b6fcaff158"),
+ "plugins/UserCountryMap/svg/DNK.svg" => array("9978", "d37dbea01e44e3118b3dee05a12349f5"),
+ "plugins/UserCountryMap/svg/DOM.svg" => array("14160", "e50ee7a6ca6cafe54381dfefbe4bd270"),
+ "plugins/UserCountryMap/svg/DZA.svg" => array("17878", "c2c3da63bb4c62d62603362e2d91c745"),
+ "plugins/UserCountryMap/svg/ECU.svg" => array("12539", "46d4aa8cb9ef09ec89dd2b9ecac7ea00"),
+ "plugins/UserCountryMap/svg/EGY.svg" => array("11510", "57f29b3e9ed28faf642afeaa52f0ea52"),
+ "plugins/UserCountryMap/svg/ERI.svg" => array("6973", "1544b71d14f85199264d33015788e01c"),
+ "plugins/UserCountryMap/svg/ESP.svg" => array("12764", "e4ad35deab6afad37dfbce809c99114e"),
+ "plugins/UserCountryMap/svg/EST.svg" => array("11928", "d3603a128708ae2bedaf23ecb7fee1cb"),
+ "plugins/UserCountryMap/svg/ETH.svg" => array("14322", "ba96e2b01c5819a0c7b351a2d0299459"),
+ "plugins/UserCountryMap/svg/EU.svg" => array("37771", "f938be05f59da930474ef1ffa8d9b722"),
+ "plugins/UserCountryMap/svg/FIN.svg" => array("21076", "dc397dce4ea165a2340e4d86d5eddd78"),
+ "plugins/UserCountryMap/svg/FJI.svg" => array("3094", "8373dc52335d4bd13f7b1387f1367ef0"),
+ "plugins/UserCountryMap/svg/FRA.svg" => array("21479", "c430b20f88c1c541390f0786c4d4f24b"),
+ "plugins/UserCountryMap/svg/FRO.svg" => array("3931", "1ffdfd6531763d207f644a2af8c492b2"),
+ "plugins/UserCountryMap/svg/GAB.svg" => array("9212", "1bd7259df6d85563e0b94a692f5baf41"),
+ "plugins/UserCountryMap/svg/GBR.svg" => array("17933", "672532d6a3646ea21313b4fe35355b74"),
+ "plugins/UserCountryMap/svg/GEO.svg" => array("10043", "c98aeeb910d5239379fcd87417aae0cf"),
+ "plugins/UserCountryMap/svg/GHA.svg" => array("12455", "fcb4f5dfe83d978d606501ab10415fbe"),
+ "plugins/UserCountryMap/svg/GIN.svg" => array("11411", "1f0b064b1ae92d526c43516a823d0f8c"),
+ "plugins/UserCountryMap/svg/GMB.svg" => array("4682", "3c8d579078d005782115b305462076ae"),
+ "plugins/UserCountryMap/svg/GNB.svg" => array("10033", "2732f9970121ea276b11f2c7ab347bf2"),
+ "plugins/UserCountryMap/svg/GNQ.svg" => array("3860", "a72dc1d82fbb9a191e3d53e7e75df20a"),
+ "plugins/UserCountryMap/svg/GRC.svg" => array("21380", "6e773ddfec71392f4b12d52511465e1b"),
+ "plugins/UserCountryMap/svg/GRL.svg" => array("29909", "6f25a107701d35d72cc0e07a02266cef"),
+ "plugins/UserCountryMap/svg/GTM.svg" => array("11659", "56b23c7702d279d54199bf7db4693275"),
+ "plugins/UserCountryMap/svg/GUY.svg" => array("10473", "45b785bd1bb1c27d14271be087d25858"),
+ "plugins/UserCountryMap/svg/HND.svg" => array("12541", "dd0bee49d2939646662fc82f4cd68601"),
+ "plugins/UserCountryMap/svg/HRV.svg" => array("17755", "ad42b1e6c8995052c3220a19c20d3186"),
+ "plugins/UserCountryMap/svg/HTI.svg" => array("8205", "c201fc50f2b16728df0719fced87f6bb"),
+ "plugins/UserCountryMap/svg/HUN.svg" => array("15307", "dad18c1268029e49b32385a83011aac8"),
+ "plugins/UserCountryMap/svg/IDN.svg" => array("21243", "3c2ce292ee278726338c864b123a7248"),
+ "plugins/UserCountryMap/svg/IND.svg" => array("33242", "67bb8d7c4ea0e5e39e0fd0975eb8de62"),
+ "plugins/UserCountryMap/svg/IRL.svg" => array("21843", "188c415933b978928dab19be26334d19"),
+ "plugins/UserCountryMap/svg/IRN.svg" => array("19271", "8740584a94e528a316a735a64a33e1da"),
+ "plugins/UserCountryMap/svg/IRQ.svg" => array("11646", "ed8ceceb5f036c290e56cc664eddc6f7"),
+ "plugins/UserCountryMap/svg/ISL.svg" => array("15230", "ddbb0b48e5c1733da330d4a98bed6751"),
+ "plugins/UserCountryMap/svg/ISR.svg" => array("6700", "d82ce823e52a4929f958291dad45863b"),
+ "plugins/UserCountryMap/svg/ITA.svg" => array("20456", "13f79c748b1e7137385190fdbb1a17a6"),
+ "plugins/UserCountryMap/svg/JAM.svg" => array("5762", "4e45064fc839de5ad9fba0f4264fcb93"),
+ "plugins/UserCountryMap/svg/JOR.svg" => array("6856", "bea29d2ae20c173248e95e1f838eb973"),
+ "plugins/UserCountryMap/svg/JPN.svg" => array("13023", "76bffd77f2b0b547debffc9c4dd2c7b3"),
+ "plugins/UserCountryMap/svg/KAZ.svg" => array("15929", "35da71a3bbe0a9db3b457d60ea25f1f5"),
+ "plugins/UserCountryMap/svg/KEN.svg" => array("8926", "c046709428eed0f6357ac6774e4fe2ee"),
+ "plugins/UserCountryMap/svg/KGZ.svg" => array("11119", "5baa6549491f424686182b3ce58a1902"),
+ "plugins/UserCountryMap/svg/KHM.svg" => array("16542", "2331a87180f4214e4eefa3efdac13e7b"),
+ "plugins/UserCountryMap/svg/KOR.svg" => array("15668", "435ab9abe0563594e545a8a670b60904"),
+ "plugins/UserCountryMap/svg/KWT.svg" => array("4015", "7616986c0f50869cbd7f803479e9cd1b"),
+ "plugins/UserCountryMap/svg/LAO.svg" => array("16993", "2a20adc6e7355f5c78231b622586ade5"),
+ "plugins/UserCountryMap/svg/LBN.svg" => array("6185", "2c73a84358417f7b2a9e5fd8a0a895b9"),
+ "plugins/UserCountryMap/svg/LBR.svg" => array("9567", "d01ff6bbd8c7ad4b742076e8696bdc85"),
+ "plugins/UserCountryMap/svg/LBY.svg" => array("10633", "5269896a2f3249c72e4dca03942f64dc"),
+ "plugins/UserCountryMap/svg/LKA.svg" => array("24701", "1a40f9c005c0d7e221dfeec48cadb393"),
+ "plugins/UserCountryMap/svg/LSO.svg" => array("8233", "a8f41221b30f121a978a22993f5c60fd"),
+ "plugins/UserCountryMap/svg/LTU.svg" => array("10762", "79288cb5afc370cae5545046abd8c950"),
+ "plugins/UserCountryMap/svg/LUX.svg" => array("5508", "f011778f5fb5b257dad08ae442b5ede7"),
+ "plugins/UserCountryMap/svg/LVA.svg" => array("16373", "1f58c503f439d1a4eb503a71210bdace"),
+ "plugins/UserCountryMap/svg/MAR.svg" => array("9031", "ad553a3b9140b0a8d5416a5edc8b7a19"),
+ "plugins/UserCountryMap/svg/MDA.svg" => array("20701", "56361ce864b7769dc0f6a4b858fe5e49"),
+ "plugins/UserCountryMap/svg/MDG.svg" => array("7909", "0074f845fac4627eb62addc0d7e00bb6"),
+ "plugins/UserCountryMap/svg/MEX.svg" => array("18091", "8c2be2255541d900867b8a6af14da261"),
+ "plugins/UserCountryMap/svg/MKD.svg" => array("31645", "05a9065d09d3c927caf54e00253a6da9"),
+ "plugins/UserCountryMap/svg/MLI.svg" => array("10667", "2a83206dc55014733030b17c6ff3d0c9"),
+ "plugins/UserCountryMap/svg/MMR.svg" => array("23662", "538b7786bc8906b06cde59ffb00b3c13"),
+ "plugins/UserCountryMap/svg/MNE.svg" => array("11388", "8c7776e6e50a5b85f1a8ea00321696e7"),
+ "plugins/UserCountryMap/svg/MNG.svg" => array("13248", "4931476b7761f0afd5d62e03c8147a5e"),
+ "plugins/UserCountryMap/svg/MOZ.svg" => array("11874", "05b9e9783e1ec2e5c87fd470d7312089"),
+ "plugins/UserCountryMap/svg/MRT.svg" => array("7109", "9383cca951fabfb2310603499a9a5c29"),
+ "plugins/UserCountryMap/svg/MWI.svg" => array("13547", "f980015835b3b2c6aec4188b83e5ba7d"),
+ "plugins/UserCountryMap/svg/MYS.svg" => array("11560", "04b76af26905b4cc7d0dc88eb3ccab0c"),
+ "plugins/UserCountryMap/svg/NAM.svg" => array("8666", "0eff414f6cd6718f034983719f488e45"),
+ "plugins/UserCountryMap/svg/NA.svg" => array("20688", "0eab8eac03efa7496aefd47007c3c642"),
+ "plugins/UserCountryMap/svg/NCL.svg" => array("3512", "8cc2afb213ee4636d2a8097757bcff7d"),
+ "plugins/UserCountryMap/svg/NER.svg" => array("6853", "e03c20fd94e0e62daca6c44e67d3b047"),
+ "plugins/UserCountryMap/svg/NFK.svg" => array("1016", "2a686707837ba4350d5a6209bbad772c"),
+ "plugins/UserCountryMap/svg/NGA.svg" => array("22091", "a050f9a00e6564a405a60a9e686e2a25"),
+ "plugins/UserCountryMap/svg/NIC.svg" => array("11167", "71c5ff669f9559869a1dbe0e057fd138"),
+ "plugins/UserCountryMap/svg/NLD.svg" => array("13533", "87ac2120d90df9341cbb7152e165d292"),
+ "plugins/UserCountryMap/svg/NOR.svg" => array("24959", "a7bd303ba1a09f0671d900f3cd7bab3a"),
+ "plugins/UserCountryMap/svg/NPL.svg" => array("19029", "7b2e2ea575a176766423c1dce432bd1b"),
+ "plugins/UserCountryMap/svg/NZL.svg" => array("9557", "111db079f5c4fef71052eec06446b229"),
+ "plugins/UserCountryMap/svg/OC.svg" => array("12434", "9f66f814498cc881b9694b29ed867371"),
+ "plugins/UserCountryMap/svg/OMN.svg" => array("6845", "372b4839372036bed607ba8c57c1c122"),
+ "plugins/UserCountryMap/svg/PAK.svg" => array("17475", "7f415118635e8e6ea456315f4b785606"),
+ "plugins/UserCountryMap/svg/PAN.svg" => array("11485", "c5da412bf9299e9de716fdece711ffa4"),
+ "plugins/UserCountryMap/svg/PER.svg" => array("17155", "be452b05f5a8e8d0f944c707b30ebb5b"),
+ "plugins/UserCountryMap/svg/PHL.svg" => array("29008", "c5ff0c378ba8bfa5e5894c22a915e080"),
+ "plugins/UserCountryMap/svg/PNG.svg" => array("12054", "c47a2c78a172ea17074d251e1843afa7"),
+ "plugins/UserCountryMap/svg/POL.svg" => array("14906", "1ee62f5428cc9ed4900a7667934ea06d"),
+ "plugins/UserCountryMap/svg/PRK.svg" => array("10398", "32e4b3a83d4ce8a75676bf402bc742e0"),
+ "plugins/UserCountryMap/svg/PRT.svg" => array("12377", "b90eb86c4816e6e9e2e2d29bf48a4d37"),
+ "plugins/UserCountryMap/svg/PRY.svg" => array("10349", "566abd56870902313ca8d004d9dadd0a"),
+ "plugins/UserCountryMap/svg/QAT.svg" => array("4459", "e4b0d267c76f0280cd42783129ee03b3"),
+ "plugins/UserCountryMap/svg/ROU.svg" => array("22605", "5a53d3b6e98992cb8bc345f0b277a8c7"),
+ "plugins/UserCountryMap/svg/RUS.svg" => array("51418", "ddea31beacbf27b77997c70329850942"),
+ "plugins/UserCountryMap/svg/RWA.svg" => array("6166", "eba84185436f49ebb6ae86bf6940af77"),
+ "plugins/UserCountryMap/svg/SA.svg" => array("19974", "dc56c3dae75129b048a60082bc7ab1b3"),
+ "plugins/UserCountryMap/svg/SAU.svg" => array("12191", "8e90f6ae44f247688013e0afe1d9324d"),
+ "plugins/UserCountryMap/svg/SDN.svg" => array("10582", "fa4f019324b16748f036d7e0ac1be268"),
+ "plugins/UserCountryMap/svg/SDS.svg" => array("9392", "dcc613f18014f156f121f8c2f7132f5d"),
+ "plugins/UserCountryMap/svg/SEN.svg" => array("10343", "881f8e0aaac32bdc49a9d0d7b4a63b73"),
+ "plugins/UserCountryMap/svg/SLB.svg" => array("4919", "955697719301436339a43477018b7975"),
+ "plugins/UserCountryMap/svg/SLE.svg" => array("6559", "42b25def79d22704f6cc76e7c0099c31"),
+ "plugins/UserCountryMap/svg/SLV.svg" => array("9294", "2bf77afa3e8caa4f1b00ae56f1649d68"),
+ "plugins/UserCountryMap/svg/SOM.svg" => array("6512", "26891247c4cb7abd5799da27a04b0b4e"),
+ "plugins/UserCountryMap/svg/SRB.svg" => array("21289", "2726a37123fbf80667206d3b3c3a7fb0"),
+ "plugins/UserCountryMap/svg/SUR.svg" => array("9441", "101bf443ca5329dafe4e4e027f177a20"),
+ "plugins/UserCountryMap/svg/SVK.svg" => array("10004", "d334bb3ae89b539679805ae28708394f"),
+ "plugins/UserCountryMap/svg/SVN.svg" => array("10197", "1ede9888c6d7990215823280b869e000"),
+ "plugins/UserCountryMap/svg/SWE.svg" => array("24034", "dc7f02aad153f19b91a32d379740f509"),
+ "plugins/UserCountryMap/svg/SWZ.svg" => array("3979", "05c2a0a2fba646300b749f8ab994a6a4"),
+ "plugins/UserCountryMap/svg/SYR.svg" => array("8819", "921ec6d095338437c680a713adb7d035"),
+ "plugins/UserCountryMap/svg/TCD.svg" => array("10905", "06ff353ea4e6e8345092264ddda58113"),
+ "plugins/UserCountryMap/svg/TGO.svg" => array("6991", "9e0c754e5c2615df3307fcb146503774"),
+ "plugins/UserCountryMap/svg/THA.svg" => array("36264", "46fffedb3489c67a1390cedae073de1d"),
+ "plugins/UserCountryMap/svg/TIB.svg" => array("16179", "b234a01aa6eceecf3918aefe5d4b52eb"),
+ "plugins/UserCountryMap/svg/TJK.svg" => array("11230", "d8de7e9d549ee744d5d64b521af213b2"),
+ "plugins/UserCountryMap/svg/TKM.svg" => array("8157", "1613ad868776a419e9e1cd96544b99e5"),
+ "plugins/UserCountryMap/svg/TLS.svg" => array("7109", "142856e1184a849f7e6005563be08077"),
+ "plugins/UserCountryMap/svg/tmp.svg" => array("21076", "dc397dce4ea165a2340e4d86d5eddd78"),
+ "plugins/UserCountryMap/svg/TTO.svg" => array("5435", "cff25edb4b12be208181830adab5a7d7"),
+ "plugins/UserCountryMap/svg/TUN.svg" => array("12961", "e05a3732f54dbfb898569f6c934cb7a7"),
+ "plugins/UserCountryMap/svg/TUR.svg" => array("32959", "4a3b1ff120ba27f0ef59759959f00d54"),
+ "plugins/UserCountryMap/svg/TWN.svg" => array("11996", "2437ad3a020bc3ca36d5ea52970ca350"),
+ "plugins/UserCountryMap/svg/TZA.svg" => array("15761", "6f35aebaeb542e2321d8407835e06db8"),
+ "plugins/UserCountryMap/svg/UGA.svg" => array("29417", "628c72ea62f742ae52d24ce27dd2037e"),
+ "plugins/UserCountryMap/svg/UKR.svg" => array("19828", "37e8ea0894ea61c5bbdccd96277ba095"),
+ "plugins/UserCountryMap/svg/URY.svg" => array("11658", "3176fa145c40a311a2e008149062f073"),
+ "plugins/UserCountryMap/svg/USA.svg" => array("42615", "81e5d5548e22e69a2b9cbd3e0c0c0d2d"),
+ "plugins/UserCountryMap/svg/UZB.svg" => array("12320", "ed73e6f04614f80a9a465fcc3f23f47e"),
+ "plugins/UserCountryMap/svg/VEN.svg" => array("15675", "1917405c4f63d358a9cbb82db61d2bd8"),
+ "plugins/UserCountryMap/svg/VNM.svg" => array("27766", "83c7dea05690c6308d9a63fa0f1e2565"),
+ "plugins/UserCountryMap/svg/VUT.svg" => array("4912", "0856c96bc733bbcca28dd68991f097d7"),
+ "plugins/UserCountryMap/svg/world.svg" => array("111845", "5955072d8e13d9f074aeb1b0a7c4cd69"),
+ "plugins/UserCountryMap/svg/YEM.svg" => array("10632", "0b124919400cbb8587811fe5c23664ff"),
+ "plugins/UserCountryMap/svg/ZAF.svg" => array("10216", "ea4566c8b873e02a443efde301ebafd3"),
+ "plugins/UserCountryMap/svg/ZMB.svg" => array("11277", "e79d992f232139d9bb4c971c57fd1803"),
+ "plugins/UserCountryMap/svg/ZWE.svg" => array("10120", "00eb861cd623329b917a94aacaf09b97"),
+ "plugins/UserCountryMap/templates/realtimeMap.twig" => array("1455", "aa4b22cc760a6dcc5690a6420aaafce8"),
+ "plugins/UserCountryMap/templates/visitorMap.twig" => array("5543", "0b26ebd13591d082358483483427eaf6"),
+ "plugins/UserCountryMap/UserCountryMap.php" => array("2953", "6cf2a6f2c7e90fbbcfae3edd21419454"),
+ "plugins/UserCountry/stylesheets/userCountry.less" => array("1122", "17952f305542ff7aa15daf21f7d1deda"),
+ "plugins/UserCountry/templates/adminIndex.twig" => array("6899", "4ec9965bec4e48d45de0e029b51ab6d9"),
+ "plugins/UserCountry/templates/getGeoIpUpdaterManageScreen.twig" => array("48", "b3ffc6408921cada77599d32ae546511"),
+ "plugins/UserCountry/templates/index.twig" => array("785", "12f762bb3dff29bb477eff7fff7238c2"),
+ "plugins/UserCountry/templates/_updaterManage.twig" => array("3168", "87f3d8ef21402d491ec9c19203fd4ed7"),
+ "plugins/UserCountry/templates/_updaterNextRunTime.twig" => array("386", "9761b8bd24bd0ebd0461a560dcb23d22"),
+ "plugins/UserCountry/UserCountry.php" => array("21375", "6a03bbebc2f88b09ddc859d37e8b1aa3"),
+ "plugins/UserSettings/API.php" => array("10763", "f32d5ea9844711996d18085b7d5ec1b9"),
+ "plugins/UserSettings/Archiver.php" => array("6825", "1cd51e9b6b66f6a066e301b058c1fbd4"),
+ "plugins/UserSettings/Controller.php" => array("2023", "0628ed431135bd3fac8ad989088df785"),
+ "plugins/UserSettings/functions.php" => array("6653", "3444e02fdef24b992dd36e164e3fa10a"),
+ "plugins/UserSettings/images/browsers/AA.gif" => array("1092", "dc62eff78dac919b00e60c5fb3e6266d"),
+ "plugins/UserSettings/images/browsers/AB.gif" => array("1064", "1123da862a558b1ccd8f9008c5c4fdcb"),
+ "plugins/UserSettings/images/browsers/AG.gif" => array("351", "6e793ad6ad5c69abc499422d6a43d836"),
+ "plugins/UserSettings/images/browsers/AM.gif" => array("198", "18c54fc3197f6e1533c06b0923db3bd0"),
+ "plugins/UserSettings/images/browsers/AN.gif" => array("144", "a1264f47256ff5b0d4feeeac833e7a96"),
+ "plugins/UserSettings/images/browsers/AR.gif" => array("1057", "377249d199156ee602e669c0afc02945"),
+ "plugins/UserSettings/images/browsers/AV.gif" => array("151", "f459c84d6ab90fdf0f35d20f9f82626d"),
+ "plugins/UserSettings/images/browsers/AW.gif" => array("574", "e4e4fa2ef432f2a86086ec58f4b27ab1"),
+ "plugins/UserSettings/images/browsers/B2.gif" => array("1070", "e1b9f6b47cffbdf05b4159d6a31b21ae"),
+ "plugins/UserSettings/images/browsers/BB.gif" => array("576", "d40bd99ba1dd881b164907341ec2079d"),
+ "plugins/UserSettings/images/browsers/BD.gif" => array("1051", "4a8ebfcd7aad4c1004b7f82b954b0a69"),
+ "plugins/UserSettings/images/browsers/BE.gif" => array("1042", "103994d17de92aa261c8034a8e35a84f"),
+ "plugins/UserSettings/images/browsers/BP.gif" => array("1070", "e1b9f6b47cffbdf05b4159d6a31b21ae"),
+ "plugins/UserSettings/images/browsers/BX.gif" => array("522", "7302ad862f4007c23efb73acbd41f5c0"),
+ "plugins/UserSettings/images/browsers/CA.gif" => array("573", "739fca054b61f68657b0bd349c958a86"),
+ "plugins/UserSettings/images/browsers/CD.gif" => array("1045", "b5484f5fc254abd52cdc94f378be977a"),
+ "plugins/UserSettings/images/browsers/CF.gif" => array("1074", "e349a7dbdadf2fca33cca13287b0eba8"),
+ "plugins/UserSettings/images/browsers/CH.gif" => array("1074", "e349a7dbdadf2fca33cca13287b0eba8"),
+ "plugins/UserSettings/images/browsers/CK.gif" => array("1024", "b6d4ebb0394c48dfcb5f21475de5c79b"),
+ "plugins/UserSettings/images/browsers/CM.gif" => array("1074", "e349a7dbdadf2fca33cca13287b0eba8"),
+ "plugins/UserSettings/images/browsers/CN.gif" => array("998", "cd878afb8cc56e7c8c6caef6d5fb2ba4"),
+ "plugins/UserSettings/images/browsers/CO.gif" => array("1042", "195487c9db2e19ffae3fd443b9266e62"),
+ "plugins/UserSettings/images/browsers/CP.gif" => array("998", "af47e47253591c272a458c4645fbaf5c"),
+ "plugins/UserSettings/images/browsers/CS.gif" => array("549", "eb5151f2f46fc09d687ce27becefb831"),
+ "plugins/UserSettings/images/browsers/DF.gif" => array("545", "f4b65ebcf304f1675088d029ed613d28"),
+ "plugins/UserSettings/images/browsers/DI.gif" => array("1068", "4eceaf5fd7808c422b67026c9329e16f"),
+ "plugins/UserSettings/images/browsers/EL.gif" => array("90", "b515db820d883f921d05d15a34dda7f9"),
+ "plugins/UserSettings/images/browsers/EP.gif" => array("316", "660436cc97429ef52365a01084b40ee0"),
+ "plugins/UserSettings/images/browsers/ES.gif" => array("1013", "92ada780fce5e3ffc318de634eba4d21"),
+ "plugins/UserSettings/images/browsers/FB.gif" => array("254", "24663f1949bae4fb7ecd0504c9c872fd"),
+ "plugins/UserSettings/images/browsers/FD.gif" => array("1050", "a569dc9dbe1b95ce2763bee8dbdc5850"),
+ "plugins/UserSettings/images/browsers/FE.gif" => array("550", "a016a52d476c4be7943e68ccf2056db1"),
+ "plugins/UserSettings/images/browsers/FF.gif" => array("197", "6b4eecf673d5461aebdd432bca086e8c"),
+ "plugins/UserSettings/images/browsers/FL.gif" => array("1034", "0ce889dc81db377eeb00f76f72942274"),
+ "plugins/UserSettings/images/browsers/FN.gif" => array("1033", "e87473b14680939e58a9a94e9dcd39e3"),
+ "plugins/UserSettings/images/browsers/GA.gif" => array("159", "576c4646cf6938dd2079516293d3a3f9"),
+ "plugins/UserSettings/images/browsers/GE.gif" => array("997", "a3d96e8576f273ecc4a864d24620fbf2"),
+ "plugins/UserSettings/images/browsers/HA.gif" => array("1009", "fccae707e311bd009be90a7b38000082"),
+ "plugins/UserSettings/images/browsers/HJ.gif" => array("1022", "8c3019d1e0867e8455d0abf1ad3eb531"),
+ "plugins/UserSettings/images/browsers/IA.gif" => array("391", "1836a7db0bc6a4a4d8b3d6063346e3f5"),
+ "plugins/UserSettings/images/browsers/IB.gif" => array("168", "b091c3e8ce2789017d581089028fa1cc"),
+ "plugins/UserSettings/images/browsers/IC.gif" => array("131", "26a6ff98d316092214c9dc9e7be45224"),
+ "plugins/UserSettings/images/browsers/ID.gif" => array("1057", "6f1f33dc4bd104e0a60000ce49e719a9"),
+ "plugins/UserSettings/images/browsers/IE.gif" => array("999", "5e002ee72167a3a78e2252766fde1046"),
+ "plugins/UserSettings/images/browsers/IR.gif" => array("610", "565b1e3acd514c1c9b88d6c2b0ec0c4d"),
+ "plugins/UserSettings/images/browsers/IW.gif" => array("1066", "8d3376b6699ccd4533191b38d7f6b8c4"),
+ "plugins/UserSettings/images/browsers/KI.gif" => array("1050", "497b4bc9b58c113ae72763559321071b"),
+ "plugins/UserSettings/images/browsers/KM.gif" => array("180", "3daa5fa7553d448cd280612491e8a6ea"),
+ "plugins/UserSettings/images/browsers/KO.gif" => array("986", "0d15a2d4a73582d1df109e0ff3463fd3"),
+ "plugins/UserSettings/images/browsers/KP.gif" => array("1037", "f29fb41537f7df91d827c3f6eea076a0"),
+ "plugins/UserSettings/images/browsers/KZ.gif" => array("1061", "cdc654ad5f3f5fdda6ac69ea7d7dbe31"),
+ "plugins/UserSettings/images/browsers/LI.gif" => array("104", "a3b302b3fffc9ed032add149d4ace210"),
+ "plugins/UserSettings/images/browsers/LS.gif" => array("1086", "c61646736ea4872a7e58bcd29716d778"),
+ "plugins/UserSettings/images/browsers/LX.gif" => array("104", "a3b302b3fffc9ed032add149d4ace210"),
+ "plugins/UserSettings/images/browsers/MC.gif" => array("1023", "0e17db9ed1e06feb1d23d134ce34693d"),
+ "plugins/UserSettings/images/browsers/MI.gif" => array("1025", "5e63fceac90a88f1db14b4e8ee44201d"),
+ "plugins/UserSettings/images/browsers/MO.gif" => array("192", "67b5dac21e8f2243a955f1d9df7ef67e"),
+ "plugins/UserSettings/images/browsers/MS.gif" => array("1094", "265861a05c27b23013cb6ae3c428dff0"),
+ "plugins/UserSettings/images/browsers/MX.gif" => array("985", "4f6f87c42bf5c6bfc2b63925da5e40c1"),
+ "plugins/UserSettings/images/browsers/NB.gif" => array("977", "d2fac7549889df9f1c0863b424543c6f"),
+ "plugins/UserSettings/images/browsers/NF.gif" => array("612", "7cb0d2713e9faf25b766ca0d13cf456b"),
+ "plugins/UserSettings/images/browsers/NL.gif" => array("1081", "f66412328676120ba3cc0eb987c16158"),
+ "plugins/UserSettings/images/browsers/NP.gif" => array("1020", "ba7d68a0f9c11647abba2f8454a7c34c"),
+ "plugins/UserSettings/images/browsers/NS.gif" => array("98", "cd8d53ec12b64294d16769dfeeaf07c7"),
+ "plugins/UserSettings/images/browsers/OB.gif" => array("1010", "67b1d28deccc92200bdc3ce4a86612c3"),
+ "plugins/UserSettings/images/browsers/ON.gif" => array("635", "f7d7eb7f8cec24f0c192e30fe29ea320"),
+ "plugins/UserSettings/images/browsers/OP.gif" => array("987", "ac0432440ad48154a6434675b1d9c27a"),
+ "plugins/UserSettings/images/browsers/OR.gif" => array("1024", "30e874c346325cd40cf58f98944ea603"),
+ "plugins/UserSettings/images/browsers/OV.gif" => array("978", "7e98fecee01438d561b791339281bd47"),
+ "plugins/UserSettings/images/browsers/OW.gif" => array("197", "b66e88cbb941f9ac326f43e0d993e572"),
+ "plugins/UserSettings/images/browsers/PL.gif" => array("1058", "759fa0100429b3b3a4dc88894454fd8a"),
+ "plugins/UserSettings/images/browsers/PM.gif" => array("1082", "271bd4b89d06a9f9a9553b5da9053bd3"),
+ "plugins/UserSettings/images/browsers/PO.gif" => array("1065", "ac773ea28693335de8e8ac62f7d20a0d"),
+ "plugins/UserSettings/images/browsers/PU.gif" => array("1094", "1ea4a15e1b326c28158b137e4e8d07af"),
+ "plugins/UserSettings/images/browsers/PW.gif" => array("1082", "ac66922861d77e9949a72f86622e0c2f"),
+ "plugins/UserSettings/images/browsers/PX.gif" => array("170", "0bd86aa95e1ae0d5975cdc2d93210e30"),
+ "plugins/UserSettings/images/browsers/RK.gif" => array("1035", "6b87220087449e134062214fbf72f5a8"),
+ "plugins/UserSettings/images/browsers/SA.gif" => array("1008", "921467088fc56c5c9cdd0bb6e58250bf"),
+ "plugins/UserSettings/images/browsers/SF.gif" => array("190", "589361249f74319b57ea98d6408bc4b3"),
+ "plugins/UserSettings/images/browsers/SL.gif" => array("900", "1210e399e978978390cfdfd9d79159e6"),
+ "plugins/UserSettings/images/browsers/SM.gif" => array("391", "1836a7db0bc6a4a4d8b3d6063346e3f5"),
+ "plugins/UserSettings/images/browsers/TB.gif" => array("1014", "79bf7ed3ad92d3da09737d2fcd3913aa"),
+ "plugins/UserSettings/images/browsers/TI.gif" => array("595", "2c09db5f54b47472971d863e183159a8"),
+ "plugins/UserSettings/images/browsers/TZ.gif" => array("973", "5858a8b149e45749424bdf2da7ebefa3"),
+ "plugins/UserSettings/images/browsers/UC.gif" => array("994", "d9622ea01cb9093592858da53443c200"),
+ "plugins/UserSettings/images/browsers/UN.gif" => array("80", "c4a7f0e333a6079fdb0b6595e11bca74"),
+ "plugins/UserSettings/images/browsers/UNK.gif" => array("80", "c4a7f0e333a6079fdb0b6595e11bca74"),
+ "plugins/UserSettings/images/browsers/WE.gif" => array("1012", "cce9216ee7bd3ef52a46003d249ab540"),
+ "plugins/UserSettings/images/browsers/WO.gif" => array("1065", "ed1504717c9af523e30c33908126c4ad"),
+ "plugins/UserSettings/images/browsers/WP.gif" => array("982", "5bba1edfb42ce1b96551f81af0be08a1"),
+ "plugins/UserSettings/images/browsers/YA.gif" => array("1048", "8d94386ab4796664de7b897dd2106c9c"),
+ "plugins/UserSettings/images/os/3DS.gif" => array("1085", "262b44579aadcf90973653ff3e759cc7"),
+ "plugins/UserSettings/images/os/AIX.gif" => array("176", "58a60503a8e92493153694d1d97d2f6d"),
+ "plugins/UserSettings/images/os/AMG.gif" => array("1001", "5d67b7bb52ed746480573c1600d9a34c"),
+ "plugins/UserSettings/images/os/AMI.gif" => array("1055", "ef341c4cc2ec3bbf860c7d3bfe685326"),
+ "plugins/UserSettings/images/os/AND.gif" => array("144", "a1264f47256ff5b0d4feeeac833e7a96"),
+ "plugins/UserSettings/images/os/ARL.gif" => array("947", "913d273e01b9031f5113fb82ce63a591"),
+ "plugins/UserSettings/images/os/BBX.gif" => array("590", "e5cff6836abf100d9d8310d9dbb9f5d4"),
+ "plugins/UserSettings/images/os/BEO.gif" => array("1035", "ae4420933ac47a072d0f47759ef830c2"),
+ "plugins/UserSettings/images/os/BLB.gif" => array("576", "d40bd99ba1dd881b164907341ec2079d"),
+ "plugins/UserSettings/images/os/BSD.gif" => array("1016", "1dc9b76bb3fc8f5529e9abe9ac8841fa"),
+ "plugins/UserSettings/images/os/BTR.gif" => array("946", "cbf9b74ee2db7714ed6d6432eff3f7c8"),
+ "plugins/UserSettings/images/os/CES.gif" => array("1011", "0cdd142972c3cc89b2ceb3b7b97e1321"),
+ "plugins/UserSettings/images/os/COS.gif" => array("1074", "e349a7dbdadf2fca33cca13287b0eba8"),
+ "plugins/UserSettings/images/os/DFB.gif" => array("326", "d61f11a900d520ef515eaa139176a5f1"),
+ "plugins/UserSettings/images/os/DSI.gif" => array("1076", "5c475f3ba76f4ec3b626e720574bcb37"),
+ "plugins/UserSettings/images/os/FED.gif" => array("1022", "e86e6f5aec6c32de7eca913a9f91c3ab"),
+ "plugins/UserSettings/images/os/FOS.gif" => array("197", "6b4eecf673d5461aebdd432bca086e8c"),
+ "plugins/UserSettings/images/os/GNT.gif" => array("1075", "4196a85df43e6a5593941dcf8262416f"),
+ "plugins/UserSettings/images/os/GTV.gif" => array("1614", "a032dd001e1a5755201a6263a669ca49"),
+ "plugins/UserSettings/images/os/HPX.gif" => array("191", "999717c37d76ca099a06cf77a781bdbf"),
+ "plugins/UserSettings/images/os/IOS.gif" => array("591", "0bd1b8e09506ae6f9ed6ab1b18acf552"),
+ "plugins/UserSettings/images/os/IPA.gif" => array("587", "9f247437bc140cc6e70ec59f34bcddb1"),
+ "plugins/UserSettings/images/os/IPD.gif" => array("351", "a215ada2aefcbca876055fe7a2f7c039"),
+ "plugins/UserSettings/images/os/IPH.gif" => array("577", "49805f402375692d40635ada7cc0f472"),
+ "plugins/UserSettings/images/os/IRI.gif" => array("152", "5e631b5adc35a05ae0d85815829c6f48"),
+ "plugins/UserSettings/images/os/KBT.gif" => array("998", "6ecd8b978a51fb4fd6ec94f9b820ae1d"),
+ "plugins/UserSettings/images/os/KNO.gif" => array("985", "b4595a673edf60051636fedf418eda30"),
+ "plugins/UserSettings/images/os/LBT.gif" => array("951", "ed14ac9707a0e01f4557ac9e8508dea3"),
+ "plugins/UserSettings/images/os/LIN.gif" => array("170", "19039ee87d8fccdba6a391ada5656de7"),
+ "plugins/UserSettings/images/os/MAC.gif" => array("171", "03548481597f28751368e26be49aea99"),
+ "plugins/UserSettings/images/os/MAE.gif" => array("137", "84600277bad6751b68b15586bca50aef"),
+ "plugins/UserSettings/images/os/MDR.gif" => array("918", "62f5e501d28e25fff6c3a75631c7c208"),
+ "plugins/UserSettings/images/os/MIN.gif" => array("1009", "153385eff242c4e10ea3835a17a65f0e"),
+ "plugins/UserSettings/images/os/NBS.gif" => array("168", "fc0d4fcb57c98f3a4dd6eab8e71ebd6a"),
+ "plugins/UserSettings/images/os/NDS.gif" => array("1061", "16bc6e0960747b402441c40e419a7d53"),
+ "plugins/UserSettings/images/os/OBS.gif" => array("571", "fcdb547b7ab768e131ba592e8733c75c"),
+ "plugins/UserSettings/images/os/OS2.gif" => array("162", "ee37bab155ad46f2530e7586f9971656"),
+ "plugins/UserSettings/images/os/POS.gif" => array("1060", "96b06842dc1cc80a8bb283ee8f4be320"),
+ "plugins/UserSettings/images/os/PPY.gif" => array("1037", "1bc770c1bd83e6cfdc5087b00b44cb9d"),
+ "plugins/UserSettings/images/os/PS3.gif" => array("628", "7aca5b93e7cc8142e2ab578e7aae7dc8"),
+ "plugins/UserSettings/images/os/PSP.gif" => array("592", "f92da90c6c6ea808422184c314215465"),
+ "plugins/UserSettings/images/os/PSV.gif" => array("200", "d82e64a4f0aeec6e4931a41988680af3"),
+ "plugins/UserSettings/images/os/QNX.gif" => array("241", "51ceb87cd6268837d830b225cb6c8120"),
+ "plugins/UserSettings/images/os/RHT.gif" => array("952", "72c775bb0f2b8ad388ee513577abc8ec"),
+ "plugins/UserSettings/images/os/ROS.gif" => array("956", "9a294e5c701171c8c777ce563d88d924"),
+ "plugins/UserSettings/images/os/SAF.gif" => array("242", "600259fe739536945b6f5cd5c9d3489b"),
+ "plugins/UserSettings/images/os/SBA.gif" => array("990", "be3cfde24b6517884d73270a20e9a06f"),
+ "plugins/UserSettings/images/os/SLW.gif" => array("883", "37ca3a7bdb0eeea402252c80dca89369"),
+ "plugins/UserSettings/images/os/SOS.gif" => array("1036", "686261ea170398b2eb078b67a24f4276"),
+ "plugins/UserSettings/images/os/SSE.gif" => array("1066", "8a6b48a38ee8ecca0fd14092872dba12"),
+ "plugins/UserSettings/images/os/SYL.gif" => array("1017", "5c73fe766f50d4a697dbdeac254db9e2"),
+ "plugins/UserSettings/images/os/SYM.gif" => array("1042", "a8d404e43206c52a7e5f3e6e7e469eea"),
+ "plugins/UserSettings/images/os/T64.gif" => array("220", "8e42601e52784216ed71b8e8de17f82e"),
+ "plugins/UserSettings/images/os/TIZ.gif" => array("958", "dbc584b7603e8865c9477b18a6fbbb7d"),
+ "plugins/UserSettings/images/os/UBT.gif" => array("986", "56d67af2d61927c290b0e5af8e8ef6fc"),
+ "plugins/UserSettings/images/os/UNK.gif" => array("80", "c4a7f0e333a6079fdb0b6595e11bca74"),
+ "plugins/UserSettings/images/os/VMS.gif" => array("572", "da6881ce3b86fdbea70ae1b405f9c40a"),
+ "plugins/UserSettings/images/os/W2K.gif" => array("185", "5e7141d138d3f7afc0a113c21a8b2d9b"),
+ "plugins/UserSettings/images/os/W61.gif" => array("1060", "721b79d12ac825df25f356f5a7714f74"),
+ "plugins/UserSettings/images/os/W65.gif" => array("1060", "721b79d12ac825df25f356f5a7714f74"),
+ "plugins/UserSettings/images/os/W75.gif" => array("1089", "8d69225f3ebb1fe2d2b6d4e2e1687b80"),
+ "plugins/UserSettings/images/os/W95.gif" => array("185", "5e7141d138d3f7afc0a113c21a8b2d9b"),
+ "plugins/UserSettings/images/os/W98.gif" => array("185", "5e7141d138d3f7afc0a113c21a8b2d9b"),
+ "plugins/UserSettings/images/os/WCE.gif" => array("185", "5e7141d138d3f7afc0a113c21a8b2d9b"),
+ "plugins/UserSettings/images/os/WI7.gif" => array("191", "53d14246a8e5a46e927a87648111a0d3"),
+ "plugins/UserSettings/images/os/WI8.gif" => array("925", "d9f78bbd9009c721cf5d64b056679a19"),
+ "plugins/UserSettings/images/os/WII.gif" => array("617", "d8f2cae9e8e7723241c6c862f12c7511"),
+ "plugins/UserSettings/images/os/WIN.gif" => array("191", "53d14246a8e5a46e927a87648111a0d3"),
+ "plugins/UserSettings/images/os/WIU.gif" => array("310", "394c491524ac263e1c4fcedb1480d281"),
+ "plugins/UserSettings/images/os/WME.gif" => array("1025", "27c3894e3585f86f9283374ed7776c3a"),
+ "plugins/UserSettings/images/os/WMO.gif" => array("1060", "721b79d12ac825df25f356f5a7714f74"),
+ "plugins/UserSettings/images/os/WNT.gif" => array("185", "5e7141d138d3f7afc0a113c21a8b2d9b"),
+ "plugins/UserSettings/images/os/WOS.gif" => array("70", "31e5c59d2fc5b195c5ea4f1afd878e04"),
+ "plugins/UserSettings/images/os/WP7.gif" => array("1089", "8d69225f3ebb1fe2d2b6d4e2e1687b80"),
+ "plugins/UserSettings/images/os/WPH.gif" => array("1089", "8d69225f3ebb1fe2d2b6d4e2e1687b80"),
+ "plugins/UserSettings/images/os/WRT.gif" => array("925", "d9f78bbd9009c721cf5d64b056679a19"),
+ "plugins/UserSettings/images/os/WS3.gif" => array("191", "53d14246a8e5a46e927a87648111a0d3"),
+ "plugins/UserSettings/images/os/WVI.gif" => array("191", "53d14246a8e5a46e927a87648111a0d3"),
+ "plugins/UserSettings/images/os/WXP.gif" => array("191", "53d14246a8e5a46e927a87648111a0d3"),
+ "plugins/UserSettings/images/os/XBT.gif" => array("968", "c0f572e03ffaf7d38c78fcbb7fba84cf"),
+ "plugins/UserSettings/images/os/XBX.gif" => array("1043", "e3e0eaa5daa2903bab1e0e9ea3ef1d46"),
+ "plugins/UserSettings/images/os/YNS.gif" => array("913", "f4d8502e11b209c209fcee3312a33139"),
+ "plugins/UserSettings/images/plugins/cookie.gif" => array("211", "9e564884defc036134b19ab38c192a6b"),
+ "plugins/UserSettings/images/plugins/director.gif" => array("198", "952c4a0e083ed089ca5c8ab2804c35ab"),
+ "plugins/UserSettings/images/plugins/flash.gif" => array("1018", "f315906f425f089dd4664ee530c691bd"),
+ "plugins/UserSettings/images/plugins/gears.gif" => array("558", "d4ec944ef01420637a709619f067953e"),
+ "plugins/UserSettings/images/plugins/java.gif" => array("565", "b4d24f76a64e2df762292e892e9215c4"),
+ "plugins/UserSettings/images/plugins/pdf.gif" => array("1021", "79b3d68c112942cefbb24dda9b421464"),
+ "plugins/UserSettings/images/plugins/quicktime.gif" => array("1003", "0feda8dc4ddec39e35b6f4925603c8bd"),
+ "plugins/UserSettings/images/plugins/realplayer.gif" => array("1025", "95739f527d29cab050a3d4eff35e93c7"),
+ "plugins/UserSettings/images/plugins/silverlight.gif" => array("1012", "8449c3a43f42c44bc5b82948d179b4b4"),
+ "plugins/UserSettings/images/plugins/windowsmedia.gif" => array("1026", "a374aec2d5488c5dc8d4eaf21ec59728"),
+ "plugins/UserSettings/images/screens/dual.gif" => array("1082", "7903e070f4c4aa7933030e547e78073e"),
+ "plugins/UserSettings/images/screens/mobile.gif" => array("324", "04942ef60dd75380cfd682314e87e857"),
+ "plugins/UserSettings/images/screens/normal.gif" => array("1088", "b0c3de8704745e58c67844e54282ee42"),
+ "plugins/UserSettings/images/screens/unknown.gif" => array("80", "c4a7f0e333a6079fdb0b6595e11bca74"),
+ "plugins/UserSettings/images/screens/wide.gif" => array("1025", "c0104958e6fb23668d0406fd4d89095e"),
+ "plugins/UserSettings/templates/index.twig" => array("985", "64dbe064f25bce041a9ea0ac4b401509"),
+ "plugins/UserSettings/UserSettings.php" => array("16493", "546f136ebeb5cbe7e1ffa364be6f1fdc"),
+ "plugins/UsersManager/API.php" => array("21685", "ff11d871854f92e6e309b563ba4ed8cd"),
+ "plugins/UsersManager/Controller.php" => array("12751", "27d35da3855e76319f08f5cfeb4e24cd"),
+ "plugins/UsersManager/images/add.png" => array("1366", "79f8c85304af007fb571bd67b5b84a65"),
+ "plugins/UsersManager/images/no-access.png" => array("653", "e92421cec0f4c4f344b00a2ff72f96ec"),
+ "plugins/UsersManager/images/ok.png" => array("851", "94672a5d1482b1afaafce7802155f518"),
+ "plugins/UsersManager/javascripts/usersManager.js" => array("10779", "e4711ac89dcb705f5002c39a26608dbe"),
+ "plugins/UsersManager/javascripts/usersSettings.js" => array("3254", "6f10ea2cac2509e07988389078687af4"),
+ "plugins/UsersManager/LastSeenTimeLogger.php" => array("2296", "11eb62ec3cc2588fb571769330aefbba"),
+ "plugins/UsersManager/Model.php" => array("9294", "93aa4779b992fc0991aaa9a6cc8e533e"),
+ "plugins/UsersManager/stylesheets/usersManager.less" => array("400", "34e7c27854b8a5e2b5c2e55df5de755c"),
+ "plugins/UsersManager/templates/index.twig" => array("9706", "50e07c93f864be7e52418c05d96c5588"),
+ "plugins/UsersManager/templates/userSettings.twig" => array("9580", "25708d48635daf6f1114220df0c43fc2"),
+ "plugins/UsersManager/UsersManager.php" => array("5028", "42c49b25bd17cc81567855b42e0e4736"),
+ "plugins/VisitFrequency/API.php" => array("2542", "bd9a2e5d38ce65bc72af6abfe65166a6"),
+ "plugins/VisitFrequency/Controller.php" => array("4225", "9316375706f2da88c7243aa2e703d362"),
+ "plugins/VisitFrequency/templates/getSparklines.twig" => array("48", "1d2ec22a548fb6662979f19d9d3fc329"),
+ "plugins/VisitFrequency/templates/index.twig" => array("338", "3eca543636e9b14ecb0208b6e03bf283"),
+ "plugins/VisitFrequency/templates/_sparklines.twig" => array("1064", "09eecc1ac6d13181fbec549185cacb88"),
+ "plugins/VisitFrequency/VisitFrequency.php" => array("2625", "84fd90736051331df11ca37a64917cea"),
+ "plugins/VisitorInterest/API.php" => array("4802", "3ea0c6255ab8ebb2deb7c600e300460a"),
+ "plugins/VisitorInterest/Archiver.php" => array("5175", "66968c131f5b948ecb49dbf5bdb4fc5f"),
+ "plugins/VisitorInterest/Controller.php" => array("1730", "99828a062bfe59a5dccafa741e001bff"),
+ "plugins/VisitorInterest/templates/index.twig" => array("533", "5f1579ad5941e520b87275aff6cbb729"),
+ "plugins/VisitorInterest/VisitorInterest.php" => array("10661", "d019fd8854d7087dd4948f230f6ae158"),
+ "plugins/VisitsSummary/API.php" => array("6176", "0780ead1ba4050b502a18c37f8c0f322"),
+ "plugins/VisitsSummary/Controller.php" => array("8277", "31b7cdca8c5b9293af2e2eeedf6adf60"),
+ "plugins/VisitsSummary/stylesheets/datatable.less" => array("113", "8ffed702781ee6746a4701fc5f5ff9f1"),
+ "plugins/VisitsSummary/templates/getSparklines.twig" => array("47", "f8630d90a9c3ab9ba0d5929d537ec5a1"),
+ "plugins/VisitsSummary/templates/index.twig" => array("634", "6ff7b1f46d57ca9e0a4b7e6293ffa81c"),
+ "plugins/VisitsSummary/templates/_sparklines.twig" => array("3505", "6f587c7c41c00b990065c5cde5a505cd"),
+ "plugins/VisitsSummary/VisitsSummary.php" => array("2864", "820091576b5aaef9873de6b4d1a8ad43"),
+ "plugins/VisitTime/API.php" => array("5806", "04226b406a95196b3ac7d653cb79b814"),
+ "plugins/VisitTime/Archiver.php" => array("2924", "cb73804d891689ebc1a79cf3ff6d6e2a"),
+ "plugins/VisitTime/Controller.php" => array("960", "6dce46cb47d8892f3e96e13d606b2f99"),
+ "plugins/VisitTime/functions.php" => array("800", "02b598cca67def3060e538d359da9c76"),
+ "plugins/VisitTime/templates/index.twig" => array("316", "483e2fc50e9e905201576c1638326782"),
+ "plugins/VisitTime/VisitTime.php" => array("9503", "867e6b57fb7e4f64219471c6f4ffe6b1"),
+ "plugins/Widgetize/Controller.php" => array("2404", "0d55e61621a887b086c60d086b66a271"),
+ "plugins/Widgetize/javascripts/widgetize.js" => array("3815", "b142cb72e9609949b895f517f1f19271"),
+ "plugins/Widgetize/stylesheets/widgetize.less" => array("523", "92ccef42a032893e61115dc1804a9d64"),
+ "plugins/Widgetize/templates/iframe_empty.twig" => array("17", "8a9aa8c925c824c53de88a0feca71be5"),
+ "plugins/Widgetize/templates/iframe.twig" => array("740", "b0e678fca11277a139e66eeaba321684"),
+ "plugins/Widgetize/templates/index.twig" => array("3680", "2e7dd9cc68bf352c6a219c989f020b16"),
+ "plugins/Widgetize/templates/testJsInclude1.twig" => array("662", "be3cc3e650ab96eade1172057d2577dc"),
+ "plugins/Widgetize/templates/testJsInclude2.twig" => array("709", "429886f9491d07389f73d803a0737a9b"),
+ "plugins/Widgetize/Widgetize.php" => array("2089", "9f1aa8d49cd9bdc7dd6e42991c8b982a"),
+ "plugins/Zeitgeist/images/affix-arrow.png" => array("3179", "542723bef4d8a61f3dc217352dc1873a"),
+ "plugins/Zeitgeist/images/annotations.png" => array("1627", "d8dab24c4203b462aa522642fcdd6947"),
+ "plugins/Zeitgeist/images/annotations_starred.png" => array("1618", "9fcbf35415857fb2a89435106641b0be"),
+ "plugins/Zeitgeist/images/arr_r.png" => array("195", "2708a22e4d851aff01d4db4f2fddf1da"),
+ "plugins/Zeitgeist/images/background-submit.png" => array("1347", "b1822012b50008b3e478d7ae890ead92"),
+ "plugins/Zeitgeist/images/chart_bar.png" => array("170", "05be19310c2236ac9c62e661bae95989"),
+ "plugins/Zeitgeist/images/chart_line_edit.png" => array("993", "7d7239985b27ced21c55d6de9810284c"),
+ "plugins/Zeitgeist/images/chart_pie.png" => array("355", "de043ca76bd7419b20150ceafbcf9312"),
+ "plugins/Zeitgeist/images/close.png" => array("1089", "47fc9f47a998804a1cb4e5728377a1fe"),
+ "plugins/Zeitgeist/images/collapsed_arrows.gif" => array("54", "224b095cbca536e579119a47328badda"),
+ "plugins/Zeitgeist/images/configure-highlight.png" => array("933", "a975c7cf6f594786581d997c1b2cc485"),
+ "plugins/Zeitgeist/images/configure.png" => array("387", "1e0d61849e3012c025554edd97f9cdd8"),
+ "plugins/Zeitgeist/images/dashboard_h_bg_hover.png" => array("333", "d062d8b2b65f012a196878c2f05db335"),
+ "plugins/Zeitgeist/images/dashboard_h_bg.png" => array("162", "a6cf0f7cdf6d69edca6edf479aa888e3"),
+ "plugins/Zeitgeist/images/data_table_footer_active_item.png" => array("145", "70dcc41079a073d03fee2628dcd0cf74"),
+ "plugins/Zeitgeist/images/datepicker_arr_l.png" => array("191", "10c37d1f62b0c21bc99ef3c3fabe4f7e"),
+ "plugins/Zeitgeist/images/datepicker_arr_r.png" => array("196", "e2f8672222d50df21b718654c424dd5f"),
+ "plugins/Zeitgeist/images/delete.png" => array("2175", "b3b9cb547a0511ff15b5371b122a6f66"),
+ "plugins/Zeitgeist/images/download.png" => array("734", "0552d1746701df879d14c2fdf3d5ac41"),
+ "plugins/Zeitgeist/images/ecommerceAbandonedCart.gif" => array("369", "51974d5002afd9b7c8009d14a1207aad"),
+ "plugins/Zeitgeist/images/ecommerceOrder.gif" => array("570", "b0c1aa6141f0047b4bcd0cc665c945ad"),
+ "plugins/Zeitgeist/images/email.png" => array("754", "baaa6accd945fcb4480b29ab2e15bded"),
+ "plugins/Zeitgeist/images/error_medium.png" => array("2622", "d789ca042860b20782cfb8c2bfd458e0"),
+ "plugins/Zeitgeist/images/error.png" => array("1150", "16ac5f1c769e78a074144f1fe9073dfb"),
+ "plugins/Zeitgeist/images/event.png" => array("164", "8e1d701795486cdc53cbf7a5c3b4d069"),
+ "plugins/Zeitgeist/images/expanded_arrows.gif" => array("60", "a9afa92168dbbe6f693f3ff71fa27b32"),
+ "plugins/Zeitgeist/images/export.png" => array("219", "bfcbff8f64765ec78685d04283857b9b"),
+ "plugins/Zeitgeist/images/feed.png" => array("691", "55bc1130d360583e2aecbcebfbf6eda7"),
+ "plugins/Zeitgeist/images/fullscreen.png" => array("346", "629df6e9cfdf1bb8d0a612f651da69e9"),
+ "plugins/Zeitgeist/images/goal.png" => array("270", "bc6edcd9d776933ea6b6cea5be1c6383"),
+ "plugins/Zeitgeist/images/help.png" => array("942", "471c83040c41ec0922eb4540992bcd1f"),
+ "plugins/Zeitgeist/images/html_icon.png" => array("3503", "45a005cf3fa96037df5d7935d26b5f7c"),
+ "plugins/Zeitgeist/images/ico_alert.png" => array("1112", "63d124bf79386ebf6285926956ff7829"),
+ "plugins/Zeitgeist/images/ico_delete.png" => array("231", "4065203e7b7471539eeff819b4cf443b"),
+ "plugins/Zeitgeist/images/ico_edit.png" => array("255", "32e1c11ad294948cfedff7c4daf76dad"),
+ "plugins/Zeitgeist/images/ico_info.png" => array("978", "362b1589e75a151c9e4d509615851950"),
+ "plugins/Zeitgeist/images/icon-calendar.gif" => array("331", "a0cffdd9fcf6552dea43658fc217732b"),
+ "plugins/Zeitgeist/images/image.png" => array("306", "2ea0351a26cdc37e0359ab011b832902"),
+ "plugins/Zeitgeist/images/inp_bg.png" => array("137", "1f4b8e7288c5d4dc52e44c50e0d02a9b"),
+ "plugins/Zeitgeist/images/li_dbl_gray.gif" => array("48", "5b0a692984ac5b04acc0886cd374bb85"),
+ "plugins/Zeitgeist/images/link.gif" => array("75", "b8de0b2b517e1999b32353209be4e976"),
+ "plugins/Zeitgeist/images/loading-blue.gif" => array("723", "6ce8f9a2c650cf90261acfc98b2edf90"),
+ "plugins/Zeitgeist/images/login-sprite.png" => array("10200", "a2a3520f448277c3efdb871d637fc34b"),
+ "plugins/Zeitgeist/images/logo-header.png" => array("3682", "f3a7921898ffcd44e413015b596c4e68"),
+ "plugins/Zeitgeist/images/logo-marketplace.png" => array("2927", "aa3dc0cd04c23a654e7a96ceabe2a6c1"),
+ "plugins/Zeitgeist/images/logo.png" => array("11152", "bc2b73b0541589e617fbb66b54ccc7ad"),
+ "plugins/Zeitgeist/images/logo.svg" => array("4044", "7f9586818f589f658c4dd7e7e5802274"),
+ "plugins/Zeitgeist/images/maximise.png" => array("3182", "e0018bf302603d234ae3d31c44fb3d1e"),
+ "plugins/Zeitgeist/images/minimise.png" => array("2869", "e746d4f1bfcc4d565a526e40918bcd0e"),
+ "plugins/Zeitgeist/images/minus.png" => array("176", "e010a638230ff96610f0ebffdfced0ac"),
+ "plugins/Zeitgeist/images/newtab.png" => array("509", "994c19f51192a18f7b3e0bc0775313f2"),
+ "plugins/Zeitgeist/images/ok.png" => array("626", "28501b0877ea15b49c6ca58677e186c3"),
+ "plugins/Zeitgeist/images/paypal_subscribe.gif" => array("3080", "a74a883239713fb5050593c20d9fd2a5"),
+ "plugins/Zeitgeist/images/plus_blue.png" => array("157", "9d61acb98c3ac639715aba6703997ad9"),
+ "plugins/Zeitgeist/images/plus.png" => array("174", "f867099b8f18cd9f989f1fcfb77cba37"),
+ "plugins/Zeitgeist/images/refresh.png" => array("2978", "b821bde25f3e4dcd0a7f6b9bd37f57b3"),
+ "plugins/Zeitgeist/images/reload.png" => array("892", "5a0360408c248f9cde4e0d4bad31ac00"),
+ "plugins/Zeitgeist/images/row_evolution_hover.png" => array("601", "7f6833f656aaad475e02a96ae3c0adb9"),
+ "plugins/Zeitgeist/images/row_evolution.png" => array("1934", "0e3fe13d82bc0526ed94bc079152f2f2"),
+ "plugins/Zeitgeist/images/search_bg.png" => array("384", "3105cf7e5419bef110507a682f28fab7"),
+ "plugins/Zeitgeist/images/search_ico.png" => array("175", "b74de7cb2b8cfddb82b95e63cd492286"),
+ "plugins/Zeitgeist/images/sites_selection.png" => array("120", "f8f6f62a17616adce09791ffb51aab1b"),
+ "plugins/Zeitgeist/images/smileyprog_0.png" => array("4045", "0b105851f9dfc4e5a3efb933c4fa01af"),
+ "plugins/Zeitgeist/images/smileyprog_1.png" => array("4268", "cd518d27567dda069dd5fff2e3c2291f"),
+ "plugins/Zeitgeist/images/smileyprog_2.png" => array("4292", "8e61661161afe5ac8a7e2caf085fb200"),
+ "plugins/Zeitgeist/images/smileyprog_3.png" => array("4589", "1eb75d0f84042d492b7ff4a515e23ff3"),
+ "plugins/Zeitgeist/images/smileyprog_4.png" => array("4733", "1f31e1ac3c6b3602cecabf0f0c15fd1b"),
+ "plugins/Zeitgeist/images/sortasc.png" => array("173", "bf15b38f5921052cd7d402a5a6e7542d"),
+ "plugins/Zeitgeist/images/sortdesc.png" => array("171", "3c918bc6390e75b19094de3b3db01578"),
+ "plugins/Zeitgeist/images/sort_subtable_asc_light.png" => array("2866", "a8d4e15a81c63b3d7f8ddde1ad5fc78d"),
+ "plugins/Zeitgeist/images/sort_subtable_asc.png" => array("173", "457908cb087009a946c99bef46643c69"),
+ "plugins/Zeitgeist/images/sort_subtable_desc_light.png" => array("286", "ba6261eca430661a8b84155460424106"),
+ "plugins/Zeitgeist/images/sort_subtable_desc.png" => array("171", "63b74c05d158e1bf7ed0172142991b24"),
+ "plugins/Zeitgeist/images/star_empty.png" => array("658", "31809a80055eb2aa02b51e6c11ecb02d"),
+ "plugins/Zeitgeist/images/star.png" => array("757", "872b7a1a8101bcf7ef6c7cf7c8f78ff7"),
+ "plugins/Zeitgeist/images/success_medium.png" => array("1346", "28e0ba1f8492374db4946d42c69e477b"),
+ "plugins/Zeitgeist/images/table_more.png" => array("200", "a8d8a758c97a1342864b646469059a02"),
+ "plugins/Zeitgeist/images/table.png" => array("151", "327ee0e75605ab865796053f2c0aebf1"),
+ "plugins/Zeitgeist/images/tagcloud.png" => array("202", "127389e4f7146b1322dd1873ee89235b"),
+ "plugins/Zeitgeist/images/video_play.png" => array("517", "29fd1c103c9ac9987b85e053e621ab20"),
+ "plugins/Zeitgeist/images/warning_medium.png" => array("1283", "24bc193a073997740e4aa459b2bbbbdf"),
+ "plugins/Zeitgeist/images/warning.png" => array("571", "8c4ef759f46a90e7a00e1db65e49edc9"),
+ "plugins/Zeitgeist/images/warning_small.png" => array("1083", "5ff491ccd2f32beb35d96fd79a4d7329"),
+ "plugins/Zeitgeist/images/zoom-out.png" => array("289", "b91cfbc280cfbf59fddc41348a78f2b6"),
+ "plugins/Zeitgeist/javascripts/ajaxHelper.js" => array("11827", "15e4b7bb70b671885e1597b7a61221bf"),
+ "plugins/Zeitgeist/javascripts/piwikHelper.js" => array("14986", "56e3acb719973a17f35316b74c155a3c"),
+ "plugins/Zeitgeist/plugin.json" => array("156", "90381b40a6f3b6e60f3d214bccdac5ab"),
+ "plugins/Zeitgeist/stylesheets/base.less" => array("555", "8bc4b5c0409a957679d70f19ab14a660"),
+ "plugins/Zeitgeist/stylesheets/general/_default.less" => array("2087", "47a06608a3652d1fd86254e314f81a5c"),
+ "plugins/Zeitgeist/stylesheets/general/_form.less" => array("2389", "6d43004e4fad124d15e45246e77bac1b"),
+ "plugins/Zeitgeist/stylesheets/general/_jqueryUI.less" => array("5147", "130ef76ab7dc45bbd50fcb4318aa5ada"),
+ "plugins/Zeitgeist/stylesheets/general/_misc.less" => array("576", "214e501d23f7e3f95f71644fbb50c886"),
+ "plugins/Zeitgeist/stylesheets/general/_utils.less" => array("391", "75d6698e72c26ffd6b562c3d738021e0"),
+ "plugins/Zeitgeist/stylesheets/ieonly.css" => array("506", "6a9862fb951ee731f968fee99d2480b3"),
+ "plugins/Zeitgeist/stylesheets/rtl.css" => array("53", "1be4a6f544e3ed971b966692b07d2c2d"),
+ "plugins/Zeitgeist/stylesheets/simple_structure.css" => array("1592", "e46a7d0ea302017e776fccc7b3db6060"),
+ "plugins/Zeitgeist/stylesheets/ui/_dataTable.less" => array("46", "29c2aa49f73efe255d4202c37a942ef5"),
+ "plugins/Zeitgeist/stylesheets/ui/_header.less" => array("739", "969c90051d99a050d9588f0bd02c4f82"),
+ "plugins/Zeitgeist/stylesheets/ui/_headerMessage.less" => array("1179", "16bf5e43fce5c2c39ff393c6c743a1aa"),
+ "plugins/Zeitgeist/stylesheets/ui/_languageSelect.less" => array("745", "6dd7f3399125f54809bda579420c1d6c"),
+ "plugins/Zeitgeist/stylesheets/ui/_loading.less" => array("360", "a48e9d0b63da915547a353ca0ace1656"),
+ "plugins/Zeitgeist/stylesheets/ui/_periodSelect.less" => array("1453", "07709490ca6206728d5274115a4cf71e"),
+ "plugins/Zeitgeist/stylesheets/ui/_siteSelect.less" => array("3503", "d0f0fec0d21d3013cc5e2540aff9f95c"),
+ "plugins/Zeitgeist/templates/admin.twig" => array("2040", "3801afc5349d0540bf749b5133d25ce0"),
+ "plugins/Zeitgeist/templates/ajaxMacros.twig" => array("632", "b6179aa5cdabd0f26b15ba6387f5af22"),
+ "plugins/Zeitgeist/templates/dashboard.twig" => array("2097", "0bb68e2e0d617932e57f3390d35aaccc"),
+ "plugins/Zeitgeist/templates/empty.twig" => array("35", "2c6a1dccfb394fef9ef03849c39a5bec"),
+ "plugins/Zeitgeist/templates/genericForm.twig" => array("991", "9e6a247b129a8b3932e2928273cc2b2f"),
+ "plugins/Zeitgeist/templates/_iframeBuster.twig" => array("385", "1c780792c51f5d2fecbf53c0ce0d547f"),
+ "plugins/Zeitgeist/templates/javascriptCode.tpl" => array("631", "8b239b1782acbb256597e965c49a7f92"),
+ "plugins/Zeitgeist/templates/_jsCssIncludes.twig" => array("278", "64e8a45c1d762222f4d3ac0fc072afbe"),
+ "plugins/Zeitgeist/templates/_jsGlobalVariables.twig" => array("1668", "4a163e13cc7cbf4bbf2723260f38765e"),
+ "plugins/Zeitgeist/templates/macros.twig" => array("891", "3343e6b0da8c13cd7f5d73bffcc86b49"),
+ "plugins/Zeitgeist/templates/_piwikTag.twig" => array("1217", "4d267c8a76a1c2212f160267095998ae"),
+ "plugins/Zeitgeist/templates/simpleLayoutFooter.tpl" => array("23", "c64111f363b9e1a0b0ea6d1c8ccbb9bd"),
+ "plugins/Zeitgeist/templates/simpleLayoutHeader.tpl" => array("511", "422d59273c966199cc15449891d697bb"),
+ "plugins/Zeitgeist/templates/_sparklineFooter.twig" => array("102", "a9e46848aaf5613b971827cf12ab6eaa"),
+ "README.md" => array("3813", "5ba34ead653bdb77ce52a3a4eeb221ba"),
+ "tests/README.md" => array("5681", "d9e5829add2c89dc0066f0299d82d7ed"),
+ "vendor/composer/autoload_classmap.php" => array("510", "91d089f53c9e426510a6f0292c655676"),
+ "vendor/composer/autoload_files.php" => array("255", "2cc6f4ba7d74bf4e09bff1ad7ed04816"),
+ "vendor/composer/autoload_namespaces.php" => array("341", "41fba06e8ccbd2eb2cf63e44a65345cd"),
+ "vendor/composer/autoload_psr4.php" => array("143", "dd3a00f0d13eb29781edd8c77d4c5100"),
+ "vendor/composer/ClassLoader.php" => array("11571", "3adcacc118804f98f1fd888e2575f00a"),
+ "vendor/composer/installed.json" => array("9348", "a1d1990a1a293648031fd49bc098b419"),
+ "vendor/leafo/lessphp/composer.json" => array("544", "e0d0ef78bbb2d2ea3db1be3ea7a34253"),
+ "vendor/leafo/lessphp/docs/docs.md" => array("38626", "b2eb5cf232f2d136fd4d14ba4f9c45f5"),
+ "vendor/leafo/lessphp/.gitignore" => array("71", "a51238606af0d1eb55ce05237e6778eb"),
+ "vendor/leafo/lessphp/lessc.inc.php" => array("91826", "b8ddd7795cbca49cad99e4a977d0b13d"),
+ "vendor/leafo/lessphp/lessify" => array("414", "e8e87d48dd91f4838219419395f1b996"),
+ "vendor/leafo/lessphp/lessify.inc.php" => array("9696", "e0c246fc5d113d6c42417b96f2e27b54"),
+ "vendor/leafo/lessphp/LICENSE" => array("33650", "2887747a1404ae4fb71a95d440d3778f"),
+ "vendor/leafo/lessphp/Makefile" => array("57", "bf733f8f889f8351526b07f50abf1dc1"),
+ "vendor/leafo/lessphp/package.sh" => array("758", "6bcb58ac88b574f30424245f65b2655d"),
+ "vendor/leafo/lessphp/plessc" => array("4911", "fb3df9045ff35eed6f8202c27a86ef14"),
+ "vendor/leafo/lessphp/README.md" => array("2817", "1fae9319ae33c7596e67d6adba644e13"),
+ "vendor/leafo/lessphp/.travis.yml" => array("57", "a37e9778c469a928db223337a03a9f04"),
+ "vendor/mustangostang/spyc/composer.json" => array("588", "c3ed21823389befa4e8c8b81d0f3be63"),
+ "vendor/mustangostang/spyc/COPYING" => array("1077", "b0987325db5fa8e52b7076860d8e2e0a"),
+ "vendor/mustangostang/spyc/examples/yaml-dump.php" => array("974", "c18b833f0057beb6a5845bf4335c68e0"),
+ "vendor/mustangostang/spyc/examples/yaml-load.php" => array("390", "10189cd25b80e661fd5ed7019942a7df"),
+ "vendor/mustangostang/spyc/php4/5to4.php" => array("728", "1bee498adf3521f220ef2f22aff84389"),
+ "vendor/mustangostang/spyc/php4/spyc.php4" => array("30025", "013ea7fe6257e8cebe0f17dbf2115bb7"),
+ "vendor/mustangostang/spyc/php4/test.php4" => array("4487", "e7f605110f596ff0344431a08dc77b10"),
+ "vendor/mustangostang/spyc/README" => array("5772", "d426a4028be21f70da24b9593df400a1"),
+ "vendor/mustangostang/spyc/Spyc.php" => array("31690", "1ff392881801c81f27aa8ea2e2bd8f2a"),
+ "vendor/mustangostang/spyc/spyc.yaml" => array("3609", "796845442758ffc5887b517580d960f1"),
+ "vendor/piwik/device-detector/composer.json" => array("958", "8c2246083e5b6d8752263c1074522890"),
+ "vendor/piwik/device-detector/DeviceDetector.php" => array("31360", "54677c9e65f2db309b04c0ef43d856b8"),
+ "vendor/piwik/device-detector/.gitignore" => array("30", "5a959e73a428396446cf2fc274e91c26"),
+ "vendor/piwik/device-detector/README.md" => array("391", "0a04395636c6a38d3f2b5fbaddaf054c"),
+ "vendor/piwik/device-detector/regexes/browsers.yml" => array("10214", "a745684b695696c1318751816b6ca365"),
+ "vendor/piwik/device-detector/regexes/mobiles.yml" => array("27043", "a9b5c80b5d331af8f1e8734265993ac0"),
+ "vendor/piwik/device-detector/regexes/oss.yml" => array("8511", "1b49d6cab78daeb3d7fe5fdca86a2ce8"),
+ "vendor/piwik/device-detector/regexes/televisions.yml" => array("3886", "6ed102a54f0be2e7413a5799b0f32a02"),
+ "vendor/piwik/device-detector/.travis.yml" => array("155", "f7214c64927a7b580f239487842ddc32"),
+ "vendor/symfony/console/Symfony/Component/Console/Application.php" => array("35538", "d7fa5d3e678f32b207a275d566bd226b"),
+ "vendor/symfony/console/Symfony/Component/Console/CHANGELOG.md" => array("1682", "e2b61d6b18ded64f0686599db5c7fb5b"),
+ "vendor/symfony/console/Symfony/Component/Console/Command/Command.php" => array("16439", "80d4d8e858f5a83230701aed15e6f9ff"),
+ "vendor/symfony/console/Symfony/Component/Console/Command/HelpCommand.php" => array("2655", "d10467979ea85e92951425fd351347ac"),
+ "vendor/symfony/console/Symfony/Component/Console/Command/ListCommand.php" => array("2752", "7f0bd50020e8b9c39d79692f3e32db8f"),
+ "vendor/symfony/console/Symfony/Component/Console/composer.json" => array("869", "9e146f5ae7085917809444efe2d868c6"),
+ "vendor/symfony/console/Symfony/Component/Console/ConsoleEvents.php" => array("1528", "10001de60b8e31092612e73dc02300d7"),
+ "vendor/symfony/console/Symfony/Component/Console/Descriptor/ApplicationDescription.php" => array("3513", "e5ffa840297bdf9232762c38218846dc"),
+ "vendor/symfony/console/Symfony/Component/Console/Descriptor/DescriptorInterface.php" => array("737", "d5b9abe215ed28ac8ca7fe9d72fc9ddd"),
+ "vendor/symfony/console/Symfony/Component/Console/Descriptor/Descriptor.php" => array("3516", "e08a534aa99b2e90d1538a0c166973e5"),
+ "vendor/symfony/console/Symfony/Component/Console/Descriptor/JsonDescriptor.php" => array("4981", "a997223049fcf34294263e3a6c37e230"),
+ "vendor/symfony/console/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php" => array("4910", "bec9d13cc9cb6e778c4f4083199137b6"),
+ "vendor/symfony/console/Symfony/Component/Console/Descriptor/TextDescriptor.php" => array("8184", "97b571928164645af31610ea97bc43fb"),
+ "vendor/symfony/console/Symfony/Component/Console/Descriptor/XmlDescriptor.php" => array("9712", "a9214dd6cf39faa121dc4de62996071a"),
+ "vendor/symfony/console/Symfony/Component/Console/Event/ConsoleCommandEvent.php" => array("448", "2832db2aee4207de5d53d012b314436d"),
+ "vendor/symfony/console/Symfony/Component/Console/Event/ConsoleEvent.php" => array("1464", "aaaa8c390cd99e01d7bb2649a735e469"),
+ "vendor/symfony/console/Symfony/Component/Console/Event/ConsoleExceptionEvent.php" => array("1601", "22e5c027468f33b0bd20540b41af7a74"),
+ "vendor/symfony/console/Symfony/Component/Console/Event/ConsoleTerminateEvent.php" => array("1318", "82fbfa586f39a859d66a45221cb773c1"),
+ "vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterInterface.php" => array("1763", "75e7364468e427f080f86354f9bab9b9"),
+ "vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatter.php" => array("6341", "304b90bba47ad3ed5534a37ec1647980"),
+ "vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyleInterface.php" => array("1439", "091bf3ec019e14a48b1f61ef7cb6871a"),
+ "vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyle.php" => array("5926", "4d0b8e5a84926a6013be77aa123e61eb"),
+ "vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php" => array("2788", "27952fd939968e3615ce7ada4c761e0e"),
+ "vendor/symfony/console/Symfony/Component/Console/.gitignore" => array("34", "a1155c508134e9bda943ae266aee1819"),
+ "vendor/symfony/console/Symfony/Component/Console/Helper/DescriptorHelper.php" => array("2561", "d5bb73eb73014de416365c07cd81cac5"),
+ "vendor/symfony/console/Symfony/Component/Console/Helper/DialogHelper.php" => array("16446", "ae0bb3641414453024a3d71275abd1c1"),
+ "vendor/symfony/console/Symfony/Component/Console/Helper/FormatterHelper.php" => array("2236", "86ebeb32b3353bc7ba4363bf7a05fa2f"),
+ "vendor/symfony/console/Symfony/Component/Console/Helper/HelperInterface.php" => array("1011", "e9012e6f3559129d513da4ba75ebf4e9"),
+ "vendor/symfony/console/Symfony/Component/Console/Helper/Helper.php" => array("1451", "426298ce1ccd85ab527a5cef78f9a33b"),
+ "vendor/symfony/console/Symfony/Component/Console/Helper/HelperSet.php" => array("2526", "9c0badab04f9b052cec6a06c3f11fb07"),
+ "vendor/symfony/console/Symfony/Component/Console/Helper/InputAwareHelper.php" => array("747", "c75592e1f3b10c1f35961c50d55ae58d"),
+ "vendor/symfony/console/Symfony/Component/Console/Helper/ProgressHelper.php" => array("11887", "c6c85dbb2f08fa52bc6739ba6896bdc9"),
+ "vendor/symfony/console/Symfony/Component/Console/Helper/TableHelper.php" => array("12820", "1671e0f028279a642e6c0f7dcaa474c4"),
+ "vendor/symfony/console/Symfony/Component/Console/Input/ArgvInput.php" => array("10680", "e95b97c2b870d6223e84a995c93f16ce"),
+ "vendor/symfony/console/Symfony/Component/Console/Input/ArrayInput.php" => array("5925", "1e51135fb1c605381452c4d0cae3f1c4"),
+ "vendor/symfony/console/Symfony/Component/Console/Input/InputArgument.php" => array("3287", "c2f88afd2a0f1601dcfd3cce07092197"),
+ "vendor/symfony/console/Symfony/Component/Console/Input/InputAwareInterface.php" => array("606", "a67cb5e626983b20e2c03b618d7ab012"),
+ "vendor/symfony/console/Symfony/Component/Console/Input/InputDefinition.php" => array("12328", "74e92dce0c01082eb3d96959f3268509"),
+ "vendor/symfony/console/Symfony/Component/Console/Input/InputInterface.php" => array("4150", "9a40ecfaca79fa6bed0e459c035fdb5c"),
+ "vendor/symfony/console/Symfony/Component/Console/Input/InputOption.php" => array("5974", "2712657bb89ede768dfb9d0eb1f1d256"),
+ "vendor/symfony/console/Symfony/Component/Console/Input/Input.php" => array("6144", "87ba519b169574bdcaabaef68740989a"),
+ "vendor/symfony/console/Symfony/Component/Console/Input/StringInput.php" => array("2811", "eaf496e1188492448d9cfe8dfbcede6a"),
+ "vendor/symfony/console/Symfony/Component/Console/LICENSE" => array("1065", "09ce405e925cdeb923da1789121864c7"),
+ "vendor/symfony/console/Symfony/Component/Console/Output/BufferedOutput.php" => array("872", "07bca2bc77753b0b0cc4502f109c4a8e"),
+ "vendor/symfony/console/Symfony/Component/Console/Output/ConsoleOutputInterface.php" => array("843", "9cf400160437ff90febb665841a968fe"),
+ "vendor/symfony/console/Symfony/Component/Console/Output/ConsoleOutput.php" => array("2941", "64e00ce49a3a47e8ea7b8885f10c1ee6"),
+ "vendor/symfony/console/Symfony/Component/Console/Output/NullOutput.php" => array("1748", "561f460b93be0c27aa9be546af49e9ec"),
+ "vendor/symfony/console/Symfony/Component/Console/Output/OutputInterface.php" => array("2893", "ac6d776cf82c8b887395c54482f17bd7"),
+ "vendor/symfony/console/Symfony/Component/Console/Output/Output.php" => array("4155", "15c209a7581a632e6979d5441a02f241"),
+ "vendor/symfony/console/Symfony/Component/Console/Output/StreamOutput.php" => array("3149", "dc57630f625779ad86f6f3d8240ad0c3"),
+ "vendor/symfony/console/Symfony/Component/Console/phpunit.xml.dist" => array("873", "c94fc23909e8f1d27908cd0486653611"),
+ "vendor/symfony/console/Symfony/Component/Console/README.md" => array("1959", "70a92aecafcdc1f689a2a798acccefe4"),
+ "vendor/symfony/console/Symfony/Component/Console/Shell.php" => array("6395", "fedc847f6c992a242e45a167b0cf1866"),
+ "vendor/symfony/console/Symfony/Component/Console/Tester/ApplicationTester.php" => array("3430", "f2c7b88716f13cb54825a933832a74a8"),
+ "vendor/symfony/console/Symfony/Component/Console/Tester/CommandTester.php" => array("3612", "9b7bf197b61b1b3c23772c57a2ec3ee5"),
+ "vendor/tedivm/jshrink/composer.json" => array("418", "b7603f6ba06352f101cc74dc6b72c5b1"),
+ "vendor/tedivm/jshrink/.gitignore" => array("70", "416a3b6d772480ee1f7833d8a790a6a2"),
+ "vendor/tedivm/jshrink/LICENSE" => array("1493", "0a8fd596034db85a8ae22c1d2330edfb"),
+ "vendor/tedivm/jshrink/README.md" => array("745", "57a8f87308e7b475d9b34689d8c1f381"),
+ "vendor/tedivm/jshrink/src/JShrink/Minifier.php" => array("11416", "d245eafc47812bd47bf18fb4ede5508d"),
+ "vendor/tedivm/jshrink/.travis.yml" => array("59", "cbac6ff01d60d6cf2045eb78150dabe3"),
+ "vendor/twig/twig/CHANGELOG" => array("32353", "03b21b00ace91062257dd2855ee196cf"),
+ "vendor/twig/twig/composer.json" => array("1102", "b2d4cf29abf14c07d3eace63816c58c3"),
+ "vendor/twig/twig/.editorconfig" => array("224", "c1be7a30bae43bc6616f3f266e199030"),
+ "vendor/twig/twig/ext/twig/config.m4" => array("221", "20ad1d1005402766ddd16b7110aaf4ca"),
+ "vendor/twig/twig/ext/twig/config.w32" => array("149", "2b1bdfd1a2b8c54966b04f7d143c6632"),
+ "vendor/twig/twig/ext/twig/.gitignore" => array("328", "7567b95d7259dea2d26ea326730c357d"),
+ "vendor/twig/twig/ext/twig/LICENSE" => array("1527", "c74e1ac7caf1b0a49bd267b04f9205de"),
+ "vendor/twig/twig/ext/twig/php_twig.h" => array("1113", "dfc045ec57553713c0db3688a1e69500"),
+ "vendor/twig/twig/ext/twig/twig.c" => array("30216", "0c2a8fd5aabd9417c98c271b625ab36d"),
+ "vendor/twig/twig/.gitignore" => array("27", "e8e8b05dcada28af47c4a7f92a2106c0"),
+ "vendor/twig/twig/lib/Twig/Autoloader.php" => array("1161", "636fed90535196cf757198769e7f26bd"),
+ "vendor/twig/twig/lib/Twig/CompilerInterface.php" => array("779", "5c6bd37854846a6b1df5e948f2129d53"),
+ "vendor/twig/twig/lib/Twig/Compiler.php" => array("6906", "61f080c47c7a7f8cd7b9b19a0b418aaf"),
+ "vendor/twig/twig/lib/Twig/Environment.php" => array("36964", "1a9da7fe09974a4423d8a844fc9de7bb"),
+ "vendor/twig/twig/lib/Twig/Error/Loader.php" => array("946", "0297a085a7341500baa1706db2917a59"),
+ "vendor/twig/twig/lib/Twig/Error.php" => array("7496", "08acca21a4a57fc7bdecf80411e6d00e"),
+ "vendor/twig/twig/lib/Twig/Error/Runtime.php" => array("395", "3a4c02d5a2b37fb343b6e9fb7056e3e7"),
+ "vendor/twig/twig/lib/Twig/Error/Syntax.php" => array("428", "41d4a9282175957c316b0ffb313d03e0"),
+ "vendor/twig/twig/lib/Twig/ExistsLoaderInterface.php" => array("692", "2dcb498891d0bea0a55754869618e032"),
+ "vendor/twig/twig/lib/Twig/ExpressionParser.php" => array("23710", "3fa6eab1d6d8c63b5014f7cadaeb3b58"),
+ "vendor/twig/twig/lib/Twig/Extension/Core.php" => array("49755", "479a2f77269f5b255b7896096aa8f2d8"),
+ "vendor/twig/twig/lib/Twig/Extension/Debug.php" => array("2009", "01bfdc7bef74ae207b06874d839acb43"),
+ "vendor/twig/twig/lib/Twig/Extension/Escaper.php" => array("2788", "1f2be1edc3e5c9fba1aa58976638d8ff"),
+ "vendor/twig/twig/lib/Twig/ExtensionInterface.php" => array("2091", "8bed99e211ba66f478342efc9faa4013"),
+ "vendor/twig/twig/lib/Twig/Extension/Optimizer.php" => array("664", "90d559bc3f34fd2952905ddd72f906b3"),
+ "vendor/twig/twig/lib/Twig/Extension.php" => array("2132", "4a3d6c03eb002ca4e15bb4bd87f33679"),
+ "vendor/twig/twig/lib/Twig/Extension/Sandbox.php" => array("2636", "1cdfeb7346b86fa17eddfd8717d57667"),
+ "vendor/twig/twig/lib/Twig/Extension/Staging.php" => array("2096", "6fc289bff03eacfd4a2136ba9a503633"),
+ "vendor/twig/twig/lib/Twig/Extension/StringLoader.php" => array("1474", "15f299f6e4c3ce7acf21f0fda6d63e37"),
+ "vendor/twig/twig/lib/Twig/FilterCallableInterface.php" => array("473", "557ea6fcaa5b7b2c503a497a3b931267"),
+ "vendor/twig/twig/lib/Twig/Filter/Function.php" => array("748", "260ca9d38bdc329f4a82405dc11bb1a4"),
+ "vendor/twig/twig/lib/Twig/FilterInterface.php" => array("846", "6e43136a50b94a80951dd34da661ccc8"),
+ "vendor/twig/twig/lib/Twig/Filter/Method.php" => array("930", "98d91cb41f5a5a9a4d67f7e967698141"),
+ "vendor/twig/twig/lib/Twig/Filter/Node.php" => array("731", "6e30432a47d6bff1ddea8e428c2b3276"),
+ "vendor/twig/twig/lib/Twig/Filter.php" => array("1869", "ffcbc10fbc3456d605d3f8510c60210d"),
+ "vendor/twig/twig/lib/Twig/FunctionCallableInterface.php" => array("479", "2f8d67c0e76b701a5e4c76f8bf685797"),
+ "vendor/twig/twig/lib/Twig/Function/Function.php" => array("784", "3aff76b4acc885f1f0f34f915c718daf"),
+ "vendor/twig/twig/lib/Twig/FunctionInterface.php" => array("804", "1a989db9f04110dff68441ab95849661"),
+ "vendor/twig/twig/lib/Twig/Function/Method.php" => array("966", "9f5db96bdcddf2cdecf4ac03cffe5072"),
+ "vendor/twig/twig/lib/Twig/Function/Node.php" => array("739", "e429ee124350f3a9b95f9d6d461d1ca3"),
+ "vendor/twig/twig/lib/Twig/Function.php" => array("1628", "cdf7c64b75e20ebe85739da5b308addc"),
+ "vendor/twig/twig/lib/Twig/LexerInterface.php" => array("761", "cd9cf96833fea0cc3e1657a1abb62e83"),
+ "vendor/twig/twig/lib/Twig/Lexer.php" => array("16213", "f3ebd10d22c844d1ba4f73326b5b77ba"),
+ "vendor/twig/twig/lib/Twig/Loader/Array.php" => array("2364", "47e8b8acf9d123975c3336f739911375"),
+ "vendor/twig/twig/lib/Twig/Loader/Chain.php" => array("3636", "82fc84ebb25255ca44b0451ea00a8e03"),
+ "vendor/twig/twig/lib/Twig/Loader/Filesystem.php" => array("6040", "38c92a7d80cae890eed1bb48fede872a"),
+ "vendor/twig/twig/lib/Twig/LoaderInterface.php" => array("1334", "72f395318a031756c876a56cc37a5260"),
+ "vendor/twig/twig/lib/Twig/Loader/String.php" => array("1334", "d60a08d13f3b0183be2602df9fdfcf6a"),
+ "vendor/twig/twig/lib/Twig/Markup.php" => array("764", "7ebff11af28eeedb1fdfe31d92132d20"),
+ "vendor/twig/twig/lib/Twig/Node/AutoEscape.php" => array("950", "25ae0b7054bb57ea1228774e9e64910a"),
+ "vendor/twig/twig/lib/Twig/Node/Block.php" => array("1080", "756f2c4dc092046f77c40c0a18e15308"),
+ "vendor/twig/twig/lib/Twig/Node/BlockReference.php" => array("915", "602786b6a5c05060cc5d55b384cb3f43"),
+ "vendor/twig/twig/lib/Twig/Node/Body.php" => array("337", "3768ca70d202fd3b29e02f0049ae5432"),
+ "vendor/twig/twig/lib/Twig/Node/Do.php" => array("839", "033e1b69c2ba4b06b81a00f9a6b34964"),
+ "vendor/twig/twig/lib/Twig/Node/Embed.php" => array("1181", "b155f10a68234505e1b9da02e2521116"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Array.php" => array("2350", "00d9b12693a06114f73ec464d9ef6099"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/AssignName.php" => array("616", "42e94b8e2a7a849e517ba1f4762feb6a"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Add.php" => array("413", "5d16cd53c83be9b5d332f2e6a80d124f"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/And.php" => array("414", "308e349bc7dae1d6e8ffe563a0e059fc"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/BitwiseAnd.php" => array("420", "621c82ab6fafa611309b08ce5f060f0c"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/BitwiseOr.php" => array("419", "b68c97739cb7bc6ff64242e0a3411fab"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/BitwiseXor.php" => array("420", "ff650ff1dcad63ac4dbc9dd57df5eef2"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Concat.php" => array("416", "702c3a6603b0138b5a6b3d82e80e2a05"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Div.php" => array("413", "5b28cc3d88ddfae12b04f22a4f2860c2"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/EndsWith.php" => array("758", "0147cde001fa855a79e976f5110745fc"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Equal.php" => array("389", "3d26fb48b35b0f5ad315d296299a4c20"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/FloorDiv.php" => array("673", "0910da99e3e01329fec591b7950dfb4f"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/GreaterEqual.php" => array("396", "eecadb39bce3c07af85c8a03e735a326"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Greater.php" => array("390", "6209c99dc1bce32340da0bebb5895bf3"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/In.php" => array("772", "1c6b54c6128846b57bab85686a34bc73"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/LessEqual.php" => array("393", "e4fd68cfbded83d91cfafafc81e80311"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Less.php" => array("387", "d2df3932fc9d04720399302098258a18"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Matches.php" => array("662", "ab9d3177aa110a6de020e466a6db65b3"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Mod.php" => array("413", "4e61aee836424a723f1c0314f32b3ec3"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Mul.php" => array("413", "a8d6077b84b988aef093159e4521969f"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/NotEqual.php" => array("392", "e5cf8bb39c1bce092fb84cbe5dbcf1f6"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/NotIn.php" => array("780", "99281fa54772d040df9301b9018875e6"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Or.php" => array("413", "835aa4edcbe9290749ca3e6f56c1fcd7"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary.php" => array("1028", "5753c84484d2fd6b4ca0920fb30513da"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Power.php" => array("764", "c960e3c92ae6311d280f491658355c63"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Range.php" => array("766", "3362365c37d9cf6d3e56c1eb0dbd104f"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/StartsWith.php" => array("669", "8d5900c492c79a667e2739a36e354d56"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Binary/Sub.php" => array("413", "47650251c8b7e7c2b82df3a699d10cd6"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/BlockReference.php" => array("1390", "b5cdd67c38367d62735ea829a4be6c53"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Call.php" => array("6426", "145a35b399a10d2d893802028d4d236f"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Conditional.php" => array("902", "94611e1b6bbc76388011855f0348fc2e"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Constant.php" => array("557", "b3703810a66fd8d2fe792ed95e9949ea"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/ExtensionReference.php" => array("806", "11d53bdac885a7604336297228ac7091"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Filter/Default.php" => array("1583", "81eb255cf59793aa37f7ab73df5b8f47"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Filter.php" => array("1378", "859a893a66e17778142c059668f5ffd2"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Function.php" => array("1249", "f5dce5675c390ae1d35497c7f76cf22a"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/GetAttr.php" => array("2240", "bf8ac92138c4f5cdab11de9a7aadff46"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/MethodCall.php" => array("1203", "ec007ddb0878bb9bc7017a12c6e4c080"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Name.php" => array("2872", "09b1ba6d503efb4d48a166fcf5c74a6f"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Parent.php" => array("1230", "c6628a0c055557b1ac412590ce76e4af"),
+ "vendor/twig/twig/lib/Twig/Node/Expression.php" => array("415", "d2fcc744c0d3ece6f32c398ae50e2cb5"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/TempName.php" => array("594", "312be4fbbf9f33caec6b64733e8f7c35"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Test/Constant.php" => array("1130", "cea24b966c64017afa46413e189956b7"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Test/Defined.php" => array("1607", "28345503b665a4d6edbbd7b06428718a"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Test/Divisibleby.php" => array("747", "cf1043b7f1479d71150e6202be527bef"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Test/Even.php" => array("636", "b67fd353edb6c3804d8ab2d41c6f8cbb"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Test/Null.php" => array("618", "d0fe20539e14edd2085ec6b0e139b415"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Test/Odd.php" => array("633", "5bb79634c28ab1e54b7ca7ded7ee9f28"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Test.php" => array("1036", "a0b2bd4d99f906c4857f057c7bb4864e"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Test/Sameas.php" => array("690", "0126ce6f962cf906f8f2370133c3ddff"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Unary/Neg.php" => array("404", "08e8112e6fabf7eba63b3a19672706c9"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Unary/Not.php" => array("404", "0d3a4717baffc2ec33b373bb265a6413"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Unary.php" => array("754", "b712815036f4e0cbca846e3088cb3fc6"),
+ "vendor/twig/twig/lib/Twig/Node/Expression/Unary/Pos.php" => array("404", "e35ab856982e8b07351a893609751c26"),
+ "vendor/twig/twig/lib/Twig/Node/Flush.php" => array("731", "4fd771f76093c1b60f1ddc1535295c19"),
+ "vendor/twig/twig/lib/Twig/Node/ForLoop.php" => array("1623", "f2fabfada4aac6f3358d99e39170508d"),
+ "vendor/twig/twig/lib/Twig/Node/For.php" => array("4367", "687d9e44460eaa3b47e2848b79e5011a"),
+ "vendor/twig/twig/lib/Twig/Node/If.php" => array("1723", "2263178b12b0fa6609b90cd5ee7dba8d"),
+ "vendor/twig/twig/lib/Twig/Node/Import.php" => array("1289", "479ef20c51f0e633c7044adead4d0456"),
+ "vendor/twig/twig/lib/Twig/Node/Include.php" => array("2897", "37a4f90abba8755171ada5732c4bda64"),
+ "vendor/twig/twig/lib/Twig/NodeInterface.php" => array("649", "2105584cf35b89563c3f86b647ce1f43"),
+ "vendor/twig/twig/lib/Twig/Node/Macro.php" => array("2655", "8cbf723f3f7518f120f1c17fc033b62e"),
+ "vendor/twig/twig/lib/Twig/Node/Module.php" => array("12182", "1702dccd13b5a0bfc63a7096327eea0b"),
+ "vendor/twig/twig/lib/Twig/NodeOutputInterface.php" => array("351", "2125948f894bcba40e4dd4bc906d47e2"),
+ "vendor/twig/twig/lib/Twig/Node.php" => array("5801", "e24ef0947fe4eb47966813c035b21058"),
+ "vendor/twig/twig/lib/Twig/Node/Print.php" => array("934", "fe93dc5534fd3c96eccfc4b6f3d5f6b9"),
+ "vendor/twig/twig/lib/Twig/Node/SandboxedModule.php" => array("2042", "c0d63404cf94209400098023f29d35c4"),
+ "vendor/twig/twig/lib/Twig/Node/SandboxedPrint.php" => array("1597", "b465d85ba2b5016df5c4f78a14bb184c"),
+ "vendor/twig/twig/lib/Twig/Node/Sandbox.php" => array("1259", "3dae5d4756cd400f5f26b18d744212cd"),
+ "vendor/twig/twig/lib/Twig/Node/Set.php" => array("3224", "d0e25c6c5329877865cc4d18cd53c473"),
+ "vendor/twig/twig/lib/Twig/Node/SetTemp.php" => array("840", "5c93ae290cfe346e9a2d155494f28626"),
+ "vendor/twig/twig/lib/Twig/Node/Spaceless.php" => array("972", "6b38e3713a5ffda4e7197602f31122e7"),
+ "vendor/twig/twig/lib/Twig/Node/Text.php" => array("872", "a4dc26dca8d39676f578401e19a05e12"),
+ "vendor/twig/twig/lib/Twig/NodeTraverser.php" => array("2354", "2a1673a9e56b086c2773d07ca47b9d02"),
+ "vendor/twig/twig/lib/Twig/NodeVisitor/Escaper.php" => array("5366", "9fe8e3ad07965534c4724211c33daafc"),
+ "vendor/twig/twig/lib/Twig/NodeVisitorInterface.php" => array("1328", "5efd03cd67a21bf09a6e09be86b570a8"),
+ "vendor/twig/twig/lib/Twig/NodeVisitor/Optimizer.php" => array("7982", "b57d389aa29b600141e2e3e94327c9c6"),
+ "vendor/twig/twig/lib/Twig/NodeVisitor/SafeAnalysis.php" => array("4594", "cba1963c52ef6763d47fff8cd7ee6d05"),
+ "vendor/twig/twig/lib/Twig/NodeVisitor/Sandbox.php" => array("2600", "c823c1317891b559dae63ee717a1b2fe"),
+ "vendor/twig/twig/lib/Twig/ParserInterface.php" => array("733", "eca931dfd8457bbbb98720da37ae683b"),
+ "vendor/twig/twig/lib/Twig/Parser.php" => array("11763", "b0d037510f563ebbef2fd4e87eb470ec"),
+ "vendor/twig/twig/lib/Twig/Sandbox/SecurityError.php" => array("384", "2038673c16bf93db76dd4e8085bb1199"),
+ "vendor/twig/twig/lib/Twig/Sandbox/SecurityPolicyInterface.php" => array("560", "d556afa8416c83b79752040a10ece3d4"),
+ "vendor/twig/twig/lib/Twig/Sandbox/SecurityPolicy.php" => array("3708", "28281244b087a7de31fde6203829fd83"),
+ "vendor/twig/twig/lib/Twig/SimpleFilter.php" => array("2117", "f539a1cf697d6df8dbe0a1ab20fa60c4"),
+ "vendor/twig/twig/lib/Twig/SimpleFunction.php" => array("1872", "3288da22d048babb201c4c8b21383672"),
+ "vendor/twig/twig/lib/Twig/SimpleTest.php" => array("920", "67c237535f937d19f9acbabe4bee2b7c"),
+ "vendor/twig/twig/lib/Twig/TemplateInterface.php" => array("1244", "58ada114eb5a42306e3b510af39dfeae"),
+ "vendor/twig/twig/lib/Twig/Template.php" => array("15939", "259e9b0fa9da7dc774a9eaf5c8a5667f"),
+ "vendor/twig/twig/lib/Twig/TestCallableInterface.php" => array("432", "680324d83f36b80a71cc3e18ce6da3bf"),
+ "vendor/twig/twig/lib/Twig/Test/Function.php" => array("705", "c5256cfda774aad3796e83c5ccc76df1"),
+ "vendor/twig/twig/lib/Twig/Test/IntegrationTestCase.php" => array("5645", "4e4581eabad75b9cbeb41c4f716e62eb"),
+ "vendor/twig/twig/lib/Twig/TestInterface.php" => array("506", "c64d3221a3e0ac3a1a27f629529ca22f"),
+ "vendor/twig/twig/lib/Twig/Test/Method.php" => array("887", "b05886b7c3bb0d5a4b4fd871a0de612e"),
+ "vendor/twig/twig/lib/Twig/Test/Node.php" => array("688", "41a3b3e4657168e15d1b1613e5ae689a"),
+ "vendor/twig/twig/lib/Twig/Test/NodeTestCase.php" => array("1593", "fcf420a35fd3b0a9117bff01ec7df9d2"),
+ "vendor/twig/twig/lib/Twig/Test.php" => array("753", "907df6428c547e9a5af3a07ea99c31a1"),
+ "vendor/twig/twig/lib/Twig/TokenParser/AutoEscape.php" => array("2611", "999df38e7f5a2dc1a0d255515e35f6d5"),
+ "vendor/twig/twig/lib/Twig/TokenParser/Block.php" => array("2622", "91f80e924e22f40739703995e18d6bcb"),
+ "vendor/twig/twig/lib/Twig/TokenParserBrokerInterface.php" => array("1270", "0a586d723c9c4a0eee1fbb8e39bad88e"),
+ "vendor/twig/twig/lib/Twig/TokenParserBroker.php" => array("3879", "29a8948f912da59f4bcab2a5d718bb59"),
+ "vendor/twig/twig/lib/Twig/TokenParser/Do.php" => array("980", "87cfd1e3e9c357fa6707126c4708f5e0"),
+ "vendor/twig/twig/lib/Twig/TokenParser/Embed.php" => array("1947", "f59d933b6e44590ed33aa6dff014fcc1"),
+ "vendor/twig/twig/lib/Twig/TokenParser/Extends.php" => array("1355", "83f589057c3b2ed4b0ee6fcd9c7140ce"),
+ "vendor/twig/twig/lib/Twig/TokenParser/Filter.php" => array("1702", "947049e09760ed46c9195e435fe4bc52"),
+ "vendor/twig/twig/lib/Twig/TokenParser/Flush.php" => array("905", "b6f0a3ec7581d0414899fc50bdaad814"),
+ "vendor/twig/twig/lib/Twig/TokenParser/For.php" => array("4799", "c4ae472d2b3821f8ae56f86464cc295e"),
+ "vendor/twig/twig/lib/Twig/TokenParser/From.php" => array("1793", "e180cccf0c62b7cad73c63edb4005baa"),
+ "vendor/twig/twig/lib/Twig/TokenParser/If.php" => array("2709", "35fdb85b56353849c394a9e4da7fe715"),
+ "vendor/twig/twig/lib/Twig/TokenParser/Import.php" => array("1296", "3e7397da9c81999b32a1b637871db37c"),
+ "vendor/twig/twig/lib/Twig/TokenParser/Include.php" => array("1846", "aafd1e3b2b6e6b4223ddc3c0eff599a0"),
+ "vendor/twig/twig/lib/Twig/TokenParserInterface.php" => array("953", "62c183068343c7dbcad42a1cc4b27581"),
+ "vendor/twig/twig/lib/Twig/TokenParser/Macro.php" => array("2023", "5f0a590691c64a79a1031a7ebe1da557"),
+ "vendor/twig/twig/lib/Twig/TokenParser.php" => array("662", "83224f13e3f3e14ed5e7c0d1c567a9c6"),
+ "vendor/twig/twig/lib/Twig/TokenParser/Sandbox.php" => array("1950", "4e7a32b6b2436967b85ac5bee63f99da"),
+ "vendor/twig/twig/lib/Twig/TokenParser/Set.php" => array("2286", "9bb035bb1ad955d96257a8a5e0e6b496"),
+ "vendor/twig/twig/lib/Twig/TokenParser/Spaceless.php" => array("1392", "ab7d1d53dd9b4e7628837e7af93b3db3"),
+ "vendor/twig/twig/lib/Twig/TokenParser/Use.php" => array("2135", "7bd09009867e76e0b006586fa3fc6275"),
+ "vendor/twig/twig/lib/Twig/Token.php" => array("6211", "4b3f25c7d4929c29712d25711bc87768"),
+ "vendor/twig/twig/lib/Twig/TokenStream.php" => array("3989", "8eb8e69d38ec4f6ee0b73d716d064957"),
+ "vendor/twig/twig/LICENSE" => array("1497", "1886505263500ef827db124cf26c2408"),
+ "vendor/twig/twig/phpunit.xml.dist" => array("651", "64f59fc76504c822331e5e5eccc3e1cf"),
+ "vendor/twig/twig/README.rst" => array("486", "32d5a3ca77dace5b9255842b15c55699"),
+ "vendor/twig/twig/.travis.yml" => array("440", "c60b7a77686a5d1a4201f9fbc4c88dbb"),
+ );
+}
diff --git a/www/analytics/console b/www/analytics/console
new file mode 100755
index 00000000..0e3dc3ff
--- /dev/null
+++ b/www/analytics/console
@@ -0,0 +1,28 @@
+#!/usr/bin/env php
+init();
+$console->run();
\ No newline at end of file
diff --git a/www/analytics/core/.htaccess b/www/analytics/core/.htaccess
new file mode 100644
index 00000000..6cd2e134
--- /dev/null
+++ b/www/analytics/core/.htaccess
@@ -0,0 +1,13 @@
+
+
+Deny from all
+
+
+
+Deny from all
+
+
+
+Deny from all
+
+
diff --git a/www/analytics/core/API/DataTableGenericFilter.php b/www/analytics/core/API/DataTableGenericFilter.php
new file mode 100644
index 00000000..965e4fde
--- /dev/null
+++ b/www/analytics/core/API/DataTableGenericFilter.php
@@ -0,0 +1,149 @@
+request = $request;
+ }
+
+ /**
+ * Filters the given data table
+ *
+ * @param DataTable $table
+ */
+ public function filter($table)
+ {
+ $this->applyGenericFilters($table);
+ }
+
+ /**
+ * Returns an array containing the information of the generic Filter
+ * to be applied automatically to the data resulting from the API calls.
+ *
+ * Order to apply the filters:
+ * 1 - Filter that remove filtered rows
+ * 2 - Filter that sort the remaining rows
+ * 3 - Filter that keep only a subset of the results
+ * 4 - Presentation filters
+ *
+ * @return array See the code for spec
+ */
+ public static function getGenericFiltersInformation()
+ {
+ if (is_null(self::$genericFiltersInfo)) {
+ self::$genericFiltersInfo = array(
+ 'Pattern' => array(
+ 'filter_column' => array('string', 'label'),
+ 'filter_pattern' => array('string'),
+ ),
+ 'PatternRecursive' => array(
+ 'filter_column_recursive' => array('string', 'label'),
+ 'filter_pattern_recursive' => array('string'),
+ ),
+ 'ExcludeLowPopulation' => array(
+ 'filter_excludelowpop' => array('string'),
+ 'filter_excludelowpop_value' => array('float', '0'),
+ ),
+ 'AddColumnsProcessedMetrics' => array(
+ 'filter_add_columns_when_show_all_columns' => array('integer')
+ ),
+ 'AddColumnsProcessedMetricsGoal' => array(
+ 'filter_update_columns_when_show_all_goals' => array('integer'),
+ 'idGoal' => array('string', AddColumnsProcessedMetricsGoal::GOALS_OVERVIEW),
+ ),
+ 'Sort' => array(
+ 'filter_sort_column' => array('string'),
+ 'filter_sort_order' => array('string', 'desc'),
+ ),
+ 'Truncate' => array(
+ 'filter_truncate' => array('integer'),
+ ),
+ 'Limit' => array(
+ 'filter_offset' => array('integer', '0'),
+ 'filter_limit' => array('integer'),
+ 'keep_summary_row' => array('integer', '0'),
+ ),
+ );
+ }
+
+ return self::$genericFiltersInfo;
+ }
+
+ /**
+ * Apply generic filters to the DataTable object resulting from the API Call.
+ * Disable this feature by setting the parameter disable_generic_filters to 1 in the API call request.
+ *
+ * @param DataTable $datatable
+ * @return bool
+ */
+ protected function applyGenericFilters($datatable)
+ {
+ if ($datatable instanceof DataTable\Map) {
+ $tables = $datatable->getDataTables();
+ foreach ($tables as $table) {
+ $this->applyGenericFilters($table);
+ }
+ return;
+ }
+
+ $genericFilters = self::getGenericFiltersInformation();
+
+ $filterApplied = false;
+ foreach ($genericFilters as $filterName => $parameters) {
+ $filterParameters = array();
+ $exceptionRaised = false;
+ foreach ($parameters as $name => $info) {
+ // parameter type to cast to
+ $type = $info[0];
+
+ // default value if specified, when the parameter doesn't have a value
+ $defaultValue = null;
+ if (isset($info[1])) {
+ $defaultValue = $info[1];
+ }
+
+ // third element in the array, if it exists, overrides the name of the request variable
+ $varName = $name;
+ if (isset($info[2])) {
+ $varName = $info[2];
+ }
+
+ try {
+ $value = Common::getRequestVar($name, $defaultValue, $type, $this->request);
+ settype($value, $type);
+ $filterParameters[] = $value;
+ } catch (Exception $e) {
+ $exceptionRaised = true;
+ break;
+ }
+ }
+
+ if (!$exceptionRaised) {
+ $datatable->filter($filterName, $filterParameters);
+ $filterApplied = true;
+ }
+ }
+ return $filterApplied;
+ }
+}
diff --git a/www/analytics/core/API/DataTableManipulator.php b/www/analytics/core/API/DataTableManipulator.php
new file mode 100644
index 00000000..5ebdd2fb
--- /dev/null
+++ b/www/analytics/core/API/DataTableManipulator.php
@@ -0,0 +1,192 @@
+apiModule = $apiModule;
+ $this->apiMethod = $apiMethod;
+ $this->request = $request;
+ }
+
+ /**
+ * This method can be used by subclasses to iterate over data tables that might be
+ * data table maps. It calls back the template method self::doManipulate for each table.
+ * This way, data table arrays can be handled in a transparent fashion.
+ *
+ * @param DataTable\Map|DataTable $dataTable
+ * @throws Exception
+ * @return DataTable\Map|DataTable
+ */
+ protected function manipulate($dataTable)
+ {
+ if ($dataTable instanceof DataTable\Map) {
+ return $this->manipulateDataTableMap($dataTable);
+ } else if ($dataTable instanceof DataTable) {
+ return $this->manipulateDataTable($dataTable);
+ } else {
+ return $dataTable;
+ }
+ }
+
+ /**
+ * Manipulates child DataTables of a DataTable\Map. See @manipulate for more info.
+ *
+ * @param DataTable\Map $dataTable
+ * @return DataTable\Map
+ */
+ protected function manipulateDataTableMap($dataTable)
+ {
+ $result = $dataTable->getEmptyClone();
+ foreach ($dataTable->getDataTables() as $tableLabel => $childTable) {
+ $newTable = $this->manipulate($childTable);
+ $result->addTable($newTable, $tableLabel);
+ }
+ return $result;
+ }
+
+ /**
+ * Manipulates a single DataTable instance. Derived classes must define
+ * this function.
+ */
+ protected abstract function manipulateDataTable($dataTable);
+
+ /**
+ * Load the subtable for a row.
+ * Returns null if none is found.
+ *
+ * @param DataTable $dataTable
+ * @param Row $row
+ *
+ * @return DataTable
+ */
+ protected function loadSubtable($dataTable, $row)
+ {
+ if (!($this->apiModule && $this->apiMethod && count($this->request))) {
+ return null;
+ }
+
+ $request = $this->request;
+
+ $idSubTable = $row->getIdSubDataTable();
+ if ($idSubTable === null) {
+ return null;
+ }
+
+ $request['idSubtable'] = $idSubTable;
+ if ($dataTable) {
+ $period = $dataTable->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX);
+ if ($period instanceof Range) {
+ $request['date'] = $period->getDateStart() . ',' . $period->getDateEnd();
+ } else {
+ $request['date'] = $period->getDateStart()->toString();
+ }
+ }
+
+ $method = $this->getApiMethodForSubtable();
+ return $this->callApiAndReturnDataTable($this->apiModule, $method, $request);
+ }
+
+ /**
+ * In this method, subclasses can clean up the request array for loading subtables
+ * in order to make ResponseBuilder behave correctly (e.g. not trigger the
+ * manipulator again).
+ *
+ * @param $request
+ * @return
+ */
+ protected abstract function manipulateSubtableRequest($request);
+
+ /**
+ * Extract the API method for loading subtables from the meta data
+ *
+ * @return string
+ */
+ private function getApiMethodForSubtable()
+ {
+ if (!$this->apiMethodForSubtable) {
+ $meta = API::getInstance()->getMetadata('all', $this->apiModule, $this->apiMethod);
+
+ if(empty($meta)) {
+ throw new Exception(sprintf(
+ "The DataTable cannot be manipulated: Metadata for report %s.%s could not be found. You can define the metadata in a hook, see example at: http://developer.piwik.org/api-reference/events#apigetreportmetadata",
+ $this->apiModule, $this->apiMethod
+ ));
+ }
+
+ if (isset($meta[0]['actionToLoadSubTables'])) {
+ $this->apiMethodForSubtable = $meta[0]['actionToLoadSubTables'];
+ } else {
+ $this->apiMethodForSubtable = $this->apiMethod;
+ }
+ }
+ return $this->apiMethodForSubtable;
+ }
+
+ protected function callApiAndReturnDataTable($apiModule, $method, $request)
+ {
+ $class = Request::getClassNameAPI($apiModule);
+
+ $request = $this->manipulateSubtableRequest($request);
+ $request['serialize'] = 0;
+ $request['expanded'] = 0;
+
+ // don't want to run recursive filters on the subtables as they are loaded,
+ // otherwise the result will be empty in places (or everywhere). instead we
+ // run it on the flattened table.
+ unset($request['filter_pattern_recursive']);
+
+ $dataTable = Proxy::getInstance()->call($class, $method, $request);
+ $response = new ResponseBuilder($format = 'original', $request);
+ $dataTable = $response->getResponse($dataTable);
+
+ if (Common::getRequestVar('disable_queued_filters', 0, 'int', $request) == 0) {
+ if (method_exists($dataTable, 'applyQueuedFilters')) {
+ $dataTable->applyQueuedFilters();
+ }
+ }
+
+ return $dataTable;
+ }
+}
diff --git a/www/analytics/core/API/DataTableManipulator/Flattener.php b/www/analytics/core/API/DataTableManipulator/Flattener.php
new file mode 100644
index 00000000..20fe2c02
--- /dev/null
+++ b/www/analytics/core/API/DataTableManipulator/Flattener.php
@@ -0,0 +1,137 @@
+includeAggregateRows = true;
+ }
+
+ /**
+ * Separator for building recursive labels (or paths)
+ * @var string
+ */
+ public $recursiveLabelSeparator = ' - ';
+
+ /**
+ * @param DataTable $dataTable
+ * @return DataTable|DataTable\Map
+ */
+ public function flatten($dataTable)
+ {
+ if ($this->apiModule == 'Actions' || $this->apiMethod == 'getWebsites') {
+ $this->recursiveLabelSeparator = '/';
+ }
+
+ return $this->manipulate($dataTable);
+ }
+
+ /**
+ * Template method called from self::manipulate.
+ * Flatten each data table.
+ *
+ * @param DataTable $dataTable
+ * @return DataTable
+ */
+ protected function manipulateDataTable($dataTable)
+ {
+ // apply filters now since subtables have their filters applied before generic filters. if we don't do this
+ // now, we'll try to apply filters to rows that have already been manipulated. this results in errors like
+ // 'column ... already exists'.
+ $keepFilters = true;
+ if (Common::getRequestVar('disable_queued_filters', 0, 'int', $this->request) == 0) {
+ $dataTable->applyQueuedFilters();
+ $keepFilters = false;
+ }
+
+ $newDataTable = $dataTable->getEmptyClone($keepFilters);
+ foreach ($dataTable->getRows() as $row) {
+ $this->flattenRow($row, $newDataTable);
+ }
+ return $newDataTable;
+ }
+
+ /**
+ * @param Row $row
+ * @param DataTable $dataTable
+ * @param string $labelPrefix
+ * @param bool $parentLogo
+ */
+ private function flattenRow(Row $row, DataTable $dataTable,
+ $labelPrefix = '', $parentLogo = false)
+ {
+ $label = $row->getColumn('label');
+ if ($label !== false) {
+ $label = trim($label);
+ if (substr($label, 0, 1) == '/' && $this->recursiveLabelSeparator == '/') {
+ $label = substr($label, 1);
+ }
+ $label = $labelPrefix . $label;
+ $row->setColumn('label', $label);
+ }
+
+ $logo = $row->getMetadata('logo');
+ if ($logo === false && $parentLogo !== false) {
+ $logo = $parentLogo;
+ $row->setMetadata('logo', $logo);
+ }
+
+ $subTable = $this->loadSubtable($dataTable, $row);
+ $row->removeSubtable();
+
+ if ($subTable === null) {
+ if ($this->includeAggregateRows) {
+ $row->setMetadata('is_aggregate', 0);
+ }
+ $dataTable->addRow($row);
+ } else {
+ if ($this->includeAggregateRows) {
+ $row->setMetadata('is_aggregate', 1);
+ $dataTable->addRow($row);
+ }
+ $prefix = $label . $this->recursiveLabelSeparator;
+ foreach ($subTable->getRows() as $row) {
+ $this->flattenRow($row, $dataTable, $prefix, $logo);
+ }
+ }
+ }
+
+ /**
+ * Remove the flat parameter from the subtable request
+ *
+ * @param array $request
+ */
+ protected function manipulateSubtableRequest($request)
+ {
+ unset($request['flat']);
+
+ return $request;
+ }
+}
diff --git a/www/analytics/core/API/DataTableManipulator/LabelFilter.php b/www/analytics/core/API/DataTableManipulator/LabelFilter.php
new file mode 100644
index 00000000..05c594b3
--- /dev/null
+++ b/www/analytics/core/API/DataTableManipulator/LabelFilter.php
@@ -0,0 +1,167 @@
+ to join them.
+ */
+class LabelFilter extends DataTableManipulator
+{
+ const SEPARATOR_RECURSIVE_LABEL = '>';
+
+ private $labels;
+ private $addLabelIndex;
+ const FLAG_IS_ROW_EVOLUTION = 'label_index';
+
+ /**
+ * Filter a data table by label.
+ * The filtered table is returned, which might be a new instance.
+ *
+ * $apiModule, $apiMethod and $request are needed load sub-datatables
+ * for the recursive search. If the label is not recursive, these parameters
+ * are not needed.
+ *
+ * @param string $labels the labels to search for
+ * @param DataTable $dataTable the data table to be filtered
+ * @param bool $addLabelIndex Whether to add label_index metadata describing which
+ * label a row corresponds to.
+ * @return DataTable
+ */
+ public function filter($labels, $dataTable, $addLabelIndex = false)
+ {
+ if (!is_array($labels)) {
+ $labels = array($labels);
+ }
+
+ $this->labels = $labels;
+ $this->addLabelIndex = (bool)$addLabelIndex;
+ return $this->manipulate($dataTable);
+ }
+
+ /**
+ * Method for the recursive descend
+ *
+ * @param array $labelParts
+ * @param DataTable $dataTable
+ * @return Row|bool
+ */
+ private function doFilterRecursiveDescend($labelParts, $dataTable)
+ {
+ // search for the first part of the tree search
+ $labelPart = array_shift($labelParts);
+
+ $row = false;
+ foreach ($this->getLabelVariations($labelPart) as $labelPart) {
+ $row = $dataTable->getRowFromLabel($labelPart);
+ if ($row !== false) {
+ break;
+ }
+ }
+
+ if ($row === false) {
+ // not found
+ return false;
+ }
+
+ // end of tree search reached
+ if (count($labelParts) == 0) {
+ return $row;
+ }
+
+ $subTable = $this->loadSubtable($dataTable, $row);
+ if ($subTable === null) {
+ // no more subtables but label parts left => no match found
+ return false;
+ }
+
+ return $this->doFilterRecursiveDescend($labelParts, $subTable);
+ }
+
+ /**
+ * Clean up request for ResponseBuilder to behave correctly
+ *
+ * @param $request
+ */
+ protected function manipulateSubtableRequest($request)
+ {
+ unset($request['label']);
+
+ return $request;
+ }
+
+ /**
+ * Use variations of the label to make it easier to specify the desired label
+ *
+ * Note: The HTML Encoded version must be tried first, since in ResponseBuilder the $label is unsanitized
+ * via Common::unsanitizeLabelParameter.
+ *
+ * @param string $label
+ * @return array
+ */
+ private function getLabelVariations($label)
+ {
+ static $pageTitleReports = array('getPageTitles', 'getEntryPageTitles', 'getExitPageTitles');
+
+ $variations = array();
+ $label = urldecode($label);
+ $label = trim($label);
+
+ $sanitizedLabel = Common::sanitizeInputValue($label);
+ $variations[] = $sanitizedLabel;
+
+ if ($this->apiModule == 'Actions'
+ && in_array($this->apiMethod, $pageTitleReports)
+ ) {
+ // special case: the Actions.getPageTitles report prefixes some labels with a blank.
+ // the blank might be passed by the user but is removed in Request::getRequestArrayFromString.
+ $variations[] = ' ' . $sanitizedLabel;
+ $variations[] = ' ' . $label;
+ }
+ $variations[] = $label;
+
+ return $variations;
+ }
+
+ /**
+ * Filter a DataTable instance. See @filter for more info.
+ *
+ * @param DataTable\Simple|DataTable\Map $dataTable
+ * @return mixed
+ */
+ protected function manipulateDataTable($dataTable)
+ {
+ $result = $dataTable->getEmptyClone();
+ foreach ($this->labels as $labelIndex => $label) {
+ $row = null;
+ foreach ($this->getLabelVariations($label) as $labelVariation) {
+ $labelVariation = explode(self::SEPARATOR_RECURSIVE_LABEL, $labelVariation);
+
+ $row = $this->doFilterRecursiveDescend($labelVariation, $dataTable);
+ if ($row) {
+ if ($this->addLabelIndex) {
+ $row->setMetadata(self::FLAG_IS_ROW_EVOLUTION, $labelIndex);
+ }
+ $result->addRow($row);
+ break;
+ }
+ }
+ }
+ return $result;
+ }
+}
diff --git a/www/analytics/core/API/DataTableManipulator/ReportTotalsCalculator.php b/www/analytics/core/API/DataTableManipulator/ReportTotalsCalculator.php
new file mode 100644
index 00000000..ee289d8b
--- /dev/null
+++ b/www/analytics/core/API/DataTableManipulator/ReportTotalsCalculator.php
@@ -0,0 +1,250 @@
+apiModule) || empty($this->apiMethod)) {
+ return $table;
+ }
+
+ try {
+ return $this->manipulate($table);
+ } catch(\Exception $e) {
+ // eg. requests with idSubtable may trigger this exception
+ // (where idSubtable was removed in
+ // ?module=API&method=Events.getNameFromCategoryId&idSubtable=1&secondaryDimension=eventName&format=XML&idSite=1&period=day&date=yesterday&flat=0
+ return $table;
+ }
+ }
+
+ /**
+ * Adds ratio metrics if possible.
+ *
+ * @param DataTable $dataTable
+ * @return DataTable
+ */
+ protected function manipulateDataTable($dataTable)
+ {
+ $report = $this->findCurrentReport();
+
+ if (!empty($report) && empty($report['dimension'])) {
+ // we currently do not calculate the total value for reports having no dimension
+ return $dataTable;
+ }
+
+ // Array [readableMetric] => [summed value]
+ $totalValues = array();
+
+ $firstLevelTable = $this->makeSureToWorkOnFirstLevelDataTable($dataTable);
+ $metricsToCalculate = Metrics::getMetricIdsToProcessReportTotal();
+
+ foreach ($metricsToCalculate as $metricId) {
+ if (!$this->hasDataTableMetric($firstLevelTable, $metricId)) {
+ continue;
+ }
+
+ foreach ($firstLevelTable->getRows() as $row) {
+ $totalValues = $this->sumColumnValueToTotal($row, $metricId, $totalValues);
+ }
+ }
+
+ $dataTable->setMetadata('totals', $totalValues);
+
+ return $dataTable;
+ }
+
+ private function hasDataTableMetric(DataTable $dataTable, $metricId)
+ {
+ $firstRow = $dataTable->getFirstRow();
+
+ if (empty($firstRow)) {
+ return false;
+ }
+
+ if (false === $this->getColumn($firstRow, $metricId)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns column from a given row.
+ * Will work with 2 types of datatable
+ * - raw datatables coming from the archive DB, which columns are int indexed
+ * - datatables processed resulting of API calls, which columns have human readable english names
+ *
+ * @param Row|array $row
+ * @param int $columnIdRaw see consts in Metrics::
+ * @return mixed Value of column, false if not found
+ */
+ private function getColumn($row, $columnIdRaw)
+ {
+ $columnIdReadable = Metrics::getReadableColumnName($columnIdRaw);
+
+ if ($row instanceof Row) {
+ $raw = $row->getColumn($columnIdRaw);
+ if ($raw !== false) {
+ return $raw;
+ }
+
+ return $row->getColumn($columnIdReadable);
+ }
+
+ return false;
+ }
+
+ private function makeSureToWorkOnFirstLevelDataTable($table)
+ {
+ if (!array_key_exists('idSubtable', $this->request)) {
+ return $table;
+ }
+
+ $firstLevelReport = $this->findFirstLevelReport();
+
+ if (empty($firstLevelReport)) {
+ // it is not a subtable report
+ $module = $this->apiModule;
+ $action = $this->apiMethod;
+ } else {
+ $module = $firstLevelReport['module'];
+ $action = $firstLevelReport['action'];
+ }
+
+ $request = $this->request;
+
+ /** @var \Piwik\Period $period */
+ $period = $table->getMetadata('period');
+
+ if (!empty($period)) {
+ // we want a dataTable, not a dataTable\map
+ if (Period::isMultiplePeriod($request['date'], $request['period']) || 'range' == $period->getLabel()) {
+ $request['date'] = $period->getRangeString();
+ $request['period'] = 'range';
+ } else {
+ $request['date'] = $period->getDateStart()->toString();
+ $request['period'] = $period->getLabel();
+ }
+ }
+
+ return $this->callApiAndReturnDataTable($module, $action, $request);
+ }
+
+ private function sumColumnValueToTotal(Row $row, $metricId, $totalValues)
+ {
+ $value = $this->getColumn($row, $metricId);
+
+ if (false === $value) {
+
+ return $totalValues;
+ }
+
+ $metricName = Metrics::getReadableColumnName($metricId);
+
+ if (array_key_exists($metricName, $totalValues)) {
+ $totalValues[$metricName] += $value;
+ } else {
+ $totalValues[$metricName] = $value;
+ }
+
+ return $totalValues;
+ }
+
+ /**
+ * Make sure to get all rows of the first level table.
+ *
+ * @param array $request
+ */
+ protected function manipulateSubtableRequest($request)
+ {
+ $request['totals'] = 0;
+ $request['expanded'] = 0;
+ $request['filter_limit'] = -1;
+ $request['filter_offset'] = 0;
+
+ $parametersToRemove = array('flat');
+
+ if (!array_key_exists('idSubtable', $this->request)) {
+ $parametersToRemove[] = 'idSubtable';
+ }
+
+ foreach ($parametersToRemove as $param) {
+ if (array_key_exists($param, $request)) {
+ unset($request[$param]);
+ }
+ }
+ return $request;
+ }
+
+ private function getReportMetadata()
+ {
+ if (!empty(static::$reportMetadata)) {
+ return static::$reportMetadata;
+ }
+
+ static::$reportMetadata = API::getInstance()->getReportMetadata();
+
+ return static::$reportMetadata;
+ }
+
+ private function findCurrentReport()
+ {
+ foreach ($this->getReportMetadata() as $report) {
+ if ($this->apiMethod == $report['action']
+ && $this->apiModule == $report['module']) {
+
+ return $report;
+ }
+ }
+ }
+
+ private function findFirstLevelReport()
+ {
+ foreach ($this->getReportMetadata() as $report) {
+ if (!empty($report['actionToLoadSubTables'])
+ && $this->apiMethod == $report['actionToLoadSubTables']
+ && $this->apiModule == $report['module']
+ ) {
+
+ return $report;
+ }
+ }
+ }
+}
diff --git a/www/analytics/core/API/DocumentationGenerator.php b/www/analytics/core/API/DocumentationGenerator.php
new file mode 100644
index 00000000..ecfece70
--- /dev/null
+++ b/www/analytics/core/API/DocumentationGenerator.php
@@ -0,0 +1,238 @@
+getLoadedPluginsName();
+ foreach ($plugins as $plugin) {
+ try {
+ $className = Request::getClassNameAPI($plugin);
+ Proxy::getInstance()->registerClass($className);
+ } catch (Exception $e) {
+ }
+ }
+ }
+
+ /**
+ * Returns a HTML page containing help for all the successfully loaded APIs.
+ * For each module it will return a mini help with the method names, parameters to give,
+ * links to get the result in Xml/Csv/etc
+ *
+ * @param bool $outputExampleUrls
+ * @param string $prefixUrls
+ * @return string
+ */
+ public function getAllInterfaceString($outputExampleUrls = true, $prefixUrls = '')
+ {
+ if (!empty($prefixUrls)) {
+ $prefixUrls = 'http://demo.piwik.org/';
+ }
+ $str = $toc = '';
+ $token_auth = "&token_auth=" . Piwik::getCurrentUserTokenAuth();
+ $parametersToSet = array(
+ 'idSite' => Common::getRequestVar('idSite', 1, 'int'),
+ 'period' => Common::getRequestVar('period', 'day', 'string'),
+ 'date' => Common::getRequestVar('date', 'today', 'string')
+ );
+
+ foreach (Proxy::getInstance()->getMetadata() as $class => $info) {
+ $moduleName = Proxy::getInstance()->getModuleNameFromClassName($class);
+ if (in_array($moduleName, $this->modulesToHide)) {
+ continue;
+ }
+ $toc .= "$moduleName ";
+ $str .= "\nModule " . $moduleName . " ";
+ $str .= " " . $info['__documentation'] . "
";
+ foreach ($info as $methodName => $infoMethod) {
+ if ($methodName == '__documentation') {
+ continue;
+ }
+ $params = $this->getParametersString($class, $methodName);
+ $str .= "\n -
$moduleName.$methodName " . $params . "";
+ $str .= '
';
+
+ if ($outputExampleUrls) {
+ // we prefix all URLs with $prefixUrls
+ // used when we include this output in the Piwik official documentation for example
+ $str .= "";
+ $exampleUrl = $this->getExampleUrl($class, $methodName, $parametersToSet);
+ if ($exampleUrl !== false) {
+ $lastNUrls = '';
+ if (preg_match('/(&period)|(&date)/', $exampleUrl)) {
+ $exampleUrlRss1 = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last10', 'period' => 'day') + $parametersToSet);
+ $exampleUrlRss2 = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last5', 'period' => 'week',) + $parametersToSet);
+ $lastNUrls = ", RSS of the last 10 days ";
+ }
+ $exampleUrl = $prefixUrls . $exampleUrl;
+ $str .= " [ Example in
+ XML ,
+ Json ,
+ Tsv (Excel)
+ $lastNUrls
+ ]";
+ } else {
+ $str .= " [ No example available ]";
+ }
+ $str .= " ";
+ }
+ $str .= ' ';
+ $str .= "
\n";
+ }
+ $str .= '';
+ }
+
+ $str = "Quick access to APIs
+ $toc
+ $str";
+ return $str;
+ }
+
+ /**
+ * Returns a string containing links to examples on how to call a given method on a given API
+ * It will export links to XML, CSV, HTML, JSON, PHP, etc.
+ * It will not export links for methods such as deleteSite or deleteUser
+ *
+ * @param string $class the class
+ * @param string $methodName the method
+ * @param array $parametersToSet parameters to set
+ * @return string|bool when not possible
+ */
+ public function getExampleUrl($class, $methodName, $parametersToSet = array())
+ {
+ $knowExampleDefaultParametersValues = array(
+ 'access' => 'view',
+ 'userLogin' => 'test',
+ 'passwordMd5ied' => 'passwordExample',
+ 'email' => 'test@example.org',
+
+ 'languageCode' => 'fr',
+ 'url' => 'http://forum.piwik.org/',
+ 'pageUrl' => 'http://forum.piwik.org/',
+ 'apiModule' => 'UserCountry',
+ 'apiAction' => 'getCountry',
+ 'lastMinutes' => '30',
+ 'abandonedCarts' => '0',
+ 'segmentName' => 'pageTitle',
+ 'ip' => '194.57.91.215',
+ 'idSites' => '1,2',
+ 'idAlert' => '1',
+// 'segmentName' => 'browserCode',
+ );
+
+ foreach ($parametersToSet as $name => $value) {
+ $knowExampleDefaultParametersValues[$name] = $value;
+ }
+
+ // no links for these method names
+ $doNotPrintExampleForTheseMethods = array(
+ //Sites
+ 'deleteSite',
+ 'addSite',
+ 'updateSite',
+ 'addSiteAliasUrls',
+ //Users
+ 'deleteUser',
+ 'addUser',
+ 'updateUser',
+ 'setUserAccess',
+ //Goals
+ 'addGoal',
+ 'updateGoal',
+ 'deleteGoal',
+ );
+
+ if (in_array($methodName, $doNotPrintExampleForTheseMethods)) {
+ return false;
+ }
+
+ // we try to give an URL example to call the API
+ $aParameters = Proxy::getInstance()->getParametersList($class, $methodName);
+ // Kindly force some known generic parameters to appear in the final list
+ // the parameter 'format' can be set to all API methods (used in tests)
+ // the parameter 'hideIdSubDatable' is used for integration tests only
+ // the parameter 'serialize' sets php outputs human readable, used in integration tests and debug
+ // the parameter 'language' sets the language for the response (eg. country names)
+ // the parameter 'flat' reduces a hierarchical table to a single level by concatenating labels
+ // the parameter 'include_aggregate_rows' can be set to include inner nodes in flat reports
+ // the parameter 'translateColumnNames' can be set to translate metric names in csv/tsv exports
+ $aParameters['format'] = false;
+ $aParameters['hideIdSubDatable'] = false;
+ $aParameters['serialize'] = false;
+ $aParameters['language'] = false;
+ $aParameters['translateColumnNames'] = false;
+ $aParameters['label'] = false;
+ $aParameters['flat'] = false;
+ $aParameters['include_aggregate_rows'] = false;
+ $aParameters['filter_limit'] = false; //@review without adding this, I can not set filter_limit in $otherRequestParameters integration tests
+ $aParameters['filter_sort_column'] = false; //@review without adding this, I can not set filter_sort_column in $otherRequestParameters integration tests
+ $aParameters['filter_truncate'] = false;
+ $aParameters['hideColumns'] = false;
+ $aParameters['showColumns'] = false;
+ $aParameters['filter_pattern_recursive'] = false;
+
+ $moduleName = Proxy::getInstance()->getModuleNameFromClassName($class);
+ $aParameters = array_merge(array('module' => 'API', 'method' => $moduleName . '.' . $methodName), $aParameters);
+
+ foreach ($aParameters as $nameVariable => &$defaultValue) {
+ if (isset($knowExampleDefaultParametersValues[$nameVariable])) {
+ $defaultValue = $knowExampleDefaultParametersValues[$nameVariable];
+ } // if there isn't a default value for a given parameter,
+ // we need a 'know default value' or we can't generate the link
+ elseif ($defaultValue instanceof NoDefaultValue) {
+ return false;
+ }
+ }
+ return '?' . Url::getQueryStringFromParameters($aParameters);
+ }
+
+ /**
+ * Returns the methods $class.$name parameters (and default value if provided) as a string.
+ *
+ * @param string $class The class name
+ * @param string $name The method name
+ * @return string For example "(idSite, period, date = 'today')"
+ */
+ public function getParametersString($class, $name)
+ {
+ $aParameters = Proxy::getInstance()->getParametersList($class, $name);
+ $asParameters = array();
+ foreach ($aParameters as $nameVariable => $defaultValue) {
+ // Do not show API parameters starting with _
+ // They are supposed to be used only in internal API calls
+ if (strpos($nameVariable, '_') === 0) {
+ continue;
+ }
+ $str = $nameVariable;
+ if (!($defaultValue instanceof NoDefaultValue)) {
+ if (is_array($defaultValue)) {
+ $str .= " = 'Array'";
+ } else {
+ $str .= " = '$defaultValue'";
+ }
+ }
+ $asParameters[] = $str;
+ }
+ $sParameters = implode(", ", $asParameters);
+ return "($sParameters)";
+ }
+}
diff --git a/www/analytics/core/API/Proxy.php b/www/analytics/core/API/Proxy.php
new file mode 100644
index 00000000..8f1e20c9
--- /dev/null
+++ b/www/analytics/core/API/Proxy.php
@@ -0,0 +1,514 @@
+noDefaultValue = new NoDefaultValue();
+ }
+
+ /**
+ * Returns array containing reflection meta data for all the loaded classes
+ * eg. number of parameters, method names, etc.
+ *
+ * @return array
+ */
+ public function getMetadata()
+ {
+ ksort($this->metadataArray);
+ return $this->metadataArray;
+ }
+
+ /**
+ * Registers the API information of a given module.
+ *
+ * The module to be registered must be
+ * - a singleton (providing a getInstance() method)
+ * - the API file must be located in plugins/ModuleName/API.php
+ * for example plugins/Referrers/API.php
+ *
+ * The method will introspect the methods, their parameters, etc.
+ *
+ * @param string $className ModuleName eg. "API"
+ */
+ public function registerClass($className)
+ {
+ if (isset($this->alreadyRegistered[$className])) {
+ return;
+ }
+ $this->includeApiFile($className);
+ $this->checkClassIsSingleton($className);
+
+ $rClass = new ReflectionClass($className);
+ foreach ($rClass->getMethods() as $method) {
+ $this->loadMethodMetadata($className, $method);
+ }
+
+ $this->setDocumentation($rClass, $className);
+ $this->alreadyRegistered[$className] = true;
+ }
+
+ /**
+ * Will be displayed in the API page
+ *
+ * @param ReflectionClass $rClass Instance of ReflectionClass
+ * @param string $className Name of the class
+ */
+ private function setDocumentation($rClass, $className)
+ {
+ // Doc comment
+ $doc = $rClass->getDocComment();
+ $doc = str_replace(" * " . PHP_EOL, " ", $doc);
+
+ // boldify the first line only if there is more than one line, otherwise too much bold
+ if (substr_count($doc, ' ') > 1) {
+ $firstLineBreak = strpos($doc, " ");
+ $doc = "" . substr($doc, 0, $firstLineBreak) . "
" . substr($doc, $firstLineBreak + strlen(" "));
+ }
+ $doc = preg_replace("/(@package)[a-z _A-Z]*/", "", $doc);
+ $doc = preg_replace("/(@method).*/", "", $doc);
+ $doc = str_replace(array("\t", "\n", "/**", "*/", " * ", " *", " ", "\t*", " * @package"), " ", $doc);
+ $this->metadataArray[$className]['__documentation'] = $doc;
+ }
+
+ /**
+ * Returns number of classes already loaded
+ * @return int
+ */
+ public function getCountRegisteredClasses()
+ {
+ return count($this->alreadyRegistered);
+ }
+
+ /**
+ * Will execute $className->$methodName($parametersValues)
+ * If any error is detected (wrong number of parameters, method not found, class not found, etc.)
+ * it will throw an exception
+ *
+ * It also logs the API calls, with the parameters values, the returned value, the performance, etc.
+ * You can enable logging in config/global.ini.php (log_api_call)
+ *
+ * @param string $className The class name (eg. API)
+ * @param string $methodName The method name
+ * @param array $parametersRequest The parameters pairs (name=>value)
+ *
+ * @return mixed|null
+ * @throws Exception|\Piwik\NoAccessException
+ */
+ public function call($className, $methodName, $parametersRequest)
+ {
+ $returnedValue = null;
+
+ // Temporarily sets the Request array to this API call context
+ $saveGET = $_GET;
+ $saveQUERY_STRING = @$_SERVER['QUERY_STRING'];
+ foreach ($parametersRequest as $param => $value) {
+ $_GET[$param] = $value;
+ }
+
+ try {
+ $this->registerClass($className);
+
+ // instanciate the object
+ $object = $className::getInstance();
+
+ // check method exists
+ $this->checkMethodExists($className, $methodName);
+
+ // get the list of parameters required by the method
+ $parameterNamesDefaultValues = $this->getParametersList($className, $methodName);
+
+ // load parameters in the right order, etc.
+ $finalParameters = $this->getRequestParametersArray($parameterNamesDefaultValues, $parametersRequest);
+
+ // allow plugins to manipulate the value
+ $pluginName = $this->getModuleNameFromClassName($className);
+
+ /**
+ * Triggered before an API request is dispatched.
+ *
+ * This event can be used to modify the arguments passed to one or more API methods.
+ *
+ * **Example**
+ *
+ * Piwik::addAction('API.Request.dispatch', function (&$parameters, $pluginName, $methodName) {
+ * if ($pluginName == 'Actions') {
+ * if ($methodName == 'getPageUrls') {
+ * // ... do something ...
+ * } else {
+ * // ... do something else ...
+ * }
+ * }
+ * });
+ *
+ * @param array &$finalParameters List of parameters that will be passed to the API method.
+ * @param string $pluginName The name of the plugin the API method belongs to.
+ * @param string $methodName The name of the API method that will be called.
+ */
+ Piwik::postEvent('API.Request.dispatch', array(&$finalParameters, $pluginName, $methodName));
+
+ /**
+ * Triggered before an API request is dispatched.
+ *
+ * This event exists for convenience and is triggered directly after the {@hook API.Request.dispatch}
+ * event is triggered. It can be used to modify the arguments passed to a **single** API method.
+ *
+ * _Note: This is can be accomplished with the {@hook API.Request.dispatch} event as well, however
+ * event handlers for that event will have to do more work._
+ *
+ * **Example**
+ *
+ * Piwik::addAction('API.Actions.getPageUrls', function (&$parameters) {
+ * // force use of a single website. for some reason.
+ * $parameters['idSite'] = 1;
+ * });
+ *
+ * @param array &$finalParameters List of parameters that will be passed to the API method.
+ */
+ Piwik::postEvent(sprintf('API.%s.%s', $pluginName, $methodName), array(&$finalParameters));
+
+ // call the method
+ $returnedValue = call_user_func_array(array($object, $methodName), $finalParameters);
+
+ $endHookParams = array(
+ &$returnedValue,
+ array('className' => $className,
+ 'module' => $pluginName,
+ 'action' => $methodName,
+ 'parameters' => $finalParameters)
+ );
+
+ /**
+ * Triggered directly after an API request is dispatched.
+ *
+ * This event exists for convenience and is triggered immediately before the
+ * {@hook API.Request.dispatch.end} event. It can be used to modify the output of a **single**
+ * API method.
+ *
+ * _Note: This can be accomplished with the {@hook API.Request.dispatch.end} event as well,
+ * however event handlers for that event will have to do more work._
+ *
+ * **Example**
+ *
+ * // append (0 hits) to the end of row labels whose row has 0 hits
+ * Piwik::addAction('API.Actions.getPageUrls', function (&$returnValue, $info)) {
+ * $returnValue->filter('ColumnCallbackReplace', 'label', function ($label, $hits) {
+ * if ($hits === 0) {
+ * return $label . " (0 hits)";
+ * } else {
+ * return $label;
+ * }
+ * }, null, array('nb_hits'));
+ * }
+ *
+ * @param mixed &$returnedValue The API method's return value. Can be an object, such as a
+ * {@link Piwik\DataTable DataTable} instance.
+ * could be a {@link Piwik\DataTable DataTable}.
+ * @param array $extraInfo An array holding information regarding the API request. Will
+ * contain the following data:
+ *
+ * - **className**: The namespace-d class name of the API instance
+ * that's being called.
+ * - **module**: The name of the plugin the API request was
+ * dispatched to.
+ * - **action**: The name of the API method that was executed.
+ * - **parameters**: The array of parameters passed to the API
+ * method.
+ */
+ Piwik::postEvent(sprintf('API.%s.%s.end', $pluginName, $methodName), $endHookParams);
+
+ /**
+ * Triggered directly after an API request is dispatched.
+ *
+ * This event can be used to modify the output of any API method.
+ *
+ * **Example**
+ *
+ * // append (0 hits) to the end of row labels whose row has 0 hits for any report that has the 'nb_hits' metric
+ * Piwik::addAction('API.Actions.getPageUrls', function (&$returnValue, $info)) {
+ * // don't process non-DataTable reports and reports that don't have the nb_hits column
+ * if (!($returnValue instanceof DataTableInterface)
+ * || in_array('nb_hits', $returnValue->getColumns())
+ * ) {
+ * return;
+ * }
+ *
+ * $returnValue->filter('ColumnCallbackReplace', 'label', function ($label, $hits) {
+ * if ($hits === 0) {
+ * return $label . " (0 hits)";
+ * } else {
+ * return $label;
+ * }
+ * }, null, array('nb_hits'));
+ * }
+ *
+ * @param mixed &$returnedValue The API method's return value. Can be an object, such as a
+ * {@link Piwik\DataTable DataTable} instance.
+ * @param array $extraInfo An array holding information regarding the API request. Will
+ * contain the following data:
+ *
+ * - **className**: The namespace-d class name of the API instance
+ * that's being called.
+ * - **module**: The name of the plugin the API request was
+ * dispatched to.
+ * - **action**: The name of the API method that was executed.
+ * - **parameters**: The array of parameters passed to the API
+ * method.
+ */
+ Piwik::postEvent('API.Request.dispatch.end', $endHookParams);
+
+ // Restore the request
+ $_GET = $saveGET;
+ $_SERVER['QUERY_STRING'] = $saveQUERY_STRING;
+ } catch (Exception $e) {
+ $_GET = $saveGET;
+ throw $e;
+ }
+
+ return $returnedValue;
+ }
+
+ /**
+ * Returns the parameters names and default values for the method $name
+ * of the class $class
+ *
+ * @param string $class The class name
+ * @param string $name The method name
+ * @return array Format array(
+ * 'testParameter' => null, // no default value
+ * 'life' => 42, // default value = 42
+ * 'date' => 'yesterday',
+ * );
+ */
+ public function getParametersList($class, $name)
+ {
+ return $this->metadataArray[$class][$name]['parameters'];
+ }
+
+ /**
+ * Returns the 'moduleName' part of '\\Piwik\\Plugins\\moduleName\\API'
+ *
+ * @param string $className "API"
+ * @return string "Referrers"
+ */
+ public function getModuleNameFromClassName($className)
+ {
+ return str_replace(array('\\Piwik\\Plugins\\', '\\API'), '', $className);
+ }
+
+ public function isExistingApiAction($pluginName, $apiAction)
+ {
+ $namespacedApiClassName = "\\Piwik\\Plugins\\$pluginName\\API";
+ $api = $namespacedApiClassName::getInstance();
+
+ return method_exists($api, $apiAction);
+ }
+
+ public function buildApiActionName($pluginName, $apiAction)
+ {
+ return sprintf("%s.%s", $pluginName, $apiAction);
+ }
+
+ /**
+ * Sets whether to hide '@ignore'd functions from method metadata or not.
+ *
+ * @param bool $hideIgnoredFunctions
+ */
+ public function setHideIgnoredFunctions($hideIgnoredFunctions)
+ {
+ $this->hideIgnoredFunctions = $hideIgnoredFunctions;
+
+ // make sure metadata gets reloaded
+ $this->alreadyRegistered = array();
+ $this->metadataArray = array();
+ }
+
+ /**
+ * Returns an array containing the values of the parameters to pass to the method to call
+ *
+ * @param array $requiredParameters array of (parameter name, default value)
+ * @param array $parametersRequest
+ * @throws Exception
+ * @return array values to pass to the function call
+ */
+ private function getRequestParametersArray($requiredParameters, $parametersRequest)
+ {
+ $finalParameters = array();
+ foreach ($requiredParameters as $name => $defaultValue) {
+ try {
+ if ($defaultValue instanceof NoDefaultValue) {
+ $requestValue = Common::getRequestVar($name, null, null, $parametersRequest);
+ } else {
+ try {
+
+ if ($name == 'segment' && !empty($parametersRequest['segment'])) {
+ // segment parameter is an exception: we do not want to sanitize user input or it would break the segment encoding
+ $requestValue = ($parametersRequest['segment']);
+ } else {
+ $requestValue = Common::getRequestVar($name, $defaultValue, null, $parametersRequest);
+ }
+ } catch (Exception $e) {
+ // Special case: empty parameter in the URL, should return the empty string
+ if (isset($parametersRequest[$name])
+ && $parametersRequest[$name] === ''
+ ) {
+ $requestValue = '';
+ } else {
+ $requestValue = $defaultValue;
+ }
+ }
+ }
+ } catch (Exception $e) {
+ throw new Exception(Piwik::translate('General_PleaseSpecifyValue', array($name)));
+ }
+ $finalParameters[] = $requestValue;
+ }
+ return $finalParameters;
+ }
+
+ /**
+ * Includes the class API by looking up plugins/UserSettings/API.php
+ *
+ * @param string $fileName api class name eg. "API"
+ * @throws Exception
+ */
+ private function includeApiFile($fileName)
+ {
+ $module = self::getModuleNameFromClassName($fileName);
+ $path = PIWIK_INCLUDE_PATH . '/plugins/' . $module . '/API.php';
+
+ if (is_readable($path)) {
+ require_once $path; // prefixed by PIWIK_INCLUDE_PATH
+ } else {
+ throw new Exception("API module $module not found.");
+ }
+ }
+
+ /**
+ * @param string $class name of a class
+ * @param ReflectionMethod $method instance of ReflectionMethod
+ */
+ private function loadMethodMetadata($class, $method)
+ {
+ if ($method->isPublic()
+ && !$method->isConstructor()
+ && $method->getName() != 'getInstance'
+ && false === strstr($method->getDocComment(), '@deprecated')
+ && (!$this->hideIgnoredFunctions || false === strstr($method->getDocComment(), '@ignore'))
+ ) {
+ $name = $method->getName();
+ $parameters = $method->getParameters();
+
+ $aParameters = array();
+ foreach ($parameters as $parameter) {
+ $nameVariable = $parameter->getName();
+
+ $defaultValue = $this->noDefaultValue;
+ if ($parameter->isDefaultValueAvailable()) {
+ $defaultValue = $parameter->getDefaultValue();
+ }
+
+ $aParameters[$nameVariable] = $defaultValue;
+ }
+ $this->metadataArray[$class][$name]['parameters'] = $aParameters;
+ $this->metadataArray[$class][$name]['numberOfRequiredParameters'] = $method->getNumberOfRequiredParameters();
+ }
+ }
+
+ /**
+ * Checks that the method exists in the class
+ *
+ * @param string $className The class name
+ * @param string $methodName The method name
+ * @throws Exception If the method is not found
+ */
+ private function checkMethodExists($className, $methodName)
+ {
+ if (!$this->isMethodAvailable($className, $methodName)) {
+ throw new Exception(Piwik::translate('General_ExceptionMethodNotFound', array($methodName, $className)));
+ }
+ }
+
+ /**
+ * Returns the number of required parameters (parameters without default values).
+ *
+ * @param string $class The class name
+ * @param string $name The method name
+ * @return int The number of required parameters
+ */
+ private function getNumberOfRequiredParameters($class, $name)
+ {
+ return $this->metadataArray[$class][$name]['numberOfRequiredParameters'];
+ }
+
+ /**
+ * Returns true if the method is found in the API of the given class name.
+ *
+ * @param string $className The class name
+ * @param string $methodName The method name
+ * @return bool
+ */
+ private function isMethodAvailable($className, $methodName)
+ {
+ return isset($this->metadataArray[$className][$methodName]);
+ }
+
+ /**
+ * Checks that the class is a Singleton (presence of the getInstance() method)
+ *
+ * @param string $className The class name
+ * @throws Exception If the class is not a Singleton
+ */
+ private function checkClassIsSingleton($className)
+ {
+ if (!method_exists($className, "getInstance")) {
+ throw new Exception("$className that provide an API must be Singleton and have a 'static public function getInstance()' method.");
+ }
+ }
+}
+
+/**
+ * To differentiate between "no value" and default value of null
+ *
+ */
+class NoDefaultValue
+{
+}
diff --git a/www/analytics/core/API/Request.php b/www/analytics/core/API/Request.php
new file mode 100644
index 00000000..68cd3fff
--- /dev/null
+++ b/www/analytics/core/API/Request.php
@@ -0,0 +1,398 @@
+process();
+ * echo $result;
+ *
+ * **Getting a unrendered DataTable**
+ *
+ * // use the convenience method 'processRequest'
+ * $dataTable = Request::processRequest('UserSettings.getWideScreen', array(
+ * 'idSite' => 1,
+ * 'date' => 'yesterday',
+ * 'period' => 'week',
+ * 'filter_limit' => 5,
+ * 'filter_offset' => 0
+ *
+ * 'format' => 'original', // this is the important bit
+ * ));
+ * echo "This DataTable has " . $dataTable->getRowsCount() . " rows.";
+ *
+ * @see http://piwik.org/docs/analytics-api
+ * @api
+ */
+class Request
+{
+ protected $request = null;
+
+ /**
+ * Converts the supplied request string into an array of query paramater name/value
+ * mappings. The current query parameters (everything in `$_GET` and `$_POST`) are
+ * forwarded to request array before it is returned.
+ *
+ * @param string|array $request The base request string or array, eg,
+ * `'module=UserSettings&action=getWidescreen'`.
+ * @return array
+ */
+ static public function getRequestArrayFromString($request)
+ {
+ $defaultRequest = $_GET + $_POST;
+
+ $requestRaw = self::getRequestParametersGET();
+ if (!empty($requestRaw['segment'])) {
+ $defaultRequest['segment'] = $requestRaw['segment'];
+ }
+
+ $requestArray = $defaultRequest;
+
+ if (!is_null($request)) {
+ if (is_array($request)) {
+ $url = array();
+ foreach ($request as $key => $value) {
+ $url[] = $key . "=" . $value;
+ }
+ $request = implode("&", $url);
+ }
+
+ $request = trim($request);
+ $request = str_replace(array("\n", "\t"), '', $request);
+
+ $requestParsed = UrlHelper::getArrayFromQueryString($request);
+ $requestArray = $requestParsed + $defaultRequest;
+ }
+
+ foreach ($requestArray as &$element) {
+ if (!is_array($element)) {
+ $element = trim($element);
+ }
+ }
+ return $requestArray;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param string|array $request Query string that defines the API call (must at least contain a **method** parameter),
+ * eg, `'method=UserSettings.getWideScreen&idSite=1&date=yesterday&period=week&format=xml'`
+ * If a request is not provided, then we use the values in the `$_GET` and `$_POST`
+ * superglobals.
+ */
+ public function __construct($request = null)
+ {
+ $this->request = self::getRequestArrayFromString($request);
+ $this->sanitizeRequest();
+ }
+
+ /**
+ * For backward compatibility: Piwik API still works if module=Referers,
+ * we rewrite to correct renamed plugin: Referrers
+ *
+ * @param $module
+ * @return string
+ * @ignore
+ */
+ public static function renameModule($module)
+ {
+ $moduleToRedirect = array(
+ 'Referers' => 'Referrers',
+ 'PDFReports' => 'ScheduledReports',
+ );
+ if (isset($moduleToRedirect[$module])) {
+ return $moduleToRedirect[$module];
+ }
+ return $module;
+ }
+
+ /**
+ * Make sure that the request contains no logical errors
+ */
+ private function sanitizeRequest()
+ {
+ // The label filter does not work with expanded=1 because the data table IDs have a different meaning
+ // depending on whether the table has been loaded yet. expanded=1 causes all tables to be loaded, which
+ // is why the label filter can't descend when a recursive label has been requested.
+ // To fix this, we remove the expanded parameter if a label parameter is set.
+ if (isset($this->request['label']) && !empty($this->request['label'])
+ && isset($this->request['expanded']) && $this->request['expanded']
+ ) {
+ unset($this->request['expanded']);
+ }
+ }
+
+ /**
+ * Dispatches the API request to the appropriate API method and returns the result
+ * after post-processing.
+ *
+ * Post-processing includes:
+ *
+ * - flattening if **flat** is 0
+ * - running generic filters unless **disable_generic_filters** is set to 1
+ * - URL decoding label column values
+ * - running queued filters unless **disable_queued_filters** is set to 1
+ * - removing columns based on the values of the **hideColumns** and **showColumns** query parameters
+ * - filtering rows if the **label** query parameter is set
+ * - converting the result to the appropriate format (ie, XML, JSON, etc.)
+ *
+ * If `'original'` is supplied for the output format, the result is returned as a PHP
+ * object.
+ *
+ * @throws PluginDeactivatedException if the module plugin is not activated.
+ * @throws Exception if the requested API method cannot be called, if required parameters for the
+ * API method are missing or if the API method throws an exception and the **format**
+ * query parameter is **original**.
+ * @return DataTable|Map|string The data resulting from the API call.
+ */
+ public function process()
+ {
+ // read the format requested for the output data
+ $outputFormat = strtolower(Common::getRequestVar('format', 'xml', 'string', $this->request));
+
+ // create the response
+ $response = new ResponseBuilder($outputFormat, $this->request);
+
+ try {
+ // read parameters
+ $moduleMethod = Common::getRequestVar('method', null, 'string', $this->request);
+
+ list($module, $method) = $this->extractModuleAndMethod($moduleMethod);
+
+ $module = $this->renameModule($module);
+
+ if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated($module)) {
+ throw new PluginDeactivatedException($module);
+ }
+ $apiClassName = $this->getClassNameAPI($module);
+
+ self::reloadAuthUsingTokenAuth($this->request);
+
+ // call the method
+ $returnedValue = Proxy::getInstance()->call($apiClassName, $method, $this->request);
+
+ $toReturn = $response->getResponse($returnedValue, $module, $method);
+ } catch (Exception $e) {
+ $toReturn = $response->getResponseException($e);
+ }
+ return $toReturn;
+ }
+
+ /**
+ * Returns the name of a plugin's API class by plugin name.
+ *
+ * @param string $plugin The plugin name, eg, `'Referrers'`.
+ * @return string The fully qualified API class name, eg, `'\Piwik\Plugins\Referrers\API'`.
+ */
+ static public function getClassNameAPI($plugin)
+ {
+ return sprintf('\Piwik\Plugins\%s\API', $plugin);
+ }
+
+ /**
+ * If the token_auth is found in the $request parameter,
+ * the current session will be authenticated using this token_auth.
+ * It will overwrite the previous Auth object.
+ *
+ * @param array $request If null, uses the default request ($_GET)
+ * @return void
+ * @ignore
+ */
+ static public function reloadAuthUsingTokenAuth($request = null)
+ {
+ // if a token_auth is specified in the API request, we load the right permissions
+ $token_auth = Common::getRequestVar('token_auth', '', 'string', $request);
+ if ($token_auth) {
+
+ /**
+ * Triggered when authenticating an API request, but only if the **token_auth**
+ * query parameter is found in the request.
+ *
+ * Plugins that provide authentication capabilities should subscribe to this event
+ * and make sure the global authentication object (the object returned by `Registry::get('auth')`)
+ * is setup to use `$token_auth` when its `authenticate()` method is executed.
+ *
+ * @param string $token_auth The value of the **token_auth** query parameter.
+ */
+ Piwik::postEvent('API.Request.authenticate', array($token_auth));
+ Access::getInstance()->reloadAccess();
+ SettingsServer::raiseMemoryLimitIfNecessary();
+ }
+ }
+
+ /**
+ * Returns array($class, $method) from the given string $class.$method
+ *
+ * @param string $parameter
+ * @throws Exception
+ * @return array
+ */
+ private function extractModuleAndMethod($parameter)
+ {
+ $a = explode('.', $parameter);
+ if (count($a) != 2) {
+ throw new Exception("The method name is invalid. Expected 'module.methodName'");
+ }
+ return $a;
+ }
+
+ /**
+ * Helper method that processes an API request in one line using the variables in `$_GET`
+ * and `$_POST`.
+ *
+ * @param string $method The API method to call, ie, `'Actions.getPageTitles'`.
+ * @param array $paramOverride The parameter name-value pairs to use instead of what's
+ * in `$_GET` & `$_POST`.
+ * @return mixed The result of the API request. See {@link process()}.
+ */
+ public static function processRequest($method, $paramOverride = array())
+ {
+ $params = array();
+ $params['format'] = 'original';
+ $params['module'] = 'API';
+ $params['method'] = $method;
+ $params = $paramOverride + $params;
+
+ // process request
+ $request = new Request($params);
+ return $request->process();
+ }
+
+ /**
+ * Returns the original request parameters in the current query string as an array mapping
+ * query parameter names with values. The result of this function will not be affected
+ * by any modifications to `$_GET` and will not include parameters in `$_POST`.
+ *
+ * @return array
+ */
+ public static function getRequestParametersGET()
+ {
+ if (empty($_SERVER['QUERY_STRING'])) {
+ return array();
+ }
+ $GET = UrlHelper::getArrayFromQueryString($_SERVER['QUERY_STRING']);
+ return $GET;
+ }
+
+ /**
+ * Returns the URL for the current requested report w/o any filter parameters.
+ *
+ * @param string $module The API module.
+ * @param string $action The API action.
+ * @param array $queryParams Query parameter overrides.
+ * @return string
+ */
+ public static function getBaseReportUrl($module, $action, $queryParams = array())
+ {
+ $params = array_merge($queryParams, array('module' => $module, 'action' => $action));
+ return Request::getCurrentUrlWithoutGenericFilters($params);
+ }
+
+ /**
+ * Returns the current URL without generic filter query parameters.
+ *
+ * @param array $params Query parameter values to override in the new URL.
+ * @return string
+ */
+ public static function getCurrentUrlWithoutGenericFilters($params)
+ {
+ // unset all filter query params so the related report will show up in its default state,
+ // unless the filter param was in $queryParams
+ $genericFiltersInfo = DataTableGenericFilter::getGenericFiltersInformation();
+ foreach ($genericFiltersInfo as $filter) {
+ foreach ($filter as $queryParamName => $queryParamInfo) {
+ if (!isset($params[$queryParamName])) {
+ $params[$queryParamName] = null;
+ }
+ }
+ }
+
+ return Url::getCurrentQueryStringWithParametersModified($params);
+ }
+
+ /**
+ * Returns whether the DataTable result will have to be expanded for the
+ * current request before rendering.
+ *
+ * @return bool
+ * @ignore
+ */
+ public static function shouldLoadExpanded()
+ {
+ // if filter_column_recursive & filter_pattern_recursive are supplied, and flat isn't supplied
+ // we have to load all the child subtables.
+ return Common::getRequestVar('filter_column_recursive', false) !== false
+ && Common::getRequestVar('filter_pattern_recursive', false) !== false
+ && !self::shouldLoadFlatten();
+ }
+
+ /**
+ * @return bool
+ */
+ public static function shouldLoadFlatten()
+ {
+ return Common::getRequestVar('flat', false) == 1;
+ }
+
+ /**
+ * Returns the segment query parameter from the original request, without modifications.
+ *
+ * @return array|bool
+ */
+ static public function getRawSegmentFromRequest()
+ {
+ // we need the URL encoded segment parameter, we fetch it from _SERVER['QUERY_STRING'] instead of default URL decoded _GET
+ $segmentRaw = false;
+ $segment = Common::getRequestVar('segment', '', 'string');
+ if (!empty($segment)) {
+ $request = Request::getRequestParametersGET();
+ if (!empty($request['segment'])) {
+ $segmentRaw = $request['segment'];
+ }
+ }
+ return $segmentRaw;
+ }
+}
diff --git a/www/analytics/core/API/ResponseBuilder.php b/www/analytics/core/API/ResponseBuilder.php
new file mode 100644
index 00000000..692a2ceb
--- /dev/null
+++ b/www/analytics/core/API/ResponseBuilder.php
@@ -0,0 +1,478 @@
+request = $request;
+ $this->outputFormat = $outputFormat;
+ }
+
+ /**
+ * This method processes the data resulting from the API call.
+ *
+ * - If the data resulted from the API call is a DataTable then
+ * - we apply the standard filters if the parameters have been found
+ * in the URL. For example to offset,limit the Table you can add the following parameters to any API
+ * call that returns a DataTable: filter_limit=10&filter_offset=20
+ * - we apply the filters that have been previously queued on the DataTable
+ * @see DataTable::queueFilter()
+ * - we apply the renderer that generate the DataTable in a given format (XML, PHP, HTML, JSON, etc.)
+ * the format can be changed using the 'format' parameter in the request.
+ * Example: format=xml
+ *
+ * - If there is nothing returned (void) we display a standard success message
+ *
+ * - If there is a PHP array returned, we try to convert it to a dataTable
+ * It is then possible to convert this datatable to any requested format (xml/etc)
+ *
+ * - If a bool is returned we convert to a string (true is displayed as 'true' false as 'false')
+ *
+ * - If an integer / float is returned, we simply return it
+ *
+ * @param mixed $value The initial returned value, before post process. If set to null, success response is returned.
+ * @param bool|string $apiModule The API module that was called
+ * @param bool|string $apiMethod The API method that was called
+ * @return mixed Usually a string, but can still be a PHP data structure if the format requested is 'original'
+ */
+ public function getResponse($value = null, $apiModule = false, $apiMethod = false)
+ {
+ $this->apiModule = $apiModule;
+ $this->apiMethod = $apiMethod;
+
+ if($this->outputFormat == 'original') {
+ @header('Content-Type: text/plain; charset=utf-8');
+ }
+ return $this->renderValue($value);
+ }
+
+ /**
+ * Returns an error $message in the requested $format
+ *
+ * @param Exception $e
+ * @throws Exception
+ * @return string
+ */
+ public function getResponseException(Exception $e)
+ {
+ $format = strtolower($this->outputFormat);
+
+ if ($format == 'original') {
+ throw $e;
+ }
+
+ try {
+ $renderer = Renderer::factory($format);
+ } catch (Exception $exceptionRenderer) {
+ return "Error: " . $e->getMessage() . " and: " . $exceptionRenderer->getMessage();
+ }
+
+ $e = $this->decorateExceptionWithDebugTrace($e);
+
+ $renderer->setException($e);
+
+ if ($format == 'php') {
+ $renderer->setSerialize($this->caseRendererPHPSerialize());
+ }
+
+ return $renderer->renderException();
+ }
+
+ /**
+ * @param $value
+ * @return string
+ */
+ protected function renderValue($value)
+ {
+ // when null or void is returned from the api call, we handle it as a successful operation
+ if (!isset($value)) {
+ return $this->handleSuccess();
+ }
+
+ // If the returned value is an object DataTable we
+ // apply the set of generic filters if asked in the URL
+ // and we render the DataTable according to the format specified in the URL
+ if ($value instanceof DataTable
+ || $value instanceof DataTable\Map
+ ) {
+ return $this->handleDataTable($value);
+ }
+
+ // Case an array is returned from the API call, we convert it to the requested format
+ // - if calling from inside the application (format = original)
+ // => the data stays unchanged (ie. a standard php array or whatever data structure)
+ // - if any other format is requested, we have to convert this data structure (which we assume
+ // to be an array) to a DataTable in order to apply the requested DataTable_Renderer (for example XML)
+ if (is_array($value)) {
+ return $this->handleArray($value);
+ }
+
+ // original data structure requested, we return without process
+ if ($this->outputFormat == 'original') {
+ return $value;
+ }
+
+ if (is_object($value)
+ || is_resource($value)
+ ) {
+ return $this->getResponseException(new Exception('The API cannot handle this data structure.'));
+ }
+
+ // bool // integer // float // serialized object
+ return $this->handleScalar($value);
+ }
+
+ /**
+ * @param Exception $e
+ * @return Exception
+ */
+ protected function decorateExceptionWithDebugTrace(Exception $e)
+ {
+ // If we are in tests, show full backtrace
+ if (defined('PIWIK_PATH_TEST_TO_ROOT')) {
+ if (\Piwik_ShouldPrintBackTraceWithMessage()) {
+ $message = $e->getMessage() . " in \n " . $e->getFile() . ":" . $e->getLine() . " \n " . $e->getTraceAsString();
+ } else {
+ $message = $e->getMessage() . "\n \n --> To temporarily debug this error further, set const PIWIK_PRINT_ERROR_BACKTRACE=true; in index.php";
+ }
+ return new Exception($message);
+ }
+ return $e;
+ }
+
+ /**
+ * Returns true if the user requested to serialize the output data (&serialize=1 in the request)
+ *
+ * @param mixed $defaultSerializeValue Default value in case the user hasn't specified a value
+ * @return bool
+ */
+ protected function caseRendererPHPSerialize($defaultSerializeValue = 1)
+ {
+ $serialize = Common::getRequestVar('serialize', $defaultSerializeValue, 'int', $this->request);
+ if ($serialize) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Apply the specified renderer to the DataTable
+ *
+ * @param DataTable|array $dataTable
+ * @return string
+ */
+ protected function getRenderedDataTable($dataTable)
+ {
+ $format = strtolower($this->outputFormat);
+
+ // if asked for original dataStructure
+ if ($format == 'original') {
+ // by default "original" data is not serialized
+ if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) {
+ $dataTable = serialize($dataTable);
+ }
+ return $dataTable;
+ }
+
+ $method = Common::getRequestVar('method', '', 'string', $this->request);
+
+ $renderer = Renderer::factory($format);
+ $renderer->setTable($dataTable);
+ $renderer->setRenderSubTables(Common::getRequestVar('expanded', false, 'int', $this->request));
+ $renderer->setHideIdSubDatableFromResponse(Common::getRequestVar('hideIdSubDatable', false, 'int', $this->request));
+
+ if ($format == 'php') {
+ $renderer->setSerialize($this->caseRendererPHPSerialize());
+ $renderer->setPrettyDisplay(Common::getRequestVar('prettyDisplay', false, 'int', $this->request));
+ } else if ($format == 'html') {
+ $renderer->setTableId($this->request['method']);
+ } else if ($format == 'csv' || $format == 'tsv') {
+ $renderer->setConvertToUnicode(Common::getRequestVar('convertToUnicode', true, 'int', $this->request));
+ }
+
+ // prepare translation of column names
+ if ($format == 'html' || $format == 'csv' || $format == 'tsv' || $format = 'rss') {
+ $renderer->setApiMethod($method);
+ $renderer->setIdSite(Common::getRequestVar('idSite', false, 'int', $this->request));
+ $renderer->setTranslateColumnNames(Common::getRequestVar('translateColumnNames', false, 'int', $this->request));
+ }
+
+ return $renderer->render();
+ }
+
+ /**
+ * Returns a success $message in the requested $format
+ *
+ * @param string $message
+ * @return string
+ */
+ protected function handleSuccess($message = 'ok')
+ {
+ // return a success message only if no content has already been buffered, useful when APIs return raw text or html content to the browser
+ if (!ob_get_contents()) {
+ switch ($this->outputFormat) {
+ case 'xml':
+ @header("Content-Type: text/xml;charset=utf-8");
+ $return =
+ "\n" .
+ "\n" .
+ "\t \n" .
+ " ";
+ break;
+ case 'json':
+ @header("Content-Type: application/json");
+ $return = '{"result":"success", "message":"' . $message . '"}';
+ break;
+ case 'php':
+ $return = array('result' => 'success', 'message' => $message);
+ if ($this->caseRendererPHPSerialize()) {
+ $return = serialize($return);
+ }
+ break;
+
+ case 'csv':
+ @header("Content-Type: application/vnd.ms-excel");
+ @header("Content-Disposition: attachment; filename=piwik-report-export.csv");
+ $return = "message\n" . $message;
+ break;
+
+ default:
+ $return = 'Success:' . $message;
+ break;
+ }
+ return $return;
+ }
+ }
+
+ /**
+ * Converts the given scalar to an data table
+ *
+ * @param mixed $scalar
+ * @return string
+ */
+ protected function handleScalar($scalar)
+ {
+ $dataTable = new Simple();
+ $dataTable->addRowsFromArray(array($scalar));
+ return $this->getRenderedDataTable($dataTable);
+ }
+
+ /**
+ * Handles the given data table
+ *
+ * @param DataTable $datatable
+ * @return string
+ */
+ protected function handleDataTable($datatable)
+ {
+ // if requested, flatten nested tables
+ if (Common::getRequestVar('flat', '0', 'string', $this->request) == '1') {
+ $flattener = new Flattener($this->apiModule, $this->apiMethod, $this->request);
+ if (Common::getRequestVar('include_aggregate_rows', '0', 'string', $this->request) == '1') {
+ $flattener->includeAggregateRows();
+ }
+ $datatable = $flattener->flatten($datatable);
+ }
+
+ if (1 == Common::getRequestVar('totals', '1', 'integer', $this->request)) {
+ $genericFilter = new ReportTotalsCalculator($this->apiModule, $this->apiMethod, $this->request);
+ $datatable = $genericFilter->calculate($datatable);
+ }
+
+ // if the flag disable_generic_filters is defined we skip the generic filters
+ if (0 == Common::getRequestVar('disable_generic_filters', '0', 'string', $this->request)) {
+ $genericFilter = new DataTableGenericFilter($this->request);
+ $genericFilter->filter($datatable);
+ }
+
+ // we automatically safe decode all datatable labels (against xss)
+ $datatable->queueFilter('SafeDecodeLabel');
+
+ // if the flag disable_queued_filters is defined we skip the filters that were queued
+ if (Common::getRequestVar('disable_queued_filters', 0, 'int', $this->request) == 0) {
+ $datatable->applyQueuedFilters();
+ }
+
+ // use the ColumnDelete filter if hideColumns/showColumns is provided (must be done
+ // after queued filters are run so processed metrics can be removed, too)
+ $hideColumns = Common::getRequestVar('hideColumns', '', 'string', $this->request);
+ $showColumns = Common::getRequestVar('showColumns', '', 'string', $this->request);
+ if ($hideColumns !== '' || $showColumns !== '') {
+ $datatable->filter('ColumnDelete', array($hideColumns, $showColumns));
+ }
+
+ // apply label filter: only return rows matching the label parameter (more than one if more than one label)
+ $label = $this->getLabelFromRequest($this->request);
+ if (!empty($label)) {
+ $addLabelIndex = Common::getRequestVar('labelFilterAddLabelIndex', 0, 'int', $this->request) == 1;
+
+ $filter = new LabelFilter($this->apiModule, $this->apiMethod, $this->request);
+ $datatable = $filter->filter($label, $datatable, $addLabelIndex);
+ }
+ return $this->getRenderedDataTable($datatable);
+ }
+
+ /**
+ * Converts the given simple array to a data table
+ *
+ * @param array $array
+ * @return string
+ */
+ protected function handleArray($array)
+ {
+ if ($this->outputFormat == 'original') {
+ // we handle the serialization. Because some php array have a very special structure that
+ // couldn't be converted with the automatic DataTable->addRowsFromSimpleArray
+ // the user may want to request the original PHP data structure serialized by the API
+ // in case he has to setup serialize=1 in the URL
+ if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) {
+ return serialize($array);
+ }
+ return $array;
+ }
+
+ $multiDimensional = $this->handleMultiDimensionalArray($array);
+ if ($multiDimensional !== false) {
+ return $multiDimensional;
+ }
+
+ return $this->getRenderedDataTable($array);
+ }
+
+ /**
+ * Is this a multi dimensional array?
+ * Multi dim arrays are not supported by the Datatable renderer.
+ * We manually render these.
+ *
+ * array(
+ * array(
+ * 1,
+ * 2 => array( 1,
+ * 2
+ * )
+ * ),
+ * array( 2,
+ * 3
+ * )
+ * );
+ *
+ * @param array $array
+ * @return string|bool false if it isn't a multidim array
+ */
+ protected function handleMultiDimensionalArray($array)
+ {
+ $first = reset($array);
+ foreach ($array as $first) {
+ if (is_array($first)) {
+ foreach ($first as $key => $value) {
+ // Yes, this is a multi dim array
+ if (is_array($value)) {
+ switch ($this->outputFormat) {
+ case 'json':
+ @header("Content-Type: application/json");
+ return self::convertMultiDimensionalArrayToJson($array);
+ break;
+
+ case 'php':
+ if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) {
+ return serialize($array);
+ }
+ return $array;
+
+ case 'xml':
+ @header("Content-Type: text/xml;charset=utf-8");
+ return $this->getRenderedDataTable($array);
+ default:
+ break;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Render a multidimensional array to Json
+ * Handle DataTable|Set elements in the first dimension only, following case does not work:
+ * array(
+ * array(
+ * DataTable,
+ * 2 => array(
+ * 1,
+ * 2
+ * ),
+ * ),
+ * );
+ *
+ * @param array $array can contain scalar, arrays, DataTable and Set
+ * @return string
+ */
+ public static function convertMultiDimensionalArrayToJson($array)
+ {
+ $jsonRenderer = new Json();
+ $jsonRenderer->setTable($array);
+ $renderedReport = $jsonRenderer->render();
+ return $renderedReport;
+ }
+
+ /**
+ * Returns the value for the label query parameter which can be either a string
+ * (ie, label=...) or array (ie, label[]=...).
+ *
+ * @param array $request
+ * @return array
+ */
+ static public function getLabelFromRequest($request)
+ {
+ $label = Common::getRequestVar('label', array(), 'array', $request);
+ if (empty($label)) {
+ $label = Common::getRequestVar('label', '', 'string', $request);
+ if (!empty($label)) {
+ $label = array($label);
+ }
+ }
+
+ $label = self::unsanitizeLabelParameter($label);
+ return $label;
+ }
+
+ static public function unsanitizeLabelParameter($label)
+ {
+ // this is needed because Proxy uses Common::getRequestVar which in turn
+ // uses Common::sanitizeInputValue. This causes the > that separates recursive labels
+ // to become > and we need to undo that here.
+ $label = Common::unsanitizeInputValues($label);
+ return $label;
+ }
+}
diff --git a/www/analytics/core/Access.php b/www/analytics/core/Access.php
new file mode 100644
index 00000000..71a5dd5f
--- /dev/null
+++ b/www/analytics/core/Access.php
@@ -0,0 +1,418 @@
+idsitesByAccess = array(
+ 'view' => array(),
+ 'admin' => array(),
+ 'superuser' => array()
+ );
+ }
+
+ /**
+ * Loads the access levels for the current user.
+ *
+ * Calls the authentication method to try to log the user in the system.
+ * If the user credentials are not correct we don't load anything.
+ * If the login/password is correct the user is either the SuperUser or a normal user.
+ * We load the access levels for this user for all the websites.
+ *
+ * @param null|Auth $auth Auth adapter
+ * @return bool true on success, false if reloading access failed (when auth object wasn't specified and user is not enforced to be Super User)
+ */
+ public function reloadAccess(Auth $auth = null)
+ {
+ if (!is_null($auth)) {
+ $this->auth = $auth;
+ }
+
+ // if the Auth wasn't set, we may be in the special case of setSuperUser(), otherwise we fail
+ if (is_null($this->auth)) {
+ if ($this->hasSuperUserAccess()) {
+ return $this->reloadAccessSuperUser();
+ }
+ return false;
+ }
+
+ // access = array ( idsite => accessIdSite, idsite2 => accessIdSite2)
+ $result = $this->auth->authenticate();
+
+ if (!$result->wasAuthenticationSuccessful()) {
+ return false;
+ }
+ $this->login = $result->getIdentity();
+ $this->token_auth = $result->getTokenAuth();
+
+ // case the superUser is logged in
+ if ($result->hasSuperUserAccess()) {
+ return $this->reloadAccessSuperUser();
+ }
+ // in case multiple calls to API using different tokens, we ensure we reset it as not SU
+ $this->setSuperUserAccess(false);
+
+ // we join with site in case there are rows in access for an idsite that doesn't exist anymore
+ // (backward compatibility ; before we deleted the site without deleting rows in _access table)
+ $accessRaw = $this->getRawSitesWithSomeViewAccess($this->login);
+ foreach ($accessRaw as $access) {
+ $this->idsitesByAccess[$access['access']][] = $access['idsite'];
+ }
+ return true;
+ }
+
+ public function getRawSitesWithSomeViewAccess($login)
+ {
+ return Db::fetchAll(self::getSqlAccessSite("access, t2.idsite"), $login);
+ }
+
+ /**
+ * Returns the SQL query joining sites and access table for a given login
+ *
+ * @param string $select Columns or expression to SELECT FROM table, eg. "MIN(ts_created)"
+ * @return string SQL query
+ */
+ public static function getSqlAccessSite($select)
+ {
+ return "SELECT " . $select . "
+ FROM " . Common::prefixTable('access') . " as t1
+ JOIN " . Common::prefixTable('site') . " as t2 USING (idsite) " .
+ " WHERE login = ?";
+ }
+
+ /**
+ * Reload Super User access
+ *
+ * @return bool
+ */
+ protected function reloadAccessSuperUser()
+ {
+ $this->hasSuperUserAccess = true;
+
+ try {
+ $allSitesId = Plugins\SitesManager\API::getInstance()->getAllSitesId();
+ } catch (\Exception $e) {
+ $allSitesId = array();
+ }
+ $this->idsitesByAccess['superuser'] = $allSitesId;
+
+ return true;
+ }
+
+ /**
+ * We bypass the normal auth method and give the current user Super User rights.
+ * This should be very carefully used.
+ *
+ * @param bool $bool
+ */
+ public function setSuperUserAccess($bool = true)
+ {
+ if ($bool) {
+ $this->reloadAccessSuperUser();
+ } else {
+ $this->hasSuperUserAccess = false;
+ $this->idsitesByAccess['superuser'] = array();
+
+ }
+ }
+
+ /**
+ * Returns true if the current user is logged in as the Super User
+ *
+ * @return bool
+ */
+ public function hasSuperUserAccess()
+ {
+ return $this->hasSuperUserAccess;
+ }
+
+ /**
+ * Returns the current user login
+ *
+ * @return string|null
+ */
+ public function getLogin()
+ {
+ return $this->login;
+ }
+
+ /**
+ * Returns the token_auth used to authenticate this user in the API
+ *
+ * @return string|null
+ */
+ public function getTokenAuth()
+ {
+ return $this->token_auth;
+ }
+
+ /**
+ * Returns an array of ID sites for which the user has at least a VIEW access.
+ * Which means VIEW or ADMIN or SUPERUSER.
+ *
+ * @return array Example if the user is ADMIN for 4
+ * and has VIEW access for 1 and 7, it returns array(1, 4, 7);
+ */
+ public function getSitesIdWithAtLeastViewAccess()
+ {
+ return array_unique(array_merge(
+ $this->idsitesByAccess['view'],
+ $this->idsitesByAccess['admin'],
+ $this->idsitesByAccess['superuser'])
+ );
+ }
+
+ /**
+ * Returns an array of ID sites for which the user has an ADMIN access.
+ *
+ * @return array Example if the user is ADMIN for 4 and 8
+ * and has VIEW access for 1 and 7, it returns array(4, 8);
+ */
+ public function getSitesIdWithAdminAccess()
+ {
+ return array_unique(array_merge(
+ $this->idsitesByAccess['admin'],
+ $this->idsitesByAccess['superuser'])
+ );
+ }
+
+
+ /**
+ * Returns an array of ID sites for which the user has a VIEW access only.
+ *
+ * @return array Example if the user is ADMIN for 4
+ * and has VIEW access for 1 and 7, it returns array(1, 7);
+ * @see getSitesIdWithAtLeastViewAccess()
+ */
+ public function getSitesIdWithViewAccess()
+ {
+ return $this->idsitesByAccess['view'];
+ }
+
+ /**
+ * Throws an exception if the user is not the SuperUser
+ *
+ * @throws \Piwik\NoAccessException
+ */
+ public function checkUserHasSuperUserAccess()
+ {
+ if (!$this->hasSuperUserAccess()) {
+ throw new NoAccessException(Piwik::translate('General_ExceptionPrivilege', array("'superuser'")));
+ }
+ }
+
+ /**
+ * If the user doesn't have an ADMIN access for at least one website, throws an exception
+ *
+ * @throws \Piwik\NoAccessException
+ */
+ public function checkUserHasSomeAdminAccess()
+ {
+ if ($this->hasSuperUserAccess()) {
+ return;
+ }
+ $idSitesAccessible = $this->getSitesIdWithAdminAccess();
+ if (count($idSitesAccessible) == 0) {
+ throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('admin')));
+ }
+ }
+
+ /**
+ * If the user doesn't have any view permission, throw exception
+ *
+ * @throws \Piwik\NoAccessException
+ */
+ public function checkUserHasSomeViewAccess()
+ {
+ if ($this->hasSuperUserAccess()) {
+ return;
+ }
+ $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess();
+ if (count($idSitesAccessible) == 0) {
+ throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAtLeastOneWebsite', array('view')));
+ }
+ }
+
+ /**
+ * This method checks that the user has ADMIN access for the given list of websites.
+ * If the user doesn't have ADMIN access for at least one website of the list, we throw an exception.
+ *
+ * @param int|array $idSites List of ID sites to check
+ * @throws \Piwik\NoAccessException If for any of the websites the user doesn't have an ADMIN access
+ */
+ public function checkUserHasAdminAccess($idSites)
+ {
+ if ($this->hasSuperUserAccess()) {
+ return;
+ }
+ $idSites = $this->getIdSites($idSites);
+ $idSitesAccessible = $this->getSitesIdWithAdminAccess();
+ foreach ($idSites as $idsite) {
+ if (!in_array($idsite, $idSitesAccessible)) {
+ throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'admin'", $idsite)));
+ }
+ }
+ }
+
+ /**
+ * This method checks that the user has VIEW or ADMIN access for the given list of websites.
+ * If the user doesn't have VIEW or ADMIN access for at least one website of the list, we throw an exception.
+ *
+ * @param int|array|string $idSites List of ID sites to check (integer, array of integers, string comma separated list of integers)
+ * @throws \Piwik\NoAccessException If for any of the websites the user doesn't have an VIEW or ADMIN access
+ */
+ public function checkUserHasViewAccess($idSites)
+ {
+ if ($this->hasSuperUserAccess()) {
+ return;
+ }
+ $idSites = $this->getIdSites($idSites);
+ $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess();
+ foreach ($idSites as $idsite) {
+ if (!in_array($idsite, $idSitesAccessible)) {
+ throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'view'", $idsite)));
+ }
+ }
+ }
+
+ /**
+ * @param int|array|string $idSites
+ * @return array
+ * @throws \Piwik\NoAccessException
+ */
+ protected function getIdSites($idSites)
+ {
+ if ($idSites === 'all') {
+ $idSites = $this->getSitesIdWithAtLeastViewAccess();
+ }
+
+ $idSites = Site::getIdSitesFromIdSitesString($idSites);
+ if (empty($idSites)) {
+ throw new NoAccessException("The parameter 'idSite=' is missing from the request.");
+ }
+ return $idSites;
+ }
+}
+
+/**
+ * Exception thrown when a user doesn't have sufficient access to a resource.
+ *
+ * @api
+ */
+class NoAccessException extends \Exception
+{
+}
diff --git a/www/analytics/core/Archive.php b/www/analytics/core/Archive.php
new file mode 100644
index 00000000..e5884aea
--- /dev/null
+++ b/www/analytics/core/Archive.php
@@ -0,0 +1,809 @@
+getDataTableFromNumeric(array('nb_visits', 'nb_actions'));
+ *
+ * // all sites and multiple dates
+ * $archive = Archive::build($idSite = 'all', $period = 'month', $date = '2013-01-02,2013-03-08');
+ * return $archive->getDataTableFromNumeric(array('nb_visits', 'nb_actions'));
+ *
+ * **_Querying and using metrics immediately_**
+ *
+ * // one site and one period
+ * $archive = Archive::build($idSite = 1, $period = 'week', $date = '2013-03-08');
+ * $data = $archive->getNumeric(array('nb_visits', 'nb_actions'));
+ *
+ * $visits = $data['nb_visits'];
+ * $actions = $data['nb_actions'];
+ *
+ * // ... do something w/ metric data ...
+ *
+ * // multiple sites and multiple dates
+ * $archive = Archive::build($idSite = '1,2,3', $period = 'month', $date = '2013-01-02,2013-03-08');
+ * $data = $archive->getNumeric('nb_visits');
+ *
+ * $janSite1Visits = $data['1']['2013-01-01,2013-01-31']['nb_visits'];
+ * $febSite1Visits = $data['1']['2013-02-01,2013-02-28']['nb_visits'];
+ * // ... etc.
+ *
+ * **_Querying for reports_**
+ *
+ * $archive = Archive::build($idSite = 1, $period = 'week', $date = '2013-03-08');
+ * $dataTable = $archive->getDataTable('MyPlugin_MyReport');
+ * // ... manipulate $dataTable ...
+ * return $dataTable;
+ *
+ * **_Querying a report for an API method_**
+ *
+ * public function getMyReport($idSite, $period, $date, $segment = false, $expanded = false)
+ * {
+ * $dataTable = Archive::getDataTableFromArchive('MyPlugin_MyReport', $idSite, $period, $date, $segment, $expanded);
+ * $dataTable->queueFilter('ReplaceColumnNames');
+ * return $dataTable;
+ * }
+ *
+ * **_Querying data for multiple range periods_**
+ *
+ * // get data for first range
+ * $archive = Archive::build($idSite = 1, $period = 'range', $date = '2013-03-08,2013-03-12');
+ * $dataTable = $archive->getDataTableFromNumeric(array('nb_visits', 'nb_actions'));
+ *
+ * // get data for second range
+ * $archive = Archive::build($idSite = 1, $period = 'range', $date = '2013-03-15,2013-03-20');
+ * $dataTable = $archive->getDataTableFromNumeric(array('nb_visits', 'nb_actions'));
+ *
+ *
+ * [1]: The archiving process will not be launched if browser archiving is disabled
+ * and the current request came from a browser (and not the **archive.php** cron
+ * script).
+ *
+ *
+ * @api
+ */
+class Archive
+{
+ const REQUEST_ALL_WEBSITES_FLAG = 'all';
+ const ARCHIVE_ALL_PLUGINS_FLAG = 'all';
+ const ID_SUBTABLE_LOAD_ALL_SUBTABLES = 'all';
+
+ /**
+ * List of archive IDs for the site, periods and segment we are querying with.
+ * Archive IDs are indexed by done flag and period, ie:
+ *
+ * array(
+ * 'done.Referrers' => array(
+ * '2010-01-01' => 1,
+ * '2010-01-02' => 2,
+ * ),
+ * 'done.VisitsSummary' => array(
+ * '2010-01-01' => 3,
+ * '2010-01-02' => 4,
+ * ),
+ * )
+ *
+ * or,
+ *
+ * array(
+ * 'done.all' => array(
+ * '2010-01-01' => 1,
+ * '2010-01-02' => 2
+ * )
+ * )
+ *
+ * @var array
+ */
+ private $idarchives = array();
+
+ /**
+ * If set to true, the result of all get functions (ie, getNumeric, getBlob, etc.)
+ * will be indexed by the site ID, even if we're only querying data for one site.
+ *
+ * @var bool
+ */
+ private $forceIndexedBySite;
+
+ /**
+ * If set to true, the result of all get functions (ie, getNumeric, getBlob, etc.)
+ * will be indexed by the period, even if we're only querying data for one period.
+ *
+ * @var bool
+ */
+ private $forceIndexedByDate;
+
+ /**
+ * @var Parameters
+ */
+ private $params;
+
+ /**
+ * @param Parameters $params
+ * @param bool $forceIndexedBySite Whether to force index the result of a query by site ID.
+ * @param bool $forceIndexedByDate Whether to force index the result of a query by period.
+ */
+ protected function __construct(Parameters $params, $forceIndexedBySite = false,
+ $forceIndexedByDate = false)
+ {
+ $this->params = $params;
+ $this->forceIndexedBySite = $forceIndexedBySite;
+ $this->forceIndexedByDate = $forceIndexedByDate;
+ }
+
+ /**
+ * Returns a new Archive instance that will query archive data for the given set of
+ * sites and periods, using an optional Segment.
+ *
+ * This method uses data that is found in query parameters, so the parameters to this
+ * function can be string values.
+ *
+ * If you want to create an Archive instance with an array of Period instances, use
+ * {@link Archive::factory()}.
+ *
+ * @param string|int|array $idSites A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
+ * or `'all'`.
+ * @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
+ * @param Date|string $strDate 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
+ * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
+ * @param bool|false|string $segment Segment definition or false if no segment should be used. {@link Piwik\Segment}
+ * @param bool|false|string $_restrictSitesToLogin Used only when running as a scheduled task.
+ * @param bool $skipAggregationOfSubTables Whether the archive, when it is processed, should also aggregate all sub-tables
+ * @return Archive
+ */
+ public static function build($idSites, $period, $strDate, $segment = false, $_restrictSitesToLogin = false, $skipAggregationOfSubTables = false)
+ {
+ $websiteIds = Site::getIdSitesFromIdSitesString($idSites, $_restrictSitesToLogin);
+
+ if (Period::isMultiplePeriod($strDate, $period)) {
+ $oPeriod = new Range($period, $strDate);
+ $allPeriods = $oPeriod->getSubperiods();
+ } else {
+ $timezone = count($websiteIds) == 1 ? Site::getTimezoneFor($websiteIds[0]) : false;
+ $oPeriod = Period::makePeriodFromQueryParams($timezone, $period, $strDate);
+ $allPeriods = array($oPeriod);
+ }
+ $segment = new Segment($segment, $websiteIds);
+ $idSiteIsAll = $idSites == self::REQUEST_ALL_WEBSITES_FLAG;
+ $isMultipleDate = Period::isMultiplePeriod($strDate, $period);
+ return Archive::factory($segment, $allPeriods, $websiteIds, $idSiteIsAll, $isMultipleDate, $skipAggregationOfSubTables);
+ }
+
+ /**
+ * Returns a new Archive instance that will query archive data for the given set of
+ * sites and periods, using an optional segment.
+ *
+ * This method uses an array of Period instances and a Segment instance, instead of strings
+ * like {@link build()}.
+ *
+ * If you want to create an Archive instance using data found in query parameters,
+ * use {@link build()}.
+ *
+ * @param Segment $segment The segment to use. For no segment, use `new Segment('', $idSites)`.
+ * @param array $periods An array of Period instances.
+ * @param array $idSites An array of site IDs (eg, `array(1, 2, 3)`).
+ * @param bool $idSiteIsAll Whether `'all'` sites are being queried or not. If true, then
+ * the result of querying functions will be indexed by site, regardless
+ * of whether `count($idSites) == 1`.
+ * @param bool $isMultipleDate Whether multiple dates are being queried or not. If true, then
+ * the result of querying functions will be indexed by period,
+ * regardless of whether `count($periods) == 1`.
+ * @param bool $skipAggregationOfSubTables Whether the archive should skip aggregation of all sub-tables
+ *
+ * @return Archive
+ */
+ public static function factory(Segment $segment, array $periods, array $idSites, $idSiteIsAll = false, $isMultipleDate = false, $skipAggregationOfSubTables = false)
+ {
+ $forceIndexedBySite = false;
+ $forceIndexedByDate = false;
+ if ($idSiteIsAll || count($idSites) > 1) {
+ $forceIndexedBySite = true;
+ }
+ if (count($periods) > 1 || $isMultipleDate) {
+ $forceIndexedByDate = true;
+ }
+
+ $params = new Parameters($idSites, $periods, $segment, $skipAggregationOfSubTables);
+
+ return new Archive($params, $forceIndexedBySite, $forceIndexedByDate);
+ }
+
+ /**
+ * Queries and returns metric data in an array.
+ *
+ * If multiple sites were requested in {@link build()} or {@link factory()} the result will
+ * be indexed by site ID.
+ *
+ * If multiple periods were requested in {@link build()} or {@link factory()} the result will
+ * be indexed by period.
+ *
+ * The site ID index is always first, so if multiple sites & periods were requested, the result
+ * will be indexed by site ID first, then period.
+ *
+ * @param string|array $names One or more archive names, eg, `'nb_visits'`, `'Referrers_distinctKeywords'`,
+ * etc.
+ * @return false|numeric|array `false` if there is no data to return, a single numeric value if we're not querying
+ * for multiple sites/periods, or an array if multiple sites, periods or names are
+ * queried for.
+ */
+ public function getNumeric($names)
+ {
+ $data = $this->get($names, 'numeric');
+
+ $resultIndices = $this->getResultIndices();
+ $result = $data->getIndexedArray($resultIndices);
+
+ // if only one metric is returned, just return it as a numeric value
+ if (empty($resultIndices)
+ && count($result) <= 1
+ && (!is_array($names) || count($names) == 1)
+ ) {
+ $result = (float)reset($result); // convert to float in case $result is empty
+ }
+
+ return $result;
+ }
+
+ /**
+ * Queries and returns blob data in an array.
+ *
+ * Reports are stored in blobs as serialized arrays of {@link DataTable\Row} instances, but this
+ * data can technically be anything. In other words, you can store whatever you want
+ * as archive data blobs.
+ *
+ * If multiple sites were requested in {@link build()} or {@link factory()} the result will
+ * be indexed by site ID.
+ *
+ * If multiple periods were requested in {@link build()} or {@link factory()} the result will
+ * be indexed by period.
+ *
+ * The site ID index is always first, so if multiple sites & periods were requested, the result
+ * will be indexed by site ID first, then period.
+ *
+ * @param string|array $names One or more archive names, eg, `'Referrers_keywordBySearchEngine'`.
+ * @param null|string $idSubtable If we're returning serialized DataTable data, then this refers
+ * to the subtable ID to return. If set to 'all', all subtables
+ * of each requested report are returned.
+ * @return array An array of appropriately indexed blob data.
+ */
+ public function getBlob($names, $idSubtable = null)
+ {
+ $data = $this->get($names, 'blob', $idSubtable);
+ return $data->getIndexedArray($this->getResultIndices());
+ }
+
+ /**
+ * Queries and returns metric data in a DataTable instance.
+ *
+ * If multiple sites were requested in {@link build()} or {@link factory()} the result will
+ * be a DataTable\Map that is indexed by site ID.
+ *
+ * If multiple periods were requested in {@link build()} or {@link factory()} the result will
+ * be a {@link DataTable\Map} that is indexed by period.
+ *
+ * The site ID index is always first, so if multiple sites & periods were requested, the result
+ * will be a {@link DataTable\Map} indexed by site ID which contains {@link DataTable\Map} instances that are
+ * indexed by period.
+ *
+ * _Note: Every DataTable instance returned will have at most one row in it. The contents of each
+ * row will be the requested metrics for the appropriate site and period._
+ *
+ * @param string|array $names One or more archive names, eg, 'nb_visits', 'Referrers_distinctKeywords',
+ * etc.
+ * @return DataTable|DataTable\Map A DataTable if multiple sites and periods were not requested.
+ * An appropriately indexed DataTable\Map if otherwise.
+ */
+ public function getDataTableFromNumeric($names)
+ {
+ $data = $this->get($names, 'numeric');
+ return $data->getDataTable($this->getResultIndices());
+ }
+
+ /**
+ * Queries and returns one or more reports as DataTable instances.
+ *
+ * This method will query blob data that is a serialized array of of {@link DataTable\Row}'s and
+ * unserialize it.
+ *
+ * If multiple sites were requested in {@link build()} or {@link factory()} the result will
+ * be a {@link DataTable\Map} that is indexed by site ID.
+ *
+ * If multiple periods were requested in {@link build()} or {@link factory()} the result will
+ * be a DataTable\Map that is indexed by period.
+ *
+ * The site ID index is always first, so if multiple sites & periods were requested, the result
+ * will be a {@link DataTable\Map} indexed by site ID which contains {@link DataTable\Map} instances that are
+ * indexed by period.
+ *
+ * @param string $name The name of the record to get. This method can only query one record at a time.
+ * @param int|string|null $idSubtable The ID of the subtable to get (if any).
+ * @return DataTable|DataTable\Map A DataTable if multiple sites and periods were not requested.
+ * An appropriately indexed {@link DataTable\Map} if otherwise.
+ */
+ public function getDataTable($name, $idSubtable = null)
+ {
+ $data = $this->get($name, 'blob', $idSubtable);
+ return $data->getDataTable($this->getResultIndices());
+ }
+
+ /**
+ * Queries and returns one report with all of its subtables loaded.
+ *
+ * If multiple sites were requested in {@link build()} or {@link factory()} the result will
+ * be a DataTable\Map that is indexed by site ID.
+ *
+ * If multiple periods were requested in {@link build()} or {@link factory()} the result will
+ * be a DataTable\Map that is indexed by period.
+ *
+ * The site ID index is always first, so if multiple sites & periods were requested, the result
+ * will be a {@link DataTable\Map indexed} by site ID which contains {@link DataTable\Map} instances that are
+ * indexed by period.
+ *
+ * @param string $name The name of the record to get.
+ * @param int|string|null $idSubtable The ID of the subtable to get (if any). The subtable will be expanded.
+ * @param int|null $depth The maximum number of subtable levels to load. If null, all levels are loaded.
+ * For example, if `1` is supplied, then the DataTable returned will have its subtables
+ * loaded. Those subtables, however, will NOT have their subtables loaded.
+ * @param bool $addMetadataSubtableId Whether to add the database subtable ID as metadata to each datatable,
+ * or not.
+ * @return DataTable|DataTable\Map
+ */
+ public function getDataTableExpanded($name, $idSubtable = null, $depth = null, $addMetadataSubtableId = true)
+ {
+ $data = $this->get($name, 'blob', self::ID_SUBTABLE_LOAD_ALL_SUBTABLES);
+ return $data->getExpandedDataTable($this->getResultIndices(), $idSubtable, $depth, $addMetadataSubtableId);
+ }
+
+ /**
+ * Returns the list of plugins that archive the given reports.
+ *
+ * @param array $archiveNames
+ * @return array
+ */
+ private function getRequestedPlugins($archiveNames)
+ {
+ $result = array();
+ foreach ($archiveNames as $name) {
+ $result[] = self::getPluginForReport($name);
+ }
+ return array_unique($result);
+ }
+
+ /**
+ * Returns an object describing the set of sites, the set of periods and the segment
+ * this Archive will query data for.
+ *
+ * @return Parameters
+ */
+ public function getParams()
+ {
+ return $this->params;
+ }
+
+ /**
+ * Helper function that creates an Archive instance and queries for report data using
+ * query parameter data. API methods can use this method to reduce code redundancy.
+ *
+ * @param string $name The name of the report to return.
+ * @param int|string|array $idSite @see {@link build()}
+ * @param string $period @see {@link build()}
+ * @param string $date @see {@link build()}
+ * @param string $segment @see {@link build()}
+ * @param bool $expanded If true, loads all subtables. See {@link getDataTableExpanded()}
+ * @param int|null $idSubtable See {@link getDataTableExpanded()}
+ * @param bool $skipAggregationOfSubTables Whether or not we should skip the aggregation of all sub-tables and only aggregate parent DataTable.
+ * @param int|null $depth See {@link getDataTableExpanded()}
+ * @return DataTable|DataTable\Map See {@link getDataTable()} and
+ * {@link getDataTableExpanded()} for more
+ * information
+ */
+ public static function getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded,
+ $idSubtable = null, $skipAggregationOfSubTables = false, $depth = null)
+ {
+ Piwik::checkUserHasViewAccess($idSite);
+
+ if($skipAggregationOfSubTables && ($expanded || $idSubtable)) {
+ throw new \Exception("Not expected to skipAggregationOfSubTables when expanded=1 or idSubtable is set.");
+ }
+ $archive = Archive::build($idSite, $period, $date, $segment, $_restrictSitesToLogin = false, $skipAggregationOfSubTables);
+ if ($idSubtable === false) {
+ $idSubtable = null;
+ }
+
+ if ($expanded) {
+ $dataTable = $archive->getDataTableExpanded($name, $idSubtable, $depth);
+ } else {
+ $dataTable = $archive->getDataTable($name, $idSubtable);
+ }
+
+ $dataTable->queueFilter('ReplaceSummaryRowLabel');
+
+ return $dataTable;
+ }
+
+ private function appendIdSubtable($recordName, $id)
+ {
+ return $recordName . "_" . $id;
+ }
+
+ /**
+ * Queries archive tables for data and returns the result.
+ * @param array|string $archiveNames
+ * @param $archiveDataType
+ * @param null|int $idSubtable
+ * @return Archive\DataCollection
+ */
+ private function get($archiveNames, $archiveDataType, $idSubtable = null)
+ {
+ if (!is_array($archiveNames)) {
+ $archiveNames = array($archiveNames);
+ }
+
+ // apply idSubtable
+ if ($idSubtable !== null
+ && $idSubtable != self::ID_SUBTABLE_LOAD_ALL_SUBTABLES
+ ) {
+ foreach ($archiveNames as &$name) {
+ $name = $this->appendIdsubtable($name, $idSubtable);
+ }
+ }
+
+ $result = new Archive\DataCollection(
+ $archiveNames, $archiveDataType, $this->params->getIdSites(), $this->params->getPeriods(), $defaultRow = null);
+
+ $archiveIds = $this->getArchiveIds($archiveNames);
+ if (empty($archiveIds)) {
+ return $result;
+ }
+
+ $loadAllSubtables = $idSubtable == self::ID_SUBTABLE_LOAD_ALL_SUBTABLES;
+ $archiveData = ArchiveSelector::getArchiveData($archiveIds, $archiveNames, $archiveDataType, $loadAllSubtables);
+ foreach ($archiveData as $row) {
+ // values are grouped by idsite (site ID), date1-date2 (date range), then name (field name)
+ $idSite = $row['idsite'];
+ $periodStr = $row['date1'] . "," . $row['date2'];
+
+ if ($archiveDataType == 'numeric') {
+ $value = $this->formatNumericValue($row['value']);
+ } else {
+ $value = $this->uncompress($row['value']);
+ $result->addMetadata($idSite, $periodStr, 'ts_archived', $row['ts_archived']);
+ }
+
+ $resultRow = & $result->get($idSite, $periodStr);
+ $resultRow[$row['name']] = $value;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns archive IDs for the sites, periods and archive names that are being
+ * queried. This function will use the idarchive cache if it has the right data,
+ * query archive tables for IDs w/o launching archiving, or launch archiving and
+ * get the idarchive from ArchiveProcessor instances.
+ */
+ private function getArchiveIds($archiveNames)
+ {
+ $plugins = $this->getRequestedPlugins($archiveNames);
+
+ // figure out which archives haven't been processed (if an archive has been processed,
+ // then we have the archive IDs in $this->idarchives)
+ $doneFlags = array();
+ $archiveGroups = array();
+ foreach ($plugins as $plugin) {
+ $doneFlag = $this->getDoneStringForPlugin($plugin);
+
+ $doneFlags[$doneFlag] = true;
+ if (!isset($this->idarchives[$doneFlag])) {
+ $archiveGroup = $this->getArchiveGroupOfPlugin($plugin);
+
+ if($archiveGroup == self::ARCHIVE_ALL_PLUGINS_FLAG) {
+ $archiveGroup = reset($plugins);
+ }
+ $archiveGroups[] = $archiveGroup;
+ }
+ }
+
+ $archiveGroups = array_unique($archiveGroups);
+
+ // cache id archives for plugins we haven't processed yet
+ if (!empty($archiveGroups)) {
+ if (!Rules::isArchivingDisabledFor($this->params->getIdSites(), $this->params->getSegment(), $this->getPeriodLabel())) {
+ $this->cacheArchiveIdsAfterLaunching($archiveGroups, $plugins);
+ } else {
+ $this->cacheArchiveIdsWithoutLaunching($plugins);
+ }
+ }
+
+ // order idarchives by the table month they belong to
+ $idArchivesByMonth = array();
+ foreach (array_keys($doneFlags) as $doneFlag) {
+ if (empty($this->idarchives[$doneFlag])) {
+ continue;
+ }
+
+ foreach ($this->idarchives[$doneFlag] as $dateRange => $idarchives) {
+ foreach ($idarchives as $id) {
+ $idArchivesByMonth[$dateRange][] = $id;
+ }
+ }
+ }
+
+ return $idArchivesByMonth;
+ }
+
+ /**
+ * Gets the IDs of the archives we're querying for and stores them in $this->archives.
+ * This function will launch the archiving process for each period/site/plugin if
+ * metrics/reports have not been calculated/archived already.
+ *
+ * @param array $archiveGroups @see getArchiveGroupOfReport
+ * @param array $plugins List of plugin names to archive.
+ */
+ private function cacheArchiveIdsAfterLaunching($archiveGroups, $plugins)
+ {
+ $today = Date::today();
+
+ foreach ($this->params->getPeriods() as $period) {
+ $twoDaysBeforePeriod = $period->getDateStart()->subDay(2);
+ $twoDaysAfterPeriod = $period->getDateEnd()->addDay(2);
+
+ foreach ($this->params->getIdSites() as $idSite) {
+ $site = new Site($idSite);
+
+ // if the END of the period is BEFORE the website creation date
+ // we already know there are no stats for this period
+ // we add one day to make sure we don't miss the day of the website creation
+ if ($twoDaysAfterPeriod->isEarlier($site->getCreationDate())) {
+ Log::verbose("Archive site %s, %s (%s) skipped, archive is before the website was created.",
+ $idSite, $period->getLabel(), $period->getPrettyString());
+ continue;
+ }
+
+ // if the starting date is in the future we know there is no visiidsite = ?t
+ if ($twoDaysBeforePeriod->isLater($today)) {
+ Log::verbose("Archive site %s, %s (%s) skipped, archive is after today.",
+ $idSite, $period->getLabel(), $period->getPrettyString());
+ continue;
+ }
+
+ $this->prepareArchive($archiveGroups, $site, $period);
+ }
+ }
+ }
+
+ /**
+ * Gets the IDs of the archives we're querying for and stores them in $this->archives.
+ * This function will not launch the archiving process (and is thus much, much faster
+ * than cacheArchiveIdsAfterLaunching).
+ *
+ * @param array $plugins List of plugin names from which data is being requested.
+ */
+ private function cacheArchiveIdsWithoutLaunching($plugins)
+ {
+ $idarchivesByReport = ArchiveSelector::getArchiveIds(
+ $this->params->getIdSites(), $this->params->getPeriods(), $this->params->getSegment(), $plugins, $this->params->isSkipAggregationOfSubTables());
+
+ // initialize archive ID cache for each report
+ foreach ($plugins as $plugin) {
+ $doneFlag = $this->getDoneStringForPlugin($plugin);
+ $this->initializeArchiveIdCache($doneFlag);
+ }
+
+ foreach ($idarchivesByReport as $doneFlag => $idarchivesByDate) {
+ foreach ($idarchivesByDate as $dateRange => $idarchives) {
+ foreach ($idarchives as $idarchive) {
+ $this->idarchives[$doneFlag][$dateRange][] = $idarchive;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the done string flag for a plugin using this instance's segment & periods.
+ * @param string $plugin
+ * @return string
+ */
+ private function getDoneStringForPlugin($plugin)
+ {
+ return Rules::getDoneStringFlagFor(
+ $this->params->getIdSites(),
+ $this->params->getSegment(),
+ $this->getPeriodLabel(),
+ $plugin,
+ $this->params->isSkipAggregationOfSubTables()
+ );
+ }
+
+ private function getPeriodLabel()
+ {
+ $periods = $this->params->getPeriods();
+ return reset($periods)->getLabel();
+ }
+
+ /**
+ * Returns an array describing what metadata to use when indexing a query result.
+ * For use with DataCollection.
+ *
+ * @return array
+ */
+ private function getResultIndices()
+ {
+ $indices = array();
+
+ if (count($this->params->getIdSites()) > 1
+ || $this->forceIndexedBySite
+ ) {
+ $indices['site'] = 'idSite';
+ }
+
+ if (count($this->params->getPeriods()) > 1
+ || $this->forceIndexedByDate
+ ) {
+ $indices['period'] = 'date';
+ }
+
+ return $indices;
+ }
+
+ private function formatNumericValue($value)
+ {
+ // If there is no dot, we return as is
+ // Note: this could be an integer bigger than 32 bits
+ if (strpos($value, '.') === false) {
+ if ($value === false) {
+ return 0;
+ }
+ return (float)$value;
+ }
+
+ // Round up the value with 2 decimals
+ // we cast the result as float because returns false when no visitors
+ return round((float)$value, 2);
+ }
+
+ private function uncompress($data)
+ {
+ return @gzuncompress($data);
+ }
+
+ /**
+ * Initializes the archive ID cache ($this->idarchives) for a particular 'done' flag.
+ *
+ * It is necessary that each archive ID caching function call this method for each
+ * unique 'done' flag it encounters, since the getArchiveIds function determines
+ * whether archiving should be launched based on whether $this->idarchives has a
+ * an entry for a specific 'done' flag.
+ *
+ * If this function is not called, then periods with no visits will not add
+ * entries to the cache. If the archive is used again, SQL will be executed to
+ * try and find the archive IDs even though we know there are none.
+ */
+ private function initializeArchiveIdCache($doneFlag)
+ {
+ if (!isset($this->idarchives[$doneFlag])) {
+ $this->idarchives[$doneFlag] = array();
+ }
+ }
+
+ /**
+ * Returns the archiving group identifier given a plugin.
+ *
+ * More than one plugin can be called at once when archiving. In such a case
+ * we don't want to launch archiving three times for three plugins if doing
+ * it once is enough, so getArchiveIds makes sure to get the archive group of
+ * all reports.
+ *
+ * If the period isn't a range, then all plugins' archiving code is executed.
+ * If the period is a range, then archiving code is executed individually for
+ * each plugin.
+ */
+ private function getArchiveGroupOfPlugin($plugin)
+ {
+ if ($this->getPeriodLabel() != 'range') {
+ return self::ARCHIVE_ALL_PLUGINS_FLAG;
+ }
+
+ return $plugin;
+ }
+
+ /**
+ * Returns the name of the plugin that archives a given report.
+ *
+ * @param string $report Archive data name, eg, `'nb_visits'`, `'UserSettings_...'`, etc.
+ * @return string Plugin name.
+ * @throws \Exception If a plugin cannot be found or if the plugin for the report isn't
+ * activated.
+ */
+ private static function getPluginForReport($report)
+ {
+ // Core metrics are always processed in Core, for the requested date/period/segment
+ if (in_array($report, Metrics::getVisitsMetricNames())) {
+ $report = 'VisitsSummary_CoreMetrics';
+ } // Goal_* metrics are processed by the Goals plugin (HACK)
+ else if (strpos($report, 'Goal_') === 0) {
+ $report = 'Goals_Metrics';
+ } else if (strrpos($report, '_returning') === strlen($report) - strlen('_returning')) { // HACK
+ $report = 'VisitFrequency_Metrics';
+ }
+
+ $plugin = substr($report, 0, strpos($report, '_'));
+ if (empty($plugin)
+ || !\Piwik\Plugin\Manager::getInstance()->isPluginActivated($plugin)
+ ) {
+ throw new \Exception("Error: The report '$report' was requested but it is not available at this stage."
+ . " (Plugin '$plugin' is not activated.)");
+ }
+ return $plugin;
+ }
+
+ /**
+ * @param $archiveGroups
+ * @param $site
+ * @param $period
+ */
+ private function prepareArchive(array $archiveGroups, Site $site, Period $period)
+ {
+ $parameters = new ArchiveProcessor\Parameters($site, $period, $this->params->getSegment(), $this->params->isSkipAggregationOfSubTables());
+ $archiveLoader = new ArchiveProcessor\Loader($parameters);
+
+ $periodString = $period->getRangeString();
+
+ // process for each plugin as well
+ foreach ($archiveGroups as $plugin) {
+ $doneFlag = $this->getDoneStringForPlugin($plugin);
+ $this->initializeArchiveIdCache($doneFlag);
+
+ $idArchive = $archiveLoader->prepareArchive($plugin);
+
+ if($idArchive) {
+ $this->idarchives[$doneFlag][$periodString][] = $idArchive;
+ }
+ }
+ }
+}
diff --git a/www/analytics/core/Archive/DataCollection.php b/www/analytics/core/Archive/DataCollection.php
new file mode 100644
index 00000000..5f57e6c3
--- /dev/null
+++ b/www/analytics/core/Archive/DataCollection.php
@@ -0,0 +1,336 @@
+ array(
+ * array(
+ * '2012-01-01,2012-01-01' => array(...),
+ * '2012-01-02,2012-01-02' => array(...),
+ * )
+ * ),
+ * '1' => array(
+ * array(
+ * '2012-01-01,2012-01-01' => array(...),
+ * )
+ * )
+ * )
+ *
+ * Archive data can be either a numeric value or a serialized string blob. Every
+ * piece of archive data is associated by it's archive name. For example,
+ * the array(...) above could look like:
+ *
+ * array(
+ * 'nb_visits' => 1,
+ * 'nb_actions' => 2
+ * )
+ *
+ * There is a special element '_metadata' in data rows that holds values treated
+ * as DataTable metadata.
+ */
+ private $data = array();
+
+ /**
+ * The whole list of metric/record names that were used in the archive query.
+ *
+ * @var array
+ */
+ private $dataNames;
+
+ /**
+ * The type of data that was queried for (ie, "blob" or "numeric").
+ *
+ * @var string
+ */
+ private $dataType;
+
+ /**
+ * The default values to use for each metric/record name that's being queried
+ * for.
+ *
+ * @var array
+ */
+ private $defaultRow;
+
+ /**
+ * The list of all site IDs that were queried for.
+ *
+ * @var array
+ */
+ private $sitesId;
+
+ /**
+ * The list of all periods that were queried for. Each period is associated with
+ * the period's range string. Eg,
+ *
+ * array(
+ * '2012-01-01,2012-01-31' => new Period(...),
+ * '2012-02-01,2012-02-28' => new Period(...),
+ * )
+ *
+ * @var \Piwik\Period[]
+ */
+ private $periods;
+
+ /**
+ * Constructor.
+ *
+ * @param array $dataNames @see $this->dataNames
+ * @param string $dataType @see $this->dataType
+ * @param array $sitesId @see $this->sitesId
+ * @param \Piwik\Period[] $periods @see $this->periods
+ * @param array $defaultRow @see $this->defaultRow
+ */
+ public function __construct($dataNames, $dataType, $sitesId, $periods, $defaultRow = null)
+ {
+ $this->dataNames = $dataNames;
+ $this->dataType = $dataType;
+
+ if ($defaultRow === null) {
+ $defaultRow = array_fill_keys($dataNames, 0);
+ }
+
+ $this->sitesId = $sitesId;
+
+ foreach ($periods as $period) {
+ $this->periods[$period->getRangeString()] = $period;
+ }
+ $this->defaultRow = $defaultRow;
+ }
+
+ /**
+ * Returns a reference to the data for a specific site & period. If there is
+ * no data for the given site ID & period, it is set to the default row.
+ *
+ * @param int $idSite
+ * @param string $period eg, '2012-01-01,2012-01-31'
+ */
+ public function &get($idSite, $period)
+ {
+ if (!isset($this->data[$idSite][$period])) {
+ $this->data[$idSite][$period] = $this->defaultRow;
+ }
+ return $this->data[$idSite][$period];
+ }
+
+ /**
+ * Adds a new metadata to the data for specific site & period. If there is no
+ * data for the given site ID & period, it is set to the default row.
+ *
+ * Note: Site ID and period range string are two special types of metadata. Since
+ * the data stored in this class is indexed by site & period, this metadata is not
+ * stored in individual data rows.
+ *
+ * @param int $idSite
+ * @param string $period eg, '2012-01-01,2012-01-31'
+ * @param string $name The metadata name.
+ * @param mixed $value The metadata name.
+ */
+ public function addMetadata($idSite, $period, $name, $value)
+ {
+ $row = & $this->get($idSite, $period);
+ $row[self::METADATA_CONTAINER_ROW_KEY][$name] = $value;
+ }
+
+ /**
+ * Returns archive data as an array indexed by metadata.
+ *
+ * @param array $resultIndices An array mapping metadata names to pretty labels
+ * for them. Each archive data row will be indexed
+ * by the metadata specified here.
+ *
+ * Eg, array('site' => 'idSite', 'period' => 'Date')
+ * @return array
+ */
+ public function getIndexedArray($resultIndices)
+ {
+ $indexKeys = array_keys($resultIndices);
+
+ $result = $this->createOrderedIndex($indexKeys);
+ foreach ($this->data as $idSite => $rowsByPeriod) {
+ foreach ($rowsByPeriod as $period => $row) {
+ // FIXME: This hack works around a strange bug that occurs when getting
+ // archive IDs through ArchiveProcessing instances. When a table
+ // does not already exist, for some reason the archive ID for
+ // today (or from two days ago) will be added to the Archive
+ // instances list. The Archive instance will then select data
+ // for periods outside of the requested set.
+ // working around the bug here, but ideally, we need to figure
+ // out why incorrect idarchives are being selected.
+ if (empty($this->periods[$period])) {
+ continue;
+ }
+
+ $this->putRowInIndex($result, $indexKeys, $row, $idSite, $period);
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Returns archive data as a DataTable indexed by metadata. Indexed data will
+ * be represented by Map instances.
+ *
+ * @param array $resultIndices An array mapping metadata names to pretty labels
+ * for them. Each archive data row will be indexed
+ * by the metadata specified here.
+ *
+ * Eg, array('site' => 'idSite', 'period' => 'Date')
+ * @return DataTable|DataTable\Map
+ */
+ public function getDataTable($resultIndices)
+ {
+ $dataTableFactory = new DataTableFactory(
+ $this->dataNames, $this->dataType, $this->sitesId, $this->periods, $this->defaultRow);
+
+ $index = $this->getIndexedArray($resultIndices);
+ return $dataTableFactory->make($index, $resultIndices);
+ }
+
+ /**
+ * Returns archive data as a DataTable indexed by metadata. Indexed data will
+ * be represented by Map instances. Each DataTable will have
+ * its subtable IDs set.
+ *
+ * This function will only work if blob data was loaded and only one record
+ * was loaded (not including subtables of the record).
+ *
+ * @param array $resultIndices An array mapping metadata names to pretty labels
+ * for them. Each archive data row will be indexed
+ * by the metadata specified here.
+ *
+ * Eg, array('site' => 'idSite', 'period' => 'Date')
+ * @param int|null $idSubTable The subtable to return.
+ * @param int|null $depth max depth for subtables.
+ * @param bool $addMetadataSubTableId Whether to add the DB subtable ID as metadata
+ * to each datatable, or not.
+ * @throws Exception
+ * @return DataTable|DataTable\Map
+ */
+ public function getExpandedDataTable($resultIndices, $idSubTable = null, $depth = null, $addMetadataSubTableId = false)
+ {
+ if ($this->dataType != 'blob') {
+ throw new Exception("DataCollection: cannot call getExpandedDataTable with "
+ . "{$this->dataType} data types. Only works with blob data.");
+ }
+
+ if (count($this->dataNames) !== 1) {
+ throw new Exception("DataCollection: cannot call getExpandedDataTable with "
+ . "more than one record.");
+ }
+
+ $dataTableFactory = new DataTableFactory(
+ $this->dataNames, 'blob', $this->sitesId, $this->periods, $this->defaultRow);
+ $dataTableFactory->expandDataTable($depth, $addMetadataSubTableId);
+ $dataTableFactory->useSubtable($idSubTable);
+
+ $index = $this->getIndexedArray($resultIndices);
+ return $dataTableFactory->make($index, $resultIndices);
+ }
+
+ /**
+ * Returns metadata for a data row.
+ *
+ * @param array $data The data row.
+ * @return array
+ */
+ public static function getDataRowMetadata($data)
+ {
+ if (isset($data[self::METADATA_CONTAINER_ROW_KEY])) {
+ return $data[self::METADATA_CONTAINER_ROW_KEY];
+ } else {
+ return array();
+ }
+ }
+
+ /**
+ * Removes all table metadata from a data row.
+ *
+ * @param array $data The data row.
+ */
+ public static function removeMetadataFromDataRow(&$data)
+ {
+ unset($data[self::METADATA_CONTAINER_ROW_KEY]);
+ }
+
+ /**
+ * Creates an empty index using a list of metadata names. If the 'site' and/or
+ * 'period' metadata names are supplied, empty rows are added for every site/period
+ * that was queried for.
+ *
+ * Using this function ensures consistent ordering in the indexed result.
+ *
+ * @param array $metadataNamesToIndexBy List of metadata names to index archive data by.
+ * @return array
+ */
+ private function createOrderedIndex($metadataNamesToIndexBy)
+ {
+ $result = array();
+
+ if (!empty($metadataNamesToIndexBy)) {
+ $metadataName = array_shift($metadataNamesToIndexBy);
+
+ if ($metadataName == DataTableFactory::TABLE_METADATA_SITE_INDEX) {
+ $indexKeyValues = array_values($this->sitesId);
+ } else if ($metadataName == DataTableFactory::TABLE_METADATA_PERIOD_INDEX) {
+ $indexKeyValues = array_keys($this->periods);
+ }
+
+ foreach ($indexKeyValues as $key) {
+ $result[$key] = $this->createOrderedIndex($metadataNamesToIndexBy);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Puts an archive data row in an index.
+ */
+ private function putRowInIndex(&$index, $metadataNamesToIndexBy, $row, $idSite, $period)
+ {
+ $currentLevel = & $index;
+
+ foreach ($metadataNamesToIndexBy as $metadataName) {
+ if ($metadataName == DataTableFactory::TABLE_METADATA_SITE_INDEX) {
+ $key = $idSite;
+ } else if ($metadataName == DataTableFactory::TABLE_METADATA_PERIOD_INDEX) {
+ $key = $period;
+ } else {
+ $key = $row[self::METADATA_CONTAINER_ROW_KEY][$metadataName];
+ }
+
+ if (!isset($currentLevel[$key])) {
+ $currentLevel[$key] = array();
+ }
+
+ $currentLevel = & $currentLevel[$key];
+ }
+
+ $currentLevel = $row;
+ }
+}
diff --git a/www/analytics/core/Archive/DataTableFactory.php b/www/analytics/core/Archive/DataTableFactory.php
new file mode 100644
index 00000000..44beba02
--- /dev/null
+++ b/www/analytics/core/Archive/DataTableFactory.php
@@ -0,0 +1,426 @@
+dataNames = $dataNames;
+ $this->dataType = $dataType;
+ $this->sitesId = $sitesId;
+
+ //here index period by string only
+ $this->periods = $periods;
+ $this->defaultRow = $defaultRow;
+ }
+
+ /**
+ * Tells the factory instance to expand the DataTables that are created by
+ * creating subtables and setting the subtable IDs of rows w/ subtables correctly.
+ *
+ * @param null|int $maxSubtableDepth max depth for subtables.
+ * @param bool $addMetadataSubtableId Whether to add the subtable ID used in the
+ * database to the in-memory DataTables as
+ * metadata or not.
+ */
+ public function expandDataTable($maxSubtableDepth = null, $addMetadataSubtableId = false)
+ {
+ $this->expandDataTable = true;
+ $this->maxSubtableDepth = $maxSubtableDepth;
+ $this->addMetadataSubtableId = $addMetadataSubtableId;
+ }
+
+ /**
+ * Tells the factory instance to create a DataTable using a blob with the
+ * supplied subtable ID.
+ *
+ * @param int $idSubtable An in-database subtable ID.
+ * @throws \Exception
+ */
+ public function useSubtable($idSubtable)
+ {
+ if (count($this->dataNames) !== 1) {
+ throw new \Exception("DataTableFactory: Getting subtables for multiple records in one"
+ . " archive query is not currently supported.");
+ }
+
+ $this->idSubtable = $idSubtable;
+ }
+
+ /**
+ * Creates a DataTable|Set instance using an index of
+ * archive data.
+ *
+ * @param array $index @see DataCollection
+ * @param array $resultIndices an array mapping metadata names with pretty metadata
+ * labels.
+ * @return DataTable|DataTable\Map
+ */
+ public function make($index, $resultIndices)
+ {
+ if (empty($resultIndices)) {
+ // for numeric data, if there's no index (and thus only 1 site & period in the query),
+ // we want to display every queried metric name
+ if (empty($index)
+ && $this->dataType == 'numeric'
+ ) {
+ $index = $this->defaultRow;
+ }
+
+ $dataTable = $this->createDataTable($index, $keyMetadata = array());
+ } else {
+ $dataTable = $this->createDataTableMapFromIndex($index, $resultIndices, $keyMetadata = array());
+ }
+
+ $this->transformMetadata($dataTable);
+ return $dataTable;
+ }
+
+ /**
+ * Creates a DataTable|Set instance using an array
+ * of blobs.
+ *
+ * If only one record is being queried, a single DataTable will
+ * be returned. Otherwise, a DataTable\Map is returned that indexes
+ * DataTables by record name.
+ *
+ * If expandDataTable was called, and only one record is being queried,
+ * the created DataTable's subtables will be expanded.
+ *
+ * @param array $blobRow
+ * @return DataTable|DataTable\Map
+ */
+ private function makeFromBlobRow($blobRow)
+ {
+ if ($blobRow === false) {
+ return new DataTable();
+ }
+
+ if (count($this->dataNames) === 1) {
+ return $this->makeDataTableFromSingleBlob($blobRow);
+ } else {
+ return $this->makeIndexedByRecordNameDataTable($blobRow);
+ }
+ }
+
+ /**
+ * Creates a DataTable for one record from an archive data row.
+ *
+ * @see makeFromBlobRow
+ *
+ * @param array $blobRow
+ * @return DataTable
+ */
+ private function makeDataTableFromSingleBlob($blobRow)
+ {
+ $recordName = reset($this->dataNames);
+ if ($this->idSubtable !== null) {
+ $recordName .= '_' . $this->idSubtable;
+ }
+
+ if (!empty($blobRow[$recordName])) {
+ $table = DataTable::fromSerializedArray($blobRow[$recordName]);
+ } else {
+ $table = new DataTable();
+ }
+
+ // set table metadata
+ $table->setMetadataValues(DataCollection::getDataRowMetadata($blobRow));
+
+ if ($this->expandDataTable) {
+ $table->enableRecursiveFilters();
+ $this->setSubtables($table, $blobRow);
+ }
+
+ return $table;
+ }
+
+ /**
+ * Creates a DataTable for every record in an archive data row and puts them
+ * in a DataTable\Map instance.
+ *
+ * @param array $blobRow
+ * @return DataTable\Map
+ */
+ private function makeIndexedByRecordNameDataTable($blobRow)
+ {
+ $table = new DataTable\Map();
+ $table->setKeyName('recordName');
+
+ $tableMetadata = DataCollection::getDataRowMetadata($blobRow);
+
+ foreach ($blobRow as $name => $blob) {
+ $newTable = DataTable::fromSerializedArray($blob);
+ $newTable->setAllTableMetadata($tableMetadata);
+
+ $table->addTable($newTable, $name);
+ }
+
+ return $table;
+ }
+
+ /**
+ * Creates a Set from an array index.
+ *
+ * @param array $index @see DataCollection
+ * @param array $resultIndices @see make
+ * @param array $keyMetadata The metadata to add to the table when it's created.
+ * @return DataTable\Map
+ */
+ private function createDataTableMapFromIndex($index, $resultIndices, $keyMetadata = array())
+ {
+ $resultIndexLabel = reset($resultIndices);
+ $resultIndex = key($resultIndices);
+
+ array_shift($resultIndices);
+
+ $result = new DataTable\Map();
+ $result->setKeyName($resultIndexLabel);
+
+ foreach ($index as $label => $value) {
+ $keyMetadata[$resultIndex] = $label;
+
+ if (empty($resultIndices)) {
+ $newTable = $this->createDataTable($value, $keyMetadata);
+ } else {
+ $newTable = $this->createDataTableMapFromIndex($value, $resultIndices, $keyMetadata);
+ }
+
+ $result->addTable($newTable, $this->prettifyIndexLabel($resultIndex, $label));
+ }
+
+ return $result;
+ }
+
+ /**
+ * Creates a DataTable instance from an index row.
+ *
+ * @param array $data An archive data row.
+ * @param array $keyMetadata The metadata to add to the table(s) when created.
+ * @return DataTable|DataTable\Map
+ */
+ private function createDataTable($data, $keyMetadata)
+ {
+ if ($this->dataType == 'blob') {
+ $result = $this->makeFromBlobRow($data);
+ } else {
+ $result = $this->makeFromMetricsArray($data);
+ }
+ $this->setTableMetadata($keyMetadata, $result);
+ return $result;
+ }
+
+ /**
+ * Creates DataTables from $dataTable's subtable blobs (stored in $blobRow) and sets
+ * the subtable IDs of each DataTable row.
+ *
+ * @param DataTable $dataTable
+ * @param array $blobRow An array associating record names (w/ subtable if applicable)
+ * with blob values. This should hold every subtable blob for
+ * the loaded DataTable.
+ * @param int $treeLevel
+ */
+ private function setSubtables($dataTable, $blobRow, $treeLevel = 0)
+ {
+ if ($this->maxSubtableDepth
+ && $treeLevel >= $this->maxSubtableDepth
+ ) {
+ // unset the subtables so DataTableManager doesn't throw
+ foreach ($dataTable->getRows() as $row) {
+ $row->removeSubtable();
+ }
+
+ return;
+ }
+
+ $dataName = reset($this->dataNames);
+
+ foreach ($dataTable->getRows() as $row) {
+ $sid = $row->getIdSubDataTable();
+ if ($sid === null) {
+ continue;
+ }
+
+ $blobName = $dataName . "_" . $sid;
+ if (isset($blobRow[$blobName])) {
+ $subtable = DataTable::fromSerializedArray($blobRow[$blobName]);
+ $this->setSubtables($subtable, $blobRow, $treeLevel + 1);
+
+ // we edit the subtable ID so that it matches the newly table created in memory
+ // NB: we dont overwrite the datatableid in the case we are displaying the table expanded.
+ if ($this->addMetadataSubtableId) {
+ // this will be written back to the column 'idsubdatatable' just before rendering,
+ // see Renderer/Php.php
+ $row->addMetadata('idsubdatatable_in_db', $row->getIdSubDataTable());
+ }
+
+ $row->setSubtable($subtable);
+ }
+ }
+ }
+
+ /**
+ * Converts site IDs and period string ranges into Site instances and
+ * Period instances in DataTable metadata.
+ */
+ private function transformMetadata($table)
+ {
+ $periods = $this->periods;
+ $table->filter(function ($table) use ($periods) {
+ $table->setMetadata(DataTableFactory::TABLE_METADATA_SITE_INDEX, new Site($table->getMetadata(DataTableFactory::TABLE_METADATA_SITE_INDEX)));
+ $table->setMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX, $periods[$table->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)]);
+ });
+ }
+
+ /**
+ * Returns the pretty version of an index label.
+ *
+ * @param string $labelType eg, 'site', 'period', etc.
+ * @param string $label eg, '0', '1', '2012-01-01,2012-01-31', etc.
+ * @return string
+ */
+ private function prettifyIndexLabel($labelType, $label)
+ {
+ if ($labelType == self::TABLE_METADATA_PERIOD_INDEX) { // prettify period labels
+ return $this->periods[$label]->getPrettyString();
+ }
+ return $label;
+ }
+
+ /**
+ * @param $keyMetadata
+ * @param $result
+ */
+ private function setTableMetadata($keyMetadata, $result)
+ {
+ if (!isset($keyMetadata[DataTableFactory::TABLE_METADATA_SITE_INDEX])) {
+ $keyMetadata[DataTableFactory::TABLE_METADATA_SITE_INDEX] = reset($this->sitesId);
+ }
+
+ if (!isset($keyMetadata[DataTableFactory::TABLE_METADATA_PERIOD_INDEX])) {
+ reset($this->periods);
+ $keyMetadata[DataTableFactory::TABLE_METADATA_PERIOD_INDEX] = key($this->periods);
+ }
+
+ // Note: $result can be a DataTable\Map
+ $result->filter(function ($table) use ($keyMetadata) {
+ foreach ($keyMetadata as $name => $value) {
+ $table->setMetadata($name, $value);
+ }
+ });
+ }
+
+ /**
+ * @param $data
+ * @return DataTable\Simple
+ */
+ private function makeFromMetricsArray($data)
+ {
+ $table = new DataTable\Simple();
+
+ if (!empty($data)) {
+ $table->setAllTableMetadata(DataCollection::getDataRowMetadata($data));
+
+ DataCollection::removeMetadataFromDataRow($data);
+
+ $table->addRow(new Row(array(Row::COLUMNS => $data)));
+ } else {
+ // if we're querying numeric data, we couldn't find any, and we're only
+ // looking for one metric, add a row w/ one column w/ value 0. this is to
+ // ensure that the PHP renderer outputs 0 when only one column is queried.
+ // w/o this code, an empty array would be created, and other parts of Piwik
+ // would break.
+ if (count($this->dataNames) == 1
+ && $this->dataType == 'numeric'
+ ) {
+ $name = reset($this->dataNames);
+ $table->addRow(new Row(array(Row::COLUMNS => array($name => 0))));
+ }
+ }
+
+ $result = $table;
+ return $result;
+ }
+}
+
diff --git a/www/analytics/core/Archive/Parameters.php b/www/analytics/core/Archive/Parameters.php
new file mode 100644
index 00000000..1f700819
--- /dev/null
+++ b/www/analytics/core/Archive/Parameters.php
@@ -0,0 +1,73 @@
+segment;
+ }
+
+ public function __construct($idSites, $periods, Segment $segment, $skipAggregationOfSubTables)
+ {
+ $this->idSites = $idSites;
+ $this->periods = $periods;
+ $this->segment = $segment;
+ $this->skipAggregationOfSubTables = $skipAggregationOfSubTables;
+ }
+
+ public function getPeriods()
+ {
+ return $this->periods;
+ }
+
+ public function getIdSites()
+ {
+ return $this->idSites;
+ }
+
+ public function isSkipAggregationOfSubTables()
+ {
+ return $this->skipAggregationOfSubTables;
+ }
+
+}
+
diff --git a/www/analytics/core/ArchiveProcessor.php b/www/analytics/core/ArchiveProcessor.php
new file mode 100644
index 00000000..20c4c2df
--- /dev/null
+++ b/www/analytics/core/ArchiveProcessor.php
@@ -0,0 +1,489 @@
+getProcessor();
+ *
+ * $myFancyMetric = // ... calculate the metric value ...
+ * $archiveProcessor->insertNumericRecord('MyPlugin_myFancyMetric', $myFancyMetric);
+ * }
+ *
+ * **Inserting serialized DataTables**
+ *
+ * // function in an Archiver descendant
+ * public function aggregateDayReport()
+ * {
+ * $archiveProcessor = $this->getProcessor();
+ *
+ * $maxRowsInTable = Config::getInstance()->General['datatable_archiving_maximum_rows_standard'];j
+ *
+ * $dataTable = // ... build by aggregating visits ...
+ * $serializedData = $dataTable->getSerialized($maxRowsInTable, $maxRowsInSubtable = $maxRowsInTable,
+ * $columnToSortBy = Metrics::INDEX_NB_VISITS);
+ *
+ * $archiveProcessor->insertBlobRecords('MyPlugin_myFancyReport', $serializedData);
+ * }
+ *
+ * **Aggregating archive data**
+ *
+ * // function in Archiver descendant
+ * public function aggregateMultipleReports()
+ * {
+ * $archiveProcessor = $this->getProcessor();
+ *
+ * // aggregate a metric
+ * $archiveProcessor->aggregateNumericMetrics('MyPlugin_myFancyMetric');
+ * $archiveProcessor->aggregateNumericMetrics('MyPlugin_mySuperFancyMetric', 'max');
+ *
+ * // aggregate a report
+ * $archiveProcessor->aggregateDataTableRecords('MyPlugin_myFancyReport');
+ * }
+ *
+ */
+class ArchiveProcessor
+{
+ /**
+ * @var \Piwik\DataAccess\ArchiveWriter
+ */
+ protected $archiveWriter;
+
+ /**
+ * @var \Piwik\DataAccess\LogAggregator
+ */
+ protected $logAggregator;
+
+ /**
+ * @var Archive
+ */
+ public $archive = null;
+
+ /**
+ * @var Parameters
+ */
+ protected $params;
+
+ /**
+ * @var int
+ */
+ protected $numberOfVisits = false;
+ protected $numberOfVisitsConverted = false;
+
+ public function __construct(Parameters $params, ArchiveWriter $archiveWriter)
+ {
+ $this->params = $params;
+ $this->logAggregator = new LogAggregator($params);
+ $this->archiveWriter = $archiveWriter;
+ }
+
+ protected function getArchive()
+ {
+ if(empty($this->archive)) {
+ $subPeriods = $this->params->getSubPeriods();
+ $idSites = $this->params->getIdSites();
+ $this->archive = Archive::factory($this->params->getSegment(), $subPeriods, $idSites);
+ }
+ return $this->archive;
+ }
+
+ public function setNumberOfVisits($visits, $visitsConverted)
+ {
+ $this->numberOfVisits = $visits;
+ $this->numberOfVisitsConverted = $visitsConverted;
+ }
+
+ /**
+ * Returns the {@link Parameters} object containing the site, period and segment we're archiving
+ * data for.
+ *
+ * @return Parameters
+ * @api
+ */
+ public function getParams()
+ {
+ return $this->params;
+ }
+
+ /**
+ * Returns a `{@link Piwik\DataAccess\LogAggregator}` instance for the site, period and segment this
+ * ArchiveProcessor will insert archive data for.
+ *
+ * @return LogAggregator
+ * @api
+ */
+ public function getLogAggregator()
+ {
+ return $this->logAggregator;
+ }
+
+ /**
+ * Array of (column name before => column name renamed) of the columns for which sum operation is invalid.
+ * These columns will be renamed as per this mapping.
+ * @var array
+ */
+ protected static $columnsToRenameAfterAggregation = array(
+ Metrics::INDEX_NB_UNIQ_VISITORS => Metrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS
+ );
+
+ /**
+ * Sums records for every subperiod of the current period and inserts the result as the record
+ * for this period.
+ *
+ * DataTables are summed recursively so subtables will be summed as well.
+ *
+ * @param string|array $recordNames Name(s) of the report we are aggregating, eg, `'Referrers_type'`.
+ * @param int $maximumRowsInDataTableLevelZero Maximum number of rows allowed in the top level DataTable.
+ * @param int $maximumRowsInSubDataTable Maximum number of rows allowed in each subtable.
+ * @param string $columnToSortByBeforeTruncation The name of the column to sort by before truncating a DataTable.
+ * @param array $columnsAggregationOperation Operations for aggregating columns, see {@link Row::sumRow()}.
+ * @param array $columnsToRenameAfterAggregation Columns mapped to new names for columns that must change names
+ * when summed because they cannot be summed, eg,
+ * `array('nb_uniq_visitors' => 'sum_daily_nb_uniq_visitors')`.
+ * @return array Returns the row counts of each aggregated report before truncation, eg,
+ *
+ * array(
+ * 'report1' => array('level0' => $report1->getRowsCount,
+ * 'recursive' => $report1->getRowsCountRecursive()),
+ * 'report2' => array('level0' => $report2->getRowsCount,
+ * 'recursive' => $report2->getRowsCountRecursive()),
+ * ...
+ * )
+ * @api
+ */
+ public function aggregateDataTableRecords($recordNames,
+ $maximumRowsInDataTableLevelZero = null,
+ $maximumRowsInSubDataTable = null,
+ $columnToSortByBeforeTruncation = null,
+ &$columnsAggregationOperation = null,
+ $columnsToRenameAfterAggregation = null)
+ {
+ if (!is_array($recordNames)) {
+ $recordNames = array($recordNames);
+ }
+ $nameToCount = array();
+ foreach ($recordNames as $recordName) {
+ $latestUsedTableId = Manager::getInstance()->getMostRecentTableId();
+
+ $table = $this->aggregateDataTableRecord($recordName, $columnsAggregationOperation, $columnsToRenameAfterAggregation);
+
+ $rowsCount = $table->getRowsCount();
+ $nameToCount[$recordName]['level0'] = $rowsCount;
+
+ $rowsCountRecursive = $rowsCount;
+ if($this->isAggregateSubTables()) {
+ $rowsCountRecursive = $table->getRowsCountRecursive();
+ }
+ $nameToCount[$recordName]['recursive'] = $rowsCountRecursive;
+
+ $blob = $table->getSerialized($maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation);
+ Common::destroy($table);
+ $this->insertBlobRecord($recordName, $blob);
+
+ unset($blob);
+ DataTable\Manager::getInstance()->deleteAll($latestUsedTableId);
+ }
+
+ return $nameToCount;
+ }
+
+ /**
+ * Aggregates one or more metrics for every subperiod of the current period and inserts the results
+ * as metrics for the current period.
+ *
+ * @param array|string $columns Array of metric names to aggregate.
+ * @param bool|string $operationToApply The operation to apply to the metric. Either `'sum'`, `'max'` or `'min'`.
+ * @return array|int Returns the array of aggregate values. If only one metric was aggregated,
+ * the aggregate value will be returned as is, not in an array.
+ * For example, if `array('nb_visits', 'nb_hits')` is supplied for `$columns`,
+ *
+ * array(
+ * 'nb_visits' => 3040,
+ * 'nb_hits' => 405
+ * )
+ *
+ * could be returned. If `array('nb_visits')` or `'nb_visits'` is used for `$columns`,
+ * then `3040` would be returned.
+ * @api
+ */
+ public function aggregateNumericMetrics($columns, $operationToApply = false)
+ {
+ $metrics = $this->getAggregatedNumericMetrics($columns, $operationToApply);
+
+ foreach($metrics as $column => $value) {
+ $this->archiveWriter->insertRecord($column, $value);
+ }
+ // if asked for only one field to sum
+ if (count($metrics) == 1) {
+ return reset($metrics);
+ }
+
+ // returns the array of records once summed
+ return $metrics;
+ }
+
+ public function getNumberOfVisits()
+ {
+ if($this->numberOfVisits === false) {
+ throw new Exception("visits should have been set here");
+ }
+ return $this->numberOfVisits;
+ }
+
+ public function getNumberOfVisitsConverted()
+ {
+ return $this->numberOfVisitsConverted;
+ }
+
+ /**
+ * Caches multiple numeric records in the archive for this processor's site, period
+ * and segment.
+ *
+ * @param array $numericRecords A name-value mapping of numeric values that should be
+ * archived, eg,
+ *
+ * array('Referrers_distinctKeywords' => 23, 'Referrers_distinctCampaigns' => 234)
+ * @api
+ */
+ public function insertNumericRecords($numericRecords)
+ {
+ foreach ($numericRecords as $name => $value) {
+ $this->insertNumericRecord($name, $value);
+ }
+ }
+
+ /**
+ * Caches a single numeric record in the archive for this processor's site, period and
+ * segment.
+ *
+ * Numeric values are not inserted if they equal `0`.
+ *
+ * @param string $name The name of the numeric value, eg, `'Referrers_distinctKeywords'`.
+ * @param float $value The numeric value.
+ * @api
+ */
+ public function insertNumericRecord($name, $value)
+ {
+ $value = round($value, 2);
+ $this->archiveWriter->insertRecord($name, $value);
+ }
+
+ /**
+ * Caches one or more blob records in the archive for this processor's site, period
+ * and segment.
+ *
+ * @param string $name The name of the record, eg, 'Referrers_type'.
+ * @param string|array $values A blob string or an array of blob strings. If an array
+ * is used, the first element in the array will be inserted
+ * with the `$name` name. The others will be inserted with
+ * `$name . '_' . $index` as the record name (where $index is
+ * the index of the blob record in `$values`).
+ * @api
+ */
+ public function insertBlobRecord($name, $values)
+ {
+ $this->archiveWriter->insertBlobRecord($name, $values);
+ }
+
+ /**
+ * This method selects all DataTables that have the name $name over the period.
+ * All these DataTables are then added together, and the resulting DataTable is returned.
+ *
+ * @param string $name
+ * @param array $columnsAggregationOperation Operations for aggregating columns, @see Row::sumRow()
+ * @param array $columnsToRenameAfterAggregation columns in the array (old name, new name) to be renamed as the sum operation is not valid on them (eg. nb_uniq_visitors->sum_daily_nb_uniq_visitors)
+ * @return DataTable
+ */
+ protected function aggregateDataTableRecord($name, $columnsAggregationOperation = null, $columnsToRenameAfterAggregation = null)
+ {
+ if($this->isAggregateSubTables()) {
+ // By default we shall aggregate all sub-tables.
+ $dataTable = $this->getArchive()->getDataTableExpanded($name, $idSubTable = null, $depth = null, $addMetadataSubtableId = false);
+ } else {
+ // In some cases (eg. Actions plugin when period=range),
+ // for better performance we will only aggregate the parent table
+ $dataTable = $this->getArchive()->getDataTable($name, $idSubTable = null);
+ }
+
+ $dataTable = $this->getAggregatedDataTableMap($dataTable, $columnsAggregationOperation);
+ $this->renameColumnsAfterAggregation($dataTable, $columnsToRenameAfterAggregation);
+ return $dataTable;
+ }
+
+ protected function getOperationForColumns($columns, $defaultOperation)
+ {
+ $operationForColumn = array();
+ foreach ($columns as $name) {
+ $operation = $defaultOperation;
+ if (empty($operation)) {
+ $operation = $this->guessOperationForColumn($name);
+ }
+ $operationForColumn[$name] = $operation;
+ }
+ return $operationForColumn;
+ }
+
+ protected function enrichWithUniqueVisitorsMetric(Row $row)
+ {
+ if(!$this->getParams()->isSingleSite() ) {
+ // we only compute unique visitors for a single site
+ return;
+ }
+ if ( $row->getColumn('nb_uniq_visitors') !== false) {
+ if (SettingsPiwik::isUniqueVisitorsEnabled($this->getParams()->getPeriod()->getLabel())) {
+ $uniqueVisitors = (float)$this->computeNbUniqVisitors();
+ $row->setColumn('nb_uniq_visitors', $uniqueVisitors);
+ } else {
+ $row->deleteColumn('nb_uniq_visitors');
+ }
+ }
+ }
+
+ protected function guessOperationForColumn($column)
+ {
+ if (strpos($column, 'max_') === 0) {
+ return 'max';
+ }
+ if (strpos($column, 'min_') === 0) {
+ return 'min';
+ }
+ return 'sum';
+ }
+
+ /**
+ * Processes number of unique visitors for the given period
+ *
+ * This is the only Period metric (ie. week/month/year/range) that we process from the logs directly,
+ * since unique visitors cannot be summed like other metrics.
+ *
+ * @return int
+ */
+ protected function computeNbUniqVisitors()
+ {
+ $logAggregator = $this->getLogAggregator();
+ $query = $logAggregator->queryVisitsByDimension(array(), false, array(), array(Metrics::INDEX_NB_UNIQ_VISITORS));
+ $data = $query->fetch();
+ return $data[Metrics::INDEX_NB_UNIQ_VISITORS];
+ }
+
+ /**
+ * If the DataTable is a Map, sums all DataTable in the map and return the DataTable.
+ *
+ *
+ * @param $data DataTable|DataTable\Map
+ * @param $columnsToRenameAfterAggregation array
+ * @return DataTable
+ */
+ protected function getAggregatedDataTableMap($data, $columnsAggregationOperation)
+ {
+ $table = new DataTable();
+ if (!empty($columnsAggregationOperation)) {
+ $table->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnsAggregationOperation);
+ }
+ if ($data instanceof DataTable\Map) {
+ // as $date => $tableToSum
+ $this->aggregatedDataTableMapsAsOne($data, $table);
+ } else {
+ $table->addDataTable($data, $this->isAggregateSubTables());
+ }
+ return $table;
+ }
+
+ /**
+ * Aggregates the DataTable\Map into the destination $aggregated
+ * @param $map
+ * @param $aggregated
+ */
+ protected function aggregatedDataTableMapsAsOne(Map $map, DataTable $aggregated)
+ {
+ foreach ($map->getDataTables() as $tableToAggregate) {
+ if($tableToAggregate instanceof Map) {
+ $this->aggregatedDataTableMapsAsOne($tableToAggregate, $aggregated);
+ } else {
+ $aggregated->addDataTable($tableToAggregate, $this->isAggregateSubTables());
+ }
+ }
+ }
+
+ protected function renameColumnsAfterAggregation(DataTable $table, $columnsToRenameAfterAggregation = null)
+ {
+ // Rename columns after aggregation
+ if (is_null($columnsToRenameAfterAggregation)) {
+ $columnsToRenameAfterAggregation = self::$columnsToRenameAfterAggregation;
+ }
+ foreach ($columnsToRenameAfterAggregation as $oldName => $newName) {
+ $table->renameColumn($oldName, $newName, $this->isAggregateSubTables());
+ }
+ }
+
+ protected function getAggregatedNumericMetrics($columns, $operationToApply)
+ {
+ if (!is_array($columns)) {
+ $columns = array($columns);
+ }
+ $operationForColumn = $this->getOperationForColumns($columns, $operationToApply);
+
+ $dataTable = $this->getArchive()->getDataTableFromNumeric($columns);
+
+ $results = $this->getAggregatedDataTableMap($dataTable, $operationForColumn);
+ if ($results->getRowsCount() > 1) {
+ throw new Exception("A DataTable is an unexpected state:" . var_export($results, true));
+ }
+
+ $rowMetrics = $results->getFirstRow();
+ if($rowMetrics === false) {
+ $rowMetrics = new Row;
+ }
+ $this->enrichWithUniqueVisitorsMetric($rowMetrics);
+ $this->renameColumnsAfterAggregation($results);
+
+ $metrics = $rowMetrics->getColumns();
+
+ foreach ($columns as $name) {
+ if (!isset($metrics[$name])) {
+ $metrics[$name] = 0;
+ }
+ }
+ return $metrics;
+ }
+
+ /**
+ * @return bool
+ */
+ protected function isAggregateSubTables()
+ {
+ return !$this->getParams()->isSkipAggregationOfSubTables();
+ }
+}
diff --git a/www/analytics/core/ArchiveProcessor/Loader.php b/www/analytics/core/ArchiveProcessor/Loader.php
new file mode 100644
index 00000000..03a054a6
--- /dev/null
+++ b/www/analytics/core/ArchiveProcessor/Loader.php
@@ -0,0 +1,214 @@
+params = $params;
+ }
+
+ /**
+ * @return bool
+ */
+ protected function isThereSomeVisits($visits)
+ {
+ return $visits > 0;
+ }
+
+ /**
+ * @return bool
+ */
+ protected function mustProcessVisitCount($visits)
+ {
+ return $visits === false;
+ }
+
+ public function prepareArchive($pluginName)
+ {
+ $this->params->setRequestedPlugin($pluginName);
+
+ list($idArchive, $visits, $visitsConverted) = $this->loadExistingArchiveIdFromDb();
+ if (!empty($idArchive)) {
+ return $idArchive;
+ }
+
+ list($visits, $visitsConverted) = $this->prepareCoreMetricsArchive($visits, $visitsConverted);
+ list($idArchive, $visits) = $this->prepareAllPluginsArchive($visits, $visitsConverted);
+
+ if ($this->isThereSomeVisits($visits)) {
+ return $idArchive;
+ }
+ return false;
+ }
+
+ /**
+ * Prepares the core metrics if needed.
+ *
+ * @param $visits
+ */
+ protected function prepareCoreMetricsArchive($visits, $visitsConverted)
+ {
+ $createSeparateArchiveForCoreMetrics = $this->mustProcessVisitCount($visits)
+ && !$this->doesRequestedPluginIncludeVisitsSummary();
+
+ if ($createSeparateArchiveForCoreMetrics) {
+ $requestedPlugin = $this->params->getRequestedPlugin();
+
+ $this->params->setRequestedPlugin('VisitsSummary');
+
+ $pluginsArchiver = new PluginsArchiver($this->params, $this->isArchiveTemporary());
+ $metrics = $pluginsArchiver->callAggregateCoreMetrics();
+ $pluginsArchiver->finalizeArchive();
+
+ $this->params->setRequestedPlugin($requestedPlugin);
+
+ $visits = $metrics['nb_visits'];
+ $visitsConverted = $metrics['nb_visits_converted'];
+ }
+ return array($visits, $visitsConverted);
+ }
+
+ protected function prepareAllPluginsArchive($visits, $visitsConverted)
+ {
+ $pluginsArchiver = new PluginsArchiver($this->params, $this->isArchiveTemporary());
+ if ($this->mustProcessVisitCount($visits)
+ || $this->doesRequestedPluginIncludeVisitsSummary()
+ ) {
+ $metrics = $pluginsArchiver->callAggregateCoreMetrics();
+ $visits = $metrics['nb_visits'];
+ $visitsConverted = $metrics['nb_visits_converted'];
+ }
+ if ($this->isThereSomeVisits($visits)) {
+ $pluginsArchiver->callAggregateAllPlugins($visits, $visitsConverted);
+ }
+ $idArchive = $pluginsArchiver->finalizeArchive();
+
+ if (!$this->params->isSingleSiteDayArchive() && $visits) {
+ ArchiveSelector::purgeOutdatedArchives($this->params->getPeriod()->getDateStart());
+ }
+
+ return array($idArchive, $visits);
+ }
+
+ protected function doesRequestedPluginIncludeVisitsSummary()
+ {
+ $processAllReportsIncludingVisitsSummary =
+ Rules::shouldProcessReportsAllPlugins($this->params->getIdSites(), $this->params->getSegment(), $this->params->getPeriod()->getLabel());
+ $doesRequestedPluginIncludeVisitsSummary = $processAllReportsIncludingVisitsSummary
+ || $this->params->getRequestedPlugin() == 'VisitsSummary';
+ return $doesRequestedPluginIncludeVisitsSummary;
+ }
+
+ protected function isArchivingForcedToTrigger()
+ {
+ $period = $this->params->getPeriod()->getLabel();
+ $debugSetting = 'always_archive_data_period'; // default
+ if ($period == 'day') {
+ $debugSetting = 'always_archive_data_day';
+ } elseif ($period == 'range') {
+ $debugSetting = 'always_archive_data_range';
+ }
+ return (bool) Config::getInstance()->Debug[$debugSetting];
+ }
+
+ /**
+ * Returns the idArchive if the archive is available in the database for the requested plugin.
+ * Returns false if the archive needs to be processed.
+ *
+ * @return array
+ */
+ protected function loadExistingArchiveIdFromDb()
+ {
+ $noArchiveFound = array(false, false, false);
+
+ // see isArchiveTemporary()
+ $minDatetimeArchiveProcessedUTC = $this->getMinTimeArchiveProcessed();
+
+ if ($this->isArchivingForcedToTrigger()) {
+ return $noArchiveFound;
+ }
+
+ $idAndVisits = ArchiveSelector::getArchiveIdAndVisits($this->params, $minDatetimeArchiveProcessedUTC);
+ if (!$idAndVisits) {
+ return $noArchiveFound;
+ }
+ return $idAndVisits;
+ }
+
+ /**
+ * Returns the minimum archive processed datetime to look at. Only public for tests.
+ *
+ * @return int|bool Datetime timestamp, or false if must look at any archive available
+ */
+ protected function getMinTimeArchiveProcessed()
+ {
+ $endDateTimestamp = self::determineIfArchivePermanent($this->params->getDateEnd());
+ $isArchiveTemporary = ($endDateTimestamp === false);
+ $this->temporaryArchive = $isArchiveTemporary;
+
+ if ($endDateTimestamp) {
+ // Permanent archive
+ return $endDateTimestamp;
+ }
+ // Temporary archive
+ return Rules::getMinTimeProcessedForTemporaryArchive($this->params->getDateStart(), $this->params->getPeriod(), $this->params->getSegment(), $this->params->getSite());
+ }
+
+ protected static function determineIfArchivePermanent(Date $dateEnd)
+ {
+ $now = time();
+ $endTimestampUTC = strtotime($dateEnd->getDateEndUTC());
+ if ($endTimestampUTC <= $now) {
+ // - if the period we are looking for is finished, we look for a ts_archived that
+ // is greater than the last day of the archive
+ return $endTimestampUTC;
+ }
+ return false;
+ }
+
+ protected function isArchiveTemporary()
+ {
+ if (is_null($this->temporaryArchive)) {
+ throw new \Exception("getMinTimeArchiveProcessed() should be called prior to isArchiveTemporary()");
+ }
+ return $this->temporaryArchive;
+ }
+
+}
+
diff --git a/www/analytics/core/ArchiveProcessor/Parameters.php b/www/analytics/core/ArchiveProcessor/Parameters.php
new file mode 100644
index 00000000..9d9a9088
--- /dev/null
+++ b/www/analytics/core/ArchiveProcessor/Parameters.php
@@ -0,0 +1,194 @@
+site = $site;
+ $this->period = $period;
+ $this->segment = $segment;
+ $this->skipAggregationOfSubTables = $skipAggregationOfSubTables;
+ }
+
+ /**
+ * @ignore
+ */
+ public function setRequestedPlugin($plugin)
+ {
+ $this->requestedPlugin = $plugin;
+ }
+
+ /**
+ * @ignore
+ */
+ public function getRequestedPlugin()
+ {
+ return $this->requestedPlugin;
+ }
+
+ /**
+ * Returns the period we are computing statistics for.
+ *
+ * @return Period
+ * @api
+ */
+ public function getPeriod()
+ {
+ return $this->period;
+ }
+
+ /**
+ * Returns the array of Period which make up this archive.
+ *
+ * @return \Piwik\Period[]
+ * @ignore
+ */
+ public function getSubPeriods()
+ {
+ if($this->getPeriod()->getLabel() == 'day') {
+ return array( $this->getPeriod() );
+ }
+ return $this->getPeriod()->getSubperiods();
+ }
+
+ /**
+ * @return array
+ * @ignore
+ */
+ public function getIdSites()
+ {
+ $idSite = $this->getSite()->getId();
+
+ $idSites = array($idSite);
+
+ Piwik::postEvent('ArchiveProcessor.Parameters.getIdSites', array(&$idSites, $this->getPeriod()));
+
+ return $idSites;
+ }
+
+ /**
+ * Returns the site we are computing statistics for.
+ *
+ * @return Site
+ * @api
+ */
+ public function getSite()
+ {
+ return $this->site;
+ }
+
+ /**
+ * The Segment used to limit the set of visits that are being aggregated.
+ *
+ * @return Segment
+ * @api
+ */
+ public function getSegment()
+ {
+ return $this->segment;
+ }
+
+ /**
+ * Returns the end day of the period in the site's timezone.
+ *
+ * @return Date
+ */
+ public function getDateEnd()
+ {
+ return $this->getPeriod()->getDateEnd()->setTimezone($this->getSite()->getTimezone());
+ }
+
+ /**
+ * Returns the start day of the period in the site's timezone.
+ *
+ * @return Date
+ */
+ public function getDateStart()
+ {
+ return $this->getPeriod()->getDateStart()->setTimezone($this->getSite()->getTimezone());
+ }
+
+ /**
+ * @return bool
+ */
+ public function isSingleSiteDayArchive()
+ {
+ $oneSite = $this->isSingleSite();
+ $oneDay = $this->getPeriod()->getLabel() == 'day';
+ return $oneDay && $oneSite;
+ }
+
+ public function isSingleSite()
+ {
+ return count($this->getIdSites()) == 1;
+ }
+
+ public function isSkipAggregationOfSubTables()
+ {
+ return $this->skipAggregationOfSubTables;
+ }
+
+ public function logStatusDebug($isTemporary)
+ {
+ $temporary = 'definitive archive';
+ if ($isTemporary) {
+ $temporary = 'temporary archive';
+ }
+ Log::verbose(
+ "%s archive, idSite = %d (%s), segment '%s', report = '%s', UTC datetime [%s -> %s]",
+ $this->getPeriod()->getLabel(),
+ $this->getSite()->getId(),
+ $temporary,
+ $this->getSegment()->getString(),
+ $this->getRequestedPlugin(),
+ $this->getDateStart()->getDateStartUTC(),
+ $this->getDateEnd()->getDateEndUTC()
+ );
+ }
+}
diff --git a/www/analytics/core/ArchiveProcessor/PluginsArchiver.php b/www/analytics/core/ArchiveProcessor/PluginsArchiver.php
new file mode 100644
index 00000000..82f361f7
--- /dev/null
+++ b/www/analytics/core/ArchiveProcessor/PluginsArchiver.php
@@ -0,0 +1,197 @@
+params = $params;
+
+ $this->archiveWriter = new ArchiveWriter($this->params, $isTemporaryArchive);
+ $this->archiveWriter->initNewArchive();
+
+ $this->archiveProcessor = new ArchiveProcessor($this->params, $this->archiveWriter);
+
+ $this->isSingleSiteDayArchive = $this->params->isSingleSiteDayArchive();
+ }
+
+ /**
+ * If period is day, will get the core metrics (including visits) from the logs.
+ * If period is != day, will sum the core metrics from the existing archives.
+ * @return array Core metrics
+ */
+ public function callAggregateCoreMetrics()
+ {
+ if($this->isSingleSiteDayArchive) {
+ $metrics = $this->aggregateDayVisitsMetrics();
+ } else {
+ $metrics = $this->aggregateMultipleVisitsMetrics();
+ }
+
+ if (empty($metrics)) {
+ return array(
+ 'nb_visits' => false,
+ 'nb_visits_converted' => false
+ );
+ }
+ return array(
+ 'nb_visits' => $metrics['nb_visits'],
+ 'nb_visits_converted' => $metrics['nb_visits_converted']
+ );
+ }
+
+ /**
+ * Instantiates the Archiver class in each plugin that defines it,
+ * and triggers Aggregation processing on these plugins.
+ */
+ public function callAggregateAllPlugins($visits, $visitsConverted)
+ {
+ $this->archiveProcessor->setNumberOfVisits($visits, $visitsConverted);
+
+ $archivers = $this->getPluginArchivers();
+
+ foreach($archivers as $pluginName => $archiverClass) {
+
+ // We clean up below all tables created during this function call (and recursive calls)
+ $latestUsedTableId = Manager::getInstance()->getMostRecentTableId();
+
+ /** @var Archiver $archiver */
+ $archiver = new $archiverClass($this->archiveProcessor);
+
+ if(!$archiver->isEnabled()) {
+ continue;
+ }
+ if($this->shouldProcessReportsForPlugin($pluginName)) {
+ if($this->isSingleSiteDayArchive) {
+ $archiver->aggregateDayReport();
+ } else {
+ $archiver->aggregateMultipleReports();
+ }
+ }
+
+ Manager::getInstance()->deleteAll($latestUsedTableId);
+ unset($archiver);
+ }
+ }
+
+ public function finalizeArchive()
+ {
+ $this->params->logStatusDebug( $this->archiveWriter->isArchiveTemporary );
+ $this->archiveWriter->finalizeArchive();
+ return $this->archiveWriter->getIdArchive();
+ }
+
+ /**
+ * Loads Archiver class from any plugin that defines one.
+ *
+ * @return \Piwik\Plugin\Archiver[]
+ */
+ protected function getPluginArchivers()
+ {
+ if (empty(static::$archivers)) {
+ $pluginNames = \Piwik\Plugin\Manager::getInstance()->getActivatedPlugins();
+ $archivers = array();
+ foreach ($pluginNames as $pluginName) {
+ $archivers[$pluginName] = self::getPluginArchiverClass($pluginName);
+ }
+ static::$archivers = array_filter($archivers);
+ }
+ return static::$archivers;
+ }
+
+ private static function getPluginArchiverClass($pluginName)
+ {
+ $klassName = 'Piwik\\Plugins\\' . $pluginName . '\\Archiver';
+ if (class_exists($klassName)
+ && is_subclass_of($klassName, 'Piwik\\Plugin\\Archiver')) {
+ return $klassName;
+ }
+ return false;
+ }
+
+ /**
+ * Whether the specified plugin's reports should be archived
+ * @param string $pluginName
+ * @return bool
+ */
+ protected function shouldProcessReportsForPlugin($pluginName)
+ {
+ if ($this->params->getRequestedPlugin() == $pluginName) {
+ return true;
+ }
+ if (Rules::shouldProcessReportsAllPlugins(
+ $this->params->getIdSites(),
+ $this->params->getSegment(),
+ $this->params->getPeriod()->getLabel())) {
+ return true;
+ }
+
+ if (!\Piwik\Plugin\Manager::getInstance()->isPluginLoaded($this->params->getRequestedPlugin())) {
+ return true;
+ }
+ return false;
+ }
+
+ protected function aggregateDayVisitsMetrics()
+ {
+ $query = $this->archiveProcessor->getLogAggregator()->queryVisitsByDimension();
+ $data = $query->fetch();
+
+ $metrics = $this->convertMetricsIdToName($data);
+ $this->archiveProcessor->insertNumericRecords($metrics);
+ return $metrics;
+ }
+
+ protected function convertMetricsIdToName($data)
+ {
+ $metrics = array();
+ foreach ($data as $metricId => $value) {
+ $readableMetric = Metrics::$mappingFromIdToName[$metricId];
+ $metrics[$readableMetric] = $value;
+ }
+ return $metrics;
+ }
+
+ protected function aggregateMultipleVisitsMetrics()
+ {
+ $toSum = Metrics::getVisitsMetricNames();
+ $metrics = $this->archiveProcessor->aggregateNumericMetrics($toSum);
+ return $metrics;
+ }
+
+}
diff --git a/www/analytics/core/ArchiveProcessor/Rules.php b/www/analytics/core/ArchiveProcessor/Rules.php
new file mode 100644
index 00000000..6bcd5fbd
--- /dev/null
+++ b/www/analytics/core/ArchiveProcessor/Rules.php
@@ -0,0 +1,304 @@
+isEmpty() && $periodLabel != 'range') {
+ return true;
+ }
+
+ return self::isSegmentPreProcessed($idSites, $segment);
+ }
+
+ /**
+ * @param $idSites
+ * @return array
+ */
+ private static function getSegmentsToProcess($idSites)
+ {
+ $knownSegmentsToArchiveAllSites = SettingsPiwik::getKnownSegmentsToArchive();
+
+ $segmentsToProcess = $knownSegmentsToArchiveAllSites;
+ foreach ($idSites as $idSite) {
+ $segmentForThisWebsite = SettingsPiwik::getKnownSegmentsToArchiveForSite($idSite);
+ $segmentsToProcess = array_merge($segmentsToProcess, $segmentForThisWebsite);
+ }
+ $segmentsToProcess = array_unique($segmentsToProcess);
+ return $segmentsToProcess;
+ }
+
+ public static function getDoneFlagArchiveContainsOnePlugin(Segment $segment, $plugin, $isSkipAggregationOfSubTables = false)
+ {
+ $partial = self::isFlagArchivePartial($plugin, $isSkipAggregationOfSubTables);
+ return 'done' . $segment->getHash() . '.' . $plugin . $partial ;
+ }
+
+ private static function getDoneFlagArchiveContainsAllPlugins(Segment $segment)
+ {
+ return 'done' . $segment->getHash();
+ }
+
+ /**
+ * @param $plugin
+ * @param $isSkipAggregationOfSubTables
+ * @return string
+ */
+ private static function isFlagArchivePartial($plugin, $isSkipAggregationOfSubTables)
+ {
+ $partialArchive = '';
+ if ($plugin != "VisitsSummary" // VisitsSummary is always called when segmenting and should not have its own .partial archive
+ && $isSkipAggregationOfSubTables
+ ) {
+ $partialArchive = '.partial';
+ }
+ return $partialArchive;
+ }
+
+ /**
+ * @param array $plugins
+ * @param $segment
+ * @return array
+ */
+ public static function getDoneFlags(array $plugins, Segment $segment, $isSkipAggregationOfSubTables)
+ {
+ $doneFlags = array();
+ $doneAllPlugins = self::getDoneFlagArchiveContainsAllPlugins($segment);
+ $doneFlags[$doneAllPlugins] = $doneAllPlugins;
+
+ $plugins = array_unique($plugins);
+ foreach ($plugins as $plugin) {
+ $doneOnePlugin = self::getDoneFlagArchiveContainsOnePlugin($segment, $plugin, $isSkipAggregationOfSubTables);
+ $doneFlags[$plugin] = $doneOnePlugin;
+ }
+ return $doneFlags;
+ }
+
+ /**
+ * Given a monthly archive table, will delete all reports that are now outdated,
+ * or reports that ended with an error
+ *
+ * @param \Piwik\Date $date
+ * @return int|bool False, or timestamp indicating which archives to delete
+ */
+ public static function shouldPurgeOutdatedArchives(Date $date)
+ {
+ if (self::$purgeDisabledByTests) {
+ return false;
+ }
+ $key = self::FLAG_TABLE_PURGED . "blob_" . $date->toString('Y_m');
+ $timestamp = Option::get($key);
+
+ // we shall purge temporary archives after their timeout is finished, plus an extra 6 hours
+ // in case archiving is disabled or run once a day, we give it this extra time to run
+ // and re-process more recent records...
+ $temporaryArchivingTimeout = self::getTodayArchiveTimeToLive();
+ $hoursBetweenPurge = 6;
+ $purgeEveryNSeconds = max($temporaryArchivingTimeout, $hoursBetweenPurge * 3600);
+
+ // we only delete archives if we are able to process them, otherwise, the browser might process reports
+ // when &segment= is specified (or custom date range) and would below, delete temporary archives that the
+ // browser is not able to process until next cron run (which could be more than 1 hour away)
+ if (self::isRequestAuthorizedToArchive()
+ && (!$timestamp
+ || $timestamp < time() - $purgeEveryNSeconds)
+ ) {
+ Option::set($key, time());
+
+ if (self::isBrowserTriggerEnabled()) {
+ // If Browser Archiving is enabled, it is likely there are many more temporary archives
+ // We delete more often which is safe, since reports are re-processed on demand
+ $purgeArchivesOlderThan = Date::factory(time() - 2 * $temporaryArchivingTimeout)->getDateTime();
+ } else {
+ // If archive.php via Cron is building the reports, we should keep all temporary reports from today
+ $purgeArchivesOlderThan = Date::factory('today')->getDateTime();
+ }
+ return $purgeArchivesOlderThan;
+ }
+
+ Log::info("Purging temporary archives: skipped.");
+ return false;
+ }
+
+ public static function getMinTimeProcessedForTemporaryArchive(
+ Date $dateStart, \Piwik\Period $period, Segment $segment, Site $site)
+ {
+ $now = time();
+ $minimumArchiveTime = $now - Rules::getTodayArchiveTimeToLive();
+
+ $idSites = array($site->getId());
+ $isArchivingDisabled = Rules::isArchivingDisabledFor($idSites, $segment, $period->getLabel());
+ if ($isArchivingDisabled) {
+ if ($period->getNumberOfSubperiods() == 0
+ && $dateStart->getTimestamp() <= $now
+ ) {
+ // Today: accept any recent enough archive
+ $minimumArchiveTime = false;
+ } else {
+ // This week, this month, this year:
+ // accept any archive that was processed today after 00:00:01 this morning
+ $timezone = $site->getTimezone();
+ $minimumArchiveTime = Date::factory(Date::factory('now', $timezone)->getDateStartUTC())->setTimezone($timezone)->getTimestamp();
+ }
+ }
+ return $minimumArchiveTime;
+ }
+
+ public static function setTodayArchiveTimeToLive($timeToLiveSeconds)
+ {
+ $timeToLiveSeconds = (int)$timeToLiveSeconds;
+ if ($timeToLiveSeconds <= 0) {
+ throw new Exception(Piwik::translate('General_ExceptionInvalidArchiveTimeToLive'));
+ }
+ Option::set(self::OPTION_TODAY_ARCHIVE_TTL, $timeToLiveSeconds, $autoLoad = true);
+ }
+
+ public static function getTodayArchiveTimeToLive()
+ {
+ $uiSettingIsEnabled = Controller::isGeneralSettingsAdminEnabled();
+
+ if($uiSettingIsEnabled) {
+ $timeToLive = Option::get(self::OPTION_TODAY_ARCHIVE_TTL);
+ if ($timeToLive !== false) {
+ return $timeToLive;
+ }
+ }
+ return Config::getInstance()->General['time_before_today_archive_considered_outdated'];
+ }
+
+ public static function isArchivingDisabledFor(array $idSites, Segment $segment, $periodLabel)
+ {
+ if ($periodLabel == 'range') {
+ return false;
+ }
+ $processOneReportOnly = !self::shouldProcessReportsAllPlugins($idSites, $segment, $periodLabel);
+ $isArchivingDisabled = !self::isRequestAuthorizedToArchive() || self::$archivingDisabledByTests;
+
+ if ($processOneReportOnly) {
+
+ // When there is a segment, we disable archiving when browser_archiving_disabled_enforce applies
+ if (!$segment->isEmpty()
+ && $isArchivingDisabled
+ && Config::getInstance()->General['browser_archiving_disabled_enforce']
+ && !SettingsServer::isArchivePhpTriggered() // Only applies when we are not running archive.php
+ ) {
+ Log::debug("Archiving is disabled because of config setting browser_archiving_disabled_enforce=1");
+ return true;
+ }
+
+ // Always allow processing one report
+ return false;
+ }
+ return $isArchivingDisabled;
+ }
+
+ protected static function isRequestAuthorizedToArchive()
+ {
+ return Rules::isBrowserTriggerEnabled() || SettingsServer::isArchivePhpTriggered();
+ }
+
+ public static function isBrowserTriggerEnabled()
+ {
+ $uiSettingIsEnabled = Controller::isGeneralSettingsAdminEnabled();
+
+ if($uiSettingIsEnabled) {
+ $browserArchivingEnabled = Option::get(self::OPTION_BROWSER_TRIGGER_ARCHIVING);
+ if ($browserArchivingEnabled !== false) {
+ return (bool)$browserArchivingEnabled;
+ }
+ }
+ return (bool)Config::getInstance()->General['enable_browser_archiving_triggering'];
+ }
+
+ public static function setBrowserTriggerArchiving($enabled)
+ {
+ if (!is_bool($enabled)) {
+ throw new Exception('Browser trigger archiving must be set to true or false.');
+ }
+ Option::set(self::OPTION_BROWSER_TRIGGER_ARCHIVING, (int)$enabled, $autoLoad = true);
+ Cache::clearCacheGeneral();
+ }
+
+ /**
+ * @param array $idSites
+ * @param Segment $segment
+ * @return bool
+ */
+ protected static function isSegmentPreProcessed(array $idSites, Segment $segment)
+ {
+ $segmentsToProcess = self::getSegmentsToProcess($idSites);
+
+ if (empty($segmentsToProcess)) {
+ return false;
+ }
+ // If the requested segment is one of the segments to pre-process
+ // we ensure that any call to the API will trigger archiving of all reports for this segment
+ $segment = $segment->getString();
+
+ // Turns out the getString() above returns the URL decoded segment string
+ $segmentsToProcessUrlDecoded = array_map('urldecode', $segmentsToProcess);
+
+ if (in_array($segment, $segmentsToProcess)
+ || in_array($segment, $segmentsToProcessUrlDecoded)
+ ) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/www/analytics/core/AssetManager.php b/www/analytics/core/AssetManager.php
new file mode 100644
index 00000000..972c84c3
--- /dev/null
+++ b/www/analytics/core/AssetManager.php
@@ -0,0 +1,405 @@
+\n";
+ const JS_IMPORT_DIRECTIVE = "\n";
+ const GET_CSS_MODULE_ACTION = "index.php?module=Proxy&action=getCss";
+ const GET_CORE_JS_MODULE_ACTION = "index.php?module=Proxy&action=getCoreJs";
+ const GET_NON_CORE_JS_MODULE_ACTION = "index.php?module=Proxy&action=getNonCoreJs";
+
+ /**
+ * @var UIAssetCacheBuster
+ */
+ private $cacheBuster;
+
+ /**
+ * @var UIAssetFetcher
+ */
+ private $minimalStylesheetFetcher;
+
+ /**
+ * @var Theme
+ */
+ private $theme;
+
+ function __construct()
+ {
+ $this->cacheBuster = UIAssetCacheBuster::getInstance();
+ $this->minimalStylesheetFetcher = new StaticUIAssetFetcher(array('plugins/Zeitgeist/stylesheets/base.less'), array(), $this->theme);
+
+ $theme = Manager::getInstance()->getThemeEnabled();
+ if(!empty($theme)) {
+ $this->theme = new Theme();
+ }
+ }
+
+ /**
+ * @param UIAssetCacheBuster $cacheBuster
+ */
+ public function setCacheBuster($cacheBuster)
+ {
+ $this->cacheBuster = $cacheBuster;
+ }
+
+ /**
+ * @param UIAssetFetcher $minimalStylesheetFetcher
+ */
+ public function setMinimalStylesheetFetcher($minimalStylesheetFetcher)
+ {
+ $this->minimalStylesheetFetcher = $minimalStylesheetFetcher;
+ }
+
+ /**
+ * @param Theme $theme
+ */
+ public function setTheme($theme)
+ {
+ $this->theme = $theme;
+ }
+
+ /**
+ * Return CSS file inclusion directive(s) using the markup
+ *
+ * @return string
+ */
+ public function getCssInclusionDirective()
+ {
+ return sprintf(self::CSS_IMPORT_DIRECTIVE, self::GET_CSS_MODULE_ACTION);
+ }
+
+ /**
+ * Return JS file inclusion directive(s) using the markup ";
+
+ if ($this->isMergedAssetsDisabled()) {
+
+ $this->getMergedCoreJSAsset()->delete();
+ $this->getMergedNonCoreJSAsset()->delete();
+
+ $result .= $this->getIndividualJsIncludes();
+
+ } else {
+
+ $result .= sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_CORE_JS_MODULE_ACTION);
+ $result .= sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_NON_CORE_JS_MODULE_ACTION);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Return the base.less compiled to css
+ *
+ * @return UIAsset
+ */
+ public function getCompiledBaseCss()
+ {
+ $mergedAsset = new InMemoryUIAsset();
+
+ $assetMerger = new StylesheetUIAssetMerger($mergedAsset, $this->minimalStylesheetFetcher, $this->cacheBuster);
+
+ $assetMerger->generateFile();
+
+ return $mergedAsset;
+ }
+
+ /**
+ * Return the css merged file absolute location.
+ * If there is none, the generation process will be triggered.
+ *
+ * @return UIAsset
+ */
+ public function getMergedStylesheet()
+ {
+ $mergedAsset = $this->getMergedStylesheetAsset();
+
+ $assetFetcher = new StylesheetUIAssetFetcher(Manager::getInstance()->getLoadedPluginsName(), $this->theme);
+
+ $assetMerger = new StylesheetUIAssetMerger($mergedAsset, $assetFetcher, $this->cacheBuster);
+
+ $assetMerger->generateFile();
+
+ return $mergedAsset;
+ }
+
+ /**
+ * Return the core js merged file absolute location.
+ * If there is none, the generation process will be triggered.
+ *
+ * @return UIAsset
+ */
+ public function getMergedCoreJavaScript()
+ {
+ return $this->getMergedJavascript($this->getCoreJScriptFetcher(), $this->getMergedCoreJSAsset());
+ }
+
+ /**
+ * Return the non core js merged file absolute location.
+ * If there is none, the generation process will be triggered.
+ *
+ * @return UIAsset
+ */
+ public function getMergedNonCoreJavaScript()
+ {
+ return $this->getMergedJavascript($this->getNonCoreJScriptFetcher(), $this->getMergedNonCoreJSAsset());
+ }
+
+ /**
+ * @param boolean $core
+ * @return string[]
+ */
+ public function getLoadedPlugins($core)
+ {
+ $loadedPlugins = array();
+
+ foreach(Manager::getInstance()->getPluginsLoadedAndActivated() as $plugin) {
+
+ $pluginName = $plugin->getPluginName();
+ $pluginIsCore = Manager::getInstance()->isPluginBundledWithCore($pluginName);
+
+ if(($pluginIsCore && $core) || (!$pluginIsCore && !$core))
+ $loadedPlugins[] = $pluginName;
+ }
+
+ return $loadedPlugins;
+ }
+
+ /**
+ * Remove previous merged assets
+ */
+ public function removeMergedAssets($pluginName = false)
+ {
+ $assetsToRemove = array($this->getMergedStylesheetAsset());
+
+ if($pluginName) {
+
+ if($this->pluginContainsJScriptAssets($pluginName)) {
+
+ PiwikConfig::getInstance()->init();
+ if(Manager::getInstance()->isPluginBundledWithCore($pluginName)) {
+
+ $assetsToRemove[] = $this->getMergedCoreJSAsset();
+
+ } else {
+
+ $assetsToRemove[] = $this->getMergedNonCoreJSAsset();
+ }
+ }
+
+ } else {
+
+ $assetsToRemove[] = $this->getMergedCoreJSAsset();
+ $assetsToRemove[] = $this->getMergedNonCoreJSAsset();
+ }
+
+ $this->removeAssets($assetsToRemove);
+ }
+
+ /**
+ * Check if the merged file directory exists and is writable.
+ *
+ * @return string The directory location
+ * @throws Exception if directory is not writable.
+ */
+ public function getAssetDirectory()
+ {
+ $mergedFileDirectory = PIWIK_USER_PATH . "/tmp/assets";
+ $mergedFileDirectory = SettingsPiwik::rewriteTmpPathWithHostname($mergedFileDirectory);
+
+ if (!is_dir($mergedFileDirectory)) {
+ Filesystem::mkdir($mergedFileDirectory);
+ }
+
+ if (!is_writable($mergedFileDirectory)) {
+ throw new Exception("Directory " . $mergedFileDirectory . " has to be writable.");
+ }
+
+ return $mergedFileDirectory;
+ }
+
+ /**
+ * Return the global option disable_merged_assets
+ *
+ * @return boolean
+ */
+ public function isMergedAssetsDisabled()
+ {
+ return Config::getInstance()->Debug['disable_merged_assets'];
+ }
+
+ /**
+ * @param UIAssetFetcher $assetFetcher
+ * @param UIAsset $mergedAsset
+ * @return UIAsset
+ */
+ private function getMergedJavascript($assetFetcher, $mergedAsset)
+ {
+ $assetMerger = new JScriptUIAssetMerger($mergedAsset, $assetFetcher, $this->cacheBuster);
+
+ $assetMerger->generateFile();
+
+ return $mergedAsset;
+ }
+
+ /**
+ * Return individual JS file inclusion directive(s) using the markup ';
+
+ /**
+ * @var Twig_Environment
+ */
+ private $twig;
+
+ public function __construct()
+ {
+ $loader = $this->getDefaultThemeLoader();
+
+ $this->addPluginNamespaces($loader);
+
+ // If theme != default we need to chain
+ $chainLoader = new Twig_Loader_Chain(array($loader));
+
+ // Create new Twig Environment and set cache dir
+ $templatesCompiledPath = PIWIK_USER_PATH . '/tmp/templates_c';
+ $templatesCompiledPath = SettingsPiwik::rewriteTmpPathWithHostname($templatesCompiledPath);
+
+ $this->twig = new Twig_Environment($chainLoader,
+ array(
+ 'debug' => true, // to use {{ dump(var) }} in twig templates
+ 'strict_variables' => true, // throw an exception if variables are invalid
+ 'cache' => $templatesCompiledPath,
+ )
+ );
+ $this->twig->addExtension(new Twig_Extension_Debug());
+ $this->twig->clearTemplateCache();
+
+ $this->addFilter_translate();
+ $this->addFilter_urlRewriteWithParameters();
+ $this->addFilter_sumTime();
+ $this->addFilter_money();
+ $this->addFilter_truncate();
+ $this->addFilter_notificiation();
+ $this->addFilter_percentage();
+ $this->addFilter_prettyDate();
+ $this->twig->addFilter(new Twig_SimpleFilter('implode', 'implode'));
+ $this->twig->addFilter(new Twig_SimpleFilter('ucwords', 'ucwords'));
+
+ $this->addFunction_includeAssets();
+ $this->addFunction_linkTo();
+ $this->addFunction_sparkline();
+ $this->addFunction_postEvent();
+ $this->addFunction_isPluginLoaded();
+ $this->addFunction_getJavascriptTranslations();
+
+ $this->twig->addTokenParser(new RenderTokenParser());
+ }
+
+ protected function addFunction_getJavascriptTranslations()
+ {
+ $getJavascriptTranslations = new Twig_SimpleFunction(
+ 'getJavascriptTranslations',
+ array('Piwik\\Translate', 'getJavascriptTranslations')
+ );
+ $this->twig->addFunction($getJavascriptTranslations);
+ }
+
+ protected function addFunction_isPluginLoaded()
+ {
+ $isPluginLoadedFunction = new Twig_SimpleFunction('isPluginLoaded', function ($pluginName) {
+ return \Piwik\Plugin\Manager::getInstance()->isPluginLoaded($pluginName);
+ });
+ $this->twig->addFunction($isPluginLoadedFunction);
+ }
+
+ protected function addFunction_includeAssets()
+ {
+ $includeAssetsFunction = new Twig_SimpleFunction('includeAssets', function ($params) {
+ if (!isset($params['type'])) {
+ throw new Exception("The function includeAssets needs a 'type' parameter.");
+ }
+
+ $assetType = strtolower($params['type']);
+ switch ($assetType) {
+ case 'css':
+ return AssetManager::getInstance()->getCssInclusionDirective();
+ case 'js':
+ return AssetManager::getInstance()->getJsInclusionDirective();
+ default:
+ throw new Exception("The twig function includeAssets 'type' parameter needs to be either 'css' or 'js'.");
+ }
+ });
+ $this->twig->addFunction($includeAssetsFunction);
+ }
+
+ protected function addFunction_postEvent()
+ {
+ $postEventFunction = new Twig_SimpleFunction('postEvent', function ($eventName) {
+ // get parameters to twig function
+ $params = func_get_args();
+ // remove the first value (event name)
+ array_shift($params);
+
+ // make the first value the string that will get output in the template
+ // plugins can modify this string
+ $str = '';
+ $params = array_merge( array( &$str ), $params);
+
+ Piwik::postEvent($eventName, $params);
+ return $str;
+ }, array('is_safe' => array('html')));
+ $this->twig->addFunction($postEventFunction);
+ }
+
+ protected function addFunction_sparkline()
+ {
+ $sparklineFunction = new Twig_SimpleFunction('sparkline', function ($src) {
+ $width = Sparkline::DEFAULT_WIDTH;
+ $height = Sparkline::DEFAULT_HEIGHT;
+ return sprintf(Twig::SPARKLINE_TEMPLATE, $src, $width, $height);
+ }, array('is_safe' => array('html')));
+ $this->twig->addFunction($sparklineFunction);
+ }
+
+ protected function addFunction_linkTo()
+ {
+ $urlFunction = new Twig_SimpleFunction('linkTo', function ($params) {
+ return 'index.php' . Url::getCurrentQueryStringWithParametersModified($params);
+ });
+ $this->twig->addFunction($urlFunction);
+ }
+
+ /**
+ * @return Twig_Loader_Filesystem
+ */
+ private function getDefaultThemeLoader()
+ {
+ $themeLoader = new Twig_Loader_Filesystem(array(
+ sprintf("%s/plugins/%s/templates/", PIWIK_INCLUDE_PATH, \Piwik\Plugin\Manager::DEFAULT_THEME)
+ ));
+
+ return $themeLoader;
+ }
+
+ public function getTwigEnvironment()
+ {
+ return $this->twig;
+ }
+
+ protected function addFilter_notificiation()
+ {
+ $twigEnv = $this->getTwigEnvironment();
+ $notificationFunction = new Twig_SimpleFilter('notification', function ($message, $options) use ($twigEnv) {
+
+ $template = ' $value) {
+ if (ctype_alpha($key)) {
+ $template .= sprintf('data-%s="%s" ', $key, twig_escape_filter($twigEnv, $value, 'html_attr'));
+ }
+ }
+
+ $template .= '>';
+
+ if (!empty($options['raw'])) {
+ $template .= $message;
+ } else {
+ $template .= twig_escape_filter($twigEnv, $message, 'html');
+ }
+
+ $template .= '
';
+
+ return $template;
+
+ }, array('is_safe' => array('html')));
+ $this->twig->addFilter($notificationFunction);
+ }
+
+ protected function addFilter_prettyDate()
+ {
+ $prettyDate = new Twig_SimpleFilter('prettyDate', function ($dateString, $period) {
+ return Range::factory($period, $dateString)->getLocalizedShortString();
+ });
+ $this->twig->addFilter($prettyDate);
+ }
+
+ protected function addFilter_percentage()
+ {
+ $percentage = new Twig_SimpleFilter('percentage', function ($string, $totalValue, $precision = 1) {
+ return Piwik::getPercentageSafe($string, $totalValue, $precision) . '%';
+ });
+ $this->twig->addFilter($percentage);
+ }
+
+ protected function addFilter_truncate()
+ {
+ $truncateFilter = new Twig_SimpleFilter('truncate', function ($string, $size) {
+ if (strlen($string) < $size) {
+ return $string;
+ } else {
+ $array = str_split($string, $size);
+ return array_shift($array) . "...";
+ }
+ });
+ $this->twig->addFilter($truncateFilter);
+ }
+
+ protected function addFilter_money()
+ {
+ $moneyFilter = new Twig_SimpleFilter('money', function ($amount) {
+ if (func_num_args() != 2) {
+ throw new Exception('the money modifier expects one parameter: the idSite.');
+ }
+ $idSite = func_get_args();
+ $idSite = $idSite[1];
+ return MetricsFormatter::getPrettyMoney($amount, $idSite);
+ });
+ $this->twig->addFilter($moneyFilter);
+ }
+
+ protected function addFilter_sumTime()
+ {
+ $sumtimeFilter = new Twig_SimpleFilter('sumtime', function ($numberOfSeconds) {
+ return MetricsFormatter::getPrettyTimeFromSeconds($numberOfSeconds);
+ });
+ $this->twig->addFilter($sumtimeFilter);
+ }
+
+ protected function addFilter_urlRewriteWithParameters()
+ {
+ $urlRewriteFilter = new Twig_SimpleFilter('urlRewriteWithParameters', function ($parameters) {
+ $parameters['updated'] = null;
+ $url = Url::getCurrentQueryStringWithParametersModified($parameters);
+ return $url;
+ });
+ $this->twig->addFilter($urlRewriteFilter);
+ }
+
+ protected function addFilter_translate()
+ {
+ $translateFilter = new Twig_SimpleFilter('translate', function ($stringToken) {
+ if (func_num_args() <= 1) {
+ $aValues = array();
+ } else {
+ $aValues = func_get_args();
+ array_shift($aValues);
+ }
+
+ try {
+ $stringTranslated = Piwik::translate($stringToken, $aValues);
+ } catch (Exception $e) {
+ $stringTranslated = $stringToken;
+ }
+ return $stringTranslated;
+ });
+ $this->twig->addFilter($translateFilter);
+ }
+
+ private function addPluginNamespaces(Twig_Loader_Filesystem $loader)
+ {
+ $plugins = \Piwik\Plugin\Manager::getInstance()->getAllPluginsNames();
+ foreach ($plugins as $name) {
+ $path = sprintf("%s/plugins/%s/templates/", PIWIK_INCLUDE_PATH, $name);
+ if (is_dir($path)) {
+ $loader->addPath(PIWIK_INCLUDE_PATH . '/plugins/' . $name . '/templates', $name);
+ }
+ }
+ }
+
+ /**
+ * Prepend relative paths with absolute Piwik path
+ *
+ * @param string $value relative path (pass by reference)
+ * @param int $key (don't care)
+ * @param string $path Piwik root
+ */
+ public static function addPiwikPath(&$value, $key, $path)
+ {
+ if ($value[0] != '/' && $value[0] != DIRECTORY_SEPARATOR) {
+ $value = $path . "/$value";
+ }
+ }
+}
diff --git a/www/analytics/core/Unzip.php b/www/analytics/core/Unzip.php
new file mode 100644
index 00000000..2ba8cfcd
--- /dev/null
+++ b/www/analytics/core/Unzip.php
@@ -0,0 +1,52 @@
+filename = $filename;
+ }
+
+ /**
+ * Extracts the contents of the .gz file to $pathExtracted.
+ *
+ * @param string $pathExtracted Must be file, not directory.
+ * @return bool true if successful, false if otherwise.
+ */
+ public function extract($pathExtracted)
+ {
+ $file = gzopen($this->filename, 'r');
+ if ($file === false) {
+ $this->error = "gzopen failed";
+ return false;
+ }
+
+ $output = fopen($pathExtracted, 'w');
+ while (!feof($file)) {
+ fwrite($output, fread($file, 1024 * 1024));
+ }
+ fclose($output);
+
+ $success = gzclose($file);
+ if ($success === false) {
+ $this->error = "gzclose failed";
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get error status string for the latest error.
+ *
+ * @return string
+ */
+ public function errorInfo()
+ {
+ return $this->error;
+ }
+}
+
diff --git a/www/analytics/core/Unzip/PclZip.php b/www/analytics/core/Unzip/PclZip.php
new file mode 100644
index 00000000..79fd7963
--- /dev/null
+++ b/www/analytics/core/Unzip/PclZip.php
@@ -0,0 +1,89 @@
+pclzip = new \PclZip($filename);
+ $this->filename = $filename;
+ }
+
+ /**
+ * Extract files from archive to target directory
+ *
+ * @param string $pathExtracted Absolute path of target directory
+ * @return mixed Array of filenames if successful; or 0 if an error occurred
+ */
+ public function extract($pathExtracted)
+ {
+ $pathExtracted = str_replace('\\', '/', $pathExtracted);
+ $list = $this->pclzip->listContent();
+ if (empty($list)) {
+ return 0;
+ }
+
+ foreach ($list as $entry) {
+ $filename = str_replace('\\', '/', $entry['stored_filename']);
+ $parts = explode('/', $filename);
+
+ if (!strncmp($filename, '/', 1) ||
+ array_search('..', $parts) !== false ||
+ strpos($filename, ':') !== false
+ ) {
+ return 0;
+ }
+ }
+
+ // PCLZIP_CB_PRE_EXTRACT callback returns 0 to skip, 1 to resume, or 2 to abort
+ return $this->pclzip->extract(
+ PCLZIP_OPT_PATH, $pathExtracted,
+ PCLZIP_OPT_STOP_ON_ERROR,
+ PCLZIP_OPT_REPLACE_NEWER,
+ PCLZIP_CB_PRE_EXTRACT, function ($p_event, &$p_header) use ($pathExtracted) {
+ return strncmp($p_header['filename'], $pathExtracted, strlen($pathExtracted)) ? 0 : 1;
+ }
+ );
+ }
+
+ /**
+ * Get error status string for the latest error
+ *
+ * @return string
+ */
+ public function errorInfo()
+ {
+ return $this->pclzip->errorInfo(true);
+ }
+}
diff --git a/www/analytics/core/Unzip/Tar.php b/www/analytics/core/Unzip/Tar.php
new file mode 100755
index 00000000..919fcc91
--- /dev/null
+++ b/www/analytics/core/Unzip/Tar.php
@@ -0,0 +1,84 @@
+tarArchive = new Archive_Tar($filename, $compression);
+ }
+
+ /**
+ * Extracts the contents of the tar file to $pathExtracted.
+ *
+ * @param string $pathExtracted Directory to extract into.
+ * @return bool true if successful, false if otherwise.
+ */
+ public function extract($pathExtracted)
+ {
+ return $this->tarArchive->extract($pathExtracted);
+ }
+
+ /**
+ * Extracts one file held in a tar archive and returns the deflated file
+ * as a string.
+ *
+ * @param string $inArchivePath Path to file in the tar archive.
+ * @return bool true if successful, false if otherwise.
+ */
+ public function extractInString($inArchivePath)
+ {
+ return $this->tarArchive->extractInString($inArchivePath);
+ }
+
+ /**
+ * Lists the files held in the tar archive.
+ *
+ * @return array List of paths describing everything held in the tar archive.
+ */
+ public function listContent()
+ {
+ return $this->tarArchive->listContent();
+ }
+
+ /**
+ * Get error status string for the latest error.
+ *
+ * @return string
+ */
+ public function errorInfo()
+ {
+ return $this->tarArchive->error_object->getMessage();
+ }
+}
diff --git a/www/analytics/core/Unzip/UncompressInterface.php b/www/analytics/core/Unzip/UncompressInterface.php
new file mode 100644
index 00000000..d3dccf20
--- /dev/null
+++ b/www/analytics/core/Unzip/UncompressInterface.php
@@ -0,0 +1,39 @@
+filename = $filename;
+ $this->ziparchive = new \ZipArchive;
+ if ($this->ziparchive->open($filename) !== true) {
+ throw new Exception('Error opening ' . $filename);
+ }
+ }
+
+ /**
+ * Extract files from archive to target directory
+ *
+ * @param string $pathExtracted Absolute path of target directory
+ * @return mixed Array of filenames if successful; or 0 if an error occurred
+ */
+ public function extract($pathExtracted)
+ {
+ if (substr_compare($pathExtracted, '/', -1))
+ $pathExtracted .= '/';
+
+ $fileselector = array();
+ $list = array();
+ $count = $this->ziparchive->numFiles;
+ if ($count === 0) {
+ return 0;
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ $entry = $this->ziparchive->statIndex($i);
+
+ $filename = str_replace('\\', '/', $entry['name']);
+ $parts = explode('/', $filename);
+
+ if (!strncmp($filename, '/', 1) ||
+ array_search('..', $parts) !== false ||
+ strpos($filename, ':') !== false
+ ) {
+ return 0;
+ }
+ $fileselector[] = $entry['name'];
+ $list[] = array(
+ 'filename' => $pathExtracted . $entry['name'],
+ 'stored_filename' => $entry['name'],
+ 'size' => $entry['size'],
+ 'compressed_size' => $entry['comp_size'],
+ 'mtime' => $entry['mtime'],
+ 'index' => $i,
+ 'crc' => $entry['crc'],
+ );
+ }
+
+ $res = $this->ziparchive->extractTo($pathExtracted, $fileselector);
+ if ($res === false)
+ return 0;
+ return $list;
+ }
+
+ /**
+ * Get error status string for the latest error
+ *
+ * @return string
+ */
+ public function errorInfo()
+ {
+ static $statusStrings = array(
+ \ZIPARCHIVE::ER_OK => 'No error',
+ \ZIPARCHIVE::ER_MULTIDISK => 'Multi-disk zip archives not supported',
+ \ZIPARCHIVE::ER_RENAME => 'Renaming temporary file failed',
+ \ZIPARCHIVE::ER_CLOSE => 'Closing zip archive failed',
+ \ZIPARCHIVE::ER_SEEK => 'Seek error',
+ \ZIPARCHIVE::ER_READ => 'Read error',
+ \ZIPARCHIVE::ER_WRITE => 'Write error',
+ \ZIPARCHIVE::ER_CRC => 'CRC error',
+ \ZIPARCHIVE::ER_ZIPCLOSED => 'Containing zip archive was closed',
+ \ZIPARCHIVE::ER_NOENT => 'No such file',
+ \ZIPARCHIVE::ER_EXISTS => 'File already exists',
+ \ZIPARCHIVE::ER_OPEN => 'Can\'t open file',
+ \ZIPARCHIVE::ER_TMPOPEN => 'Failure to create temporary file',
+ \ZIPARCHIVE::ER_ZLIB => 'Zlib error',
+ \ZIPARCHIVE::ER_MEMORY => 'Malloc failure',
+ \ZIPARCHIVE::ER_CHANGED => 'Entry has been changed',
+ \ZIPARCHIVE::ER_COMPNOTSUPP => 'Compression method not supported',
+ \ZIPARCHIVE::ER_EOF => 'Premature EOF',
+ \ZIPARCHIVE::ER_INVAL => 'Invalid argument',
+ \ZIPARCHIVE::ER_NOZIP => 'Not a zip archive',
+ \ZIPARCHIVE::ER_INTERNAL => 'Internal error',
+ \ZIPARCHIVE::ER_INCONS => 'Zip archive inconsistent',
+ \ZIPARCHIVE::ER_REMOVE => 'Can\'t remove file',
+ \ZIPARCHIVE::ER_DELETED => 'Entry has been deleted',
+ );
+
+ if (isset($statusStrings[$this->ziparchive->status])) {
+ $statusString = $statusStrings[$this->ziparchive->status];
+ } else {
+ $statusString = 'Unknown status';
+ }
+ return $statusString . '(' . $this->ziparchive->status . ')';
+ }
+}
diff --git a/www/analytics/core/UpdateCheck.php b/www/analytics/core/UpdateCheck.php
new file mode 100644
index 00000000..76b3a248
--- /dev/null
+++ b/www/analytics/core/UpdateCheck.php
@@ -0,0 +1,110 @@
+General['enable_auto_update'];
+ }
+
+ /**
+ * Check for a newer version
+ *
+ * @param bool $force Force check
+ * @param int $interval Interval used for update checks
+ */
+ public static function check($force = false, $interval = null)
+ {
+ if(!self::isAutoUpdateEnabled()) {
+ return;
+ }
+
+ if ($interval === null) {
+ $interval = self::CHECK_INTERVAL;
+ }
+
+ $lastTimeChecked = Option::get(self::LAST_TIME_CHECKED);
+ if ($force
+ || $lastTimeChecked === false
+ || time() - $interval > $lastTimeChecked
+ ) {
+ // set the time checked first, so that parallel Piwik requests don't all trigger the http requests
+ Option::set(self::LAST_TIME_CHECKED, time(), $autoLoad = 1);
+ $parameters = array(
+ 'piwik_version' => Version::VERSION,
+ 'php_version' => PHP_VERSION,
+ 'url' => Url::getCurrentUrlWithoutQueryString(),
+ 'trigger' => Common::getRequestVar('module', '', 'string'),
+ 'timezone' => API::getInstance()->getDefaultTimezone(),
+ );
+
+ $url = Config::getInstance()->General['api_service_url']
+ . '/1.0/getLatestVersion/'
+ . '?' . http_build_query($parameters, '', '&');
+ $timeout = self::SOCKET_TIMEOUT;
+
+ if (@Config::getInstance()->Debug['allow_upgrades_to_beta']) {
+ $url = 'http://builds.piwik.org/LATEST_BETA';
+ }
+
+ try {
+ $latestVersion = Http::sendHttpRequest($url, $timeout);
+ if (!preg_match('~^[0-9][0-9a-zA-Z_.-]*$~D', $latestVersion)) {
+ $latestVersion = '';
+ }
+ } catch (Exception $e) {
+ // e.g., disable_functions = fsockopen; allow_url_open = Off
+ $latestVersion = '';
+ }
+ Option::set(self::LATEST_VERSION, $latestVersion);
+ }
+ }
+
+ /**
+ * Returns the latest available version number. Does not perform a check whether a later version is available.
+ *
+ * @return false|string
+ */
+ public static function getLatestVersion()
+ {
+ return Option::get(self::LATEST_VERSION);
+ }
+
+ /**
+ * Returns version number of a newer Piwik release.
+ *
+ * @return string|bool false if current version is the latest available,
+ * or the latest version number if a newest release is available
+ */
+ public static function isNewestVersionAvailable()
+ {
+ $latestVersion = self::getLatestVersion();
+ if (!empty($latestVersion)
+ && version_compare(Version::VERSION, $latestVersion) == -1
+ ) {
+ return $latestVersion;
+ }
+ return false;
+ }
+}
diff --git a/www/analytics/core/Updater.php b/www/analytics/core/Updater.php
new file mode 100644
index 00000000..a1f39fb7
--- /dev/null
+++ b/www/analytics/core/Updater.php
@@ -0,0 +1,335 @@
+pathUpdateFileCore = PIWIK_INCLUDE_PATH . '/core/Updates/';
+ $this->pathUpdateFilePlugins = PIWIK_INCLUDE_PATH . '/plugins/%s/Updates/';
+ }
+
+ /**
+ * Add component to check
+ *
+ * @param string $name
+ * @param string $version
+ */
+ public function addComponentToCheck($name, $version)
+ {
+ $this->componentsToCheck[$name] = $version;
+ }
+
+ /**
+ * Record version of successfully completed component update
+ *
+ * @param string $name
+ * @param string $version
+ */
+ public static function recordComponentSuccessfullyUpdated($name, $version)
+ {
+ try {
+ Option::set(self::getNameInOptionTable($name), $version, $autoLoad = 1);
+ } catch (\Exception $e) {
+ // case when the option table is not yet created (before 0.2.10)
+ }
+ }
+
+ /**
+ * Returns the flag name to use in the option table to record current schema version
+ * @param string $name
+ * @return string
+ */
+ private static function getNameInOptionTable($name)
+ {
+ return 'version_' . $name;
+ }
+
+ /**
+ * Returns a list of components (core | plugin) that need to run through the upgrade process.
+ *
+ * @return array( componentName => array( file1 => version1, [...]), [...])
+ */
+ public function getComponentsWithUpdateFile()
+ {
+ $this->componentsWithNewVersion = $this->getComponentsWithNewVersion();
+ $this->componentsWithUpdateFile = $this->loadComponentsWithUpdateFile();
+ return $this->componentsWithUpdateFile;
+ }
+
+ /**
+ * Component has a new version?
+ *
+ * @param string $componentName
+ * @return bool TRUE if compoment is to be updated; FALSE if not
+ */
+ public function hasNewVersion($componentName)
+ {
+ return isset($this->componentsWithNewVersion) &&
+ isset($this->componentsWithNewVersion[$componentName]);
+ }
+
+ /**
+ * Does one of the new versions involve a major database update?
+ * Note: getSqlQueriesToExecute() must be called before this method!
+ *
+ * @return bool
+ */
+ public function hasMajorDbUpdate()
+ {
+ return $this->hasMajorDbUpdate;
+ }
+
+ /**
+ * Returns the list of SQL queries that would be executed during the update
+ *
+ * @return array of SQL queries
+ * @throws \Exception
+ */
+ public function getSqlQueriesToExecute()
+ {
+ $queries = array();
+ foreach ($this->componentsWithUpdateFile as $componentName => $componentUpdateInfo) {
+ foreach ($componentUpdateInfo as $file => $fileVersion) {
+ require_once $file; // prefixed by PIWIK_INCLUDE_PATH
+
+ $className = $this->getUpdateClassName($componentName, $fileVersion);
+ if (!class_exists($className, false)) {
+ throw new \Exception("The class $className was not found in $file");
+ }
+ $queriesForComponent = call_user_func(array($className, 'getSql'));
+ foreach ($queriesForComponent as $query => $error) {
+ $queries[] = $query . ';';
+ }
+ $this->hasMajorDbUpdate = $this->hasMajorDbUpdate || call_user_func(array($className, 'isMajorUpdate'));
+ }
+ // unfortunately had to extract this query from the Option class
+ $queries[] = 'UPDATE `' . Common::prefixTable('option') . '` '.
+ 'SET option_value = \'' . $fileVersion . '\' '.
+ 'WHERE option_name = \'' . self::getNameInOptionTable($componentName) . '\';';
+ }
+ return $queries;
+ }
+
+ private function getUpdateClassName($componentName, $fileVersion)
+ {
+ $suffix = strtolower(str_replace(array('-', '.'), '_', $fileVersion));
+ $className = 'Updates_' . $suffix;
+
+ if ($componentName == 'core') {
+ return '\\Piwik\\Updates\\' . $className;
+ }
+ return '\\Piwik\\Plugins\\' . $componentName . '\\' . $className;
+ }
+
+ /**
+ * Update the named component
+ *
+ * @param string $componentName 'core', or plugin name
+ * @throws \Exception|UpdaterErrorException
+ * @return array of warning strings if applicable
+ */
+ public function update($componentName)
+ {
+ $warningMessages = array();
+ foreach ($this->componentsWithUpdateFile[$componentName] as $file => $fileVersion) {
+ try {
+ require_once $file; // prefixed by PIWIK_INCLUDE_PATH
+
+ $className = $this->getUpdateClassName($componentName, $fileVersion);
+ if (class_exists($className, false)) {
+ // update()
+ call_user_func(array($className, 'update'));
+ }
+
+ self::recordComponentSuccessfullyUpdated($componentName, $fileVersion);
+ } catch (UpdaterErrorException $e) {
+ throw $e;
+ } catch (\Exception $e) {
+ $warningMessages[] = $e->getMessage();
+ }
+ }
+
+ // to debug, create core/Updates/X.php, update the core/Version.php, throw an Exception in the try, and comment the following line
+ self::recordComponentSuccessfullyUpdated($componentName, $this->componentsWithNewVersion[$componentName][self::INDEX_NEW_VERSION]);
+ return $warningMessages;
+ }
+
+ /**
+ * Construct list of update files for the outdated components
+ *
+ * @return array( componentName => array( file1 => version1, [...]), [...])
+ */
+ private function loadComponentsWithUpdateFile()
+ {
+ $componentsWithUpdateFile = array();
+ foreach ($this->componentsWithNewVersion as $name => $versions) {
+ $currentVersion = $versions[self::INDEX_CURRENT_VERSION];
+ $newVersion = $versions[self::INDEX_NEW_VERSION];
+
+ if ($name == 'core') {
+ $pathToUpdates = $this->pathUpdateFileCore . '*.php';
+ } else {
+ $pathToUpdates = sprintf($this->pathUpdateFilePlugins, $name) . '*.php';
+ }
+
+ $files = _glob($pathToUpdates);
+ if ($files == false) {
+ $files = array();
+ }
+
+ foreach ($files as $file) {
+ $fileVersion = basename($file, '.php');
+ if ( // if the update is from a newer version
+ version_compare($currentVersion, $fileVersion) == -1
+ // but we don't execute updates from non existing future releases
+ && version_compare($fileVersion, $newVersion) <= 0
+ ) {
+ $componentsWithUpdateFile[$name][$file] = $fileVersion;
+ }
+ }
+
+ if (isset($componentsWithUpdateFile[$name])) {
+ // order the update files by version asc
+ uasort($componentsWithUpdateFile[$name], "version_compare");
+ } else {
+ // there are no update file => nothing to do, update to the new version is successful
+ self::recordComponentSuccessfullyUpdated($name, $newVersion);
+ }
+ }
+ return $componentsWithUpdateFile;
+ }
+
+ /**
+ * Construct list of outdated components
+ *
+ * @throws \Exception
+ * @return array array( componentName => array( oldVersion, newVersion), [...])
+ */
+ public function getComponentsWithNewVersion()
+ {
+ $componentsToUpdate = array();
+
+ // we make sure core updates are processed before any plugin updates
+ if (isset($this->componentsToCheck['core'])) {
+ $coreVersions = $this->componentsToCheck['core'];
+ unset($this->componentsToCheck['core']);
+ $this->componentsToCheck = array_merge(array('core' => $coreVersions), $this->componentsToCheck);
+ }
+
+ foreach ($this->componentsToCheck as $name => $version) {
+ try {
+ $currentVersion = Option::get(self::getNameInOptionTable($name));
+ } catch (\Exception $e) {
+ // mysql error 1146: table doesn't exist
+ if (Db::get()->isErrNo($e, '1146')) {
+ // case when the option table is not yet created (before 0.2.10)
+ $currentVersion = false;
+ } else {
+ // failed for some other reason
+ throw $e;
+ }
+ }
+ if ($currentVersion === false) {
+ if ($name === 'core') {
+ // This should not happen
+ $currentVersion = Version::VERSION;
+ } else {
+ // When plugins have been installed since Piwik 2.0 this should not happen
+ // We "fix" the data for any plugin that may have been ported from Piwik 1.x
+ $currentVersion = $version;
+ }
+ self::recordComponentSuccessfullyUpdated($name, $currentVersion);
+ }
+
+ $versionCompare = version_compare($currentVersion, $version);
+ if ($versionCompare == -1) {
+ $componentsToUpdate[$name] = array(
+ self::INDEX_CURRENT_VERSION => $currentVersion,
+ self::INDEX_NEW_VERSION => $version
+ );
+ } else if ($versionCompare == 1) {
+ // the version in the DB is newest.. we choose to ignore
+ }
+ }
+ return $componentsToUpdate;
+ }
+
+ /**
+ * Performs database update(s)
+ *
+ * @param string $file Update script filename
+ * @param array $sqlarray An array of SQL queries to be executed
+ * @throws UpdaterErrorException
+ */
+ static function updateDatabase($file, $sqlarray)
+ {
+ foreach ($sqlarray as $update => $ignoreError) {
+ self::executeMigrationQuery($update, $ignoreError, $file);
+ }
+ }
+
+ /**
+ * Executes a database update query.
+ *
+ * @param string $updateSql Update SQL query.
+ * @param int|false $errorToIgnore A MySQL error code to ignore.
+ * @param string $file The Update file that's calling this method.
+ */
+ public static function executeMigrationQuery($updateSql, $errorToIgnore, $file)
+ {
+ try {
+ Db::exec($updateSql);
+ } catch (\Exception $e) {
+ self::handleQueryError($e, $updateSql, $errorToIgnore, $file);
+ }
+ }
+
+ /**
+ * Handle an error that is thrown from a database query.
+ *
+ * @param \Exception $e the exception thrown.
+ * @param string $updateSql Update SQL query.
+ * @param int|false $errorToIgnore A MySQL error code to ignore.
+ * @param string $file The Update file that's calling this method.
+ */
+ public static function handleQueryError($e, $updateSql, $errorToIgnore, $file)
+ {
+ if (($errorToIgnore === false)
+ || !Db::get()->isErrNo($e, $errorToIgnore)
+ ) {
+ $message = $file . ":\nError trying to execute the query '" . $updateSql . "'.\nThe error was: " . $e->getMessage();
+ throw new UpdaterErrorException($message);
+ }
+ }
+}
+
+/**
+ * Exception thrown by updater if a non-recoverable error occurs
+ *
+ */
+class UpdaterErrorException extends \Exception
+{
+}
diff --git a/www/analytics/core/Updates.php b/www/analytics/core/Updates.php
new file mode 100644
index 00000000..ff989cdd
--- /dev/null
+++ b/www/analytics/core/Updates.php
@@ -0,0 +1,108 @@
+ '1234', // if the query fails, it will be ignored if the error code is 1234
+ * 'ALTER .... ' => false, // if an error occurs, the update will stop and fail
+ * // and user will have to manually run the query
+ * )
+ */
+ static function getSql()
+ {
+ return array();
+ }
+
+ /**
+ * Incremental version update
+ */
+ static function update()
+ {
+ }
+
+ /**
+ * Tell the updater that this is a major update.
+ * Leads to a more visible notice.
+ *
+ * @return bool
+ */
+ static function isMajorUpdate()
+ {
+ return false;
+ }
+
+ /**
+ * Helper method to enable maintenance mode during large updates
+ */
+ static function enableMaintenanceMode()
+ {
+ $config = Config::getInstance();
+ $config->init();
+
+ $tracker = $config->Tracker;
+ $tracker['record_statistics'] = 0;
+ $config->Tracker = $tracker;
+
+ $general = $config->General;
+ $general['maintenance_mode'] = 1;
+ $config->General = $general;
+
+ $config->forceSave();
+ }
+
+ /**
+ * Helper method to disable maintenance mode after large updates
+ */
+ static function disableMaintenanceMode()
+ {
+ $config = Config::getInstance();
+ $config->init();
+
+ $tracker = $config->Tracker;
+ $tracker['record_statistics'] = 1;
+ $config->Tracker = $tracker;
+
+ $general = $config->General;
+ $general['maintenance_mode'] = 0;
+ $config->General = $general;
+
+ $config->forceSave();
+ }
+
+ public static function deletePluginFromConfigFile($pluginToDelete)
+ {
+ $config = Config::getInstance();
+ $config->init();
+ if (isset($config->Plugins['Plugins'])) {
+ $plugins = $config->Plugins['Plugins'];
+ if (($key = array_search($pluginToDelete, $plugins)) !== false) {
+ unset($plugins[$key]);
+ }
+ $config->Plugins['Plugins'] = $plugins;
+
+ $pluginsInstalled = $config->PluginsInstalled['PluginsInstalled'];
+ if (($key = array_search($pluginToDelete, $pluginsInstalled)) !== false) {
+ unset($pluginsInstalled[$key]);
+ }
+ $config->PluginsInstalled = array('PluginsInstalled' => $pluginsInstalled);
+
+ $config->forceSave();
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/0.2.10.php b/www/analytics/core/Updates/0.2.10.php
new file mode 100644
index 00000000..bb3bba97
--- /dev/null
+++ b/www/analytics/core/Updates/0.2.10.php
@@ -0,0 +1,73 @@
+ false,
+
+ // 0.1.7 [463]
+ 'ALTER IGNORE TABLE `' . Common::prefixTable('log_visit') . '`
+ CHANGE `location_provider` `location_provider` VARCHAR( 100 ) DEFAULT NULL' => '1054',
+
+ // 0.1.7 [470]
+ 'ALTER TABLE `' . Common::prefixTable('logger_api_call') . '`
+ CHANGE `parameter_names_default_values` `parameter_names_default_values` TEXT,
+ CHANGE `parameter_values` `parameter_values` TEXT,
+ CHANGE `returned_value` `returned_value` TEXT' => false,
+ 'ALTER TABLE `' . Common::prefixTable('logger_error') . '`
+ CHANGE `message` `message` TEXT' => false,
+ 'ALTER TABLE `' . Common::prefixTable('logger_exception') . '`
+ CHANGE `message` `message` TEXT' => false,
+ 'ALTER TABLE `' . Common::prefixTable('logger_message') . '`
+ CHANGE `message` `message` TEXT' => false,
+
+ // 0.2.2 [489]
+ 'ALTER IGNORE TABLE `' . Common::prefixTable('site') . '`
+ CHANGE `feedburnerName` `feedburnerName` VARCHAR( 100 ) DEFAULT NULL' => '1054',
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+
+ $obsoleteFile = '/plugins/ExamplePlugin/API.php';
+ if (file_exists(PIWIK_INCLUDE_PATH . $obsoleteFile)) {
+ @unlink(PIWIK_INCLUDE_PATH . $obsoleteFile);
+ }
+
+ $obsoleteDirectories = array(
+ '/plugins/AdminHome',
+ '/plugins/Home',
+ '/plugins/PluginsAdmin',
+ );
+ foreach ($obsoleteDirectories as $dir) {
+ if (file_exists(PIWIK_INCLUDE_PATH . $dir)) {
+ Filesystem::unlinkRecursive(PIWIK_INCLUDE_PATH . $dir, true);
+ }
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/0.2.12.php b/www/analytics/core/Updates/0.2.12.php
new file mode 100644
index 00000000..ff8fa18b
--- /dev/null
+++ b/www/analytics/core/Updates/0.2.12.php
@@ -0,0 +1,38 @@
+ false,
+ 'ALTER TABLE `' . Common::prefixTable('log_visit') . '`
+ DROP `config_color_depth`' => false,
+
+ // 0.2.12 [673]
+ // Note: requires INDEX privilege
+ 'DROP INDEX index_idaction ON `' . Common::prefixTable('log_action') . '`' => '1091',
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.2.13.php b/www/analytics/core/Updates/0.2.13.php
new file mode 100644
index 00000000..93228ad6
--- /dev/null
+++ b/www/analytics/core/Updates/0.2.13.php
@@ -0,0 +1,38 @@
+ false,
+
+ 'CREATE TABLE `' . Common::prefixTable('option') . "` (
+ option_name VARCHAR( 64 ) NOT NULL ,
+ option_value LONGTEXT NOT NULL ,
+ autoload TINYINT NOT NULL DEFAULT '1',
+ PRIMARY KEY ( option_name )
+ )" => false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.2.24.php b/www/analytics/core/Updates/0.2.24.php
new file mode 100644
index 00000000..e627f64f
--- /dev/null
+++ b/www/analytics/core/Updates/0.2.24.php
@@ -0,0 +1,36 @@
+ false,
+ 'CREATE INDEX index_idsite_date
+ ON ' . Common::prefixTable('log_visit') . ' (idsite, visit_server_date)' => false,
+ 'DROP INDEX index_idsite ON ' . Common::prefixTable('log_visit') => false,
+ 'DROP INDEX index_visit_server_date ON ' . Common::prefixTable('log_visit') => false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.2.27.php b/www/analytics/core/Updates/0.2.27.php
new file mode 100644
index 00000000..4b505194
--- /dev/null
+++ b/www/analytics/core/Updates/0.2.27.php
@@ -0,0 +1,81 @@
+ false,
+ // 0.2.27 [826]
+ 'ALTER IGNORE TABLE `' . Common::prefixTable('log_visit') . '`
+ CHANGE `visit_goal_converted` `visit_goal_converted` TINYINT(1) NOT NULL' => false,
+
+ 'CREATE TABLE `' . Common::prefixTable('goal') . "` (
+ `idsite` int(11) NOT NULL,
+ `idgoal` int(11) NOT NULL,
+ `name` varchar(50) NOT NULL,
+ `match_attribute` varchar(20) NOT NULL,
+ `pattern` varchar(255) NOT NULL,
+ `pattern_type` varchar(10) NOT NULL,
+ `case_sensitive` tinyint(4) NOT NULL,
+ `revenue` float NOT NULL,
+ `deleted` tinyint(4) NOT NULL default '0',
+ PRIMARY KEY (`idsite`,`idgoal`)
+ )" => false,
+
+ 'CREATE TABLE `' . Common::prefixTable('log_conversion') . '` (
+ `idvisit` int(10) unsigned NOT NULL,
+ `idsite` int(10) unsigned NOT NULL,
+ `visitor_idcookie` char(32) NOT NULL,
+ `server_time` datetime NOT NULL,
+ `visit_server_date` date NOT NULL,
+ `idaction` int(11) NOT NULL,
+ `idlink_va` int(11) NOT NULL,
+ `referer_idvisit` int(10) unsigned default NULL,
+ `referer_visit_server_date` date default NULL,
+ `referer_type` int(10) unsigned default NULL,
+ `referer_name` varchar(70) default NULL,
+ `referer_keyword` varchar(255) default NULL,
+ `visitor_returning` tinyint(1) NOT NULL,
+ `location_country` char(3) NOT NULL,
+ `location_continent` char(3) NOT NULL,
+ `url` text NOT NULL,
+ `idgoal` int(10) unsigned NOT NULL,
+ `revenue` float default NULL,
+ PRIMARY KEY (`idvisit`,`idgoal`),
+ KEY `index_idsite_date` (`idsite`,`visit_server_date`)
+ )' => false,
+ );
+
+ $tables = DbHelper::getTablesInstalled();
+ foreach ($tables as $tableName) {
+ if (preg_match('/archive_/', $tableName) == 1) {
+ $sqlarray['CREATE INDEX index_all ON ' . $tableName . ' (`idsite`,`date1`,`date2`,`name`,`ts_archived`)'] = false;
+ }
+ }
+
+ return $sqlarray;
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.2.32.php b/www/analytics/core/Updates/0.2.32.php
new file mode 100644
index 00000000..7f697cc4
--- /dev/null
+++ b/www/analytics/core/Updates/0.2.32.php
@@ -0,0 +1,39 @@
+ false,
+ 'ALTER TABLE `' . Common::prefixTable('user') . '`
+ CHANGE `login` `login` VARCHAR( 100 ) NOT NULL' => false,
+ 'ALTER TABLE `' . Common::prefixTable('user_dashboard') . '`
+ CHANGE `login` `login` VARCHAR( 100 ) NOT NULL' => '1146',
+ 'ALTER TABLE `' . Common::prefixTable('user_language') . '`
+ CHANGE `login` `login` VARCHAR( 100 ) NOT NULL' => '1146',
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.2.33.php b/www/analytics/core/Updates/0.2.33.php
new file mode 100644
index 00000000..0b4d281f
--- /dev/null
+++ b/www/analytics/core/Updates/0.2.33.php
@@ -0,0 +1,45 @@
+ '1146',
+ 'ALTER TABLE `' . Common::prefixTable('user_language') . '`
+ CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci ' => '1146',
+ );
+
+ // alter table to set the utf8 collation
+ $tablesToAlter = DbHelper::getTablesInstalled(true);
+ foreach ($tablesToAlter as $table) {
+ $sqlarray['ALTER TABLE `' . $table . '`
+ CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci '] = false;
+ }
+
+ return $sqlarray;
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.2.34.php b/www/analytics/core/Updates/0.2.34.php
new file mode 100644
index 00000000..49c128f1
--- /dev/null
+++ b/www/analytics/core/Updates/0.2.34.php
@@ -0,0 +1,28 @@
+getAllSitesId();
+ Cache::regenerateCacheWebsiteAttributes($allSiteIds);
+ }
+}
diff --git a/www/analytics/core/Updates/0.2.35.php b/www/analytics/core/Updates/0.2.35.php
new file mode 100644
index 00000000..4c0c53ff
--- /dev/null
+++ b/www/analytics/core/Updates/0.2.35.php
@@ -0,0 +1,32 @@
+ false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.2.37.php b/www/analytics/core/Updates/0.2.37.php
new file mode 100644
index 00000000..47c8e78d
--- /dev/null
+++ b/www/analytics/core/Updates/0.2.37.php
@@ -0,0 +1,33 @@
+ false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.4.1.php b/www/analytics/core/Updates/0.4.1.php
new file mode 100644
index 00000000..b4562d4f
--- /dev/null
+++ b/www/analytics/core/Updates/0.4.1.php
@@ -0,0 +1,34 @@
+ false,
+ 'ALTER TABLE `' . Common::prefixTable('log_conversion') . '`
+ CHANGE `idaction` `idaction` INT(11) DEFAULT NULL' => '1054',
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.4.2.php b/www/analytics/core/Updates/0.4.2.php
new file mode 100644
index 00000000..15039367
--- /dev/null
+++ b/www/analytics/core/Updates/0.4.2.php
@@ -0,0 +1,38 @@
+ '1060',
+ 'ALTER TABLE `' . Common::prefixTable('log_visit') . '`
+ ADD `config_quicktime` TINYINT(1) NOT NULL AFTER `config_director`' => '1060',
+ 'ALTER TABLE `' . Common::prefixTable('log_visit') . '`
+ ADD `config_gears` TINYINT(1) NOT NULL AFTER `config_windowsmedia`,
+ ADD `config_silverlight` TINYINT(1) NOT NULL AFTER `config_gears`' => false,
+ );
+ }
+
+ // when restoring (possibly) previousy dropped columns, ignore mysql code error 1060: duplicate column
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.4.4.php b/www/analytics/core/Updates/0.4.4.php
new file mode 100644
index 00000000..44cc1690
--- /dev/null
+++ b/www/analytics/core/Updates/0.4.4.php
@@ -0,0 +1,29 @@
+ false,
+ 'ALTER TABLE `' . Common::prefixTable('log_visit') . '`
+ CHANGE `location_ip` `location_ip` BIGINT UNSIGNED NOT NULL' => false,
+ 'UPDATE `' . Common::prefixTable('logger_api_call') . '`
+ SET caller_ip=caller_ip+CAST(POW(2,32) AS UNSIGNED) WHERE caller_ip < 0' => false,
+ 'ALTER TABLE `' . Common::prefixTable('logger_api_call') . '`
+ CHANGE `caller_ip` `caller_ip` BIGINT UNSIGNED' => false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.5.4.php b/www/analytics/core/Updates/0.5.4.php
new file mode 100644
index 00000000..235391db
--- /dev/null
+++ b/www/analytics/core/Updates/0.5.4.php
@@ -0,0 +1,65 @@
+ false,
+ );
+ }
+
+ static function update()
+ {
+ $salt = Common::generateUniqId();
+ $config = Config::getInstance();
+ $superuser = $config->superuser;
+ if (!isset($superuser['salt'])) {
+ try {
+ if (is_writable(Config::getLocalConfigPath())) {
+ $superuser['salt'] = $salt;
+ $config->superuser = $superuser;
+ $config->forceSave();
+ } else {
+ throw new \Exception('mandatory update failed');
+ }
+ } catch (\Exception $e) {
+ throw new \Piwik\UpdaterErrorException("Please edit your config/config.ini.php file and add below [superuser] the following line: salt = $salt");
+ }
+ }
+
+ $plugins = $config->Plugins;
+ if (!in_array('MultiSites', $plugins)) {
+ try {
+ if (is_writable(Config::getLocalConfigPath())) {
+ $plugins[] = 'MultiSites';
+ $config->Plugins = $plugins;
+ $config->forceSave();
+ } else {
+ throw new \Exception('optional update failed');
+ }
+ } catch (\Exception $e) {
+ throw new \Exception("You can now enable the new MultiSites plugin in the Plugins screen in the Piwik admin!");
+ }
+ }
+
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.5.5.php b/www/analytics/core/Updates/0.5.5.php
new file mode 100644
index 00000000..aaacf0e9
--- /dev/null
+++ b/www/analytics/core/Updates/0.5.5.php
@@ -0,0 +1,46 @@
+ '1091',
+ 'CREATE INDEX index_idsite_date_config ON ' . Common::prefixTable('log_visit') . ' (idsite, visit_server_date, config_md5config(8))' => '1061',
+ );
+
+ $tables = DbHelper::getTablesInstalled();
+ foreach ($tables as $tableName) {
+ if (preg_match('/archive_/', $tableName) == 1) {
+ $sqlarray['DROP INDEX index_all ON ' . $tableName] = '1091';
+ }
+ if (preg_match('/archive_numeric_/', $tableName) == 1) {
+ $sqlarray['CREATE INDEX index_idsite_dates_period ON ' . $tableName . ' (idsite, date1, date2, period)'] = '1061';
+ }
+ }
+
+ return $sqlarray;
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+
+ }
+}
diff --git a/www/analytics/core/Updates/0.5.php b/www/analytics/core/Updates/0.5.php
new file mode 100644
index 00000000..f90f7f48
--- /dev/null
+++ b/www/analytics/core/Updates/0.5.php
@@ -0,0 +1,40 @@
+ '1060',
+ 'ALTER TABLE ' . Common::prefixTable('log_visit') . ' CHANGE visit_exit_idaction visit_exit_idaction_url INTEGER(11) NOT NULL;' => '1054',
+ 'ALTER TABLE ' . Common::prefixTable('log_visit') . ' CHANGE visit_entry_idaction visit_entry_idaction_url INTEGER(11) NOT NULL;' => '1054',
+ 'ALTER TABLE ' . Common::prefixTable('log_link_visit_action') . ' CHANGE `idaction_ref` `idaction_url_ref` INTEGER(10) UNSIGNED NOT NULL;' => '1054',
+ 'ALTER TABLE ' . Common::prefixTable('log_link_visit_action') . ' CHANGE `idaction` `idaction_url` INTEGER(10) UNSIGNED NOT NULL;' => '1054',
+ 'ALTER TABLE ' . Common::prefixTable('log_link_visit_action') . ' ADD COLUMN `idaction_name` INTEGER(10) UNSIGNED AFTER `idaction_url_ref`;' => '1060',
+ 'ALTER TABLE ' . Common::prefixTable('log_conversion') . ' CHANGE `idaction` `idaction_url` INTEGER(11) UNSIGNED NOT NULL;' => '1054',
+ 'UPDATE ' . Common::prefixTable('log_action') . ' SET `hash` = CRC32(name);' => false,
+ 'CREATE INDEX index_type_hash ON ' . Common::prefixTable('log_action') . ' (type, hash);' => '1061',
+ 'DROP INDEX index_type_name ON ' . Common::prefixTable('log_action') . ';' => '1091',
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.6-rc1.php b/www/analytics/core/Updates/0.6-rc1.php
new file mode 100644
index 00000000..43ab8e4e
--- /dev/null
+++ b/www/analytics/core/Updates/0.6-rc1.php
@@ -0,0 +1,67 @@
+ false,
+ 'ALTER TABLE ' . Common::prefixTable('site') . ' CHANGE ts_created ts_created TIMESTAMP NULL' => false,
+ 'ALTER TABLE ' . Common::prefixTable('site') . ' ADD `timezone` VARCHAR( 50 ) NOT NULL AFTER `ts_created` ;' => false,
+ 'UPDATE ' . Common::prefixTable('site') . ' SET `timezone` = "' . $defaultTimezone . '";' => false,
+ 'ALTER TABLE ' . Common::prefixTable('site') . ' ADD currency CHAR( 3 ) NOT NULL AFTER `timezone` ;' => false,
+ 'UPDATE ' . Common::prefixTable('site') . ' SET `currency` = "' . $defaultCurrency . '";' => false,
+ 'ALTER TABLE ' . Common::prefixTable('site') . ' ADD `excluded_ips` TEXT NOT NULL AFTER `currency` ;' => false,
+ 'ALTER TABLE ' . Common::prefixTable('site') . ' ADD excluded_parameters VARCHAR( 255 ) NOT NULL AFTER `excluded_ips` ;' => false,
+ 'ALTER TABLE ' . Common::prefixTable('log_visit') . ' ADD INDEX `index_idsite_datetime_config` ( `idsite` , `visit_last_action_time` , `config_md5config` ( 8 ) ) ;' => false,
+ 'ALTER TABLE ' . Common::prefixTable('log_visit') . ' ADD INDEX index_idsite_idvisit (idsite, idvisit) ;' => false,
+ 'ALTER TABLE ' . Common::prefixTable('log_conversion') . ' DROP INDEX index_idsite_date' => false,
+ 'ALTER TABLE ' . Common::prefixTable('log_conversion') . ' DROP visit_server_date;' => false,
+ 'ALTER TABLE ' . Common::prefixTable('log_conversion') . ' ADD INDEX index_idsite_datetime ( `idsite` , `server_time` )' => false,
+ );
+ }
+
+ static function update()
+ {
+ // first we disable the plugins and keep an array of warnings messages
+ $pluginsToDisableMessage = array(
+ 'SearchEnginePosition' => "SearchEnginePosition plugin was disabled, because it is not compatible with the new Piwik 0.6. \n You can download the latest version of the plugin, compatible with Piwik 0.6.\nClick here. ",
+ 'GeoIP' => "GeoIP plugin was disabled, because it is not compatible with the new Piwik 0.6. \nYou can download the latest version of the plugin, compatible with Piwik 0.6.\nClick here. "
+ );
+ $disabledPlugins = array();
+ foreach ($pluginsToDisableMessage as $pluginToDisable => $warningMessage) {
+ if (\Piwik\Plugin\Manager::getInstance()->isPluginActivated($pluginToDisable)) {
+ \Piwik\Plugin\Manager::getInstance()->deactivatePlugin($pluginToDisable);
+ $disabledPlugins[] = $warningMessage;
+ }
+ }
+
+ // Run the SQL
+ Updater::updateDatabase(__FILE__, self::getSql());
+
+ // Outputs warning message, pointing users to the plugin download page
+ if (!empty($disabledPlugins)) {
+ throw new \Exception("The following plugins were disabled during the upgrade:"
+ . "" .
+ implode(' ', $disabledPlugins) .
+ " ");
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/0.6.2.php b/www/analytics/core/Updates/0.6.2.php
new file mode 100644
index 00000000..15236ed0
--- /dev/null
+++ b/www/analytics/core/Updates/0.6.2.php
@@ -0,0 +1,47 @@
+getAllSitesId();
+ Cache::regenerateCacheWebsiteAttributes($allSiteIds);
+ }
+}
diff --git a/www/analytics/core/Updates/0.6.3.php b/www/analytics/core/Updates/0.6.3.php
new file mode 100644
index 00000000..87ef2d27
--- /dev/null
+++ b/www/analytics/core/Updates/0.6.3.php
@@ -0,0 +1,49 @@
+ false,
+ 'ALTER TABLE `' . Common::prefixTable('logger_api_call') . '`
+ CHANGE `caller_ip` `caller_ip` INT UNSIGNED' => false,
+ );
+ }
+
+ static function update()
+ {
+ $config = Config::getInstance();
+ $dbInfos = $config->database;
+ if (!isset($dbInfos['schema'])) {
+ try {
+ if (is_writable(Config::getLocalConfigPath())) {
+ $config->database = $dbInfos;
+ $config->forceSave();
+ } else {
+ throw new \Exception('mandatory update failed');
+ }
+ } catch (\Exception $e) {
+ }
+ }
+
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.7.php b/www/analytics/core/Updates/0.7.php
new file mode 100644
index 00000000..3c6daad6
--- /dev/null
+++ b/www/analytics/core/Updates/0.7.php
@@ -0,0 +1,32 @@
+ false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/0.9.1.php b/www/analytics/core/Updates/0.9.1.php
new file mode 100644
index 00000000..5b11099b
--- /dev/null
+++ b/www/analytics/core/Updates/0.9.1.php
@@ -0,0 +1,57 @@
+ false,
+
+ 'UPDATE `' . Common::prefixTable('option') . '`
+ SET option_value = "UTC"
+ WHERE option_name = "SitesManager_DefaultTimezone"
+ AND option_value IN (' . $timezoneList . ')' => false,
+ );
+ }
+
+ static function update()
+ {
+ if (SettingsServer::isTimezoneSupportEnabled()) {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/1.1.php b/www/analytics/core/Updates/1.1.php
new file mode 100644
index 00000000..df58645c
--- /dev/null
+++ b/www/analytics/core/Updates/1.1.php
@@ -0,0 +1,44 @@
+superuser;
+ } catch (\Exception $e) {
+ return;
+ }
+
+ if (empty($superuser['login'])) {
+ return;
+ }
+
+ $rootLogin = $superuser['login'];
+ try {
+ // throws an exception if invalid
+ Piwik::checkValidLoginString($rootLogin);
+ } catch (\Exception $e) {
+ throw new \Exception('Superuser login name "' . $rootLogin . '" is no longer a valid format. '
+ . $e->getMessage()
+ . ' Edit your config/config.ini.php to change it.');
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/1.10-b4.php b/www/analytics/core/Updates/1.10-b4.php
new file mode 100755
index 00000000..5628487f
--- /dev/null
+++ b/www/analytics/core/Updates/1.10-b4.php
@@ -0,0 +1,31 @@
+activatePlugin('MobileMessaging');
+ } catch (\Exception $e) {
+ // pass
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/1.10.1.php b/www/analytics/core/Updates/1.10.1.php
new file mode 100755
index 00000000..e6051a7b
--- /dev/null
+++ b/www/analytics/core/Updates/1.10.1.php
@@ -0,0 +1,31 @@
+activatePlugin('Overlay');
+ } catch (\Exception $e) {
+ // pass
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/1.10.2-b1.php b/www/analytics/core/Updates/1.10.2-b1.php
new file mode 100755
index 00000000..bf58008b
--- /dev/null
+++ b/www/analytics/core/Updates/1.10.2-b1.php
@@ -0,0 +1,33 @@
+ 1060,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/1.10.2-b2.php b/www/analytics/core/Updates/1.10.2-b2.php
new file mode 100644
index 00000000..de177432
--- /dev/null
+++ b/www/analytics/core/Updates/1.10.2-b2.php
@@ -0,0 +1,33 @@
+ 1060,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/1.11-b1.php b/www/analytics/core/Updates/1.11-b1.php
new file mode 100644
index 00000000..dc118714
--- /dev/null
+++ b/www/analytics/core/Updates/1.11-b1.php
@@ -0,0 +1,31 @@
+activatePlugin('UserCountryMap');
+ } catch (\Exception $e) {
+ // pass
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/1.12-b1.php b/www/analytics/core/Updates/1.12-b1.php
new file mode 100644
index 00000000..5318a7cb
--- /dev/null
+++ b/www/analytics/core/Updates/1.12-b1.php
@@ -0,0 +1,38 @@
+ 1060
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+
+}
diff --git a/www/analytics/core/Updates/1.12-b15.php b/www/analytics/core/Updates/1.12-b15.php
new file mode 100644
index 00000000..96a0696c
--- /dev/null
+++ b/www/analytics/core/Updates/1.12-b15.php
@@ -0,0 +1,26 @@
+activatePlugin('SegmentEditor');
+ } catch (\Exception $e) {
+ // pass
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/1.12-b16.php b/www/analytics/core/Updates/1.12-b16.php
new file mode 100644
index 00000000..d1090674
--- /dev/null
+++ b/www/analytics/core/Updates/1.12-b16.php
@@ -0,0 +1,33 @@
+ 1060,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/1.2-rc1.php b/www/analytics/core/Updates/1.2-rc1.php
new file mode 100644
index 00000000..cb46b767
--- /dev/null
+++ b/www/analytics/core/Updates/1.2-rc1.php
@@ -0,0 +1,150 @@
+ false,
+ 'ALTER TABLE `' . Common::prefixTable('log_visit') . '`
+ ADD custom_var_k1 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_v1 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_k2 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_v2 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_k3 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_v3 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_k4 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_v4 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_k5 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_v5 VARCHAR(100) DEFAULT NULL
+ ' => 1060,
+ 'ALTER TABLE `' . Common::prefixTable('log_link_visit_action') . '`
+ ADD `idsite` INT( 10 ) UNSIGNED NOT NULL AFTER `idlink_va` ,
+ ADD `server_time` DATETIME AFTER `idsite`,
+ ADD `idvisitor` BINARY(8) NOT NULL AFTER `idsite`,
+ ADD `idaction_name_ref` INT UNSIGNED NOT NULL AFTER `idaction_name`,
+ ADD INDEX `index_idsite_servertime` ( `idsite` , `server_time` )
+ ' => false,
+
+ 'ALTER TABLE `' . Common::prefixTable('log_conversion') . '`
+ DROP `referer_idvisit`,
+ ADD `idvisitor` BINARY(8) NOT NULL AFTER `idsite`,
+ ADD visitor_count_visits SMALLINT(5) UNSIGNED NOT NULL,
+ ADD visitor_days_since_first SMALLINT(5) UNSIGNED NOT NULL
+ ' => false,
+ 'ALTER TABLE `' . Common::prefixTable('log_conversion') . '`
+ ADD custom_var_k1 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_v1 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_k2 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_v2 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_k3 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_v3 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_k4 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_v4 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_k5 VARCHAR(100) DEFAULT NULL,
+ ADD custom_var_v5 VARCHAR(100) DEFAULT NULL
+ ' => 1060,
+
+ // Migrate 128bits IDs inefficiently stored as 8bytes (256 bits) into 64bits
+ 'UPDATE ' . Common::prefixTable('log_visit') . '
+ SET idvisitor = binary(unhex(substring(visitor_idcookie,1,16))),
+ config_id = binary(unhex(substring(config_md5config,1,16)))
+ ' => false,
+ 'UPDATE ' . Common::prefixTable('log_conversion') . '
+ SET idvisitor = binary(unhex(substring(visitor_idcookie,1,16)))
+ ' => false,
+
+ // Drop migrated fields
+ 'ALTER TABLE `' . Common::prefixTable('log_visit') . '`
+ DROP visitor_idcookie,
+ DROP config_md5config
+ ' => false,
+ 'ALTER TABLE `' . Common::prefixTable('log_conversion') . '`
+ DROP visitor_idcookie
+ ' => false,
+
+ // Recreate INDEX on new field
+ 'ALTER TABLE `' . Common::prefixTable('log_visit') . '`
+ ADD INDEX `index_idsite_datetime_config` (idsite, visit_last_action_time, config_id)
+ ' => false,
+
+ // Backfill action logs as best as we can
+ 'UPDATE ' . Common::prefixTable('log_link_visit_action') . ' as action,
+ ' . Common::prefixTable('log_visit') . ' as visit
+ SET action.idsite = visit.idsite,
+ action.server_time = visit.visit_last_action_time,
+ action.idvisitor = visit.idvisitor
+ WHERE action.idvisit=visit.idvisit
+ ' => false,
+
+ 'ALTER TABLE `' . Common::prefixTable('log_link_visit_action') . '`
+ CHANGE `server_time` `server_time` DATETIME NOT NULL
+ ' => false,
+
+ // New index used max once per request, in case this table grows significantly in the future
+ 'ALTER TABLE `' . Common::prefixTable('option') . '` ADD INDEX ( `autoload` ) ' => false,
+
+ // new field for websites
+ 'ALTER TABLE `' . Common::prefixTable('site') . '` ADD `group` VARCHAR( 250 ) NOT NULL' => false,
+ );
+ }
+
+ static function update()
+ {
+ // first we disable the plugins and keep an array of warnings messages
+ $pluginsToDisableMessage = array(
+ 'GeoIP' => "GeoIP plugin was disabled, because it is not compatible with the new Piwik 1.2. \nYou can download the latest version of the plugin, compatible with Piwik 1.2.\nClick here. ",
+ 'EntryPage' => "EntryPage plugin is not compatible with this version of Piwik, it was disabled.",
+ );
+ $disabledPlugins = array();
+ foreach ($pluginsToDisableMessage as $pluginToDisable => $warningMessage) {
+ if (\Piwik\Plugin\Manager::getInstance()->isPluginActivated($pluginToDisable)) {
+ \Piwik\Plugin\Manager::getInstance()->deactivatePlugin($pluginToDisable);
+ $disabledPlugins[] = $warningMessage;
+ }
+ }
+
+ // Run the SQL
+ Updater::updateDatabase(__FILE__, self::getSql());
+
+ // Outputs warning message, pointing users to the plugin download page
+ if (!empty($disabledPlugins)) {
+ throw new \Exception("The following plugins were disabled during the upgrade:"
+ . "" .
+ implode(' ', $disabledPlugins) .
+ " ");
+ }
+
+ }
+}
+
diff --git a/www/analytics/core/Updates/1.2-rc2.php b/www/analytics/core/Updates/1.2-rc2.php
new file mode 100644
index 00000000..02cb3208
--- /dev/null
+++ b/www/analytics/core/Updates/1.2-rc2.php
@@ -0,0 +1,26 @@
+activatePlugin('CustomVariables');
+ } catch (\Exception $e) {
+ }
+ }
+}
+
diff --git a/www/analytics/core/Updates/1.2.3.php b/www/analytics/core/Updates/1.2.3.php
new file mode 100644
index 00000000..cf5c4559
--- /dev/null
+++ b/www/analytics/core/Updates/1.2.3.php
@@ -0,0 +1,41 @@
+database['dbname'] . '` DEFAULT CHARACTER SET utf8' => false,
+
+ // Various performance improvements schema updates
+ 'ALTER TABLE `' . Common::prefixTable('log_visit') . '`
+ DROP INDEX index_idsite_datetime_config,
+ DROP INDEX index_idsite_idvisit,
+ ADD INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time),
+ ADD INDEX index_idsite_datetime (idsite, visit_last_action_time)' => false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
+
diff --git a/www/analytics/core/Updates/1.2.5-rc1.php b/www/analytics/core/Updates/1.2.5-rc1.php
new file mode 100644
index 00000000..09c4dea6
--- /dev/null
+++ b/www/analytics/core/Updates/1.2.5-rc1.php
@@ -0,0 +1,37 @@
+ false,
+ 'ALTER TABLE `' . Common::prefixTable('log_conversion') . '`
+ ADD buster int unsigned NOT NULL AFTER revenue,
+ DROP PRIMARY KEY,
+ ADD PRIMARY KEY (idvisit, idgoal, buster)' => false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
+
diff --git a/www/analytics/core/Updates/1.2.5-rc7.php b/www/analytics/core/Updates/1.2.5-rc7.php
new file mode 100644
index 00000000..c4a51afc
--- /dev/null
+++ b/www/analytics/core/Updates/1.2.5-rc7.php
@@ -0,0 +1,34 @@
+ false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
+
+
diff --git a/www/analytics/core/Updates/1.4-rc1.php b/www/analytics/core/Updates/1.4-rc1.php
new file mode 100644
index 00000000..d8cc1397
--- /dev/null
+++ b/www/analytics/core/Updates/1.4-rc1.php
@@ -0,0 +1,37 @@
+ '42S22',
+ 'ALTER TABLE `' . Common::prefixTable('pdf') . '`
+ ADD COLUMN `format` VARCHAR(10)' => '42S22',
+ );
+ }
+
+ static function update()
+ {
+ try {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ } catch (\Exception $e) {
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/1.4-rc2.php b/www/analytics/core/Updates/1.4-rc2.php
new file mode 100644
index 00000000..1f2ba56b
--- /dev/null
+++ b/www/analytics/core/Updates/1.4-rc2.php
@@ -0,0 +1,44 @@
+ false,
+ // this converts the 32-bit UNSIGNED INT column to a 16 byte VARBINARY;
+ // _but_ MySQL does string conversion! (e.g., integer 1 is converted to 49 -- the ASCII code for "1")
+ 'ALTER TABLE ' . Common::prefixTable('log_visit') . '
+ MODIFY location_ip VARBINARY(16) NOT NULL' => false,
+ 'ALTER TABLE ' . Common::prefixTable('logger_api_call') . '
+ MODIFY caller_ip VARBINARY(16) NOT NULL' => false,
+
+ // fortunately, 2^32 is 10 digits long and fits in the VARBINARY(16) without truncation;
+ // to fix this, we cast to an integer, convert to hex, pad out leading zeros, and unhex it
+ 'UPDATE ' . Common::prefixTable('log_visit') . "
+ SET location_ip = UNHEX(LPAD(HEX(CONVERT(location_ip, UNSIGNED)), 8, '0'))" => false,
+ 'UPDATE ' . Common::prefixTable('logger_api_call') . "
+ SET caller_ip = UNHEX(LPAD(HEX(CONVERT(caller_ip, UNSIGNED)), 8, '0'))" => false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/1.5-b1.php b/www/analytics/core/Updates/1.5-b1.php
new file mode 100644
index 00000000..0ccd81e0
--- /dev/null
+++ b/www/analytics/core/Updates/1.5-b1.php
@@ -0,0 +1,63 @@
+ false,
+
+ 'ALTER IGNORE TABLE `' . Common::prefixTable('log_visit') . '`
+ ADD visitor_days_since_order SMALLINT(5) UNSIGNED NOT NULL AFTER visitor_days_since_last,
+ ADD visit_goal_buyer TINYINT(1) NOT NULL AFTER visit_goal_converted' => false,
+
+ 'ALTER IGNORE TABLE `' . Common::prefixTable('log_conversion') . '`
+ ADD visitor_days_since_order SMALLINT(5) UNSIGNED NOT NULL AFTER visitor_days_since_first,
+ ADD idorder varchar(100) default NULL AFTER buster,
+ ADD items SMALLINT UNSIGNED DEFAULT NULL,
+ ADD revenue_subtotal float default NULL,
+ ADD revenue_tax float default NULL,
+ ADD revenue_shipping float default NULL,
+ ADD revenue_discount float default NULL,
+ ADD UNIQUE KEY unique_idsite_idorder (idsite, idorder),
+ MODIFY idgoal int(10) NOT NULL' => false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/1.5-b2.php b/www/analytics/core/Updates/1.5-b2.php
new file mode 100644
index 00000000..00c748f8
--- /dev/null
+++ b/www/analytics/core/Updates/1.5-b2.php
@@ -0,0 +1,41 @@
+ 1060,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/1.5-b3.php b/www/analytics/core/Updates/1.5-b3.php
new file mode 100644
index 00000000..781056db
--- /dev/null
+++ b/www/analytics/core/Updates/1.5-b3.php
@@ -0,0 +1,63 @@
+ false,
+ 'ALTER TABLE `' . Common::prefixTable('log_conversion') . '`
+ CHANGE custom_var_k1 custom_var_k1 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_v1 custom_var_v1 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_k2 custom_var_k2 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_v2 custom_var_v2 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_k3 custom_var_k3 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_v3 custom_var_v3 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_k4 custom_var_k4 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_v4 custom_var_v4 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_k5 custom_var_k5 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_v5 custom_var_v5 VARCHAR(100) DEFAULT NULL' => false,
+ 'ALTER TABLE `' . Common::prefixTable('log_link_visit_action') . '`
+ CHANGE custom_var_k1 custom_var_k1 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_v1 custom_var_v1 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_k2 custom_var_k2 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_v2 custom_var_v2 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_k3 custom_var_k3 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_v3 custom_var_v3 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_k4 custom_var_k4 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_v4 custom_var_v4 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_k5 custom_var_k5 VARCHAR(100) DEFAULT NULL,
+ CHANGE custom_var_v5 custom_var_v5 VARCHAR(100) DEFAULT NULL' => false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/1.5-b4.php b/www/analytics/core/Updates/1.5-b4.php
new file mode 100644
index 00000000..32c37bf2
--- /dev/null
+++ b/www/analytics/core/Updates/1.5-b4.php
@@ -0,0 +1,32 @@
+ false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/1.5-b5.php b/www/analytics/core/Updates/1.5-b5.php
new file mode 100644
index 00000000..5e4b10fa
--- /dev/null
+++ b/www/analytics/core/Updates/1.5-b5.php
@@ -0,0 +1,37 @@
+ false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/1.5-rc6.php b/www/analytics/core/Updates/1.5-rc6.php
new file mode 100644
index 00000000..56e87e64
--- /dev/null
+++ b/www/analytics/core/Updates/1.5-rc6.php
@@ -0,0 +1,26 @@
+activatePlugin('PrivacyManager');
+ } catch (\Exception $e) {
+ }
+ }
+}
+
diff --git a/www/analytics/core/Updates/1.6-b1.php b/www/analytics/core/Updates/1.6-b1.php
new file mode 100644
index 00000000..2b73ed95
--- /dev/null
+++ b/www/analytics/core/Updates/1.6-b1.php
@@ -0,0 +1,68 @@
+ false,
+ 'ALTER TABLE `' . Common::prefixTable('log_visit') . '`
+ CHANGE custom_var_k1 custom_var_k1 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v1 custom_var_v1 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_k2 custom_var_k2 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v2 custom_var_v2 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_k3 custom_var_k3 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v3 custom_var_v3 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_k4 custom_var_k4 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v4 custom_var_v4 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_k5 custom_var_k5 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v5 custom_var_v5 VARCHAR(200) DEFAULT NULL' => 1060,
+ 'ALTER TABLE `' . Common::prefixTable('log_conversion') . '`
+ CHANGE custom_var_k1 custom_var_k1 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v1 custom_var_v1 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_k2 custom_var_k2 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v2 custom_var_v2 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_k3 custom_var_k3 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v3 custom_var_v3 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_k4 custom_var_k4 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v4 custom_var_v4 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_k5 custom_var_k5 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v5 custom_var_v5 VARCHAR(200) DEFAULT NULL' => 1060,
+ 'ALTER TABLE `' . Common::prefixTable('log_link_visit_action') . '`
+ CHANGE custom_var_k1 custom_var_k1 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v1 custom_var_v1 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_k2 custom_var_k2 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v2 custom_var_v2 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_k3 custom_var_k3 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v3 custom_var_v3 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_k4 custom_var_k4 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v4 custom_var_v4 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_k5 custom_var_k5 VARCHAR(200) DEFAULT NULL,
+ CHANGE custom_var_v5 custom_var_v5 VARCHAR(200) DEFAULT NULL' => 1060,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/1.6-rc1.php b/www/analytics/core/Updates/1.6-rc1.php
new file mode 100644
index 00000000..8255786c
--- /dev/null
+++ b/www/analytics/core/Updates/1.6-rc1.php
@@ -0,0 +1,26 @@
+activatePlugin('ImageGraph');
+ } catch (\Exception $e) {
+ }
+ }
+}
+
diff --git a/www/analytics/core/Updates/1.7-b1.php b/www/analytics/core/Updates/1.7-b1.php
new file mode 100644
index 00000000..04e45c9c
--- /dev/null
+++ b/www/analytics/core/Updates/1.7-b1.php
@@ -0,0 +1,37 @@
+ false,
+ 'UPDATE `' . Common::prefixTable('pdf') . '`
+ SET `aggregate_reports_format` = 1' => false,
+ );
+ }
+
+ static function update()
+ {
+ try {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ } catch (\Exception $e) {
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/1.7.2-rc5.php b/www/analytics/core/Updates/1.7.2-rc5.php
new file mode 100644
index 00000000..19388cc9
--- /dev/null
+++ b/www/analytics/core/Updates/1.7.2-rc5.php
@@ -0,0 +1,35 @@
+ false
+ );
+ }
+
+ static function update()
+ {
+ try {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ } catch (\Exception $e) {
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/1.7.2-rc7.php b/www/analytics/core/Updates/1.7.2-rc7.php
new file mode 100755
index 00000000..ac1871de
--- /dev/null
+++ b/www/analytics/core/Updates/1.7.2-rc7.php
@@ -0,0 +1,45 @@
+ false,
+ );
+ }
+
+ static function update()
+ {
+ try {
+ $dashboards = Db::fetchAll('SELECT * FROM `' . Common::prefixTable('user_dashboard') . '`');
+ foreach ($dashboards AS $dashboard) {
+ $idDashboard = $dashboard['iddashboard'];
+ $login = $dashboard['login'];
+ $layout = $dashboard['layout'];
+ $layout = html_entity_decode($layout);
+ $layout = str_replace("\\\"", "\"", $layout);
+ Db::query('UPDATE `' . Common::prefixTable('user_dashboard') . '` SET layout = ? WHERE iddashboard = ? AND login = ?', array($layout, $idDashboard, $login));
+ }
+ Updater::updateDatabase(__FILE__, self::getSql());
+ } catch (\Exception $e) {
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/1.8.3-b1.php b/www/analytics/core/Updates/1.8.3-b1.php
new file mode 100644
index 00000000..85f9f669
--- /dev/null
+++ b/www/analytics/core/Updates/1.8.3-b1.php
@@ -0,0 +1,115 @@
+ false,
+
+ 'CREATE TABLE `' . Common::prefixTable('report') . '` (
+ `idreport` INT(11) NOT NULL AUTO_INCREMENT,
+ `idsite` INTEGER(11) NOT NULL,
+ `login` VARCHAR(100) NOT NULL,
+ `description` VARCHAR(255) NOT NULL,
+ `period` VARCHAR(10) NOT NULL,
+ `type` VARCHAR(10) NOT NULL,
+ `format` VARCHAR(10) NOT NULL,
+ `reports` TEXT NOT NULL,
+ `parameters` TEXT NULL,
+ `ts_created` TIMESTAMP NULL,
+ `ts_last_sent` TIMESTAMP NULL,
+ `deleted` tinyint(4) NOT NULL default 0,
+ PRIMARY KEY (`idreport`)
+ ) DEFAULT CHARSET=utf8' => 1050,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ if (!\Piwik\Plugin\Manager::getInstance()->isPluginLoaded('ScheduledReports')) {
+ return;
+ }
+
+ try {
+
+ // Common::prefixTable('pdf') has been heavily refactored to be more generic
+ // The following actions are taken in this update script :
+ // - create the new generic report table Common::prefixTable('report')
+ // - migrate previous reports, if any, from Common::prefixTable('pdf') to Common::prefixTable('report')
+ // - delete Common::prefixTable('pdf')
+
+ $reports = Db::fetchAll('SELECT * FROM `' . Common::prefixTable('pdf') . '`');
+ foreach ($reports AS $report) {
+
+ $idreport = $report['idreport'];
+ $idsite = $report['idsite'];
+ $login = $report['login'];
+ $description = $report['description'];
+ $period = $report['period'];
+ $format = $report['format'];
+ $display_format = $report['display_format'];
+ $email_me = $report['email_me'];
+ $additional_emails = $report['additional_emails'];
+ $reports = $report['reports'];
+ $ts_created = $report['ts_created'];
+ $ts_last_sent = $report['ts_last_sent'];
+ $deleted = $report['deleted'];
+
+ $parameters = array();
+
+ if (!is_null($additional_emails)) {
+ $parameters[ScheduledReports::ADDITIONAL_EMAILS_PARAMETER] = preg_split('/,/', $additional_emails);
+ }
+
+ $parameters[ScheduledReports::EMAIL_ME_PARAMETER] = is_null($email_me) ? ScheduledReports::EMAIL_ME_PARAMETER_DEFAULT_VALUE : (bool)$email_me;
+ $parameters[ScheduledReports::DISPLAY_FORMAT_PARAMETER] = $display_format;
+
+ Db::query(
+ 'INSERT INTO `' . Common::prefixTable('report') . '` SET
+ idreport = ?, idsite = ?, login = ?, description = ?, period = ?,
+ type = ?, format = ?, reports = ?, parameters = ?, ts_created = ?,
+ ts_last_sent = ?, deleted = ?',
+ array(
+ $idreport,
+ $idsite,
+ $login,
+ $description,
+ is_null($period) ? ScheduledReports::DEFAULT_PERIOD : $period,
+ ScheduledReports::EMAIL_TYPE,
+ is_null($format) ? ScheduledReports::DEFAULT_REPORT_FORMAT : $format,
+ Common::json_encode(preg_split('/,/', $reports)),
+ Common::json_encode($parameters),
+ $ts_created,
+ $ts_last_sent,
+ $deleted
+ )
+ );
+ }
+
+ Db::query('DROP TABLE `' . Common::prefixTable('pdf') . '`');
+ } catch (\Exception $e) {
+ }
+
+ }
+}
diff --git a/www/analytics/core/Updates/1.8.4-b1.php b/www/analytics/core/Updates/1.8.4-b1.php
new file mode 100644
index 00000000..97e70daa
--- /dev/null
+++ b/www/analytics/core/Updates/1.8.4-b1.php
@@ -0,0 +1,190 @@
+ 1060, // ignore error 1060 Duplicate column name 'url_prefix'
+
+ // remove protocol and www and store information in url_prefix
+ " UPDATE `$action`
+ SET
+ url_prefix = IF (
+ LEFT(name, 11) = 'http://www.', 1, IF (
+ LEFT(name, 7) = 'http://', 0, IF (
+ LEFT(name, 12) = 'https://www.', 3, IF (
+ LEFT(name, 8) = 'https://', 2, NULL
+ )
+ )
+ )
+ ),
+ name = IF (
+ url_prefix = 0, SUBSTRING(name, 8), IF (
+ url_prefix = 1, SUBSTRING(name, 12), IF (
+ url_prefix = 2, SUBSTRING(name, 9), IF (
+ url_prefix = 3, SUBSTRING(name, 13), name
+ )
+ )
+ )
+ ),
+ hash = CRC32(name)
+ WHERE
+ type = 1 AND
+ url_prefix IS NULL;
+ " => false,
+
+ // find duplicates
+ " DROP TABLE IF EXISTS `$duplicates`;
+ " => false,
+ " CREATE TABLE `$duplicates` (
+ `before` int(10) unsigned NOT NULL,
+ `after` int(10) unsigned NOT NULL,
+ KEY `mainkey` (`before`)
+ ) ENGINE=InnoDB;
+ " => false,
+
+ // grouping by name only would be case-insensitive, so we GROUP BY name,hash
+ // ON (action.type = 1 AND canonical.hash = action.hash) will use index (type, hash)
+ " INSERT INTO `$duplicates` (
+ SELECT
+ action.idaction AS `before`,
+ canonical.idaction AS `after`
+ FROM
+ (
+ SELECT
+ name,
+ hash,
+ MIN(idaction) AS idaction
+ FROM
+ `$action` AS action_canonical_base
+ WHERE
+ type = 1 AND
+ url_prefix IS NOT NULL
+ GROUP BY name, hash
+ HAVING COUNT(idaction) > 1
+ )
+ AS canonical
+ LEFT JOIN
+ `$action` AS action
+ ON (action.type = 1 AND canonical.hash = action.hash)
+ AND canonical.name = action.name
+ AND canonical.idaction != action.idaction
+ );
+ " => false,
+
+ // replace idaction in log_link_visit_action
+ " UPDATE
+ `$visitAction` AS link
+ LEFT JOIN
+ `$duplicates` AS duplicates_idaction_url
+ ON link.idaction_url = duplicates_idaction_url.before
+ SET
+ link.idaction_url = duplicates_idaction_url.after
+ WHERE
+ duplicates_idaction_url.after IS NOT NULL;
+ " => false,
+ " UPDATE
+ `$visitAction` AS link
+ LEFT JOIN
+ `$duplicates` AS duplicates_idaction_url_ref
+ ON link.idaction_url_ref = duplicates_idaction_url_ref.before
+ SET
+ link.idaction_url_ref = duplicates_idaction_url_ref.after
+ WHERE
+ duplicates_idaction_url_ref.after IS NOT NULL;
+ " => false,
+
+ // replace idaction in log_conversion
+ " UPDATE
+ `$conversion` AS conversion
+ LEFT JOIN
+ `$duplicates` AS duplicates
+ ON conversion.idaction_url = duplicates.before
+ SET
+ conversion.idaction_url = duplicates.after
+ WHERE
+ duplicates.after IS NOT NULL;
+ " => false,
+
+ // replace idaction in log_visit
+ " UPDATE
+ `$visit` AS visit
+ LEFT JOIN
+ `$duplicates` AS duplicates_entry
+ ON visit.visit_entry_idaction_url = duplicates_entry.before
+ SET
+ visit.visit_entry_idaction_url = duplicates_entry.after
+ WHERE
+ duplicates_entry.after IS NOT NULL;
+ " => false,
+ " UPDATE
+ `$visit` AS visit
+ LEFT JOIN
+ `$duplicates` AS duplicates_exit
+ ON visit.visit_exit_idaction_url = duplicates_exit.before
+ SET
+ visit.visit_exit_idaction_url = duplicates_exit.after
+ WHERE
+ duplicates_exit.after IS NOT NULL;
+ " => false,
+
+ // remove duplicates from log_action
+ " DELETE action FROM
+ `$action` AS action
+ LEFT JOIN
+ `$duplicates` AS duplicates
+ ON action.idaction = duplicates.before
+ WHERE
+ duplicates.after IS NOT NULL;
+ " => false,
+
+ // remove the duplicates table
+ " DROP TABLE `$duplicates`;
+ " => false
+ );
+ }
+
+ static function update()
+ {
+ try {
+ self::enableMaintenanceMode();
+ Updater::updateDatabase(__FILE__, self::getSql());
+ self::disableMaintenanceMode();
+ } catch (\Exception $e) {
+ self::disableMaintenanceMode();
+ throw $e;
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/1.9-b16.php b/www/analytics/core/Updates/1.9-b16.php
new file mode 100755
index 00000000..3c82828d
--- /dev/null
+++ b/www/analytics/core/Updates/1.9-b16.php
@@ -0,0 +1,54 @@
+ false,
+
+
+ 'ALTER TABLE `' . Common::prefixTable('log_visit') . '`
+ ADD visit_total_searches SMALLINT(5) UNSIGNED NOT NULL AFTER `visit_total_actions`'
+ => 1060,
+
+ 'ALTER TABLE `' . Common::prefixTable('site') . '`
+ ADD sitesearch TINYINT DEFAULT 1 AFTER `excluded_parameters`,
+ ADD sitesearch_keyword_parameters TEXT NOT NULL AFTER `sitesearch`,
+ ADD sitesearch_category_parameters TEXT NOT NULL AFTER `sitesearch_keyword_parameters`'
+ => 1060,
+
+ // enable Site Search for all websites, users can manually disable the setting
+ 'UPDATE `' . Common::prefixTable('site') . '`
+ SET `sitesearch` = 1' => false,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
+
diff --git a/www/analytics/core/Updates/1.9-b19.php b/www/analytics/core/Updates/1.9-b19.php
new file mode 100755
index 00000000..d2496cbb
--- /dev/null
+++ b/www/analytics/core/Updates/1.9-b19.php
@@ -0,0 +1,43 @@
+ false,
+ 'ALTER TABLE `' . Common::prefixTable('log_visit') . '`
+ CHANGE `visit_exit_idaction_url` `visit_exit_idaction_url` INT( 10 ) UNSIGNED NULL DEFAULT 0'
+ => false
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+
+
+ try {
+ \Piwik\Plugin\Manager::getInstance()->activatePlugin('Transitions');
+ } catch (\Exception $e) {
+ }
+ }
+}
+
diff --git a/www/analytics/core/Updates/1.9-b9.php b/www/analytics/core/Updates/1.9-b9.php
new file mode 100755
index 00000000..dce8c9e2
--- /dev/null
+++ b/www/analytics/core/Updates/1.9-b9.php
@@ -0,0 +1,57 @@
+ 1091,
+
+ // add geoip columns to log_conversion
+ "ALTER TABLE `$logConversion` $addColumns" => 1091,
+ );
+ }
+
+ static function update()
+ {
+ try {
+ self::enableMaintenanceMode();
+ Updater::updateDatabase(__FILE__, self::getSql());
+ self::disableMaintenanceMode();
+ } catch (\Exception $e) {
+ self::disableMaintenanceMode();
+ throw $e;
+ }
+ }
+}
+
diff --git a/www/analytics/core/Updates/1.9.1-b2.php b/www/analytics/core/Updates/1.9.1-b2.php
new file mode 100644
index 00000000..d79dd992
--- /dev/null
+++ b/www/analytics/core/Updates/1.9.1-b2.php
@@ -0,0 +1,36 @@
+ 1091
+ );
+ }
+
+ static function update()
+ {
+ // manually remove ExampleFeedburner column
+ Updater::updateDatabase(__FILE__, self::getSql());
+
+ // remove ExampleFeedburner plugin
+ $pluginToDelete = 'ExampleFeedburner';
+ self::deletePluginFromConfigFile($pluginToDelete);
+ }
+}
diff --git a/www/analytics/core/Updates/1.9.3-b10.php b/www/analytics/core/Updates/1.9.3-b10.php
new file mode 100755
index 00000000..22583103
--- /dev/null
+++ b/www/analytics/core/Updates/1.9.3-b10.php
@@ -0,0 +1,31 @@
+activatePlugin('Annotations');
+ } catch (\Exception $e) {
+ // pass
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/1.9.3-b3.php b/www/analytics/core/Updates/1.9.3-b3.php
new file mode 100644
index 00000000..0a511b14
--- /dev/null
+++ b/www/analytics/core/Updates/1.9.3-b3.php
@@ -0,0 +1,28 @@
+deletePluginFromFilesystem($pluginToDelete);
+
+ // We also clean up 1.9.1 and delete Feedburner plugin
+ \Piwik\Plugin\Manager::getInstance()->deletePluginFromFilesystem('Feedburner');
+ }
+}
diff --git a/www/analytics/core/Updates/1.9.3-b8.php b/www/analytics/core/Updates/1.9.3-b8.php
new file mode 100755
index 00000000..03f75ce1
--- /dev/null
+++ b/www/analytics/core/Updates/1.9.3-b8.php
@@ -0,0 +1,34 @@
+ 1060,
+ );
+ }
+
+ static function update()
+ {
+ // add excluded_user_agents column to site table
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/2.0-a12.php b/www/analytics/core/Updates/2.0-a12.php
new file mode 100644
index 00000000..cc202d4e
--- /dev/null
+++ b/www/analytics/core/Updates/2.0-a12.php
@@ -0,0 +1,48 @@
+ false
+ );
+
+ $unneededLogTables = array('logger_exception', 'logger_error', 'logger_api_call');
+ foreach ($unneededLogTables as $table) {
+ $tableName = Common::prefixTable($table);
+
+ try {
+ $rows = Db::fetchOne("SELECT COUNT(*) FROM $tableName");
+ if ($rows == 0) {
+ $result["DROP TABLE $tableName"] = false;
+ }
+ } catch (\Exception $ex) {
+ // ignore
+ }
+ }
+
+ return $result;
+ }
+
+ public static function update()
+ {
+ // change level column in logger_message table to string & remove other logging tables if empty
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/2.0-a13.php b/www/analytics/core/Updates/2.0-a13.php
new file mode 100644
index 00000000..70ca0590
--- /dev/null
+++ b/www/analytics/core/Updates/2.0-a13.php
@@ -0,0 +1,66 @@
+activatePlugin('Referrers');
+ } catch (\Exception $e) {
+ }
+ try {
+ \Piwik\Plugin\Manager::getInstance()->activatePlugin('ScheduledReports');
+ } catch (\Exception $e) {
+ }
+
+ }
+}
diff --git a/www/analytics/core/Updates/2.0-a17.php b/www/analytics/core/Updates/2.0-a17.php
new file mode 100644
index 00000000..ec00c2ab
--- /dev/null
+++ b/www/analytics/core/Updates/2.0-a17.php
@@ -0,0 +1,42 @@
+" . implode(" ", $errors));
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/2.0-a7.php b/www/analytics/core/Updates/2.0-a7.php
new file mode 100644
index 00000000..cdbbc7e2
--- /dev/null
+++ b/www/analytics/core/Updates/2.0-a7.php
@@ -0,0 +1,37 @@
+ 1060,
+
+ 'ALTER TABLE ' . Common::prefixTable('logger_message')
+ . " ADD COLUMN level TINYINT AFTER timestamp" => 1060,
+ );
+ }
+
+ static function update()
+ {
+ // add tag & level columns to logger_message table
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/2.0-b10.php b/www/analytics/core/Updates/2.0-b10.php
new file mode 100644
index 00000000..273c4d08
--- /dev/null
+++ b/www/analytics/core/Updates/2.0-b10.php
@@ -0,0 +1,23 @@
+" . implode(" ", $errors));
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/2.0-b3.php b/www/analytics/core/Updates/2.0-b3.php
new file mode 100644
index 00000000..ae30df77
--- /dev/null
+++ b/www/analytics/core/Updates/2.0-b3.php
@@ -0,0 +1,46 @@
+ 1060,
+
+ 'ALTER TABLE ' . Common::prefixTable('log_link_visit_action')
+ . " ADD COLUMN idaction_event_category INTEGER(10) UNSIGNED AFTER idaction_name_ref,
+ ADD COLUMN idaction_event_action INTEGER(10) UNSIGNED AFTER idaction_event_category" => 1060,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+
+ try {
+ \Piwik\Plugin\Manager::getInstance()->activatePlugin('Events');
+ } catch (\Exception $e) {
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/2.0-b9.php b/www/analytics/core/Updates/2.0-b9.php
new file mode 100644
index 00000000..612c79d1
--- /dev/null
+++ b/www/analytics/core/Updates/2.0-b9.php
@@ -0,0 +1,33 @@
+ 1060,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/www/analytics/core/Updates/2.0-rc1.php b/www/analytics/core/Updates/2.0-rc1.php
new file mode 100644
index 00000000..beda1cda
--- /dev/null
+++ b/www/analytics/core/Updates/2.0-rc1.php
@@ -0,0 +1,24 @@
+activatePlugin('Morpheus');
+ } catch(\Exception $e) {
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/2.0.3-b7.php b/www/analytics/core/Updates/2.0.3-b7.php
new file mode 100644
index 00000000..2e9b593a
--- /dev/null
+++ b/www/analytics/core/Updates/2.0.3-b7.php
@@ -0,0 +1,67 @@
+isPluginActivated('DoNotTrack')) {
+ DoNotTrackHeaderChecker::activate();
+ }
+
+ // enable IP anonymization if AnonymizeIP plugin was enabled
+ if (\Piwik\Plugin\Manager::getInstance()->isPluginActivated('AnonymizeIP')) {
+ IPAnonymizer::activate();
+ }
+ } catch (\Exception $ex) {
+ // pass
+ }
+
+ // disable & delete old plugins
+ $oldPlugins = array('DoNotTrack', 'AnonymizeIP');
+ foreach ($oldPlugins as $plugin) {
+ try {
+ \Piwik\Plugin\Manager::getInstance()->deactivatePlugin($plugin);
+ } catch(\Exception $e) {
+
+ }
+
+ $dir = PIWIK_INCLUDE_PATH . "/plugins/$plugin";
+
+ if (file_exists($dir)) {
+ Filesystem::unlinkRecursive($dir, true);
+ }
+
+ if (file_exists($dir)) {
+ $errors[] = "Please delete this directory manually (eg. using your FTP software): $dir \n";
+ }
+
+ }
+ if(!empty($errors)) {
+ throw new \Exception("Warnings during the update: " . implode(" ", $errors));
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/2.0.4-b5.php b/www/analytics/core/Updates/2.0.4-b5.php
new file mode 100644
index 00000000..78c8ab2b
--- /dev/null
+++ b/www/analytics/core/Updates/2.0.4-b5.php
@@ -0,0 +1,92 @@
+ 1060,
+ );
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+
+ try {
+ self::migrateConfigSuperUserToDb();
+ } catch (\Exception $e) {
+ throw new UpdaterErrorException($e->getMessage());
+ }
+ }
+
+ private static function migrateConfigSuperUserToDb()
+ {
+ $config = Config::getInstance();
+
+ if (!$config->existsLocalConfig()) {
+ return;
+ }
+
+ try {
+ $superUser = $config->superuser;
+ } catch (\Exception $e) {
+ $superUser = null;
+ }
+
+ if (!empty($superUser['bridge']) || empty($superUser)) {
+ // there is a super user which is not from the config but from the bridge, that means we already have
+ // a super user in the database
+ return;
+ }
+
+ $userApi = UsersManagerApi::getInstance();
+
+ try {
+ Db::get()->insert(Common::prefixTable('user'), array(
+ 'login' => $superUser['login'],
+ 'password' => $superUser['password'],
+ 'alias' => $superUser['login'],
+ 'email' => $superUser['email'],
+ 'token_auth' => $userApi->getTokenAuth($superUser['login'], $superUser['password']),
+ 'date_registered' => Date::now()->getDatetime(),
+ 'superuser_access' => 1
+ )
+ );
+ } catch(\Exception $e) {
+ echo "There was an issue, but we proceed: " . $e->getMessage();
+ }
+
+ if (array_key_exists('salt', $superUser)) {
+ $salt = $superUser['salt'];
+ } else {
+ $salt = Common::generateUniqId();
+ }
+
+ $config->General['salt'] = $salt;
+ $config->superuser = array();
+ $config->forceSave();
+ }
+}
diff --git a/www/analytics/core/Updates/2.0.4-b7.php b/www/analytics/core/Updates/2.0.4-b7.php
new file mode 100644
index 00000000..83b19903
--- /dev/null
+++ b/www/analytics/core/Updates/2.0.4-b7.php
@@ -0,0 +1,73 @@
+getMessage());
+ }
+ }
+
+ private static function migrateExistingMobileMessagingOptions()
+ {
+ if (Option::get(MobileMessaging::DELEGATED_MANAGEMENT_OPTION) == 'true') {
+ return;
+ }
+
+ // copy $superUserLogin_MobileMessagingSettings -> _MobileMessagingSettings as settings are managed globally
+
+ $optionName = MobileMessaging::USER_SETTINGS_POSTFIX_OPTION;
+ $superUsers = UsersManagerApi::getInstance()->getUsersHavingSuperUserAccess();
+
+ if (empty($superUsers)) {
+ return;
+ }
+
+ $firstSuperUser = array_shift($superUsers);
+
+ if (empty($firstSuperUser)) {
+ return;
+ }
+
+ $superUserLogin = $firstSuperUser['login'];
+ $optionPrefixed = $superUserLogin . $optionName;
+
+ // $superUserLogin_MobileMessagingSettings
+ $value = Option::get($optionPrefixed);
+
+ if (false !== $value) {
+ // _MobileMessagingSettings
+ Option::set($optionName, $value);
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/2.0.4-b8.php b/www/analytics/core/Updates/2.0.4-b8.php
new file mode 100644
index 00000000..354ae17f
--- /dev/null
+++ b/www/analytics/core/Updates/2.0.4-b8.php
@@ -0,0 +1,78 @@
+forceSave();
+
+ } catch (\Exception $e) {
+ throw new UpdaterErrorException($e->getMessage());
+ }
+ }
+
+ private static function migrateBrandingConfig(Config $config)
+ {
+ $useCustomLogo = self::getValueAndDelete($config, 'branding', 'use_custom_logo');
+
+ $customLogo = new CustomLogo();
+ $useCustomLogo ? $customLogo->enable() : $customLogo->disable();
+ }
+
+ private static function migratePrivacyManagerConfig(Config $oldConfig, PrivacyManagerConfig $newConfig)
+ {
+ $ipVisitEnrichment = self::getValueAndDelete($oldConfig, 'Tracker', 'use_anonymized_ip_for_visit_enrichment');
+ $ipAddressMarkLength = self::getValueAndDelete($oldConfig, 'Tracker', 'ip_address_mask_length');
+
+ if (null !== $ipVisitEnrichment) {
+ $newConfig->useAnonymizedIpForVisitEnrichment = $ipVisitEnrichment;
+ }
+ if (null !== $ipAddressMarkLength) {
+ $newConfig->ipAddressMaskLength = $ipAddressMarkLength;
+ }
+ }
+
+ private static function getValueAndDelete(Config $config, $section, $key)
+ {
+ if (!$config->$section || !array_key_exists($key, $config->$section)) {
+ return null;
+ }
+
+ $values = $config->$section;
+ $value = $values[$key];
+ unset($values[$key]);
+
+ $config->$section = $values;
+
+ return $value;
+ }
+}
diff --git a/www/analytics/core/Updates/2.1.1-b11.php b/www/analytics/core/Updates/2.1.1-b11.php
new file mode 100644
index 00000000..4eb924d3
--- /dev/null
+++ b/www/analytics/core/Updates/2.1.1-b11.php
@@ -0,0 +1,133 @@
+getDatetime();
+
+ $archiveNumericTables = Db::get()->fetchCol("SHOW TABLES LIKE '%archive_numeric%'");
+
+ // for each numeric archive table, copy *_returning metrics to VisitsSummary metrics w/ the appropriate
+ // returning visit segment
+ foreach ($archiveNumericTables as $table) {
+ // get archives w/ *._returning
+ $sql = "SELECT idarchive, idsite, period, date1, date2
+ FROM $table
+ WHERE name IN ('" . implode("','", $returningMetrics) . "')
+ GROUP BY idarchive";
+ $idArchivesWithReturning = Db::fetchAll($sql);
+
+ // get archives for visitssummary returning visitor segment
+ $sql = "SELECT idarchive, idsite, period, date1, date2
+ FROM $table
+ WHERE name = ?
+ GROUP BY idarchive";
+ $visitSummaryReturningSegmentDone = Rules::getDoneFlagArchiveContainsOnePlugin(
+ new Segment(VisitFrequencyApi::RETURNING_VISITOR_SEGMENT, $idSites = array()), 'VisitsSummary');
+ $idArchivesWithVisitReturningSegment = Db::fetchAll($sql, array($visitSummaryReturningSegmentDone));
+
+ // collect info for new visitssummary archives have to be created to match archives w/ *._returning
+ // metrics
+ $missingIdArchives = array();
+ $idArchiveMappings = array();
+ foreach ($idArchivesWithReturning as $row) {
+ $withMetricsIdArchive = $row['idarchive'];
+ foreach ($idArchivesWithVisitReturningSegment as $segmentRow) {
+ if ($row['idsite'] == $segmentRow['idsite']
+ && $row['period'] == $segmentRow['period']
+ && $row['date1'] == $segmentRow['date1']
+ && $row['date2'] == $segmentRow['date2']
+ ) {
+ $idArchiveMappings[$withMetricsIdArchive] = $segmentRow['idarchive'];
+ }
+ }
+
+ if (!isset($idArchiveMappings[$withMetricsIdArchive])) {
+ $missingIdArchives[$withMetricsIdArchive] = $row;
+ }
+ }
+
+ // if there are missing idarchives, fill out new archive row values
+ if (!empty($missingIdArchives)) {
+ $newIdArchiveStart = Db::fetchOne("SELECT MAX(idarchive) FROM $table") + 1;
+ foreach ($missingIdArchives as $withMetricsIdArchive => &$rowToInsert) {
+ $idArchiveMappings[$withMetricsIdArchive] = $newIdArchiveStart;
+
+ $rowToInsert['idarchive'] = $newIdArchiveStart;
+ $rowToInsert['ts_archived'] = $now;
+ $rowToInsert['name'] = $visitSummaryReturningSegmentDone;
+ $rowToInsert['value'] = ArchiveWriter::DONE_OK;
+
+ ++$newIdArchiveStart;
+ }
+
+ // add missing archives
+ try {
+ $params = array();
+ foreach ($missingIdArchives as $missingIdArchive) {
+ $params[] = array_values($missingIdArchive);
+ }
+ BatchInsert::tableInsertBatch($table, array_keys(reset($missingIdArchives)), $params, $throwException = false);
+ } catch (\Exception $ex) {
+ Updater::handleQueryError($ex, "", false, __FILE__);
+ }
+ }
+
+ // update idarchive & name columns in rows with *._returning metrics
+ $updateSqlPrefix = "UPDATE $table
+ SET idarchive = CASE idarchive ";
+ $updateSqlSuffix = " END, name = CASE name ";
+ foreach ($returningMetrics as $metric) {
+ $newMetricName = substr($metric, 0, strlen($metric) - strlen(VisitFrequencyApi::COLUMN_SUFFIX));
+ $updateSqlSuffix .= "WHEN '$metric' THEN '" . $newMetricName . "' ";
+ }
+ $updateSqlSuffix .= " END WHERE idarchive IN (%s)
+ AND name IN ('" . implode("','", $returningMetrics) . "')";
+
+ // update only 1000 rows at a time so we don't send too large an SQL query to MySQL
+ foreach (array_chunk($missingIdArchives, 1000, $preserveKeys = true) as $chunk) {
+ $idArchives = array();
+
+ $updateSql = $updateSqlPrefix;
+ foreach ($chunk as $withMetricsIdArchive => $row) {
+ $updateSql .= "WHEN $withMetricsIdArchive THEN {$row['idarchive']} ";
+
+ $idArchives[] = $withMetricsIdArchive;
+ }
+ $updateSql .= sprintf($updateSqlSuffix, implode(',', $idArchives));
+
+ Updater::executeMigrationQuery($updateSql, false, __FILE__);
+ }
+ }
+ }
+}
diff --git a/www/analytics/core/Updates/2.2.0-b15.php b/www/analytics/core/Updates/2.2.0-b15.php
new file mode 100644
index 00000000..8437a83d
--- /dev/null
+++ b/www/analytics/core/Updates/2.2.0-b15.php
@@ -0,0 +1,27 @@
+ 'UserSettings',
+ * 'action' => 'index'
+ * ));
+ * Url::redirectToUrl($url);
+ * }
+ *
+ * **Link to a different controller action in a template**
+ *
+ * public function myControllerAction()
+ * {
+ * $url = Url::getCurrentQueryStringWithParametersModified(array(
+ * 'module' => 'UserCountryMap',
+ * 'action' => 'realtimeMap',
+ * 'changeVisitAlpha' => 0,
+ * 'removeOldVisits' => 0
+ * ));
+ * $view = new View("@MyPlugin/myPopup");
+ * $view->realtimeMapUrl = $url;
+ * return $view->render();
+ * }
+ *
+ */
+class Url
+{
+ /**
+ * List of hosts that are never checked for validity.
+ */
+ private static $alwaysTrustedHosts = array('localhost', '127.0.0.1', '::1', '[::1]');
+
+ /**
+ * Returns the current URL.
+ *
+ * @return string eg, `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"`
+ * @api
+ */
+ static public function getCurrentUrl()
+ {
+ return self::getCurrentScheme() . '://'
+ . self::getCurrentHost()
+ . self::getCurrentScriptName()
+ . self::getCurrentQueryString();
+ }
+
+ /**
+ * Returns the current URL without the query string.
+ *
+ * @param bool $checkTrustedHost Whether to do trusted host check. Should ALWAYS be true,
+ * except in {@link Piwik\Plugin\Controller}.
+ * @return string eg, `"http://example.org/dir1/dir2/index.php"` if the current URL is
+ * `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"`.
+ * @api
+ */
+ static public function getCurrentUrlWithoutQueryString($checkTrustedHost = true)
+ {
+ return self::getCurrentScheme() . '://'
+ . self::getCurrentHost($default = 'unknown', $checkTrustedHost)
+ . self::getCurrentScriptName();
+ }
+
+ /**
+ * Returns the current URL without the query string and without the name of the file
+ * being executed.
+ *
+ * @return string eg, `"http://example.org/dir1/dir2/"` if the current URL is
+ * `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"`.
+ * @api
+ */
+ static public function getCurrentUrlWithoutFileName()
+ {
+ return self::getCurrentScheme() . '://'
+ . self::getCurrentHost()
+ . self::getCurrentScriptPath();
+ }
+
+ /**
+ * Returns the path to the script being executed. The script file name is not included.
+ *
+ * @return string eg, `"/dir1/dir2/"` if the current URL is
+ * `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"`
+ * @api
+ */
+ static public function getCurrentScriptPath()
+ {
+ $queryString = self::getCurrentScriptName();
+
+ //add a fake letter case /test/test2/ returns /test which is not expected
+ $urlDir = dirname($queryString . 'x');
+ $urlDir = str_replace('\\', '/', $urlDir);
+ // if we are in a subpath we add a trailing slash
+ if (strlen($urlDir) > 1) {
+ $urlDir .= '/';
+ }
+ return $urlDir;
+ }
+
+ /**
+ * Returns the path to the script being executed. Includes the script file name.
+ *
+ * @return string eg, `"/dir1/dir2/index.php"` if the current URL is
+ * `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"`
+ * @api
+ */
+ static public function getCurrentScriptName()
+ {
+ $url = '';
+
+ if (!empty($_SERVER['REQUEST_URI'])) {
+ $url = $_SERVER['REQUEST_URI'];
+
+ // strip http://host (Apache+Rails anomaly)
+ if (preg_match('~^https?://[^/]+($|/.*)~D', $url, $matches)) {
+ $url = $matches[1];
+ }
+
+ // strip parameters
+ if (($pos = strpos($url, "?")) !== false) {
+ $url = substr($url, 0, $pos);
+ }
+
+ // strip path_info
+ if (isset($_SERVER['PATH_INFO'])) {
+ $url = substr($url, 0, -strlen($_SERVER['PATH_INFO']));
+ }
+ }
+
+ /**
+ * SCRIPT_NAME is our fallback, though it may not be set correctly
+ *
+ * @see http://php.net/manual/en/reserved.variables.php
+ */
+ if (empty($url)) {
+ if (isset($_SERVER['SCRIPT_NAME'])) {
+ $url = $_SERVER['SCRIPT_NAME'];
+ } elseif (isset($_SERVER['SCRIPT_FILENAME'])) {
+ $url = $_SERVER['SCRIPT_FILENAME'];
+ } elseif (isset($_SERVER['argv'])) {
+ $url = $_SERVER['argv'][0];
+ }
+ }
+
+ if (!isset($url[0]) || $url[0] !== '/') {
+ $url = '/' . $url;
+ }
+ return $url;
+ }
+
+ /**
+ * Returns the current URL's protocol.
+ *
+ * @return string `'https'` or `'http'`
+ * @api
+ */
+ static public function getCurrentScheme()
+ {
+ try {
+ $assume_secure_protocol = @Config::getInstance()->General['assume_secure_protocol'];
+ } catch (Exception $e) {
+ $assume_secure_protocol = false;
+ }
+ if ($assume_secure_protocol
+ || (isset($_SERVER['HTTPS'])
+ && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] === true))
+ || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')
+ ) {
+ return 'https';
+ }
+ return 'http';
+ }
+
+ /**
+ * Validates the **Host** HTTP header (untrusted user input). Used to prevent Host header
+ * attacks.
+ *
+ * @param string|bool $host Contents of Host: header from the HTTP request. If `false`, gets the
+ * value from the request.
+ * @return bool `true` if valid; `false` otherwise.
+ */
+ static public function isValidHost($host = false)
+ {
+ // only do trusted host check if it's enabled
+ if (isset(Config::getInstance()->General['enable_trusted_host_check'])
+ && Config::getInstance()->General['enable_trusted_host_check'] == 0
+ ) {
+ return true;
+ }
+
+ if ($host === false) {
+ $host = @$_SERVER['HTTP_HOST'];
+ if (empty($host)) // if no current host, assume valid
+ {
+ return true;
+ }
+ }
+ // if host is in hardcoded whitelist, assume it's valid
+ if (in_array($host, self::$alwaysTrustedHosts)) {
+ return true;
+ }
+
+ $trustedHosts = self::getTrustedHosts();
+
+ // if no trusted hosts, just assume it's valid
+ if (empty($trustedHosts)) {
+ self::saveTrustedHostnameInConfig($host);
+ return true;
+ }
+
+ // Only punctuation we allow is '[', ']', ':', '.' and '-'
+ $hostLength = strlen($host);
+ if ($hostLength !== strcspn($host, '`~!@#$%^&*()_+={}\\|;"\'<>,?/ ')) {
+ return false;
+ }
+
+ foreach ($trustedHosts as &$trustedHost) {
+ $trustedHost = preg_quote($trustedHost);
+ }
+ $untrustedHost = Common::mb_strtolower($host);
+ $untrustedHost = rtrim($untrustedHost, '.');
+
+ $hostRegex = Common::mb_strtolower('/(^|.)' . implode('|', $trustedHosts) . '$/');
+
+ $result = preg_match($hostRegex, $untrustedHost);
+ return 0 !== $result;
+ }
+
+ /**
+ * Records one host, or an array of hosts in the config file,
+ * if user is Super User
+ *
+ * @static
+ * @param $host string|array
+ * @return bool
+ */
+ public static function saveTrustedHostnameInConfig($host)
+ {
+ if (Piwik::hasUserSuperUserAccess()
+ && file_exists(Config::getLocalConfigPath())
+ ) {
+ $general = Config::getInstance()->General;
+ if (!is_array($host)) {
+ $host = array($host);
+ }
+ $host = array_filter($host);
+ if (empty($host)) {
+ return false;
+ }
+ $general['trusted_hosts'] = $host;
+ Config::getInstance()->General = $general;
+ Config::getInstance()->forceSave();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the current host.
+ *
+ * @param bool $checkIfTrusted Whether to do trusted host check. Should ALWAYS be true,
+ * except in Controller.
+ * @return string|bool eg, `"demo.piwik.org"` or false if no host found.
+ */
+ static public function getHost($checkIfTrusted = true)
+ {
+ // HTTP/1.1 request
+ if (isset($_SERVER['HTTP_HOST'])
+ && strlen($host = $_SERVER['HTTP_HOST'])
+ && (!$checkIfTrusted
+ || self::isValidHost($host))
+ ) {
+ return $host;
+ }
+
+ // HTTP/1.0 request doesn't include Host: header
+ if (isset($_SERVER['SERVER_ADDR'])) {
+ return $_SERVER['SERVER_ADDR'];
+ }
+
+ return false;
+ }
+
+ /**
+ * Sets the host. Useful for CLI scripts, eg. archive.php
+ *
+ * @param $host string
+ */
+ static public function setHost($host)
+ {
+ $_SERVER['HTTP_HOST'] = $host;
+ }
+
+ /**
+ * Returns the current host.
+ *
+ * @param string $default Default value to return if host unknown
+ * @param bool $checkTrustedHost Whether to do trusted host check. Should ALWAYS be true,
+ * except in Controller.
+ * @return string eg, `"example.org"` if the current URL is
+ * `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"`
+ * @api
+ */
+ static public function getCurrentHost($default = 'unknown', $checkTrustedHost = true)
+ {
+ $hostHeaders = array();
+
+ $config = Config::getInstance()->General;
+ if(isset($config['proxy_host_headers'])) {
+ $hostHeaders = $config['proxy_host_headers'];
+ }
+
+ if (!is_array($hostHeaders)) {
+ $hostHeaders = array();
+ }
+
+ $host = self::getHost($checkTrustedHost);
+ $default = Common::sanitizeInputValue($host ? $host : $default);
+
+ return IP::getNonProxyIpFromHeader($default, $hostHeaders);
+ }
+
+ /**
+ * Returns the query string of the current URL.
+ *
+ * @return string eg, `"?param1=value1¶m2=value2"` if the current URL is
+ * `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"`
+ * @api
+ */
+ static public function getCurrentQueryString()
+ {
+ $url = '';
+ if (isset($_SERVER['QUERY_STRING'])
+ && !empty($_SERVER['QUERY_STRING'])
+ ) {
+ $url .= "?" . $_SERVER['QUERY_STRING'];
+ }
+ return $url;
+ }
+
+ /**
+ * Returns an array mapping query paramater names with query parameter values for
+ * the current URL.
+ *
+ * @return array If current URL is `"http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2"`
+ * this will return:
+ *
+ * array(
+ * 'param1' => string 'value1',
+ * 'param2' => string 'value2'
+ * )
+ * @api
+ */
+ static public function getArrayFromCurrentQueryString()
+ {
+ $queryString = self::getCurrentQueryString();
+ $urlValues = UrlHelper::getArrayFromQueryString($queryString);
+ return $urlValues;
+ }
+
+ /**
+ * Modifies the current query string with the supplied parameters and returns
+ * the result. Parameters in the current URL will be overwritten with values
+ * in `$params` and parameters absent from the current URL but present in `$params`
+ * will be added to the result.
+ *
+ * @param array $params set of parameters to modify/add in the current URL
+ * eg, `array('param3' => 'value3')`
+ * @return string eg, `"?param2=value2¶m3=value3"`
+ * @api
+ */
+ static function getCurrentQueryStringWithParametersModified($params)
+ {
+ $urlValues = self::getArrayFromCurrentQueryString();
+ foreach ($params as $key => $value) {
+ $urlValues[$key] = $value;
+ }
+ $query = self::getQueryStringFromParameters($urlValues);
+ if (strlen($query) > 0) {
+ return '?' . $query;
+ }
+ return '';
+ }
+
+ /**
+ * Converts an array of parameters name => value mappings to a query
+ * string.
+ *
+ * @param array $parameters eg. `array('param1' => 10, 'param2' => array(1,2))`
+ * @return string eg. `"param1=10¶m2[]=1¶m2[]=2"`
+ * @api
+ */
+ static public function getQueryStringFromParameters($parameters)
+ {
+ $query = '';
+ foreach ($parameters as $name => $value) {
+ if (is_null($value) || $value === false) {
+ continue;
+ }
+ if (is_array($value)) {
+ foreach ($value as $theValue) {
+ $query .= $name . "[]=" . $theValue . "&";
+ }
+ } else {
+ $query .= $name . "=" . $value . "&";
+ }
+ }
+ $query = substr($query, 0, -1);
+ return $query;
+ }
+
+ static public function getQueryStringFromUrl($url)
+ {
+ return parse_url($url, PHP_URL_QUERY);
+ }
+
+ /**
+ * Redirects the user to the referrer. If no referrer exists, the user is redirected
+ * to the current URL without query string.
+ *
+ * @api
+ */
+ static public function redirectToReferrer()
+ {
+ $referrer = self::getReferrer();
+ if ($referrer !== false) {
+ self::redirectToUrl($referrer);
+ }
+ self::redirectToUrl(self::getCurrentUrlWithoutQueryString());
+ }
+
+ /**
+ * Redirects the user to the specified URL.
+ *
+ * @param string $url
+ * @api
+ */
+ static public function redirectToUrl($url)
+ {
+ if (UrlHelper::isLookLikeUrl($url)
+ || strpos($url, 'index.php') === 0
+ ) {
+ @header("Location: $url");
+ } else {
+ echo "Invalid URL to redirect to.";
+ }
+
+ if(Common::isPhpCliMode()) {
+ die("If you were using a browser, Piwik would redirect you to this URL: $url \n\n");
+ }
+ exit;
+ }
+
+ /**
+ * If the page is using HTTP, redirect to the same page over HTTPS
+ */
+ static public function redirectToHttps()
+ {
+ if(ProxyHttp::isHttps()) {
+ return;
+ }
+ $url = self::getCurrentUrl();
+ $url = str_replace("http://", "https://", $url);
+ self::redirectToUrl($url);
+ }
+
+ /**
+ * Returns the **HTTP_REFERER** `$_SERVER` variable, or `false` if not found.
+ *
+ * @return string|false
+ * @api
+ */
+ static public function getReferrer()
+ {
+ if (!empty($_SERVER['HTTP_REFERER'])) {
+ return $_SERVER['HTTP_REFERER'];
+ }
+ return false;
+ }
+
+ /**
+ * Returns `true` if the URL points to something on the same host, `false` if otherwise.
+ *
+ * @param string $url
+ * @return bool True if local; false otherwise.
+ * @api
+ */
+ static public function isLocalUrl($url)
+ {
+ if (empty($url)) {
+ return true;
+ }
+
+ // handle host name mangling
+ $requestUri = isset($_SERVER['SCRIPT_URI']) ? $_SERVER['SCRIPT_URI'] : '';
+ $parseRequest = @parse_url($requestUri);
+ $hosts = array(self::getHost(), self::getCurrentHost());
+ if (!empty($parseRequest['host'])) {
+ $hosts[] = $parseRequest['host'];
+ }
+
+ // drop port numbers from hostnames and IP addresses
+ $hosts = array_map(array('Piwik\IP', 'sanitizeIp'), $hosts);
+
+ $disableHostCheck = Config::getInstance()->General['enable_trusted_host_check'] == 0;
+ // compare scheme and host
+ $parsedUrl = @parse_url($url);
+ $host = IP::sanitizeIp(@$parsedUrl['host']);
+ return !empty($host)
+ && ($disableHostCheck || in_array($host, $hosts))
+ && !empty($parsedUrl['scheme'])
+ && in_array($parsedUrl['scheme'], array('http', 'https'));
+ }
+
+ public static function getTrustedHostsFromConfig()
+ {
+ $trustedHosts = @Config::getInstance()->General['trusted_hosts'];
+ if (!is_array($trustedHosts)) {
+ return array();
+ }
+ foreach ($trustedHosts as &$trustedHost) {
+ // Case user wrote in the config, http://example.com/test instead of example.com
+ if (UrlHelper::isLookLikeUrl($trustedHost)) {
+ $trustedHost = parse_url($trustedHost, PHP_URL_HOST);
+ }
+ }
+ return $trustedHosts;
+ }
+
+ public static function getTrustedHosts()
+ {
+ $trustedHosts = self::getTrustedHostsFromConfig();
+
+ /* used by Piwik PRO */
+ Piwik::postEvent('Url.filterTrustedHosts', array(&$trustedHosts));
+
+ return $trustedHosts;
+ }
+}
diff --git a/www/analytics/core/UrlHelper.php b/www/analytics/core/UrlHelper.php
new file mode 100644
index 00000000..2ce26ba1
--- /dev/null
+++ b/www/analytics/core/UrlHelper.php
@@ -0,0 +1,496 @@
+ '0', 'date' => '2012-01-01')`.
+ * @param $parametersToExclude Array of query parameter names that shouldn't be
+ * in the result query string, eg, `array('date', 'period')`.
+ * @return string A query string, eg, `"?site=0"`.
+ * @api
+ */
+ public static function getQueryStringWithExcludedParameters($queryParameters, $parametersToExclude)
+ {
+ $validQuery = '';
+ $separator = '&';
+ foreach ($queryParameters as $name => $value) {
+ // decode encoded square brackets
+ $name = str_replace(array('%5B', '%5D'), array('[', ']'), $name);
+
+ if (!in_array(strtolower($name), $parametersToExclude)) {
+ if (is_array($value)) {
+ foreach ($value as $param) {
+ if ($param === false) {
+ $validQuery .= $name . '[]' . $separator;
+ } else {
+ $validQuery .= $name . '[]=' . $param . $separator;
+ }
+ }
+ } else if ($value === false) {
+ $validQuery .= $name . $separator;
+ } else {
+ $validQuery .= $name . '=' . $value . $separator;
+ }
+ }
+ }
+ $validQuery = substr($validQuery, 0, -strlen($separator));
+ return $validQuery;
+ }
+
+ /**
+ * Reduce URL to more minimal form. 2 letter country codes are
+ * replaced by '{}', while other parts are simply removed.
+ *
+ * Examples:
+ * www.example.com -> example.com
+ * search.example.com -> example.com
+ * m.example.com -> example.com
+ * de.example.com -> {}.example.com
+ * example.de -> example.{}
+ * example.co.uk -> example.{}
+ *
+ * @param string $url
+ * @return string
+ */
+ public static function getLossyUrl($url)
+ {
+ static $countries;
+ if (!isset($countries)) {
+ $countries = implode('|', array_keys(Common::getCountriesList(true)));
+ }
+
+ return preg_replace(
+ array(
+ '/^(w+[0-9]*|search)\./',
+ '/(^|\.)m\./',
+ '/(\.(com|org|net|co|it|edu))?\.(' . $countries . ')(\/|$)/',
+ '/(^|\.)(' . $countries . ')\./',
+ ),
+ array(
+ '',
+ '$1',
+ '.{}$4',
+ '$1{}.',
+ ),
+ $url);
+ }
+
+ /**
+ * Returns true if the string passed may be a URL ie. it starts with protocol://.
+ * We don't need a precise test here because the value comes from the website
+ * tracked source code and the URLs may look very strange.
+ *
+ * @param string $url
+ * @return bool
+ */
+ public static function isLookLikeUrl($url)
+ {
+ return preg_match('~^(ftp|news|http|https)?://(.*)$~D', $url, $matches) !== 0
+ && strlen($matches[2]) > 0;
+ }
+
+ /**
+ * Returns a URL created from the result of the [parse_url](http://php.net/manual/en/function.parse-url.php)
+ * function.
+ *
+ * Copied from the PHP comments at [http://php.net/parse_url](http://php.net/parse_url).
+ *
+ * @param array $parsed Result of [parse_url](http://php.net/manual/en/function.parse-url.php).
+ * @return false|string The URL or `false` if `$parsed` isn't an array.
+ * @api
+ */
+ public static function getParseUrlReverse($parsed)
+ {
+ if (!is_array($parsed)) {
+ return false;
+ }
+
+ $uri = !empty($parsed['scheme']) ? $parsed['scheme'] . ':' . (!strcasecmp($parsed['scheme'], 'mailto') ? '' : '//') : '';
+ $uri .= !empty($parsed['user']) ? $parsed['user'] . (!empty($parsed['pass']) ? ':' . $parsed['pass'] : '') . '@' : '';
+ $uri .= !empty($parsed['host']) ? $parsed['host'] : '';
+ $uri .= !empty($parsed['port']) ? ':' . $parsed['port'] : '';
+
+ if (!empty($parsed['path'])) {
+ $uri .= (!strncmp($parsed['path'], '/', 1))
+ ? $parsed['path']
+ : ((!empty($uri) ? '/' : '') . $parsed['path']);
+ }
+
+ $uri .= !empty($parsed['query']) ? '?' . $parsed['query'] : '';
+ $uri .= !empty($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
+ return $uri;
+ }
+
+ /**
+ * Returns a URL query string as an array.
+ *
+ * @param string $urlQuery The query string, eg, `'?param1=value1¶m2=value2'`.
+ * @return array eg, `array('param1' => 'value1', 'param2' => 'value2')`
+ * @api
+ */
+ public static function getArrayFromQueryString($urlQuery)
+ {
+ if (strlen($urlQuery) == 0) {
+ return array();
+ }
+ if ($urlQuery[0] == '?') {
+ $urlQuery = substr($urlQuery, 1);
+ }
+ $separator = '&';
+
+ $urlQuery = $separator . $urlQuery;
+ // $urlQuery = str_replace(array('%20'), ' ', $urlQuery);
+ $referrerQuery = trim($urlQuery);
+
+ $values = explode($separator, $referrerQuery);
+
+ $nameToValue = array();
+
+ foreach ($values as $value) {
+ $pos = strpos($value, '=');
+ if ($pos !== false) {
+ $name = substr($value, 0, $pos);
+ $value = substr($value, $pos + 1);
+ if ($value === false) {
+ $value = '';
+ }
+ } else {
+ $name = $value;
+ $value = false;
+ }
+ if (!empty($name)) {
+ $name = Common::sanitizeInputValue($name);
+ }
+ if (!empty($value)) {
+ $value = Common::sanitizeInputValue($value);
+ }
+
+ // if array without indexes
+ $count = 0;
+ $tmp = preg_replace('/(\[|%5b)(]|%5d)$/i', '', $name, -1, $count);
+ if (!empty($tmp) && $count) {
+ $name = $tmp;
+ if (isset($nameToValue[$name]) == false || is_array($nameToValue[$name]) == false) {
+ $nameToValue[$name] = array();
+ }
+ array_push($nameToValue[$name], $value);
+ } else if (!empty($name)) {
+ $nameToValue[$name] = $value;
+ }
+ }
+ return $nameToValue;
+ }
+
+ /**
+ * Returns the value of a single query parameter from the supplied query string.
+ *
+ * @param string $urlQuery The query string.
+ * @param string $parameter The query parameter name to return.
+ * @return string|null Parameter value if found (can be the empty string!), null if not found.
+ * @api
+ */
+ public static function getParameterFromQueryString($urlQuery, $parameter)
+ {
+ $nameToValue = self::getArrayFromQueryString($urlQuery);
+ if (isset($nameToValue[$parameter])) {
+ return $nameToValue[$parameter];
+ }
+ return null;
+ }
+
+ /**
+ * Returns the path and query string of a URL.
+ *
+ * @param string $url The URL.
+ * @return string eg, `/test/index.php?module=CoreHome` if `$url` is `http://piwik.org/test/index.php?module=CoreHome`.
+ * @api
+ */
+ public static function getPathAndQueryFromUrl($url)
+ {
+ $parsedUrl = parse_url($url);
+ $result = '';
+ if (isset($parsedUrl['path'])) {
+ $result .= substr($parsedUrl['path'], 1);
+ }
+ if (isset($parsedUrl['query'])) {
+ $result .= '?' . $parsedUrl['query'];
+ }
+ return $result;
+ }
+
+
+ /**
+ * Extracts a keyword from a raw not encoded URL.
+ * Will only extract keyword if a known search engine has been detected.
+ * Returns the keyword:
+ * - in UTF8: automatically converted from other charsets when applicable
+ * - strtolowered: "QUErY test!" will return "query test!"
+ * - trimmed: extra spaces before and after are removed
+ *
+ * Lists of supported search engines can be found in /core/DataFiles/SearchEngines.php
+ * The function returns false when a keyword couldn't be found.
+ * eg. if the url is "http://www.google.com/partners.html" this will return false,
+ * as the google keyword parameter couldn't be found.
+ *
+ * @see unit tests in /tests/core/Common.test.php
+ * @param string $referrerUrl URL referrer URL, eg. $_SERVER['HTTP_REFERER']
+ * @return array|bool false if a keyword couldn't be extracted,
+ * or array(
+ * 'name' => 'Google',
+ * 'keywords' => 'my searched keywords')
+ */
+ public static function extractSearchEngineInformationFromUrl($referrerUrl)
+ {
+ $referrerParsed = @parse_url($referrerUrl);
+ $referrerHost = '';
+ if (isset($referrerParsed['host'])) {
+ $referrerHost = $referrerParsed['host'];
+ }
+ if (empty($referrerHost)) {
+ return false;
+ }
+ // some search engines (eg. Bing Images) use the same domain
+ // as an existing search engine (eg. Bing), we must also use the url path
+ $referrerPath = '';
+ if (isset($referrerParsed['path'])) {
+ $referrerPath = $referrerParsed['path'];
+ }
+
+ // no search query
+ if (!isset($referrerParsed['query'])) {
+ $referrerParsed['query'] = '';
+ }
+ $query = $referrerParsed['query'];
+
+ // Google Referrers URLs sometimes have the fragment which contains the keyword
+ if (!empty($referrerParsed['fragment'])) {
+ $query .= '&' . $referrerParsed['fragment'];
+ }
+
+ $searchEngines = Common::getSearchEngineUrls();
+
+ $hostPattern = self::getLossyUrl($referrerHost);
+ if (array_key_exists($referrerHost . $referrerPath, $searchEngines)) {
+ $referrerHost = $referrerHost . $referrerPath;
+ } elseif (array_key_exists($hostPattern . $referrerPath, $searchEngines)) {
+ $referrerHost = $hostPattern . $referrerPath;
+ } elseif (array_key_exists($hostPattern, $searchEngines)) {
+ $referrerHost = $hostPattern;
+ } elseif (!array_key_exists($referrerHost, $searchEngines)) {
+ if (!strncmp($query, 'cx=partner-pub-', 15)) {
+ // Google custom search engine
+ $referrerHost = 'google.com/cse';
+ } elseif (!strncmp($referrerPath, '/pemonitorhosted/ws/results/', 28)) {
+ // private-label search powered by InfoSpace Metasearch
+ $referrerHost = 'wsdsold.infospace.com';
+ } elseif (strpos($referrerHost, '.images.search.yahoo.com') != false) {
+ // Yahoo! Images
+ $referrerHost = 'images.search.yahoo.com';
+ } elseif (strpos($referrerHost, '.search.yahoo.com') != false) {
+ // Yahoo!
+ $referrerHost = 'search.yahoo.com';
+ } else {
+ return false;
+ }
+ }
+ $searchEngineName = $searchEngines[$referrerHost][0];
+ $variableNames = null;
+ if (isset($searchEngines[$referrerHost][1])) {
+ $variableNames = $searchEngines[$referrerHost][1];
+ }
+ if (!$variableNames) {
+ $searchEngineNames = Common::getSearchEngineNames();
+ $url = $searchEngineNames[$searchEngineName];
+ $variableNames = $searchEngines[$url][1];
+ }
+ if (!is_array($variableNames)) {
+ $variableNames = array($variableNames);
+ }
+
+ $key = null;
+ if ($searchEngineName === 'Google Images'
+ || ($searchEngineName === 'Google' && strpos($referrerUrl, '/imgres') !== false)
+ ) {
+ if (strpos($query, '&prev') !== false) {
+ $query = urldecode(trim(self::getParameterFromQueryString($query, 'prev')));
+ $query = str_replace('&', '&', strstr($query, '?'));
+ }
+ $searchEngineName = 'Google Images';
+ } else if ($searchEngineName === 'Google'
+ && (strpos($query, '&as_') !== false || strpos($query, 'as_') === 0)
+ ) {
+ $keys = array();
+ $key = self::getParameterFromQueryString($query, 'as_q');
+ if (!empty($key)) {
+ array_push($keys, $key);
+ }
+ $key = self::getParameterFromQueryString($query, 'as_oq');
+ if (!empty($key)) {
+ array_push($keys, str_replace('+', ' OR ', $key));
+ }
+ $key = self::getParameterFromQueryString($query, 'as_epq');
+ if (!empty($key)) {
+ array_push($keys, "\"$key\"");
+ }
+ $key = self::getParameterFromQueryString($query, 'as_eq');
+ if (!empty($key)) {
+ array_push($keys, "-$key");
+ }
+ $key = trim(urldecode(implode(' ', $keys)));
+ }
+
+ if ($searchEngineName === 'Google') {
+ // top bar menu
+ $tbm = self::getParameterFromQueryString($query, 'tbm');
+ switch ($tbm) {
+ case 'isch':
+ $searchEngineName = 'Google Images';
+ break;
+ case 'vid':
+ $searchEngineName = 'Google Video';
+ break;
+ case 'shop':
+ $searchEngineName = 'Google Shopping';
+ break;
+ }
+ }
+
+ if (empty($key)) {
+ foreach ($variableNames as $variableName) {
+ if ($variableName[0] == '/') {
+ // regular expression match
+ if (preg_match($variableName, $referrerUrl, $matches)) {
+ $key = trim(urldecode($matches[1]));
+ break;
+ }
+ } else {
+ // search for keywords now &vname=keyword
+ $key = self::getParameterFromQueryString($query, $variableName);
+ $key = trim(urldecode($key));
+
+ // Special case: Google & empty q parameter
+ if (empty($key)
+ && $variableName == 'q'
+
+ && (
+ // Google search with no keyword
+ ($searchEngineName == 'Google'
+ && ( // First, they started putting an empty q= parameter
+ strpos($query, '&q=') !== false
+ || strpos($query, '?q=') !== false
+ // then they started sending the full host only (no path/query string)
+ || (empty($query) && (empty($referrerPath) || $referrerPath == '/') && empty($referrerParsed['fragment']))
+ )
+ )
+ // search engines with no keyword
+ || $searchEngineName == 'Google Images'
+ || $searchEngineName == 'DuckDuckGo')
+ ) {
+ $key = false;
+ }
+ if (!empty($key)
+ || $key === false
+ ) {
+ break;
+ }
+ }
+ }
+ }
+
+ // $key === false is the special case "No keyword provided" which is a Search engine match
+ if ($key === null
+ || $key === ''
+ ) {
+ return false;
+ }
+
+ if (!empty($key)) {
+ if (function_exists('iconv')
+ && isset($searchEngines[$referrerHost][3])
+ ) {
+ // accepts string, array, or comma-separated list string in preferred order
+ $charsets = $searchEngines[$referrerHost][3];
+ if (!is_array($charsets)) {
+ $charsets = explode(',', $charsets);
+ }
+
+ if (!empty($charsets)) {
+ $charset = $charsets[0];
+ if (count($charsets) > 1
+ && function_exists('mb_detect_encoding')
+ ) {
+ $charset = mb_detect_encoding($key, $charsets);
+ if ($charset === false) {
+ $charset = $charsets[0];
+ }
+ }
+
+ $newkey = @iconv($charset, 'UTF-8//IGNORE', $key);
+ if (!empty($newkey)) {
+ $key = $newkey;
+ }
+ }
+ }
+
+ $key = Common::mb_strtolower($key);
+ }
+
+ return array(
+ 'name' => $searchEngineName,
+ 'keywords' => $key,
+ );
+ }
+
+ /**
+ * Returns the query part from any valid url and adds additional parameters to the query part if needed.
+ *
+ * @param string $url Any url eg `"http://example.com/piwik/?foo=bar"`
+ * @param array $additionalParamsToAdd If not empty the given parameters will be added to the query.
+ *
+ * @return string eg. `"foo=bar&foo2=bar2"`
+ * @api
+ */
+ public static function getQueryFromUrl($url, array $additionalParamsToAdd = array())
+ {
+ $url = @parse_url($url);
+ $query = '';
+
+ if (!empty($url['query'])) {
+ $query .= $url['query'];
+ }
+
+ if (!empty($additionalParamsToAdd)) {
+ if (!empty($query)) {
+ $query .= '&';
+ }
+
+ $query .= Url::getQueryStringFromParameters($additionalParamsToAdd);
+ }
+
+ return $query;
+ }
+
+ public static function getHostFromUrl($url)
+ {
+ if (!UrlHelper::isLookLikeUrl($url)) {
+ $url = "http://" . $url;
+ }
+ return parse_url($url, PHP_URL_HOST);
+ }
+}
diff --git a/www/analytics/core/Version.php b/www/analytics/core/Version.php
new file mode 100644
index 00000000..d187b41b
--- /dev/null
+++ b/www/analytics/core/Version.php
@@ -0,0 +1,25 @@
+property1 = "a view property";
+ * $view->property2 = "another view property";
+ * return $view->render();
+ * }
+ *
+ *
+ * @api
+ */
+class View implements ViewInterface
+{
+ private $template = '';
+
+ /**
+ * Instance
+ * @var Twig_Environment
+ */
+ private $twig;
+ protected $templateVars = array();
+ private $contentType = 'text/html; charset=utf-8';
+ private $xFrameOptions = null;
+
+ /**
+ * Constructor.
+ *
+ * @param string $templateFile The template file to load. Must be in the following format:
+ * `"@MyPlugin/templateFileName"`. Note the absence of .twig
+ * from the end of the name.
+ */
+ public function __construct($templateFile)
+ {
+ $templateExt = '.twig';
+ if (substr($templateFile, -strlen($templateExt)) !== $templateExt) {
+ $templateFile .= $templateExt;
+ }
+ $this->template = $templateFile;
+
+ $this->initializeTwig();
+
+ $this->piwik_version = Version::VERSION;
+ $this->userLogin = Piwik::getCurrentUserLogin();
+ $this->isSuperUser = Access::getInstance()->hasSuperUserAccess();
+
+ try {
+ $this->piwikUrl = SettingsPiwik::getPiwikUrl();
+ } catch (Exception $ex) {
+ // pass (occurs when DB cannot be connected to, perhaps piwik URL cache should be stored in config file...)
+ }
+ }
+
+ /**
+ * Returns the template filename.
+ *
+ * @return string
+ */
+ public function getTemplateFile()
+ {
+ return $this->template;
+ }
+
+ /**
+ * Returns the variables to bind to the template when rendering.
+ *
+ * @param array $override Template variable override values. Mainly useful
+ * when including View templates in other templates.
+ * @return array
+ */
+ public function getTemplateVars($override = array())
+ {
+ return $override + $this->templateVars;
+ }
+
+ /**
+ * Directly assigns a variable to the view script.
+ * Variable names may not be prefixed with '_'.
+ *
+ * @param string $key The variable name.
+ * @param mixed $val The variable value.
+ */
+ public function __set($key, $val)
+ {
+ $this->templateVars[$key] = $val;
+ }
+
+ /**
+ * Retrieves an assigned variable.
+ * Variable names may not be prefixed with '_'.
+ *
+ * @param string $key The variable name.
+ * @return mixed The variable value.
+ */
+ public function &__get($key)
+ {
+ return $this->templateVars[$key];
+ }
+
+ private function initializeTwig()
+ {
+ $piwikTwig = new Twig();
+ $this->twig = $piwikTwig->getTwigEnvironment();
+ }
+
+ /**
+ * Renders the current view. Also sends the stored 'Content-Type' HTML header.
+ * See {@link setContentType()}.
+ *
+ * @return string Generated template.
+ */
+ public function render()
+ {
+ try {
+ $this->currentModule = Piwik::getModule();
+ $this->currentAction = Piwik::getAction();
+
+ $this->url = Common::sanitizeInputValue(Url::getCurrentUrl());
+ $this->token_auth = Piwik::getCurrentUserTokenAuth();
+ $this->userHasSomeAdminAccess = Piwik::isUserHasSomeAdminAccess();
+ $this->userIsSuperUser = Piwik::hasUserSuperUserAccess();
+ $this->latest_version_available = UpdateCheck::isNewestVersionAvailable();
+ $this->disableLink = Common::getRequestVar('disableLink', 0, 'int');
+ $this->isWidget = Common::getRequestVar('widget', 0, 'int');
+ $this->cacheBuster = UIAssetCacheBuster::getInstance()->piwikVersionBasedCacheBuster();
+
+ $this->loginModule = Piwik::getLoginPluginName();
+
+ $user = APIUsersManager::getInstance()->getUser($this->userLogin);
+ $this->userAlias = $user['alias'];
+ } catch (Exception $e) {
+ // can fail, for example at installation (no plugin loaded yet)
+ }
+
+ try {
+ $this->totalTimeGeneration = Registry::get('timer')->getTime();
+ $this->totalNumberOfQueries = Profiler::getQueryCount();
+ } catch (Exception $e) {
+ $this->totalNumberOfQueries = 0;
+ }
+
+ ProxyHttp::overrideCacheControlHeaders('no-store');
+
+ @header('Content-Type: ' . $this->contentType);
+ // always sending this header, sometimes empty, to ensure that Dashboard embed loads (which could call this header() multiple times, the last one will prevail)
+ @header('X-Frame-Options: ' . (string)$this->xFrameOptions);
+
+ return $this->renderTwigTemplate();
+ }
+
+ protected function renderTwigTemplate()
+ {
+ $output = $this->twig->render($this->getTemplateFile(), $this->getTemplateVars());
+ $output = $this->applyFilter_cacheBuster($output);
+
+ $helper = new Theme;
+ $output = $helper->rewriteAssetsPathToTheme($output);
+ return $output;
+ }
+
+ protected function applyFilter_cacheBuster($output)
+ {
+ $cacheBuster = UIAssetCacheBuster::getInstance()->piwikVersionBasedCacheBuster();
+ $tag = 'cb=' . $cacheBuster;
+
+ $pattern = array(
+ '~
+