<?php

namespace modules\pkpCore;

use \Pelago\Emogrifier\CssInliner;

	require_once __DIR__ . '/model/destination.php';
	require_once __DIR__ . '/model/mailobject.php';
	require_once __DIR__ . '/model/RangeHeader.php';
	require_once __DIR__ . '/model/misc.php';

function loadAndExecute($filename) {
	include $filename;
	exit;
}

class main extends \moduleMain {
	use \modules\config\traits {
			\modules\config\traits::__construct as __ConstructConfig;
		}
	use \modules\input\traits {
			\modules\input\traits::__construct as __ConstructInput;
		}
	use \modules\database\traits {
			\modules\database\traits::__construct as __ConstructDatabase;
		}
	use \modules\output\traits {
			\modules\output\traits::__construct as __ConstructOutput;
		}
	
	private $active_theme;
	private $levels_present = [];
	private $cache = [];
	protected $return_url;
	protected $user_id;
	protected $level;
	protected $hasUser;
	protected $impersonater;
	protected $model_handlebars;

	public function __construct() {
		parent::__construct(__DIR__);
		$this->__ConstructConfig();
		$this->__ConstructInput();
		$this->__ConstructDatabase();
		$this->__ConstructOutput($this->__INPUT, $this->__CONFIG);
		
		require_once __DIR__ . '/controller.php';
		require_once __DIR__ . '/model.php';
		require_once __DIR__ . '/view.php';
		$this->model_handlebars = $this->LoadModel('handlebars_model');
		$this->autoloadRegister('LightnCandy', __DIR__ . '/lightncandy');
	}
		
	public function Activate() {		// install hooks
		$this->Register_FormatHandler('html', [$this, '__export_html']);
		$this->Register_FormatHandler('htm', [$this, '__export_html']);

		hook_add('user.levelpresent', [$this, '__levelpresent']);
		hook_add('user.info', [$this, '__user_info']);
		hook_add('user.password',  [$this, '__user_password']);		// does the account have a password;   set the account password
		hook_add('login.create', [$this, '__login_create'] );		// a user can have multiple logins
        hook_add('login.verify', [$this, '__login_verify_init'], 1 );
		hook_add('login.verify', [$this, '__login_verify'] );		// if credentials are ok, return the user_id, name and level of the user
		hook_add('group.enum', [$this, '__standardgroups']);
		hook_add('execute', [$this, '__module_exec']);
		hook_add('execute.internal', [$this, '__module_execint']);
		hook_add('module.enable', [$this, '__module_enable']);		// enable or disable a module
		hook_add('module.theme', [$this, '__module_theme']);		// get the active theme
		hook_add('module.list', [$this, '__RetrieveActiveModules']);		// get a list of all enabled moduled
		hook_add('global.load', [$this, '__module_load'], 1);		// load all enabled modules
		hook_add('global.load', [$this, '__module_load99'], 99);	// save settings file
		hook_add('login', [$this, '__login'], 1);					// log the user in - must be first
		hook_add('logout', [$this, '__logout'], 90);				// log the user out
		hook_add('menu.menu3', [$this, '__login_link']); 			// add login/logout item to menu
		hook_add('menu.menu2', [$this, '__login_link2_1'], 10); 	// home
		hook_add('menu.menu2', [$this, '__login_link2'], 99); 		// add login/logout item to menu
		hook_add('email.transform', [$this, '__html_to_text'], 90);	// once html has been finalized, extract the plaintext version
		hook_add('email.transform', [$this, '__css_to_html'], 95);	// merge in css styles
		hook_add('output.common', [$this, '__output_common']);		// add "displayPage" fields - this is used when outputting a webpage as json
		hook_add('output.includes', [$this, '__output_includes']);	// remove templates which are located in the compiled js file
		hook_add('filter.themes', [$this, '__available_themes']);
		hook_add('html.prepare', [$this, '__head'], 1);
		hook_add('html.theme', [$this, '__settheme']);				// set theme
		hook_add('global.load', [$this, '__cron_added'], 97);		// handle custom events
		hook_add('global.prepare', [$this, '__global_prepare']);		// objects to be backed up, restored or refreshed.
		hook_add('url.key.select', [$this, '__availablePages'], 99);	// public entry points
		
		$this->link('/login', [$this, '__action_login']);			// matches:  /login /login? /login# but not /login/ or /login/...
		$this->link('/logout', [$this, '__action_logout']);
		$this->link('/static/templates.js', [$this, '__action_handlebars']);	// all the compiled handlebars templates
		$this->return_url = $this->__INPUT->request_uri();
		if (defined('RUNAS')) {
			$this->_impersonate_user(RUNAS);
		}
		$this->level = isset($_SESSION['current_user']) ? $_SESSION['current_user']['u_level'] : 0;
		$this->hasUser = hook_execute('user.levelpresent', FALSE, 10);
		$this->impersonater = isset($_SESSION['impersonate']) ? $_SESSION['impersonate']['u_id'] : 0;
		$this->user_id = isset($_SESSION['current_user']) ? $_SESSION['current_user']['u_id'] : 0;
        $GLOBALS['loader']->UserLevel($this->level);
	}
	
