<?php

class AppController extends Controller {

	/** @var string[] Actions allowed without logging in to CMS. */
	var $front = [];

	var $back = [];

	var $isBackend = false;

	var $theme = null;

	var $uses = [];
	var $helpers = [ 'Html', 'Form', 'Format', 'Session', 'Front', 'Text', 'Seo.Seo' ];
	var $components = [ 'Session', 'Cookie', 'Auth' => [ 'authenticate' => [ 'Form' => [ 'userModel' => 'Administrator' ] ] ] ];
	var $restrictions = [];

	var $paginate = [ 'maxLimit' => PHP_INT_MAX ];

	var $filterDefaults = [
		'is_deleted' => 0
	];

	var $pageLinks = [];

	var $root = APP;

	function beforeFilter() {
		$this->request->webroot = '/';
		$this->request->here = preg_replace('~/VectorCMS/~', '/', $this->request->here);

		# Skip missing static files, except if handled by the upload
		if ($this->name !== 'Upload' && !empty($_SERVER['REQUEST_URI'])) {
			$static = preg_match('~\.' . STATIC_EXTENSIONS . '$~ i', $_SERVER['REQUEST_URI']);
			if ($static) {
				header('HTTP/1.1 404 Not Found', true, 404);
				exit(404);
			}
		}

		Timer::start('AppController::beforeFilter()');
		{

			$this->set('modelClass', $this->modelClass);
			$this->set('singular', strtolower(Inflector::humanize(Inflector::singularize(Inflector::tableize($this->modelClass)))));
			$this->set('plural', Inflector::pluralize($this->viewVars['singular']));

			# Languages
			$this->setupPolyglot();

			# Clear cache
			$this->clearCache();

			# Authentication
			$this->setupAuthentication();

			# Load
			if ($this->modelClass && !empty($this->{$this->modelClass}->name)) {

				# Apply restrictions
				if (empty($this->{$this->modelClass}->restrictions)) {
					$this->{$this->modelClass}->restrictions = $this->restrictions;
				}

				# Set values for all enum fields
				foreach ($this->{$this->modelClass}->_schema as $field => $schema) {
					if ($schema['type'] == 'enum') {
						$this->set(Inflector::variable(Inflector::pluralize($field)), array_combine($schema['enum'], __a($schema['enum'])));
					}
				}

				# Set values for all belongsTo associations
				foreach ($this->{$this->modelClass}->belongsTo as $alias => $params) {
					$params['conditions'] = [];
					$params['order'] = '';
					$Model = $this->{$this->modelClass}->$alias;
					$var = Inflector::variable(Inflector::pluralize($alias));
					if ($Model->isModule) {
						$var = 'cms' . ucfirst($var);
					}

					# Special scenarios
					switch ($alias) {
						case 'Page':
							if ($page_id = $this->{$this->modelClass}->pageId) {

								# Try to find the host page
								$host = $Model->find('first', [
									'Get host for the module',
									'recursive'  => -1,
									'fields'     => [
										'lft', 'rght' ],
									'conditions' => [
										'Page.id'   => $page_id,
										'Page.type' => [ 'Page', 'Wrapper' ] ]
								]);

								# If host found
								if ($host) {
									$params['conditions'][] = [
										'Page.lft >'  => $host['Page']['lft'],
										'Page.rght <' => $host['Page']['rght']
									];
								} else {
									$params['conditions'] = [ 'FALSE' ];
								}
							}
							$params['conditions']['Page.is_category'] = true;

							break;

						case 'Category':
							$var = 'categories';
							break;

						case 'Group':
							$var = 'groups';
							break;
					}

					# Filter by model
					if ($Model->schema('model')) {
						$params['conditions']["{$alias}.model"] = $this->modelClass;
					}

					# Retrieve plain list
					if (!$Model->schema('lft')) {

						# Dont show deleted.
						if ($Model->schema('is_deleted')) {
							$params['conditions']['is_deleted'] = false;
						}

						$params['conditions'] = array_merge(
							$params['conditions'],
							$Model->getSelectConditions()
						);

						$params['order'] = $Model->getSelectOrder();

						$value = $Model->find('list', [
							"Get list of {$var}",
							'conditions' => $params['conditions'],
							'order'      => $params['order']
						]);

						# Retrieve tree list
					} else {
						if ($Model->schema('is_deleted')) {
							$params['conditions'][$Model->alias . '.is_deleted'] = false;
						}
						$value = $Model->getNodeList($params['conditions']);
					}

					# Set default value
					if ($Model->schema('is_default')) {
						$this->set(Inflector::variable($alias) . '_default_value', $Model->field('id', [ 'is_default' => true ]));
					}

					unset($Model);

					# Add empty option but preserve the keys
					$schema = $this->{$this->modelClass}->_schema[$params['foreignKey']];
					if (!empty($schema['null'])) {
						$value = [ '' => null ] + $value;
					}

					$this->set($var, $value);
				}

				# Set values for all hasSet associations
				foreach ($this->{$this->modelClass}->hasSet as $alias => $params) {
					$this->set(Inflector::variable(Inflector::pluralize($alias)), ClassRegistry::init($alias)->find('list', [ 'conditions' => $params['conditions'] ]));
				}

				# Parents
				if ($this->{$this->modelClass}->schema('parent_id')) {
					$parents = $this->{$this->modelClass}->getNodeList();
					$this->set('parents', [ '' => '' ] + $parents);
				}

				# Set administrator related lists
				$administrators = $this->Administrator->find('list', [ 'conditions' => [ 'Administrator.is_active' => true ] ]);
				$this->set('createdBies', $administrators);
				$this->set('modifiedBies', $administrators);
				$this->set('ownedBies', $this->Administrator->getOwnable($this->modelClass, $this->Auth->user('id')));
				$this->set('assignedTos', $this->Administrator->getAssignable($this->modelClass, $this->Auth->user('id')));

				//TODO
				//				# Associate uploads
				//				foreach($this->{$this->modelClass}->hasOne as $association => $params) if($params['className'] == 'Upload' && !empty($this->request->data[$association])) {
				//					if(!empty($params['extensions'])) {
				//						$this->{$this->modelClass}->$association->allowedExtensions = $params['extensions'];
				//					}
				//					$this->request->data[$association]['model'] = $this->modelClass;
				//					$this->request->data[$association]['association'] = $association;
				//				}
				//
				//				foreach($this->{$this->modelClass}->hasMany as $association => $params) if($params['className'] == 'Upload' && !empty($this->request->data[$association])) {
				//					if(!empty($params['extensions'])) {
				//						$this->{$this->modelClass}->$association->allowedExtensions = $params['extensions'];
				//					}
				//					foreach($this->request->data[$association] as $i => $data) {
				//						$this->request->data[$association][$i]['model'] = $this->modelClass;
				//						$this->request->data[$association][$i]['association'] = $association;
				//					}
				//				}
			}

			# Which view is to be used: app, default or scaffold
			$dir = 'View/' . substr(get_class($this), 0, -10) . DS;
			$view = Inflector::underscore($this->request->action);
			$locations = [ APP . $dir, VECTORCMS_ROOT . $dir ];

			# Add locations for plugin
			if (!empty($this->plugin)) {
				$locations[] = APP . 'Plugin' . DS . $this->plugin . DS . $dir;
				$locations[] = VECTORCMS_ROOT . 'Plugin' . DS . $this->plugin . DS . $dir;
			}

			$this->viewPath = 'Scaffolds';
			foreach ($locations as $location)
				if (is_file("{$location}{$view}.ctp")) {
					$this->viewPath = '../..' . substr($location, strlen(ROOT));
					break;
				}

			# Prefix
			$this->prefix = !empty($this->request->params['prefix'])
				? $this->request->params['prefix'] . '/'
				: '';

			# Default values
			$this->set('controller', $this->request->params['controller']);
			$this->set('_actions', []);
			$this->set('fieldOptions', []);
			$this->set('referer', $this->referer());

			# AJAX part
			$this->set('isAjax', true);
			Configure::write('ajax', false);
			if (Configure::read('ajax')) {
				if ($this->request->isAjax()) {
					$this->layout = 'ajax';
					$this->set('target', 'content');
					$this->Session->delete('ajax');
				} else if (!$this->Session->Read('ajax')) {
					$this->Session->write('ajax', true);
					$this->redirect('/#' . $this->request->params['url']['url']);
				}

				# Requested by AJAX
			} else if ($this->request->isAjax()) {
				$this->layout = false;
				$this->set('isAjax', true);
				Configure::write('isAjax', true);
			} else {
				$this->set('isAjax', false);
				Configure::write('isAjax', false);
			}

			# Redirect
			$ctrl = isset($this->request->params['ctrl']) ? $this->request->params['ctrl'] : null;
			$this->redirect = isset($this->request->data[$this->modelClass]['redirect'])
				? $this->request->data[$this->modelClass]['redirect']
				: [ C => $this->prefix . $this->request->params['controller'], A => 'index', 'ctrl' => $ctrl ];

		}
		Timer::end('AppController::beforeFilter()');

		# beforeAction
		$beforeAction = 'before' . ucfirst($this->request->params['action']);
		if ($beforeAction != 'beforeFilter' && is_callable([ $this, $beforeAction ])) {
			Timer::start(get_class($this) . "::{$beforeAction}()");
			{
				call_user_func_array([ $this, $beforeAction ], $this->request->params['pass']);
			}
			Timer::end(get_class($this) . "::{$beforeAction}()");
		}
		Timer::start('Filter');
	}

