<?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
 */

/**
 * cfCustomFieldList Class
 *
 * Class to manage a list of custom fields.
 *
 * @package clearFusionCMS
 */
class cfCustomFieldList {
	/**
	 * Array of all fields added to the form.
	 *
	 * @var array
	 */
	public $fields = array();

	/**
	 * Fields not grouped into a category.
	 *
	 * @var array
	 */
	public $ungrouped = array();

	/**
	 * Field group names by id.
	 *
	 * @var array
	 */
	public $groupNames = array();

	/**
	 * Fields in group by id.
	 *
	 * @var array
	 */
	public $groupFields = array();

	/**
	 * Flags of group before content.
	 * @var bool
	 */
	public $before;

	/**
	 * Create form groups for elements.
	 *
	 * @param flForm $form The form to group the elements on.
	 */
	function createGroups($form) {
		$key = 'content_fields' . ($this->before ? 'before' : 'after') . '_';
		foreach($this->groupFields as $catID => $elems) {
			$form->groupElements($key . $catID, $elems)
				->setGroupLabel($key . $catID, $this->groupNames[$catID]);
		}
	}
}

/**
 * cfCustomFields Class
 *
 * Class to create, manage and use custom fields.
 *
 * @package clearFusionCMS
 */
class cfCustomFields {

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

	/**
	 * Cache of category ids to names.
	 *
	 * @var array
	 */
	protected $catCache = null;

	/**
	 * Create the object.
	 *
	 * @param flDb $db The database to use.
	 */
	function __construct($db) {
		$this->db = $db;
	}

	/**
	 * Get the count of custom fields.
	 *
	 * @param int $catId If given is limits the count to a category.
	 * @return int The number of custom fields.
	 */
	function getCount($catId = null) {
		if($catId !== null)
			return $this->db->fetchOne('SELECT COUNT(*) FROM cf_customflds WHERE catid=?', $catId);
		else
			return $this->db->fetchOne('SELECT COUNT(*) FROM cf_customflds');
	}

	/**
	 * Get the list of fields that are in a category.
	 *
	 * @param int $catId The category id.
	 * @return array Array of fields in the category.
	 */
	function getFieldsInCat($catId) {
		$r = array();
		$result = $this->db->query('SELECT placeholder,caption FROM cf_customflds WHERE catid=? ORDER BY caption', $catId);
		while($row = $result->fetch())
			$r[$row['placeholder']] = $row['caption'];
		return $r;
	}

	/**
	 * Get the list of fields.
	 *
	 * @return array Array of fields.
	 */
	function getFields() {
		$r = array();
		$result = $this->db->query('SELECT placeholder,caption FROM cf_customflds ORDER BY caption');
		while($row = $result->fetch())
			$r[$row['placeholder']] = $row['caption'];
		return $r;
	}

	/**
	 * Moves all fields from the deleted category to the root.
	 *
	 * @param int $catId ID of the category being deleted.
	 */
	function categoryDeleted($catId) {
		$this->db->query('UPDATE cf_customflds SET catid=0 WHERE catid=?', $catId);
	}

	/**
	 * Removes all field to template mappings when a template is deleted.
	 *
	 * @param string $tpl The template that was deleted.
	 */
	function templateDeleted($tpl) {
		$this->db->delete('cf_customflds_templates', 'template=?', $tpl);
	}

	/**
	 * Get the data for a field.
	 *
	 * @param string $id ID of the field to get.
	 * @return array Associative array of data or false if failed to read.
	 */
	function getField($id) {
		$templates = array();
		$result = $this->db->query('SELECT template FROM cf_customflds_templates WHERE placeholder=?', $id);
		while($row = $result->fetch())
			$templates[] = $row['template'];
		if(!($r = $this->db->fetchRow('SELECT placeholder,fldtype,caption,description,editperm,validation,catid,disporder,ignoregroup,editor,snippet FROM cf_customflds WHERE placeholder=?', $id)))
			return false;
		$r['templates'] = $templates;
		$r['validation'] = unserialize($r['validation']);
		return $r;
	}

