<?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 -2016 clearFusionCMS. All rights reserved.
 * @link http://clearfusioncms.com
 */

/**
 * cfContent Class
 *
 * Class to manage and access content in the database.
 *
 * @package clearFusionCMS
 */
class cfContent extends flDbNestedSet {

	/**
	 * The site configuration.
	 *
	 * @var flConfig
	 */
	protected $config;

	/**
	 * Map of content types to file extensions.
	 *
	 * @var array
	 */
	protected $extCache = array();

	/**
	 * The site name.
	 *
	 * @var string
	 */
	protected $site;

	/**
	 * Cache of module container lookups.
	 *
	 * @var array
	 */
	protected $containerCache = array();

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

	/**
	 * Tracks if the system is in preview mode.
	 * @var bool
	 */
	static protected $inPreview = false;

	/**
	 * Construct the content object.
	 *
	 * @param flDb $db The database object to use.
	 * @param string $site The name of the site to operate on.
	 */
	function __construct($db, $site) {
		parent::__construct($db, "cf_content_$site");
		$this->site = $site;
		$this->config = new flConfigDb('cfSystemSite', $db, "cf_config_$site");
		$this->cache = clearFusionCMS::getCache();
	}

	/**
	 * Get if the system is in preview mode.
	 *
	 * @return bool true if in preview mode; else false.
	 */
	static function isPreview() {
		return self::$inPreview;
	}

	/**
	 * Put content into preview mode.
	 */
	static function setPreview() {
		self::$inPreview = true;
	}

	/**
	 * Test if a node has a child with the given slug.
	 *
	 * @param int $id The parent node to test.
	 * @param string $slug The slug to look for.
	 * @return bool true if has a child with the given slug; else false.
	 */
	function hasChildSlug($id, $slug) {
		return $this->db->fetchOne("SELECT COUNT(*) FROM $this->table WHERE slug=? && parentid=?", $slug, $id) != 0;
	}

	/**
	 * Get the permissions required to edit the document.
	 *
	 * @param int $id ID of the document to get the permissions for.
	 * @return array Array of all required permissions.
	 */
	function getEditPermissions($id) {
		// Get the permissions required to access the page
		$perms = array();
		$result = $this->db->query("SELECT DISTINCT p.editperm FROM $this->table AS c,$this->table AS p WHERE c.lft BETWEEN p.lft AND p.rgt && c.id=?", $id);
		while($row = $result->fetch()) {
			if(!empty($row['editperm']))
				$perms[] = $row['editperm'];
		}
		return $perms;
	}

	/**
	 * Get the default page to use.
	 *
	 * @return int The ID of the default page.
	 */
	function getDefaultPage() {
		return $this->config->siteDefault;
	}

	/**
	 * Get the ID of the error page.
	 *
	 * @param int $code The error code.
	 * @return int The ID of the error page or 0 if none set.
	 */
	function getErrorPage($code) {
		switch($code) {
			case 404: $r = $this->config->site404; break;
			case 403: $r = $this->config->site403; break;
			default: $r = $this->config->get('siteError', 0); break;
		}
		return $r;
	}

	/**
	 * Get the theme to use for the site.
	 *
	 * @return string The theme to use.
	 */
	function getTheme() {
		return $this->config->siteTheme;
	}

	/**
	 * Get the sub theme to use for the site.
	 *
	 * @return string The child theme to use.
	 */
	function getChildTheme() {
		return $this->config->get('siteChildTheme', '');
	}

	/**
	 * Get the CSS framework used by the site theme.
	 *
	 * @return string The CSS Framework used by the theme.
	 */
	function getCSSFramework() {
		return $this->config->get('siteThemeCSSFramework', '');
	}