	private function _impersonate_user($uid) {
		$users = $this->LoadModel('user_model');
		$users->Impersonate($uid);
	}

	public function __output_common(&$data) {
		//to do: load all related handlebar templates
		$this->commonItems($data);
	}
	
	public function __head($modulemain) {
		$ext = $this->getJsExtension();
		$base = $this->StaticUriModule(__DIR__);
		$id = $this->IncludeFile("$base/jquery.min.js", 'jquery')
					->Version('3.7.1')
					->tag('jquery');
		
		$this->IncludeFile($base . '/main.css', 'maincss');
		$id = $this->IncludeFile('/static/templates.js')
					->Version($this->model_handlebars->getFileVersion());
		// now includes history: https://github.com/devote/HTML5-History-API
		$this->IncludeFile($base . "/main{$ext}", 'mainjs')->depends('jquery');
	}
	
	public function commonItems(&$data) {
		// Load all the Handlebars templates
		$this->__OUTPUT->EnumHandleBars();		// html format needs associated handlebars templates; should also be used for on_same_page

		$data['user'] = ['id' => (int)$this->user_id, 'level' => (int)$this->level];
		$data['host'] = $this->hostname(FALSE);

		$param = [ 'uri' => $this->__INPUT->request_uri() ];
		
		$template = !empty($data['template']) ? $data['template'] : '';
		if ($template != '') {
			$param['template'] = $this->__OUTPUT->GetTemplate($template);
		}
		
		$data = hook_execute('output.data', $data, $param);
		// set page title 
		if (!empty($data['template']) && empty($data['page_title'])) {
			$data['page_title'] = $this->__OUTPUT->GetTitleFromTemplate($data['template']);
		}
		if (isset($data['popup']) && is_array($data['popup'])) {
			$node = &$data['popup'];
			if (!empty($node['template']) && empty($node['title'])) {
				$node['title'] = $this->__OUTPUT->GetTitleFromTemplate($node['template']);
			}
		}
	}
	
	// add common items needed for html templating
	// page may have many handlebars templates - this chooses the one based on the entrypoint uri
	public function _setPrimaryItem(&$data) {
		$data['current'] = $this->current_url();

		$this->commonItems($data);
		/*
		if (isset($this->on_same_page)) {
			ksort($this->on_same_page);		// move index, if present, to start
			$data['samepage'] = array_values($this->on_same_page);
		}
		*/

		// final json block
		$id = $this->IncludeFile('');
		$id->JavascriptData('current_page', $data);
		$id->JavascriptCommand("\t\tmain.initialize(current_page)", 100);
	}
	
	public function __standardgroups(&$groups) {
		if (empty($groups)) $groups = [];
		$groups[0] = 'everyone';
		$groups[1] = 'member';
		$groups[3] = 'special';     // company level
		$groups[5] = 'moderator';
		$groups[9] = 'staff';
		$groups[10] = 'admin';
	}

	public function __login_verify_init(&$result) {
        $_SESSION['domain'] = FALSE;
        $_SESSION['x-name'] = FALSE;
        $_SESSION['x-email'] = FALSE;

    }
	
	public function __export_html($data) {
		if ($data !== NULL ) {	// only if html
			$this->_setPrimaryItem($data);
		}
		// pass data to theme
		$current_theme = $this->_initTheme();
		$current_theme->run($data);
	}
	

