<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package clearFusionCMS
 * @copyright Copyright (c) 2012 - 2017 clearFusionCMS. All rights reserved.
 * @link http://clearfusioncms.com
 */

/**
 * cfBackendControllerAction Class
 *
 * Base class for manager controller action classes.
 *
 * @package clearFusionCMS
 */
abstract class cfBackendControllerAction extends flControllerAction {
	/**
	 * The database object.
	 *
	 * @var flDb
	 */
	protected $db;

	/**
	 * The template engine.
	 *
	 * @var flTemplateEngine
	 */
	protected $tpl;

	/**
	 * The user object.
	 *
	 * @var flUser
	 */
	protected $user;

	/**
	 * The session object.
	 *
	 * @var flSession
	 */
	protected $session;

	/**
	 * The breadcrumb trail.
	 *
	 * @var flWidgetBreadcrumb
	 */
	protected $breadcrumbTrail = null;

	/**
	 * Cache object to use.
	 *
	 * @var flCache
	 */
	protected $cache;

	/**
	 * The manager session data.
	 *
	 * @var flSessionData
	 */
	protected $managerSession;

	/**
	 * Construct the object.
	 *
	 * @param flController $controller The parent controller object.
	 */
	function __construct($controller) {
		parent::__construct($controller);

		foreach(get_class_vars('cfBackendControllerAction') as $v => $d) {
			if($this->$v === null)
				$this->$v = $controller->_getVar($v);
		}
	}

	/**
	 * Test if the logged in user has access permission. If the user doesn't have permission then
	 * they are directed to the dashboard.
	 *
	 * @param string|array $permission A permission to test for or an array of permissions.
	 */
	function testPermission($permission) {
		$this->controller->testPermission($permission);
	}

	/**
	 * Assemble a manager URI.
	 *
	 * @param string $module The module name.
	 * @param string $action The action name.
	 * @param array $data Array of data to include.
	 * @param bool $absolute true to create an absolute URI else false for relative.
	 * @param bool $secure true for https, false for http and null for as per request.
	 * @return string The composed URI.
	 */
	function assemble($module, $action, $data = array(), $absolute = false, $secure = null) {
		return $this->controller->assemble($module, $action, $data, $absolute, $secure);
	}

	/**
	 * Render the page.
	 *
	 * @param string $title The page title.
	 * @param string $content The main HTML content for the page.
	 * @param string $content2 The optional secondary HTML content for tha page.
	 */
	function renderPage($title, $content, $content2 = null) {
		$this->controller->renderPage($title, $content, $content2);
	}

	/**
	 * Assign an alert message to a template variable.
	 *
	 * @param string $type The message type Error, Success
	 * @param string $message The message text.
	 * @return string The HTML for the alert.
	 */
	function alertMessage($type, $message) {
		return $this->controller->alertMessage($type, $message);
	}

	/**
	 * Render a page block.
	 *
	 * @param string $title The title for the block.
	 * @param string $content The HTML content.
	 * @param string $desc The description for the block.
	 * @param string $msg The message for the content area.
	 * @param flWidgetPagination $pagination The pagination widget it any.
	 * @return string The rendered HTML.
	 */
	function renderBlock($title, $content, $desc = '', $msg = '', $pagination = null) {
		return $this->controller->renderBlock($title, $content, $desc, $msg, $pagination);
	}

	/**
	 * Render a page block with a toolbar.
	 *
	 * @param flWidgetToolbar $toolbar The toolbar
	 * @param string $content The HTML content.
	 * @param string $msg The message for the content area.
	 * @param flWidgetPagination $pagination The pagination widget it any.
	 * @return string The rendered HTML.
	 */
	function renderBlockToolbar($toolbar, $content, $msg = '', $pagination = null) {
		return $this->controller->renderBlockToolbar($toolbar, $content, $msg, $pagination);
	}

	/**
	 * Lock the module object to the current user.
	 *
	 * @param string $module The module name.
	 * @param int $id The ID of the object to lock.
	 * @param bool $global Lock the object across all sites.
	 * @return string true if locked; else HTML for a block informing object in use.
	 */
	function lockObject($module, $id, $global = false) {
		return $this->controller->lockObject($module, $id, $global);
	}