	/**
	 * Get the ID of the document that is the container for a module.
	 *
	 * @param string $module The module to get the container document of.
	 * @return int The document ID or false if no container.
	 */
	function getContainer($module) {
		if(!isset($this->containerCache[$module]))
			$this->containerCache[$module] = $this->db->fetchOne("SELECT id FROM $this->table WHERE " . (self::$inPreview ? '' : 'published && ') . 'container=?', $module);
		return $this->containerCache[$module];
	}

	/**
	 * Get the URI of a content page.
	 *
	 * @param int $id ID of the content page to get the URI of.
	 * @param bool $absolute true if an absolute path is required; else false.
	 * @param bool $secure true for https, false for http, null for as per request.
	 * @return string The URI of the page, or false if the page is unknown.
	 */
	function getPageURI($id, $absolute = false, $secure = null) {
		if(($uri = $this->getPagePath($id)) === false)
			return false;

		$uri = $uri[0] . (empty($uri[0]) ? '' : (empty($uri[1]) ? '/' : $uri[1]));
		return (empty($uri) ? clearFusionCMS::getRequest()->getBasePath($absolute, $secure) : clearFusionCMS::getRequest()->getBaseUri($absolute, $secure)) . $uri;
	}

	/**
	 * Get the router object for a container page.
	 *
	 * @param int $id ID of the content page acting as a container.
	 * @param string $route The route string to match, exact/:var/*
	 * @param array $defaults Array of defaults.
	 * @param array $varReq Array of variables and the regular expressions they must match.
	 * @param string $ext Optional extension to use for matching rather than pages.
	 * @return flRouterRoute The route object.
	 */
	function getContainerRoute($id, $route, $defaults = array(), $varReq = array(), $ext = null) {
		if(($uri = $this->getPagePath($id, true)) === false)
			return false;
		return new flRouterRoute(($route != '' ? "$uri[0]/$route" : $uri[0]) . ($ext === null ? $uri[1] : $ext), $defaults, $varReq, clearFusionCMS::getRequest());
	}

	/**
	 * Get the path to a page.
	 *
	 * @param int $id ID of the content page containing the virtual pages.
	 * @param bool $full true if the full path should be returned or false to shorten the default page URI.
	 * @return array The path to the page and extension, or false if the page is unknown.
	 */
	protected function getPagePath($id, $full = false) {
		$path = ($this->pathCache[$id] = $this->getPath($id, array('id', 'slug', 'contenttype')));
		$uri = $ext = '';

		if(!($segments = count($path)))
			return false;
		else if($segments > 1 || $path[0]['id'] != $this->config->siteDefault || $full) {
			for($i=0;$i<$segments-1;$i++)
				$uri .= $path[$i]['slug'] . '/';
			$uri .= $path[$i]['slug'];

			if(!isset($this->extCache[$path[$i]['contenttype']])) {
				$ext = $this->db->fetchOne('SELECT fileext FROM cf_contenttype WHERE id=?', $path[$i]['contenttype']);
				$ext = ($this->extCache[$path[$i]['contenttype']] = empty($ext) ? '' : '.' . $ext);
			}
			else
				$ext = $this->extCache[$path[$i]['contenttype']];
		}

		return array($uri, $ext);
	}

	/**
	 * Get the permissions required to view the document.
	 *
	 * @param int $id The ID of the document to get the permissions for.
	 * @return array Array of access permissions.
	 * @throws flDbException
	 */
	function getViewPermissions($id) {
		// Get the permissions required to access the page
		$perms = array();
		$result = $this->db->query("SELECT DISTINCT p.accessperm FROM $this->table AS c,$this->table AS p WHERE c.lft BETWEEN p.lft AND p.rgt && c.id=?", $id);
		while($row = $result->fetch()) {
			if(!empty($row['accessperm']))
				$perms[] = $row['accessperm'];
		}
		return $perms;
	}