	private function _apply_late(&$data) {
		foreach($data as $key => &$value) {
			if (is_array($value)) {
				$this->_apply_late($value);
			} elseif (is_string($value)) {
				$value = $this->hook_apply_late($value);
			}
		}
	}
	
	private function isStaticFile($segments) {
		array_shift($segments);
		$module = array_shift($segments);
		$path = join('/', $segments);
		if ($path == '') return '';
		$filename = HOME . '/modules/' . $module . '/static/' . $path;
		$filename = str_replace(['..', '&', '<', '>', '|'], '', $filename);
		return file_exists($filename) ? $filename : '';
	}
	
	public function __module_exec(&$result, $segments) {
		$element0 = isset($segments[0]) ? $segments[0] : '';
		if ($element0 == '~' && $result === FALSE) {
			$filename = $this->isStaticFile($segments);
			if ($filename != '') {						// output file/mimetype
				$mime = mime_content_type($filename);
				$ext = pathinfo($filename, PATHINFO_EXTENSION);
				if ($ext == 'css') $mime = 'text/css';
				if ($ext == 'js') $mime = 'application/javascript; charset=UTF-8';
				if ($ext == 'php') loadAndExecute($filename);			// execute the php
				
				header("Content-Type: $mime");
				readfile($filename);
				exit;
			} else {
				$GLOBALS['loader']->Intercept($result, $segments);
			}
		}
	}

	public function __module_theme(&$result, $default = 'themeDefault') {
		if ($result === NULL) {
			$result = load_module($default);
			$result->Load();
		}
	}
	
	public function _initTheme($default = '') {
		if (empty($this->active_theme)) {
			if ($default != '') {
				$requestedTheme = $default;
			} else {
				if (is_object($this->settings['theme'] ?? '')) {
					$requestedTheme = json_decode($this->settings['theme']->pd_text);
				} else {
					$requestedTheme = $this->settings['theme'] ?? '';
				}
			}
			if ($requestedTheme == '') $requestedTheme = 'themeDefault';
			$this->active_theme = load_module($requestedTheme);
			$this->active_theme->Load();
		}
		return $this->active_theme;
	}
	
	public function __module_execint(&$data, $uri) {
		$data = $this->ExecuteInternal($uri);
	}
	
	public function ExecuteInternal($uri) {
		$dest = new \Destination();
		$dest->uri($uri)
				->Input($this->__INPUT)
				->Output($this->__OUTPUT);
		$routes = $this->link();
		if ($routes !== NULL) $dest->Routes($routes);
		$dest->Parse();
				
		$errnum = $dest->GetError();
		if ($errnum) {
			return FALSE;
		} elseif (!$dest->isHandled()) {
			return $dest->Execute();			// ** execute the command **
		} else {
			return FALSE;
		}
	}
	
	public function run() {
		$data = NULL;
		$uri = $this->request_uri();
		
		$dest = new \Destination();
		$dest->uri($uri)
				->Input($this->__INPUT)
				->Output($this->__OUTPUT);
		$routes = $this->link();
		if ($routes !== NULL) $dest->Routes($routes);
		$dest->Parse();
				
		$errnum = $dest->GetError();
		if ($errnum) {
			$this->HttpStatus($errnum);
		} elseif (!$dest->isHandled()) {
			$data = $dest->Execute();			// ** execute the command **
			$errnum = $dest->GetError();		// handler returned an error?
			if ($errnum) {
				$this->HttpStatus($errnum);
			}
		}
		
		// load theme, if HTML or JSON-with-Late-Hook (if the JSON data is not being displayed, there's no need to use a late hook)
		// work out which theme was requested. Use Settings when page-specific theme is missing
		// if JSON-with-Late-Hook, replace placeholder values
		
		$http_status = $this->HttpStatus();
		$format = $this->outputFormat();
		if ($format == 'html' || $format == 'htm' || $format == '__cms' || ($format == 'json' && $this->hasLate()) ) {  // json values may have template-sourced data 
			$data = hook_execute('output.filter', $data, $this);		// allow the data to be patched - eg theme
			if (isset($data['theme'])) {
				$theme = $data['theme'];
				unset($data['theme']);
			} else {
				$theme = '';
			}
			$this->_initTheme($theme);
			if ($this->hasLate()) {
				$this->_apply_late($data);
			}
		}
		
		if ($http_status->isError()) {
			hook_execute('output.error', FALSE, $http_status, $this->__OUTPUT);
		} else {
		// send to appropriate handler
			$this->__OUTPUT->ExportHandler($data);
		//		send message, if handled then exit
		//		die: unsupported output format error
		}
		
//		foreach($this->__CONFIG as $key => $value) {
//			echo "\nconfig: $key => $value";
//		}

	}