	/**
	 * Unlock the module object from the current user.
	 *
	 * @param string $module The module name.
	 * @param int $id The ID of the object to unlock.
	 * @param bool $global Lock the object across all sites.
	 */
	function unlockObject($module, $id, $global = false) {
		$this->controller->unlockObject($module, $id, $global);
	}

	/**
	 * Add a JavaScript confirmation when the user tries to leave a page.
	 */
	function confirmLeavePage() {
		$this->controller->confirmLeavePage();
	}

	/**
	 * Get the editor module.
	 *
	 * @param string $override Optional override editor.
	 * @return string The WYSIWYG editor module.
	 */
	function getEditorModule($override = null) {
		return $this->controller->getEditorModule($override);
	}

	/**
	 * Get the code editor module.
	 *
	 * @return string The code editor module.
	 */
	function getCodeEditorModule() {
		return $this->controller->getCodeEditorModule();
	}
}

/**
 * cfBackendController Class
 *
 * The base class for manager controllers.
 *
 * @package clearFusionCMS
 */
class cfBackendController extends flController {

	/**
	 * The database object.
	 *
	 * @var flDb
	 */
	protected $db;

	/**
	 * The template engine.
	 *
	 * @var flTemplateEngine
	 */
	protected $tpl;

	/**
	 * The user object.
	 *
	 * @var flUser
	 */
	protected $user;

	/**
	 * The session object.
	 *
	 * @var flSession
	 */
	protected $session = null;

	/**
	 * The breadcrumb trail.
	 *
	 * @var flWidgetBreadcrumb
	 */
	protected $breadcrumbTrail = null;

	/**
	 * Cache object to use.
	 *
	 * @var flCache
	 */
	protected $cache;

	/**
	 * The manager session data.
	 *
	 * @var flSessionData
	 */
	protected $managerSession;

	/**
	 * The content object.
	 *
	 * @var cfContent
	 */
	public $content;