	/**
	 * Get the page data referenced by a path.
	 *
	 * @param array $segments Path segments to the page or ID of page to get.
	 * @param string $extension The file extension, used only if $segments is an array.
	 * @return array Array of page data or false if the page is not found.
	 */
	function getPage($segments, $extension = '') {
		$cacheKey = '_cms_contentPage_raw_' . json_encode(func_get_args()) . $extension;

		if(self::$inPreview || !($data = $this->cache->get($cacheKey))) {
			if(is_array($segments))
				$idx = count($segments) - 1;
			else
				$idx = -2;

			$elems = array('id', 'contenttype', 'cachetime', 'published', 'publishedat', 'lastupdated', 'restype', 'asattachment', 'template', 'container', 'title', 'subtitle', 'meta_title', 'meta_description', 'meta_keywords', 'summary', 'content', 'ogtitle', 'ogdesc', 'twittertitle', 'twitterdesc', 'ogimageid', 'twitterimageid');
			if(self::$inPreview)
				$elems[] = 'draft';

			// If no path then try the page by ID or default page
			if($idx < 0) {
				if(is_array($data = $this->getNodeData($idx == -2 ? $segments : $this->config->siteDefault, $elems)))
					$data['childpath'] = array();
			} // Query for the document based on the URI
			else {
				// If not an exact match but page is acting as a page not a container then not found.
				if(($data = $this->getByPath($segments, 'slug', $elems, false)) && count($data['childpath']) && empty($data['container']))
					$data = null;
			}

			// If failed to get a page
			if(!$data)
				return false;

			// If preview & draft present use it
			if(self::$inPreview && !empty($data['draft'])) {
				$d = unserialize($data['draft']);
				array_pop($elems);
				foreach($elems as $e) {
					if(isset($d[$e]))
						$data[$e] = $d[$e];
				}
			}

			// Get the permissions required to access the page
			$data['permissions'] = $this->getViewPermissions($data['id']);

			// Get custom fields
			$data['customfields'] = $this->getCustomFields($data['id']);

			// Check the extension/content type
			if(!($contentType = $this->db->fetchRow('SELECT gzip,mimetype,fileext,snippet AS contenttype_snippet FROM cf_contenttype WHERE id=?', $data['contenttype'])))
				return false;

			$data = $data + $contentType;
			$data['page_extension'] = $idx >= 0 ? $extension : $contentType['fileext'];
			$data['page_extensionmatch'] = $idx < 0 || $contentType['fileext'] == $extension;
			$data['iscontainer'] = !empty($data['container']);

			if(!self::$inPreview) {
				$this->cache->store($cacheKey, $data);
			}
		}

		return $data;
	}