	public function __module_enable($module_name, $enable) {
        if (empty($module_name)) {
            die('Unable to enable empty module');
        }
		$key = 'state:' . md5($module_name);
		$this->tablename('config');
		if ($enable) {
			$this->InsertOrUpdate([
				'cfg_hash' => $key
			], [
				'cfg_text' => $module_name			// simple value,  not serialized
			]);
		} else {
			$this->Delete(['cfg_hash' => $key]);
			// to do: remove from htconfig ($config->DATA)
		}
//		$this->DbErr();
		
		$GLOBALS['loader']->LoadModule($module_name);
		list($module_name, $node) = $GLOBALS['loader']->LocateModule($module_name);
		if (isset($node['main'])) {
			$item = $node['main'];
			try {
				if ($enable) {
					if (method_exists($item, 'onEnable')) $item->onEnable();
				} else {
					if (method_exists($item, 'onDisable')) $item->onDisable();
				}
			} catch (\Exception $e) {
			}
		}
	}
	
	// these are only the modules that have the Enabled checkbox checked.
    public function __RetrieveActiveModules(&$result) {
		if (empty($result)) {
			$data = $this->tablename('config')
						 ->Query('select cfg_hash,cfg_text from `config`')
						 ->where('cfg_hash like %s', 'state:%')
						 ->get();
			$result = [];
			foreach($data as $dataitem) {
                if (!empty($dataitem->cfg_text)) {
                    $result[] = $dataitem->cfg_text;
                }
			}
		}
    }

	
	// load all enabled modules, (if not previously loaded, with their dependencies)
	public function __module_load(&$unused) {
        $module_list = NULL;
		$this->__RetrieveActiveModules($module_list);					// get list of active modules
		foreach($module_list as $module_name) {
			// if module not loaded, then load it;
			$res = $GLOBALS['loader']->LoadModule($module_name);
			if (is_string($res)) {	// failed to load
			// error?
				continue;
			}
			// mark all as Enabled
			$GLOBALS['loader']->MarkEnabled($module_name);
		}	
	}

	public function __module_load99(&$unused) {
		$GLOBALS['loader']->flush();
	}
	
	// Add a new entry to the user & login tables
	public function __login_create(&$result, $param) {
		$users = $this->LoadModel('user_model');
		$user_id = empty($param->user_id) ? 0 : $param->user_id;
		if (!$user_id) {
			$res = $users->FromLogin($param, TRUE);
			$user_id = $res === NULL ? 0 : $res->user_id;
		}
		
		if (!$user_id) {
			$level = empty($param->level) ? 0 : $param->level;
			$name = $param->name;
			$email = isset($param->email) ? $param->email : '';		// primary email
			$user_id = $users->createUser($level, $name, $email);
		}
		$result = $users->AddLogin($user_id, $param);
	}
	
	public function __login_verify(&$result, $param) {
		if (!$result) {
			$users = $this->LoadModel('user_model');
			$result = $users->FromLogin($param, FALSE);
		}
	}
	
	// does a user exist having the  requested access level?
	public function __levelpresent(&$result, $level) {
		$users = $this->LoadModel('user_model');
		if (!isset($this->levels_present[$level])) {
			$this->levels_present[$level] = $users->LevelPresent($level);
		}
		$result = $this->levels_present[$level];
	}
	
	public function __login(&$res, $user_info) {
		// login
		$this->user_id = isset($user_info->us_user_id) ? $user_info->us_user_id : $user_info->user_id;
		$this->level = $user_info->level;
		$_SESSION['current_user'] = [
			'u_level' => $this->level,
			'u_id' => $this->user_id,
			'u_name' => trim($user_info->name_first . ' ' . $user_info->name_last)
		];
		
		$impersonate = empty($user_info->x_impersonate) ? FALSE : $user_info->x_impersonate;
		if (!$impersonate) {					// only if we are not impersonating.
			if ($user_info->level >= 9) {		// allow to impersonate others. Staff can only impersonate someone with a lower level
				$_SESSION['impersonate'] = $_SESSION['current_user'];
			} else {
				unset($_SESSION['impersonate']);
			}
		}
	}
	