	/**
	 * Construct the controller.
	 *
	 * @param flRouter $router The router object.
	 * @param flRequest $request The request object.
	 * @param string $permission A permission required to be able to access the controller or array of permissions.
	 * @throws flExceptionNotFound
	 */
	function __construct($router, $request, $permission = null) {
		parent::__construct($router, $request);

		// Ensure that browser caching doesn't happen in the admin
		flApp::getResponse()->noCache();

		$this->db = flApp::getDb();
		$this->cache = flApp::getCache();
		$module = $router->getModule();
		if($module === null) $module = 'content'; // Use content module for main system

		// Load the site info
		$row = $this->db->fetchRow('SELECT shortname,displayname,editperm,twitterusername,puressl FROM cf_sites WHERE domain=? && redirectto=\'\'', $this->request->getServerName());
		if(!$row) {
			// try again without puressl as we're doing an upgrade
			if(!($row = $this->db->fetchRow('SELECT shortname,displayname,editperm,twitterusername FROM cf_sites WHERE domain=? && redirectto=\'\'', $this->request->getServerName())))
				throw new flExceptionNotFound();
			else {
				$row['puressl'] = 0;
			}
		}
		clearFusionCMS::setSite($row['shortname'], $siteName = $row['displayname'], $row['twitterusername'], $row['puressl']);

		// Get the session
		$this->session = flApp::getSession();
		$this->managerSession = $this->session->getNamespace('cfManagerSession');

		// Load the config
		$systemCfg = clearFusionCMS::getSiteConfig('cfSystemManager');

		// Set the locale for the manager
		fusionLib::setLocale($systemCfg->get('locale', 'en_US'));

		// If module not installed
		if(!($instModuleVer = $this->db->fetchOne('SELECT instver FROM fl_modules WHERE modname=?', $module)))
			flApp::getResponse()->redirectTo($this->assemble('', 'dashboard', array(), true));

		// Check the installed version against the loaded version & upgrade if required
		$mm = new flModuleMgr($this->db);
		$minfo = $mm->getInfo($module);
		if(version_compare($instModuleVer, $minfo->version) < 0) {
			$mm->upgrade($module);
			$this->cache->flush();
		}

		// Force the admin templates not to load from the database and configure the template engine
		flTemplateEngine::setDB(null);
		$this->tpl = new flTemplateEngine();
		$this->tpl->setCodeBuffer(flApp::getCodeBuffer()->useJQuery(flApp::getConfig()->get('managerUsejQuery', false)));
		$this->tpl->setTheme($systemCfg->template);
		$this->tpl->setPlaceholder('siteuri', $this->request->getBasePath(true));

		// Create the user object
		$this->user = clearFusionCMS::getUser();

		// Test if the user logged in & has permission to access site
		if(!$this->user->hasPermission('core.accessmgr', $row['editperm']) || $this->request->getCookie(clearFusionCMS::getConfig()->get('authCookieMgrName', '__managerauth'), '') != hash_hmac('md5', $this->user->getId() . ($this->user->isRememberMe() ? '1' : '0'), $this->user->getSecret())) {
			if($module == 'content' && (
					$router->getAction() == 'login' ||
					$router->getAction() == 'logout' ||
					$router->getAction() == 'forgotpwd' ||
					$router->getAction() == 'resetpwd'))
				return;

			// User logged in then log them out; else offer login
			flApp::getResponse()->redirectTo($this->assemble('', $this->user->isAuthenticated() ? 'logout' : 'login', array(), true));
		}
		flApp::getResponse()->setCookie(clearFusionCMS::getConfig()->get('authCookieMgrName', '__managerauth'), hash_hmac('md5', $this->user->getId() . ($this->user->isRememberMe() ? '1' : '0'), $this->user->getSecret()), $this->user->isRememberMe() ? flUser::getRememberMeTime() : 0, clearFusionCMS::getConfig()->get('authCookieDomain', ''), clearFusionCMS::getConfig()->get('authCookiePath', '/'), clearFusionCMS::usingSecureCookies(), true);

		// Check other permissions
		if($permission !== null)
			$this->testPermission($permission);

		// Set placeholders
		$this->tpl->setPlaceholder(array(
			'mgr_user' => sprintf(__('User: %s', 'cfBackend'), $this->user->getDisplayName()),
			'mgr_logout' => $this->assemble('', 'logout'),
			'mgr_profile' => $this->assemble('', 'myprofile'),
			'mgr_siteName' => $siteName,
			'mgr_selectsite' => $this->assemble('', 'selectsite'),
			'mgr_dashboard' => $this->assemble('', 'dashboard'),
			'mgr_label_logout' => __('Logout', 'cfBackend'),
			'mgr_label_profile' => __('My Profile', 'cfBackend'),
			'mgr_label_menu' => __('Menu', 'cfBackend'),
			'mgr_label_dashboard' => __('Dashboard', 'cfBackend'),
			'mgr_label_notifications' => __('Notifications', 'cfBackend'),
			'mgr_lang' => fusionLib::getLocale(true),
			'mgr_path' => $mgrPath = $this->request->getBaseUri() . clearFusionCMS::getConfig()->get('managerPath', 'manager') .'/'
		));

		// Create the breadcrumb
		$this->breadcrumbTrail = new flWidgetBreadcrumb();
		$this->breadcrumbTrail->addCrumb(__('Dashboard', 'cfBackend'), $this->assemble('', 'dashboard'));

		// Add common snippets
		$this->tpl->registerSnippet('breadcrumb', array($this, 'breadcrumbSnippet'));

		$this->content = new cfContent($this->db, clearFusionCMS::getSite());
	}

	/**
	 * Function used by cfBackendControllerAction to get variables when initialising the action.
	 *
	 * @param string $name The name of the variable to get.
	 * @return mixed The value of the variable.
	 */
	function _getVar($name) {
		return $this->$name;
	}

	/**
	 * Snippet to draw the bread crumb trail.
	 *
	 * @param cfTemplateEngine $tpl The template engine.
	 * @param array $params The parameters being passed to the snippet.
	 */
	function breadcrumbSnippet($tpl, $params) {
		if(isset($params['class']))
			$this->breadcrumbTrail->setClass('breadcrumbwidget ' . $params['class']);
		echo $this->breadcrumbTrail->render();
	}

	/**
	 * Test if the logged in user has access permission. If the user doesn't have permission then
	 * they are directed to the dashboard.
	 *
	 * @param string|array $permission A permission to test for or an array of permissions.
	 */
	function testPermission($permission) {
		if(!$this->user->hasPermission($permission))
			flApp::getResponse()->redirectTo($this->assemble('', 'dashboard', array('nopermission'), true));
	}