	/**
	 * Create a field.
	 *
	 * @param string $name The name of the field.
	 * @param string $type The field type.
	 * @param string $caption The field caption.
	 * @param string $description The description of the field.
	 * @param string $edPerm The editor permission required if any.
	 * @param int $catId The category to add the element to.
	 * @param int $dispOrder The display order.
	 * @param array $templates Array of templates the field is assigned to.
	 * @param array $validation Array of validation data.
	 * @param bool $ignoregroup true if display grouping should be ignored; else false.
	 * @param string $editor The editor to use for the field.
	 * @param string $snippet The pre-processor snippet to use.
	 * @return bool true if created; or false if failed.
	 */
	function createField($name, $type, $caption, $description, $edPerm, $catId, $dispOrder, $templates, $validation, $ignoregroup, $editor = '', $snippet = '') {
		if($this->db->insert('cf_customflds', array(
			'placeholder' => $name,
			'fldtype' => $type,
			'caption' => $caption,
			'description' => $description,
			'editperm' => $edPerm,
			'catid' => $catId,
			'disporder' => $dispOrder,
			'validation' => serialize($validation),
			'ignoregroup' => $ignoregroup,
			'editor' => in_array($type, array('textarea', 'textareasmall', 'htmlarea', 'htmlareasmall')) ? $editor : '',
			'snippet' => $snippet
		))) {
			foreach($templates as $tplname)
				$this->db->insert('cf_customflds_templates', array('template' => $tplname, 'placeholder' => $name));
			return true;
		}
		return false;
	}

	/**
	 * Update a field.
	 *
	 * @param string $id The id of the field to update.
	 * @param string $name The name of the field.
	 * @param string $type The field type.
	 * @param string $caption The field caption.
	 * @param string $description The description of the field.
	 * @param string $edPerm The editor permission required if any.
	 * @param int $catId The category to add the element to.
	 * @param int $dispOrder The display order.
	 * @param array $templates Array of templates the field is assigned to.
	 * @param array $validation Array of validation data.
	 * @param bool true if display grouping should be ignored; else false.
	 * @param string $editor The editor to use for the field.
	 * @param string $snippet The pre-processor snippet to use.
	 * @return bool true if updated; or false if failed.
	 */
	function updateField($id, $name, $type, $caption, $description, $edPerm, $catId, $dispOrder, $templates, $validation, $ignoregroup, $editor = '', $snippet = '') {
		if($this->db->update('cf_customflds', array(
			'placeholder' => $name,
			'fldtype' => $type,
			'caption' => $caption,
			'description' => $description,
			'editperm' => $edPerm,
			'catid' => $catId,
			'disporder' => $dispOrder,
			'validation' => serialize($validation),
			'ignoregroup' => $ignoregroup,
			'editor' => in_array($type, array('textarea', 'textareasmall', 'htmlarea', 'htmlareasmall')) ? $editor : '',
			'snippet' => $snippet
		), 'placeholder=?', $id)) {

			$mgr = new cfSiteManager($this->db);
			$sites = $mgr->listSites();

			// If name changed
			if($id != $name) {
				// Update template map
				$this->db->update('cf_customflds_templates', array('placeholder' => $name), 'placeholder=?', $id);

				// Update placeholder name for all sites
				foreach($sites as $site => $desc)
					$this->db->update('cf_customflds_ctnt_' . $site, array('placeholder' => $name), 'placeholder=?', $id);
			}

			// Get the old template list
			$old = array();
			$result = $this->db->query('SELECT template FROM cf_customflds_templates WHERE placeholder=?', $name);
			while($row = $result->fetch())
				$old[] = $row['template'];

			// Find the deleted templates
			$toDelete = array_diff($old, $templates);

			// Find the new templates
			$toAdd = array_diff($templates, $old);

			// Delete the removed templates
			foreach($toDelete as $t) {
				$this->db->delete('cf_customflds_templates', 'template=? && placeholder=?', $t, $name);

				// Delete fields from all content of all sites
				foreach($sites as $site => $desc)
					$this->db->query("DELETE f FROM cf_customflds_ctnt_{$site} AS f LEFT JOIN cf_content_{$site} AS c USING(id) WHERE template=? && placeholder=?", $t, $name);
			}

			// Add new field to template mappings
			foreach($toAdd as $t)
				$this->db->insert('cf_customflds_templates', array('template' => $t, 'placeholder' => $name));

			return true;
		}
		return false;
	}