	function __action_login() {
		$bounce = isset($_GET['return']) ? $_GET['return'] : '';
		if (strpos($bounce, '//') !== FALSE) $bounce = '';
		if ($bounce == '') $bounce = '/account';
		$id = isset($_GET['id']) ? intval($_GET['id']) : 0;
		$auth = isset($_GET['auth']) ? $_GET['auth'] : '';
		if ($auth != '') {		// impersonate
			if (hook_execute('nonce.verify', FALSE, 'impersonate', $id, $auth)) {
				$users = $this->LoadModel('user_model');
				$users->Impersonate($id);
			}
			$this->Redirect($bounce);		// auth may be invalid
		}
		
		$items = hook_execute('auth.*.css', []);
		foreach($items as $item) {
			$this->IncludeFile($item);
		}
		
		$handlers = hook_execute('auth.*.login', [], ['bounce' => $bounce]);
		
		$result = [];
		$list = [];
		$callback = NULL;
		foreach($handlers as $module => $info) {
			if (isset($info['theme'])) $result['theme'] = $info['theme'];
			if (isset($info['login'])) $callback = $info['login'];
			if (!empty($info['button'])) {
				$list[] = ['link' => $info['link'], 'value' => $info['button'], 'footer' => $info['footer'] ?? FALSE];
			}
		}
		$result['provider'] = $list;
		if (is_callable($callback)) {
			$param = [&$result];
			call_user_func_array($callback, $param);
		} else {
			$view = $this->__LoadView('admin_view');
			$view->render_login();
			$result['template'] = 'login_view';
		}
		return $result;
	}
	
	public function __logout() {
		if ($this->impersonater && $this->user_id != $this->impersonater) {		// finish impersonating the user
			$_SESSION['current_user'] = $_SESSION['impersonate'];
		} else {
			unset($_SESSION['current_user']);
		}
	}
	
	public function __action_logout() {
        $_SESSION['domain']   = FALSE;
        $_SESSION['x-name']   = FALSE;
        $_SESSION['x-email']  = FALSE;
        
		hook_execute('logout', []);
		session_write_close();
		$return = $this->return_url;
		if (strpos($return, '/logout') !== FALSE || strpos($return, '/login') !== FALSE ) {
			$return = '/';
		}
		return ['redirect' => $return];
	}
		
	public function __login_link(&$menu) {
		if ($this->hasUser) {
			//to do: remove login link from login (and logout?) pages
			// logout -> don't bounce to logout.
			// login -> don't bounce to login
			$return = rawurlencode($this->return_url);
			if ($this->level) {
				$menu[] = ['caption' => 'Logout', 'href' => '/logout?return=' . $return, 's' => 0, 'rel' => 'nofollow'];
			} else {
				$menu[] = ['caption' => 'Login', 'href' => '/login?return=' . $return, 's' => 0, 'rel' => 'nofollow'];
			}
		}
	}

	public function __login_link2_1(&$menu) {
		if ($this->hasUser) {
			$menu[] = [
				'caption' => 'Home', 
				'href' => '/', 
				's' => 0, 
				'icon' => '<i class="largeicon fas fa-home"></i>', 
				'hint' => 'Home',
				'class' => 'nologin',			// not to appear on login page
			];
		}
	}
	