	/**
	 * Assemble a manager URI.
	 *
	 * @param string $module The module name.
	 * @param string $action The action name.
	 * @param array $data Array of data to include.
	 * @param bool $absolute true to create an absolute URI else false for relative.
	 * @param bool $secure true for https, false for http and null for as per request.
	 * @return string The composed URI.
	 */
	function assemble($module, $action, $data = array(), $absolute = false, $secure = null) {
		if($module == '')
			return $this->getRouter()->assemble('cfManager', array('action' => $action, '*' => $data), $absolute, $secure);
		else
			return $this->getRouter()->assemble('cfManagerModule', array('module' => $module, 'action' => $action, '*' => $data), $absolute, $secure);
	}

	/**
	 * Render the page.
	 *
	 * @param string $title The page title.
	 * @param string $content The main HTML content for the page.
	 * @param string $content2 The optional secondary HTML content for tha page.
	 */
	function renderPage($title, $content, $content2 = null) {
		clearFusionCMS::getEventManager()->notify(new flEvent('system.beforeAdminPageRender', array('templateEngine' => $this->tpl)));
		$this->response
			->setHeader('X-XSS-Protection', '0')
			->setHeader('X-Frame-Options', 'SAMEORIGIN')
			->setHeader('X-Content-Type-Options', 'nosniff')
			->setBody($this->tpl->parseDocument($content2 === null ? 'managerPage' : 'managerPage2Col',
				'backend_' . $this->getRequest()->getSelf(),
				array('page_title' => fusionLib::htmlChars($title),
					'content' => $content,
					'content2' => $content2)))
			->sendResponse();
	}

	/**
	 * Assign an alert message to a template variable.
	 *
	 * @param string $type The message type Error, Success or Info
	 * @param string $message The message text.
	 * @return string The HTML for the alert.
	 */
	function alertMessage($type, $message) {
		switch($type) {
			case 'Error': $title = __('Error', 'cfBackend'); break;
			case 'Info': $title = __('Information', 'cfBackend'); break;
			case 'Success': $title = __('Success', 'cfBackend'); break;
			case 'Warning':
			default:
				$title = __('Warning', 'cfBackend'); break;
		}
		return $this->tpl->parseChunk('managerAlert' . $type, array(
			'title' => $title,
			'message' => $message,
			'dismiss' => __('Dismiss', 'cfBackend')
		));
	}

	/**
	 * Render a page block.
	 *
	 * @param string $title The title for the block.
	 * @param string $content The HTML content.
	 * @param string $desc The description for the block.
	 * @param string $msg The message for the content area.
	 * @param string $pagination The pagination widget if any.
	 * @return string The rendered HTML.
	 */
	function renderBlock($title, $content, $desc = '', $msg = '', $pagination = '') {
		return $this->tpl->parseChunk('managerPageBlock', array(
			'block_title' => $title,
			'block_desc' => $desc,
			'block_msg' => $msg,
			'block_msg_raw' => str_replace(array("'", "\n"), array("\'", " "), strip_tags($msg)),
			'block_content' => $content,
			'block_pagination' => $pagination));
	}

	/**
	 * Render a page block with a toolbar.
	 *
	 * @param flWidgetToolbar $toolbar The toolbar
	 * @param string $content The HTML content.
	 * @param string $msg The message for the content area.
	 * @param string $pagination The pagination widget if any.
	 * @return string The rendered HTML.
	 */
	function renderBlockToolbar($toolbar, $content, $msg = '', $pagination = '') {
		return $this->tpl->parseChunk('managerPageBlockToolbar', array(
			'block_toolbar' => $toolbar->render(),
			'block_msg' => $msg,
			'block_msg_raw' => str_replace(array("'", "\n"), array("\'", " "), strip_tags($msg)),
			'block_content' => $content,
			'block_pagination' => $pagination));
	}

	/**
	 * Get the editor module.
	 *
	 * @param string $override Optional override editor.
	 * @return string The WYSIWYG editor module.
	 */
	function getEditorModule($override = null) {
		$cfg = clearFusionCMS::getSiteConfig('cfSystemCtntEdit');
		return empty($override) ? $this->managerSession->get('editor', $cfg->editor) : $override;
	}