	# ~ AfterFilter	 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	function afterFilter() {
		Timer::end('Filter');
	}

	# ~ Pack PHP array into json - - - - - - - - - - - - - - - - - - - - - - - - - #
	function json($object, $exit = true) {
		$json = [];
		foreach ($object as $key => $value) {
			if (is_bool($value)) {
				$value = $value ? 'true' : 'false';
			} else if (is_array($value)) {
				$value = $this->json($value, false);
			} else if (!preg_match('/^\[\s*"[^"]*"(\s*,\s*"[^"]*")*\s*\]$/', $value)) {
				$value = '"' . str_replace('"', '\\"', $value) . '"';
			}
			$json[] = "\"{$key}\":{$value}";
		}
		$json = '{' . implode(',', $json) . '}';
		if ($exit) {
			header('Content-Type: application/json');
			header('Content-Length: ' . strlen($json));
			echo $json;
			exit;
		}
		return $json;
	}

	# ~ Pack PHP array into javascript array - - - - - - - - - - - - - - - - - - - #
	function jsa($array, $exit = true) {
		foreach ($array as $value) {
			if (is_bool($value)) {
				$value = $value ? 'true' : 'false';
			} else if (is_array($value)) {
				$value = $this->jsa($value, false);
			} else if (!preg_match('/^\[\s*"[^"]*"(\s*,\s*"[^"]*")*\s*\]$/', $value)) {
				$value = '"' . str_replace('"', '\\"', $value) . '"';
			}
			$jsa[] = $value;
		}
		$jsa = '[' . implode(',', $jsa) . ']';
		if ($exit) {
			header('Content-Type: text/plain');
			header('Content-Length: ' . strlen($jsa));
			echo $jsa;
			exit;
		}
		return $jsa;
	}

	# ~ Clears the cache based on the debug value  - - - - - - - - - - - - - - - - #
	public function clearCache() {
		if (env('HTTP_CACHE_CONTROL') || Configure::read('debug') >= 0) {
			Timer::start('clearCache');
			{
				ClassRegistry::init('Layout')->refreshLive();
				ClassRegistry::init('Element')->refreshLive();
				ClassRegistry::init('Template')->refreshLive();
				ClassRegistry::init('Stylesheet')->refreshLive();
				ClassRegistry::init('Javascript')->refreshLive();
			}
			Timer::end('clearCache');
		}
	}

	# ~ Initializes the authentication and authorization engines - - - - - - - - - #
	function setupAuthentication() {
		if (CMS) {
			Timer::start('setupAuthentication');
			{

				AuthComponent::$sessionKey = 'Auth.Administrator';
				$this->Auth->userScope = [ 'Administrator.is_active' => true ];
				$this->Auth->loginError = __('Login failed. Unrecognized username or misstyped password.');
				$this->Auth->loginAction = [ P => null, C => 'administrators', A => 'login', 'language' => $this->request->params['language'] ];

				$this->Auth->actionMap = [
					'index'      => 'read',
					'filter'     => 'read',
					'view'       => 'read',
					'form'       => 'write',
					'delete'     => 'write',
					'populate'   => 'write',
					'sort_tree'  => 'write',
					'ajaxSwitch' => 'write',
				];
				foreach ($this->back as $action) {
					$this->Auth->actionMap[$action] = 'write';
				}

				if (!empty($this->front)) {
					foreach ($this->front as $action) {
						$this->Auth->allow($action);
					}
				}

				if (false && $this->name != 'Front' && !$this->Auth->user() && !in_array($this->request->action, $this->front)) {
					echo $this->requestAction([ P => null, C => 'administrators', A => 'login' ], [ 'return' ]);
					die;
				}

				$this->set('administrator', ClassRegistry::init('Administrator')->find('first', $this->Auth->user('id')));

			}
			Timer::end('setupAuthentication');
		}
	}

	# ~ Initializes the polyglot engine for the CMS	 - - - - - - - - - - - - - - - #
	function setupPolyglot() {

		Timer::start('setupPolyglot');
		{

			# Get active languages #
			$languages = $this->Language->getLanguages();
			Configure::write('Config.Languages', $languages);
			$this->Session->write('Config.Languages', $languages);

			// Get the language from the browser
			$defaultLanguage = null;
			$acceptLanguage = env('HTTP_ACCEPT_LANGUAGE');
			if ($acceptLanguage) {
				$accept = substr($acceptLanguage, 0, 2);

				// Check if we have requested language
				foreach ($languages as $locale => $localeOptions) {
					if ($locale == $accept) {
						$defaultLanguage = $accept;
						break;
					}
				}
			}

			# Get default language
			if (!$defaultLanguage) {
				foreach ($languages as $locale => $localeOptions) {
					if ($localeOptions['Language']['is_default']) {
						$defaultLanguage = $locale;
						break;
					}
				}
			}

			Configure::write('Config.default_language', $defaultLanguage);

			# Read language from params #
			if (empty($this->request->params['language'])) {

				# Set localelessHere var ($this->here without locale);
				$this->set('localelessHere', $this->request->here);

				# Get full path
				$redirectPath = $this->request->here;
				if(!empty($this->request->query)) {
					$redirectPath .= '?' . http_build_query($this->request->query);
				}

				# Check for language transition #
				$locales = join('|', array_keys($languages));
				if (preg_match("~^/({$locales}):({$locales})/?(?:([^?]*)(\?.*)?)?$~", $redirectPath, $match)) {
					Configure::write('Config.language', $match[1]);

					if (!empty($match[3])) {
						$page = $this->Page->fetch(explode('/', urldecode($match[3])));

						# Page
						switch ($page['Page']['type']) {
							case 'Page':
								$url = $this->Page->getPageLink($page['Page']['id'], $match[2]);
								break;
							case 'Module':
								$module = ClassRegistry::init($page['Module']['name']);
								$module->setVirtual($match[2]);
								$record = $module->find('first', [
									'recursive'  => -1,
									'fields'     => [
										'slug' ],
									'conditions' => [
										$module->alias . '.id' => $page['Data'][$module->alias]['id'] ]
								]);

								$slug = $record ? reset(reset($record)) : null;
								$url = preg_replace('~/[^/]*$~ Uu', '/', $this->Page->getPageLink($page['Page']['id'], $match[2])) . $slug;
								break;

							default:
								$url = '/';
						}
					}

					if (empty($url)) {
						$url = '/' . $match[2] . '/';
					}

					if(!empty($match[4])) {
						$url .= $match[4];
					}

					# Redirect
					$this->redirect(FULL_BASE_URL . ($url ? $url : '/'), 302);
				}

				# Read from cookie #
				if ($this->Cookie->read('language')) {
					$this->request->params['language'] = $this->Cookie->read('language');

					# Default language
				} else {
					$this->request->params['language'] = $defaultLanguage;
				}

				# By default, redirect if there are more than one active language
				$langRedirect = true;

				# Handle language prefix
				if (!CMS && Configure::read('Feature.hide_default_language_prefix')) {
					# Set language and prevent redirection
					$this->request->params['language'] = $defaultLanguage;
					$langRedirect = false;
				}

				# Redirect if more then one language active
				if ($langRedirect && $languages > 1 && empty($this->request->data) && $this->name === 'Front') {
					header('Location: ' . FULL_BASE_URL . "/{$this->request->params['language']}{$this->request->here}");
					exit;
					$this->redirect(FULL_BASE_URL . "/{$this->request->params['language']}{$this->request->here}");
				}
			} else {

				# Set localelessHere var ($this->here without locale);
				$this->set('localelessHere', substr($this->request->here, strlen($this->request->params['language']) + 1));
			}

			# Write into config
			Configure::write('Config.language', $this->request->params['language']);
			$this->Cookie->write('language', $this->request->params['language'], false);
			$this->Session->write('Config.language', $this->request->params['language']);

			# Set locale
			$format = Configure::read('Config.Languages');
			$format = $format[$this->request->params['language']]['Language']['format'];
			setlocale(LC_ALL, $format);

			if (!defined('BASE_URL'))
				define('BASE_URL', '/' . $this->request->params['language']);
			$this->set('locale', $this->request->params['language']);

		}
		Timer::end('setupPolyglot');
	}

	/**
	 * Handy function for sending emails using CakeEmail.
	 *
	 * @param    subject
	 *                Subject of the mail.
	 * @param    data
	 *                Data to be sent to the email template.
	 *
	 * @return    Instance of the CakeEmail component.
	 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
	public function sendEmail($subject, $data = [], $template = 'default') {
		$CakeEmail = new CakeEmail();
		return $CakeEmail->subject($subject)->viewVars($data)->template($this->request->params['action'], $template);
	}

	# ~ Return to previous page and show flash message - - - - - - - - - - - - - -
	function backtrack($message, $type = 'default', $success = true) {
		if ($this->request->isAjax()) {
			$this->json([ 'result' => $success, 'message' => $message ]);

		} else {
			$this->Session->setFlash($message, $type);

			# Select proper referer
			$referer = $this->referer();
			if ($referer === $this->request->here) {
				$referer = '/';
			}

			$this->redirect($referer, null, true);
		}
	}

	# ~ Confirm input  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function validatePost($salt, $expected, $data = null) {
		setDefault($data, $this->request->data);

		# Extract token and ip
		$ip = env('REMOTE_ADDR');
		$token = array_pop($data);

		# Check the post
		$safe = [];
		foreach ($expected as $field) {

			# Check that field exists
			if (key($data) != $field) {
				return false;
			}

			$safe[$field] = array_shift($data);
		}

		# Check for tails
		if (!empty($data)) {
			return false;
		}

		# Check the hash
		$hash = md5(implode($safe) . $salt . $ip);
		$valid = $token === $hash;

		# Debug
		if (!$valid && Configure::read('debug') > 2 && $ip === '127.0.0.1') {
			die($hash);
		}

		# Check the hash
		return $valid ? $safe : false;
	}

	# ~ Proper redirect handling - - - - - - - - - - - - - - - - - - - - - - - - - #
	function redirect($url = null, $status = null, $exit = true) {
		if (empty($url)) {
			$url = $this->referer();
		}
		if (is_array($url) && !isset($url['language']) && isset($this->request->params['language'])) {
			$url['language'] = $this->request->params['language'];
		}
		if (Configure::write('ajax')) {
			$this->Session->write('ajax', true);
		}

		parent::redirect($url, $status, $exit);
	}

	/** @inheritdoc */
	public function __get($key) {

		// If the variable does not start with uppercase, skip any checks.
		if (!preg_match('~^[A-Z]~', $key)) {
			return null;
		}

		// Special cases
		switch ($key) {

			// Skip for these
			case 'Tool':
			case 'Front':
				return null;

			case 'Email':
				return new CakeEmail;
		}

		// Load the module
		if ($model = ClassRegistry::init($key, true)) {
			return $this->$key = $model;
		}

		// Try to load from plugin
		$plugin = !empty($this->plugin) ? ($this->plugin . '.') : '';
		if ($plugin) {
			if ($model = ClassRegistry::init($plugin . $key, true)) {
				return $this->$key = $model;
			}
		}

		// No matches
		return null;
	}

	# ~ On app error redirect  - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function handleAppErrorInProduction($error) {

		# API errors - not sure why sometimes ApiExceptionRenderer is not called
		if (defined('API_ACCESS') && API_ACCESS) {
			$renderer = new ApiExceptionRenderer($error, true);
			$renderer->render();
		}

		# Show the error
		$this->Session->setFlash('An unknown error has been encountered, please contact <a href="mailto:support@intellex.rs">support</a>.', 'danger');

		# Set the redirection
		$redirectTo = CMS && $this->referer() != $this->here ? $this->referer() : '/';
		return $this->redirect($redirectTo);
	}
}