	/**
	 * Delete a field and all field data.
	 *
	 * @param string $id ID of the field to delete.
	 */
	function deleteField($id) {
		$this->db->delete('cf_customflds', 'placeholder=?', $id);
		$this->db->delete('cf_customflds_templates', 'placeholder=?', $id);

		$mgr = new cfSiteManager($this->db);
		$sites = $mgr->listSites();
		foreach($sites as $name => $desc)
			$this->db->delete('cf_customflds_ctnt_' . $name, 'placeholder=?', $id);
	}

	/**
	 * Get an array of supported field types and their descriptions
	 *
	 * @return array The available field types.
	 */
	function getTypes() {
		return array(
			'integer' => __('Integer', 'cfBackend'),
			'textarea' => __('Multi-line Textarea', 'cfBackend'),
			'textareasmall' => __('Multi-line Textarea (Small)', 'cfBackend'),
			'number' => __('Number', 'cfBackend'),
			'text' => __('Text', 'cfBackend'),
			'uri' => __('Web Site Address', 'cfBackend'),
			'htmlarea' => __('WYSIWYG HTML Editor', 'cfBackend'),
			'htmlareasmall' => __('WYSIWYG HTML Editor (Small)', 'cfBackend'),
			'checkbox' => __('Checkbox', 'cfBackend')
		);
	}

	/**
	 * Build the field list and add the fields to the form.
	 *
	 * @param flForm $form The form to add the fields to.
	 * @param string $template The name of the template being used.
	 * @param array $editorData Data for use in the editor element.
	 * @param array $data The existing data to edit.
	 * @param bool $before true if drawing fields before content box or false for those after.
	 * @return cfCustomFieldList The fields.
	 */
	function addToForm($form, $template, $editorData, $data, $before) {

		// Load the category list
		if($this->catCache === null) {
			$this->catCache = array();
			$result = $this->db->query('SELECT catid,catname FROM fl_categories');
			while($row = $result->fetch())
				$this->catCache[$row['catid']] = $row['catname'];
		}

		$list = new cfCustomFieldList();
		$list->before = $before;

		$fl = $this->db->fetchAll('SELECT placeholder,fldtype,caption,description,editperm,validation,catid,ignoregroup,editor FROM cf_customflds LEFT JOIN cf_customflds_templates USING(placeholder) WHERE template=? && disporder' . ($before ? '<0' : '>=0') . ' ORDER BY disporder', $template);

		foreach($fl as $field) {
			if(clearFusionCMS::getUser()->hasPermission($field['editperm'])) {
				$default = isset($data[$field['placeholder']]) ? $data[$field['placeholder']] : '';

				$isText = false;
				switch($field['fldtype']) {
					case 'textarea':
					case 'textareasmall':
						$isText = true;
						$e = cfEditor::create(
							empty($field['editor']) ? $editorData['moduleCode'] : $field['editor'],
							$form,
							'fld_' . $field['placeholder'],
							$default,
							$field['fldtype'] == 'textareasmall'
						);
						break;

					case 'text':
						$isText = true;
						$e = new flFormElementText($form, 'fld_' . $field['placeholder'], $default);
						break;

					case 'number':
						$e = new flFormElementText($form, 'fld_' . $field['placeholder'], $default);
						$e->addValidator(new flFormValidatorNumber());
						break;

					case 'integer':
						$e = new flFormElementText($form, 'fld_' . $field['placeholder'], $default);
						$e->addValidator(new flFormValidatorInteger());
						break;

					case 'uri':
						$e = new flFormElementText($form, 'fld_' . $field['placeholder'], $default);
						$e->addValidator(new flFormValidatorUri());
						break;

					case 'htmlarea':
					case 'htmlareasmall':
						$isText = true;
						$e = cfEditor::create(
							empty($field['editor']) ? $editorData['module'] : $field['editor'],
							$form,
							'fld_' . $field['placeholder'],
							$default,
							$field['fldtype'] == 'htmlareasmall',
							$editorData['tags']
						);

						break;

					case 'checkbox':
						$e = new flFormElementCheckbox($form, 'fld_' . $field['placeholder'], $default);
						break;
				}

				$e->setLabel($field['caption'])
					->setDescription(str_replace(array('[', ']'), array('&#91;', '&#93;'), $field['description'] . " [[+$field[placeholder]]]"));

				// Add validation
				$v = unserialize($field['validation']);
				if(isset($v['required']) && $v['required'])
					$e->setRequired()->addValidator(new flFormValidatorRequired());
				if(isset($v['min']) && $v['min'])
					$e->addValidator($isText ? new flFormValidatorMinLength($v['min']) : new flFormValidatorMin($v['min']));
				if(isset($v['max']) && $v['max'])
					$e->addValidator($isText ? new flFormValidatorMaxLength($v['max']) : new flFormValidatorMax($v['max']));

				$list->fields[$field['placeholder']] = $e;
				if($field['catid'] && !$field['ignoregroup'])
					$list->groupFields[$field['catid']][] = $e;
				else
					$list->ungrouped[] = $e;
				$list->groupNames = $this->catCache;
			}
		}

		return $list;
	}