	/**
	 * Get the code editor module.
	 *
	 * @return string The code editor module.
	 */
	function getCodeEditorModule() {
		$cfg = clearFusionCMS::getSiteConfig('cfSystemCtntEdit');
		return $this->managerSession->get('codeEditor', $cfg->get('codeEditor', ''));
	}

	/**
	 * Lock the module object to the current user.
	 *
	 * @param string $module The module name.
	 * @param int $id The ID of the object to lock.
	 * @param bool $global Lock the object across all sites.
	 * @return string true if locked; else HTML for a block informing object in use.
	 */
	function lockObject($module, $id, $global = false) {
		// Ignore AJAX requests
		if($this->request->isXmlHttpRequest())
			return true;

		$site = $global ? '' : clearFusionCMS::getSite();
		$this->db->query('LOCK TABLES cf_objlocks WRITE');

		// Calculate max age of locks
		$prune = new flDate();
		$prune->setTimestamp($prune->getTimestamp() - 604800); // Week
		$before = new flDate();
		$before->setTimestamp($before->getTimestamp() - 10);

		// Test if someone else has the lock
		$this->db->delete('cf_objlocks', 'lockedat<?', (string)$prune);
		$lockData = $this->db->fetchRow('SELECT uid,lockedat FROM cf_objlocks WHERE modulename=? && contentid=? && sitename=? && uid!=? && lockedat>=?', $module, $id, $site, $this->user->getId(), (string)$before);

		// If not locked lock the object to us
		if($lockData == null) {
			$hash = md5($this->user->getId() . $id . $module . $site);

			// If not present or older than before then replace
			if(!$this->db->fetchOne('SELECT COUNT(*) FROM cf_objlocks WHERE modulename=? && contentid=? && sitename=? && uid=? && lockedat>=?', $module, $id, $site, $this->user->getId(), (string)$before)) {
				$now = new flDate();
				$this->db->replace('cf_objlocks', array('uid' => $this->user->getId(), 'modulename' => $module, 'contentid' => $id, 'sitename' => $site, 'lockedat' => (string)$now, 'hash' => $hash, 'lockid' => 0));
			}

			// Attach event to auto unlock
			$lock = $this->assemble('', 'lockobject', array(), true);
			$this->tpl->getCodeBuffer()->requireCore()->bufferReadyJS("
	var lockid,
		lockTimer = false,
		firstLock = true,
		lockSource = null;

	if((lockid = sessionStorage.getItem('__cf_lockid')) == null) {
		sessionStorage.setItem('__cf_lockid', lockid = new Date().getTime().toString());
	}
	
	function notifyLock(r) {
		if(r == '0') {
			if(lockTimer) {
				clearInterval(lockTimer);
				lockTimer = false;
			}
			
			if(lockSource) {
				lockSource.close();
				lockSource = null;
			}
			
			if(firstLock) {
				alert('" . __('This object is already being edited by you in another browser window or tab. Changes made here may be lost or overwrite changes being made in the other tab or window.', 'cfBackend') . "');
			}
			else {
				alert('" . __('Another user has taken control of this object, you should make a copy of any changes that you have made. Attempting to save your work may cause you to overwrite their changes or their changes to overwrite yours.', 'cfBackend') . "');
			}
		}
		else if(firstLock) {			
			lockTimer = setInterval(function() {
				\$fl.post('$lock', { hash: '$hash', lockid: lockid }, function(r) {
					// Kill SSE as AJAX winning
					if(lockSource) {
						lockSource.close();
						lockSource = null;
					}
					
					notifyLock(r);
				});
			}, 5000);
			
			if(window.EventSource) {
				lockSource = new EventSource('$lock$hash/' + lockid + '/');
				lockSource.addEventListener('message', function(e) {
				
					// Kill the lock timer as SSE working
					if(lockTimer) {
						clearInterval(lockTimer);
						lockTimer = false;
					}

					notifyLock(e.data);
				});
			}
		}
		firstLock = false;
	}
	
	\$fl.post('$lock', { hash: '$hash', lockid: lockid }, notifyLock);
");
		}

		$this->db->query('UNLOCK TABLES');

		// If locked by someone else
		if($lockData != null) {

			$lockedAt = new flDate($lockData['lockedat'] . ' UTC', $this->user->getTimezone());

			$uMgr = clearFusionCMS::getUserMgr();
			$info = $uMgr->getInfo($lockData['uid']);
			$lastAccess = new flDate($info['lastaccess'] . ' UTC', $this->user->getTimezone());

			$form = new flForm($this->request, 'itemlocked', $this->request->getSelf());

			$userName = new flFormElementText($form, 'itemlockedby', $info['username']);
			$userName->setLabel(__('Locked By', 'cfBackend'))
				->setReadonly();

			$userEmail = new flFormElementText($form, 'itemlockedemail', $info['email']);
			$userEmail->setLabel(__('Users Email Address', 'cfBackend'))
				->setReadonly();

			$userTime = new flFormElementText($form, 'itemlockedaccess', $lastAccess);
			$userTime->setLabel(__('Time User Last Accessed the System', 'cfBackend'))
				->setReadonly();

			$lockTime = new flFormElementText($form, 'itemlockedtime', $lockedAt);
			$lockTime->setLabel(__('Item Locked At', 'cfBackend'))
				->setReadonly();

			$tryAgain = new flFormElementSubmit($form, 'tryagain');
			$tryAgain->setCaption(__('Try Again', 'cfBackend'))->setCSS('primary');

			$form->groupElements('lockinfo', array($userName, $userEmail, $userTime, $lockTime))
				->setGroupLabel('lockinfo', __('Lock Information', 'cfBackend'))
				->groupElements('submit', array($tryAgain), new flFormDecoratorList(new flFormDecoratorDl()));

			if($this->user->hasPermission('cfContent.forceunlock')) {
				$unlock = new flFormElementSubmit($form, 'unlock');
				$unlock->setCaption(__('Forcibly Unlock Item', 'cfBackend'))
					->setDescription($this->alertMessage('Warning', __('Forcing unlocking of the item may result in data loss if both you and the original owner continue to edit the item, use with caution.', 'cfBackend')))
					->setCSS('danger');

				$form->groupElements('forceunlock', array($unlock))
					->setGroupLabel('forceunlock', __('Force Unlock', 'cfBackend'));

				if($form->isSubmitted() && $form->isValid() && $unlock->getValue()) {
					$this->db->delete('cf_objlocks', 'uid=? && modulename=? && contentid=? && sitename=?', $lockData['uid'], $module, $id, $site);
					$this->response->redirectTo($this->request->getSelf(true));
				}
			}

			return $this->renderBlock(__('Item Locked by Another User', 'cfBackend'), $form->render($this->tpl->getCodeBuffer()), __('This item is currently in use by another user and has been locked to prevent conflicting edits.', 'cfBackend'));
		}
		else
			return true;
	}

	/**
	 * Action to lock an existing object via AJAX
	 */
	function lockobjectAction() {
		if($this->request->isXmlHttpRequest()) {
			if (preg_match('/^[a-z0-9]{32}$/', $this->request->getPost('hash', '')) && preg_match('/^\d+/', $this->request->getPost('lockid', ''))) {
				$now = new flDate();
				$this->db->update('cf_objlocks', array('lockedat' => (string)$now, 'lockid' => $this->request->getPost('lockid')), 'uid=? && hash=? && (lockid=0 || lockid=?)', $this->user->getId(), $this->request->getPost('hash'), $this->request->getPost('lockid'));
				if ($this->db->affectedRows())
					echo 1;
				// If no affected rows then might have locked too fast
				else
					echo $this->db->fetchOne('SELECT COUNT(*) FROM cf_objlocks WHERE uid=? && hash=? && (lockid=0 || lockid=?)', $this->user->getId(), $this->request->getPost('hash'), $this->request->getPost('lockid'));
			} else
				echo 0;
		}
		// Server-sent events
		else {
			$hash = $this->request->getSegment(2);
			$lockid = $this->request->getSegment(3);

			if (preg_match('/^[a-z0-9]{32}$/', $hash) && preg_match('/^\d+/', $lockid)) {

				// This is needed or the browser will hang on same sessions requests
				session_write_close();

				header("Content-Type: text/event-stream");
				header('Cache-Control: no-cache');
				header('Connection: keep-alive');
				header('X-Accel-Buffering: no'); // Make work on nginx

				while (@ob_end_flush());

				$lastId = isset($_SERVER['HTTP_LAST_EVENT_ID']) ? $_SERVER['HTTP_LAST_EVENT_ID'] : 0;
				if (isset($lastId) && !empty($lastId) && is_numeric($lastId)) {
					$lastId = intval($lastId);
					$lastId++;
				}
				else {
					$lastId = 0;
				}

				$startedAt = time();
				while(!connection_aborted()) {
					// Cap connection life, the browser will auto reconnect
					if ((time() - $startedAt) > 30) {
						die();
					}

					$now = new flDate();
					$this->db->update('cf_objlocks', array('lockedat' => (string)$now, 'lockid' => $lockid), 'uid=? && hash=? && (lockid=0 || lockid=?)', $this->user->getId(), $hash, $lockid);
					if ($this->db->affectedRows())
						$msg = 1;
					// If no affected rows then might have locked too fast
					else
						$msg = $this->db->fetchOne('SELECT COUNT(*) FROM cf_objlocks WHERE uid=? && hash=? && (lockid=0 || lockid=?)', $this->user->getId(), $hash, $lockid);

					// Send to browser if lock lost
					echo "id: $lastId\n";
					echo "data: $msg\n\n";
					$lastId++;

					@ob_flush();
					@flush();

					// Wait a while and do again
					sleep(2);
				}
			}
			else
				echo 0;
		}
	}

	/**
	 * Unlock the module object from the current user.
	 *
	 * @param string $module The module name.
	 * @param int $id The ID of the object to unlock.
	 * @param bool $global Lock the object across all sites.
	 */
	function unlockObject($module, $id, $global = false) {
		$this->db->delete('cf_objlocks', 'uid=? && modulename=? && contentid=? && sitename=?', $this->user->getId(), $module, $id, $global ? '' : clearFusionCMS::getSite());
	}

	/**
	 * Add a JavaScript confirmation when the user tries to leave a page.
	 */
	function confirmLeavePage() {
		cfConfirm::leavePage($this->tpl);
	}

	/**
	 * AJAX called/Server-sent Event function to get the list of notice messages.
	 */
	function getnoticesAction() {

		// Don't destroy session flash data
		clearFusionCMS::getSession()->flashKeepAll();

		// If not AJAX then using Server-sent events
		if(!$this->request->isXmlHttpRequest()) {
			// This is needed or the browser will hang on same sessions requests
			session_write_close();

			header("Content-Type: text/event-stream");
			header('Cache-Control: no-cache');
			header('Connection: keep-alive');
			header('X-Accel-Buffering: no'); // Make work on nginx

			while (@ob_end_flush());

			$lastId = isset($_SERVER['HTTP_LAST_EVENT_ID']) ? $_SERVER['HTTP_LAST_EVENT_ID'] : 0;
			if (isset($lastId) && !empty($lastId) && is_numeric($lastId)) {
				$lastId = intval($lastId);
				$lastId++;
			}
			else {
				$lastId = 0;
			}

			$startedAt = time();
			while(!connection_aborted()) {
				// Cap connection life, the browser will auto reconnect
				if((time() - $startedAt) > 30) {
					die();
				}

				// Query for notices
				$evt = new cfNoticesEvent();
				clearFusionCMS::getEventManager()->notify($evt);

				// Build the message
				$msg = json_encode(array(
					'hasNotices'  => $evt->hasMessages(),
					'notices'     => $evt->getList(),
					'noticeCount' => $evt->countMessages()
				));

				// Send to browser
				echo "id: $lastId\n";
				echo "data: $msg\n\n";
				$lastId++;

				@ob_flush();
				@flush();

				// Wait a while and do again
				sleep(2);
			}
		}
		// AJAX Request
		else {
			// Query for notices
			$evt = new cfNoticesEvent();
			clearFusionCMS::getEventManager()->notify($evt);

			// Send AJAX response
			$this->response->setContentType('application/json')
				->noCache()
				->setBody(json_encode(array(
					'hasNotices'  => $evt->hasMessages(),
					'notices'     => $evt->getList(),
					'noticeCount' => $evt->countMessages()
				)))
				->sendResponse();
		}
	}
}