	/**
	 * Get the custom fields for the node.
	 *
	 * @param int $id The ID of the node to get the custom fields for.
	 * @return The array of placeholders and their values.
	 * @throws flDbException
	 */
	function getCustomFields($id) {
		$fields = array();
		$result = $this->db->query('SELECT fc.placeholder,fc.content,cf.snippet FROM cf_customflds_ctnt_' . $this->site . ' AS fc
LEFT JOIN cf_customflds_templates AS ft USING(placeholder)
LEFT JOIN cf_customflds AS cf USING(placeholder)
LEFT JOIN cf_content_' . $this->site . ' AS c USING(id)
WHERE id=? && c.template=ft.template', $id);
		while($row = $result->fetch()) {
			$fields[$row['placeholder']] = cfContentFilter::apply($row['snippet'], $row['content']);
		}

		return $fields;
	}

	/**
	 * Get menus items for the given node.
	 *
	 * @param int $id The ID of the node to get the items for.
	 * @param array $include Array of page id's to include or null.
	 * @param array $exclude Array of page id's to exclude or null.
	 * @param string $sortBy The field to sort by, lft for menu order.
	 * @param string $sortOrder The order to sort by, ASC or DESC.
	 * @param int $limit The number of results to limit to or 0 for unlimited.
	 * @param bool $ignoreHide true if to ignore menu hide status.
	 * @param string $customFields Comma separated list of custom fields.
	 * @return array The fields fetched from the child nodes.
	 */
	function getMenuItems($id, $include, $exclude, $sortBy, $sortOrder, $limit, $ignoreHide, $customFields) {
		$params[] = $id;
		if($include && count($include)) {
			$ex = '&& id IN(' . str_repeat('?,', count($include) - 1) . '?)';
			$params = array_merge($params, $include);
		}
		else if($exclude && count($exclude)) {
			$ex = '&& id NOT IN(' . str_repeat('?,', count($exclude) - 1) . '?)';
			$params = array_merge($params, $exclude);
		}
		else
			$ex = '';

		$limit = $limit ? " LIMIT $limit" : '';
		$menuhide = $ignoreHide ? '' : '&& !menuhide';

		if(empty($customFields))
			return $this->db->fetchAll("SELECT id,title,slug,subtitle,IF(menutitle = '', title, menutitle) AS menutitle,summary,restype FROM $this->table WHERE parentid=? $ex && published $menuhide ORDER BY $sortBy $sortOrder $limit", $params);
		else {
			// Get the fields
			$flds = explode(',', $customFields);
			$fldQ = str_repeat(',?', count($flds) - 1);
			array_unshift($flds, '0');
			array_unshift($flds, '0');

			$data = array();
			$result = $this->db->query("SELECT id,title,slug,subtitle,IF(menutitle = '', title, menutitle) AS menutitle,summary,restype,template FROM $this->table WHERE parentid=? $ex && published $menuhide ORDER BY $sortBy $sortOrder $limit", $params);
			while($row = $result->fetch()) {
				// Get custom fields
				$flds[0] = $row['id'];
				$flds[1] = $row['template'];
				$r2 = $this->db->query('SELECT placeholder,content FROM cf_customflds_ctnt_' . $this->site . " LEFT JOIN cf_customflds_templates USING(placeholder) WHERE id=? && template=? && placeholder IN(?$fldQ)", $flds);
				while($r = $r2->fetch())
					$row[$r['placeholder']] = $r['content'];
				$data[] = $row;
			}
			return $data;
		}
	}

	/**
	 * Render content page.
	 *
	 * @param cfTemplateEngine $tpl The template engine to use.
	 * @param array $data Array of page data elements.
	 * @param string $cacheIDExt Additional information to add to the cache ID.
	 * @return bool true if the page was output; else false.
	 * @throws flExceptionNotFound
	 */
	function renderPage($tpl, $data, $cacheIDExt = '') {
		if(!$data['page_extensionmatch'] && $data['container'] != '#')
			throw new flExceptionNotFound();

		$response = clearFusionCMS::getResponse();
		$request = clearFusionCMS::getRequest();

		$response->setContentType($data['mimetype'])
			->compressOutput($data['gzip'])
			->setHeader('Content-Disposition', ($data['asattachment'] ? 'attachment' : 'inline') . '; filename=' . basename($request->getSelf()));

		if($data['restype'] == 'doc') {
			clearFusionCMS::getEventManager()->notify(new flEvent('system.beforePageRender', array('templateEngine' => $tpl)));
			$tz = clearFusionCMS::getTimezone();

			if(!($pageData = $this->cache->get($cacheID = '_cms_contentPage' . $request->getSelf()))) {

				// If content type has a processor then run it
				if(!empty($data['contenttype_snippet'])) {
					$data['content'] = $tpl->runSnippet($data['contenttype_snippet'], array(
						'content' => $data['content']
					));
				}

				$pageData = array_merge(array(
					'id' => $data['id'],
					'title' => $data['title'],
					'subtitle' => $data['subtitle'],
					'meta_title' => empty($data['meta_title']) ? $data['title'] : $data['meta_title'],
					'meta_description' => $data['meta_description'],
					'meta_keywords' => $data['meta_keywords'],
					'summary' => $data['summary'],
					'content' => $data['content'],
					'lastupdated' => new flDate($data['lastupdated'] . ' UTC', $tz),
					'publishedat' => new flDate($data['publishedat'] . ' UTC', $tz),
					'siteuri' => $request->getBasePath(true),
					'sitename' => clearFusionCMS::getSiteName(),
					'sitelang' => fusionLib::getLocale(true),
					'module' => $data['module'],
					'module_extra' => $data['container_extra']
				), $data['customfields']);

				// Build the OG data
				$ogTitle = empty($data['ogtitle']) ? (empty($data['meta_title']) ? $data['title'] : $data['meta_title']) : $data['ogtitle'];
				$ogDesc = empty($data['ogdesc']) ? (empty($data['meta_description']) ? $data['summary'] : $data['meta_description']) : $data['ogdesc'];

				$imgs = '';
				if($data['ogimageid'] || $data['twitterimageid']) {
					$cfg = clearFusionCMS::getSiteConfig('cfSystemFileStore');

					$path = $request->getBasePath(true) . $cfg->publicStorage . '/content_social/' . clearFusionCMS::getSite() . '/';

					$result = $this->db->fetchAll('SELECT imageid,ext FROM cf_socialimages_' . clearFusionCMS::getSite() . ' WHERE imageid IN(?,?)', $data['ogimageid'], $data['twitterimageid']);
					$oid = array();
					foreach($result as $r) {
						$oid[$r['imageid']] = $r['ext'];
					}

					if(isset($oid[$data['ogimageid']])) {
						$idHash = md5($data['ogimageid']);
						$file = $path . $idHash[0] . '/' . $idHash[1] . '/' . $data['ogimageid'] . $oid[$data['ogimageid']];
						$imgs .= "<meta property=\"og:image\" content=\"$file\" />\n";
					}

					if(isset($oid[$data['twitterimageid']])) {
						$idHash = md5($data['twitterimageid']);
						$file = $path . $idHash[0] . '/' . $idHash[1] . '/' . $data['twitterimageid'] . $oid[$data['twitterimageid']];
						$imgs .= "<meta name=\"twitter:image\" content=\"$file\" />\n";
					}
				}

				$pageData['_content.openGraphMeta'] = $tpl->parseChunk('content.openGraphMeta', array(
					'locale' => fusionLib::getLocale(),
					'ogtitle' => $ogTitle,
					'ogdesc' => $ogDesc,
					'site' => clearFusionCMS::getSiteName(),
					'url' => $request->getSelf(true),
					'images' => $imgs,
					'twitter.site' => clearFusionCMS::getSiteTwitter(),
					'twitter.title' => empty($data['twittertitle']) ? $ogTitle : $data['twittertitle'],
					'twitter.desc' => empty($data['twitterdesc']) ? $ogDesc : $data['twitterdesc']
				));

				$this->cache->store($cacheID, $pageData);
			}

			// Build the OG meta data chunk if not already defined
			if(!$tpl->chunkLoaded('openGraphMeta')) {
				$tpl->registerChunk('openGraphMeta', $pageData['_content.openGraphMeta']);
			}

			$response->setBody($tpl->parseDocument($data['template'], $request->getSelf(true) . $cacheIDExt, $pageData, $data['cachetime']))->sendResponse();

			return true;
		}
		else if($data['restype'] == 'link') {
			$uri = $data['content'];
			if(preg_match('/\[\[(.+)\]\]/', $uri))
				$uri = $tpl->parseString($uri);
			$response->redirectTo($uri, $data['asattachment']);
			return true;
		}
		else if($data['restype'] == 'static') {
			if(!is_readable($data['content']))
				throw new flExceptionNotFound();

			if(!$data['gzip'])
				$response->setHeader('Content-Length', filesize($data['content']));

			$response->setLastModified(filemtime($data['content']))
				->sendHeaders();

			readfile($data['content']);
			return true;
		}
		else
			return false;
	}
}