	public function __login_link2(&$menu) {
		if ($this->hasUser) {
			//to do: remove login link from login (and logout?) pages
			// logout -> don't bounce to logout.
			// login -> don't bounce to login
			$return = rawurlencode($this->return_url);
			if ($this->level) {
				$hint = "Logged in as " . htmlspecialchars($_SESSION['current_user']['u_name']);
				if ($this->level >= 9) {
					$menu[] = [
						'caption' => 'Admin', 
						'href' => '/admin', 
						'hint' => 'Admin / Settings', 
						'icon' => '<i class="largeicon fas fa-toolbox"></i>', 
						'class' => 'menu-button',
						'drop' => '/admin',
						's' => 0
					];
				}
				$menu[] = [
					'caption' => 'Your Account', 
					'href' => '/account', 
					'hint' => $hint, 
					'icon' => '<i class="largeicon fas fa-user-cog"></i>', 
					's' => 0
				];
				if ($this->impersonater && $this->user_id != $this->impersonater) {
					$menu[] = [
						'caption' => 'Impersonate', 
						'href' => '/logout?return=' . $return, 
						's' => 0, 
						'icon' => '<i class="largeicon fas fa-user-slash"></i>', 
						'hint' => 'Stop impersonating user',
						'rel'	=> 'nofollow',
					];		// end impersonating
				} else {
					$menu[] = [
						'caption' => 'Logout', 
						'href' => '/logout?return=' . $return, 
						's' => 0, 
						'icon' => '<i class="largeicon fas fa-power-off"></i>', 
						'hint' => 'Logout',
						'rel'	=> 'nofollow',
					];
				}
				
			} else {
				$menu[] = [
					'caption' => 'Login', 
					'href' => '/login?return=' . $return, 
					's' => 0, 
					'icon' => '<i class="largeicon fas fa-user"></i>', 
					'hint' => 'Login',
					'class' => 'nologin',
					'rel'	=> 'nofollow',
				];
			}
		}
	}

	
	public function __html_to_text(&$mailobject) {
		if (is_object($mailobject)) {
			$body = $mailobject->body();
			// extract text
			$parser = new \phpHTMLplain;
			$text = $parser->process_plaintext($body);
			$mailobject->text($text);
		}
	}
	
	// https://github.com/MyIntervals/emogrifier/releases
	public function __css_to_html(&$mailobject) {
		if (is_object($mailobject)) {
			$css = $mailobject->css();
			if ($css !== FALSE) {		// merge in css
                $body = CssInliner::fromHtml($mailobject->body())->inlineCss($css)->render();
				$mailobject->body($body);
			}
		}
	}
	
	public function __available_themes(&$result) {
		$GLOBALS['loader']->Unscanned(TRUE);
		$modulelist = $GLOBALS['loader']->getModuleList();
		$result = [];
		$result[] = [
			'key' => '',
			'value' => '(please select a theme)',
		];
		foreach($modulelist as $name => $module_info) {
			if (is_array($module_info)) {
				if (isset($module_info['.info']['theme'])) {
					$theme = $module_info['.info']['theme'];
					$result[] = [
						'key' => $theme,
						'value' => "{$module_info['.info']['name']} [$theme]",
					];
				}
			}
		}
	}
	
	public function __user_password(&$param, $password = FALSE) {
		$users = $this->LoadModel('user_model');
		if ($password === FALSE) {
			$param = $users->accountHasPassword($this->user_id);
		} else {
			$users->setAccountPassword($this->user_id, $password);
		}
	}
	
	public function __user_info(&$param, $user_id) {
		$users = $this->LoadModel('user_model');
		$param = $users->getUserInfo($user_id);
	}
	
	public function __action_handlebars() {
		header('Content-type: application/javascript');
		die($this->model_handlebars . '');
	}
	
	final function handlebars() {
		$payload = [
			'name' 		=> $this->post('name', ''),
			'auth' 		=> $this->post('auth', ''),
			'hash' 		=> $this->post('hash', ''),
			'content'	=> $this->post('content', ''),
		];
		return $this->model_handlebars->updateHandlebar($payload);
	}
	
	public function __output_includes(&$includes) {
		$this->model_handlebars->updateIncludes($includes);
	}
	
	public function __cron_added($param) {
		// call  event callback for each custom event - this is to register/unregister the event with the external provider
		$events = hook_list('cron');
		if ($events !== NULL) {
			foreach($events as $priority => $hook_entries) {
				foreach($hook_entries as $event) {
					$event->performCustomEventCallback();
				}
			}
		}
	}
	
	public function __global_prepare(&$object_list) {
		foreach($object_list as $key => $item) {
			if ($item['type'] == 'table' && ($item['name'] == '_migration' || $item['name'] == 'config' || $item['name'] == 'events')) {
				unset($object_list[$key]);
			}
		}
	}
	
	public function __settheme(&$active_theme) {
		$active_theme = $this->_initTheme('');
	}
	
	public function __availablePages(&$data) {
		$pages = $GLOBALS['loader']->getCorePages();
		if (is_array($pages)) $data = array_merge($data, $pages);
	}
}