	/**
	 * Get the list of fields active on a template.
	 *
	 * @param string $template The name of the template.
	 * @return array The list of placeholders.
	 */
	function getTemplateFields($template) {
		$placeholder = array();
		$result = $this->db->query('SELECT placeholder FROM cf_customflds_templates WHERE template=?', $template);
		while($row = $result->fetch())
			$placeholder[] = $row['placeholder'];
		return $placeholder;
	}

	/**
	 * Store the field data.
	 *
	 * @param int $docId The document to store the fields for.
	 * @param string $activeSite The active site name.
	 * @param array $fields Array of fields and their data.
	 */
	function storeFields($docId, $activeSite, $fields) {
		$this->db->delete('cf_customflds_ctnt_' . $activeSite, 'id=?', $docId);
		foreach($fields as $k => $v) {
			$this->db->replace('cf_customflds_ctnt_' . $activeSite, array(
				'id' => $docId,
				'placeholder' => $k,
				'content' => $v
			));
		}
	}

	/**
	 * Load the field data.
	 *
	 * @param int $docId The document to load the fields for.
	 * @param string $activeSite The active site name.
	 * @return array Array of fields and their data.
	 */
	function loadFields($docId, $activeSite) {
		$data = array();
		$result = $this->db->query('SELECT placeholder,content FROM cf_customflds_ctnt_' . $activeSite . ' WHERE id=?', $docId);
		while($row = $result->fetch())
			$data[$row['placeholder']] = $row['content'];
		return $data;
	}

	/**
	 * Clean up of deleted documents.
	 *
	 * @param string $activeSite The active site name.
	 */
	function docDeleted($activeSite) {
		// Delete all the fields for documents that don't exist in the content table
		$this->db->query("DELETE f FROM cf_customflds_ctnt_{$activeSite} AS f LEFT JOIN cf_content_{$activeSite} AS c ON(c.id = f.id) WHERE c.id IS NULL");
	}

	/**
	 * Delete the custom fields for a specific document.
	 *
	 * @param string $activeSite The active site name.
	 * @param int $id The ID of the document to delete the fields from.
	 */
	function docDeleteFields($activeSite, $id) {
		$this->db->delete('cf_customflds_ctnt_' . $activeSite, 'id=?', $id);
	}

	/**
	 * Track change in template name.
	 *
	 * @param string $old The old name.
	 * @param string $new The new name.
	 */
	function templateChanged($old, $new) {
		$this->db->update('cf_customflds_templates', array('template' => $new), 'template=?', $old);
	}

	/**
	 * Copy the custom fields between templates.
	 *
	 * @param string $src Source template.
	 * @param string $dst Destination template.
	 */
	function copy($src, $dst) {
		$results = $this->db->query('SELECT placeholder FROM cf_customflds_templates WHERE template=?', $src);
		while($row = $results->fetch())
			$this->db->insert('cf_customflds_templates', array('template' => $dst, 'placeholder' => $row['placeholder']));
	}
}
