<?php
/**
* @version	$Id: forms_eh.php 15065 2012-01-17 15:11:29Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

defined('FULL_PATH') or die('restricted access!');

class FormsEventHandler extends kDBEventHandler {

	/**
	 * Allows to override standard permission mapping
	 *
	 * @return void
	 * @access protected
	 * @see kEventHandler::$permMapping
	 */
	protected function mapPermissions()
	{
		parent::mapPermissions();

		$permissions = Array (
			// user can view any form on front-end
			'OnItemBuild' => Array ('self' => true),
		);

		$this->permMapping = array_merge($this->permMapping, $permissions);
	}

	function OnCreateSubmissionNodes(&$event)
	{
		if (defined('IS_INSTALL') && IS_INSTALL) {
			// skip any processing, because Forms table doesn't exists until install is finished
			return ;
		}

		$forms = $this->getForms();

		if (!$forms) {
			return ;
		}

		$form_subsection = Array(
			'parent'		=>	'in-portal:forms',
			'icon'			=>	'form_submission',
			'label'			=>	'',
			'url'			=>	Array('t' => 'submissions/submissions_list', 'pass' => 'm,form'),
			'permissions'	=>	Array('view', 'add', 'edit', 'delete'),
			'priority'		=>	1,
			'type'			=>	stTREE,
		);

		$priority = 1;
		$sections = $this->Application->getUnitOption($event->Prefix, 'Sections');

		foreach ($forms as $form_id => $form_name) {
			$this->Application->Phrases->AddCachedPhrase('form_sub_label_'.$form_id, $form_name);
			$this->Application->Phrases->AddCachedPhrase('la_description_in-portal:submissions:'.$form_id, $form_name.' Submissions');
			$form_subsection['label'] = 'form_sub_label_'.$form_id;
			$form_subsection['url']['form_id'] = $form_id;
			$form_subsection['priority'] = $priority++;
			$sections['in-portal:submissions:'.$form_id] = $form_subsection;
		}

		$this->Application->setUnitOption($event->Prefix, 'Sections', $sections);
	}

	function getForms()
	{
		$cache_key = 'forms[%FormSerial%]';
		$forms = $this->Application->getCache($cache_key);

		if ($forms === false) {
			$this->Conn->nextQueryCachable = true;
			$sql = 'SELECT Title, FormId
					FROM ' . TABLE_PREFIX . 'Forms
					ORDER BY Title ASC';
			$forms = $this->Conn->GetCol($sql, 'FormId');

			$this->Application->setCache($cache_key, $forms);
		}

		return $forms;
	}

	/**
	 * Saves content of temp table into live and
	 * redirects to event' default redirect (normally grid template)
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnSave(kEvent &$event)
	{
		parent::OnSave($event);

		if ( $event->status == kEvent::erSUCCESS ) {
			$this->OnCreateFormFields($event);
			$this->_deleteSectionCache();
		}
	}

	/**
	 * Deletes all selected items.
	 * Automatically recurse into sub-items using temp handler, and deletes sub-items
	 * by calling its Delete method if sub-item has AutoDelete set to true in its config file
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnMassDelete(kEvent &$event)
	{
		parent::OnMassDelete($event);

		if ( $event->status == kEvent::erSUCCESS ) {
			$this->_deleteSectionCache();
		}
	}

	function _deleteSectionCache()
	{
		$reset_event = new kEvent('adm:OnResetSections');
		$this->Application->HandleEvent($reset_event);

		$this->Application->StoreVar('RefreshStructureTree', 1);
	}

	/**
	 * Dynamically fills custom data config
	 *
	 * @param kEvent $event
	 */
	function OnCreateFormFields(&$event)
	{
		$cur_fields = $this->Conn->Query('DESCRIBE '.TABLE_PREFIX.'FormSubmissions', 'Field');
		$cur_fields = array_keys($cur_fields);

		// keep all fields, that are not created on the fly (includes ones, that are added during customizations)
		foreach ($cur_fields as $field_index => $field_name) {
			if (!preg_match('/^fld_[\d]+/', $field_name)) {
				unset($cur_fields[$field_index]);
			}
		}

		$desired_fields = $this->Conn->GetCol('SELECT CONCAT(\'fld_\', FormFieldId) FROM '.TABLE_PREFIX.'FormFields ORDER BY FormFieldId');

		$sql = array();

		$fields_to_add = array_diff($desired_fields, $cur_fields);
		foreach ($fields_to_add as $field) {
			$field_expression = $field.' Text NULL';
			$sql[] = 'ADD COLUMN '.$field_expression;
		}

		$fields_to_drop = array_diff($cur_fields, $desired_fields);
		foreach ($fields_to_drop as $field) {
			$sql[] = 'DROP COLUMN '.$field;
		}

		if ($sql) {
			$query = 'ALTER TABLE '.TABLE_PREFIX.'FormSubmissions '.implode(', ', $sql);
			$this->Conn->Query($query);
		}
	}

	/**
	 * Enter description here...
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnFormSubmit(&$event)
	{
		$object =& $event->getObject();
		/* @var $object kDBItem */

		$fields = explode(',',$this->Application->GetVar('fields'));
		$required_fields = explode(',', $this->Application->GetVar('required_fields'));
		$fields_params = $this->Application->GetVar('fields_params');

		$virtual_fields = $this->Application->getUnitOption($event->Prefix, 'VirtualFields');

		foreach ($fields as $field) {
			$virtual_fields[$field] = Array ();

			if ( in_array($field, $required_fields) ) {
				$virtual_fields[$field]['required'] = 1;
			}

			$params = getArrayValue($fields_params, $field);

			if ( $params !== false ) {
				if ( getArrayValue($params, 'Type') == 'email' ) {
					$virtual_fields[$field]['formatter'] = 'kFormatter';
					$virtual_fields[$field]['regexp'] = '/^(' . REGEX_EMAIL_USER . '@' . REGEX_EMAIL_DOMAIN . ')$/i';
					$virtual_fields[$field]['error_msgs'] = Array ('invalid_format' => '!la_invalid_email!');
				}

				if ( getArrayValue($params, 'Type') == 'file' ) {
					$virtual_fields[$field]['formatter'] = 'kUploadFormatter';
					$virtual_fields[$field]['upload_dir'] = '/uploads/sketches/';
				}
			}
		}

		$object->SetVirtualFields($virtual_fields);
		$field_values = $this->getSubmittedFields($event);
		$checkboxes = explode(',', $this->Application->GetVar('checkbox_fields')); // MailingList,In-Link,In-Newz,In-Bulletin

		foreach ($checkboxes as $checkbox) {
			if (isset($field_values[$checkbox])) {
				$field_values[$checkbox] = 1;
			}
			else {
				$field_values[$checkbox] = '0';
			}
		}

		$object->SetFieldsFromHash($field_values, $this->getRequestProtectedFields($field_values));

		if ( $object->Validate() ) {
			$event->redirect = $this->Application->GetVar('success_template');
			$this->Application->EmailEventAdmin($this->Application->GetVar('email_event'));

			$send_params = Array (
				'to_email' => $field_values[$this->Application->GetVar('email_field')],
				'to_name' => $field_values[$this->Application->GetVar('name_field')]
			);

			$this->Application->EmailEventUser($this->Application->GetVar('email_event'), null, $send_params);

			if ( $field_values['MailingList'] ) {
				$this->Application->StoreVar('SubscriberEmail', $field_values['Email']);
				$this->Application->HandleEvent($sub_event, 'u:OnSubscribeUser', Array ('no_unsubscribe' => 1));
			}
		}
		else {
			$event->status = kEvent::erFAIL;
		}
	}

	/**
	 * Don't use security image, when form requires login
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnBeforeItemCreate(kEvent &$event)
	{
		parent::OnBeforeItemCreate($event);

		$this->itemChanged($event);
	}

	/**
	 * Don't use security image, when form requires login
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnBeforeItemUpdate(kEvent &$event)
	{
		parent::OnBeforeItemUpdate($event);

		$this->itemChanged($event);
	}

	/**
	 * Occurs before item is changed
	 *
	 * @param kEvent $event
	 */
	function itemChanged(&$event)
	{
		$this->_validatePopSettings($event);
		$this->_disableSecurityImage($event);
		$this->_setRequired($event);
	}

	/**
	 * Validates POP3 settings (performs test connect)
	 *
	 * @param kEvent $event
	 */
	function _validatePopSettings(&$event)
	{
		$object =& $event->getObject();
		/* @var $object kDBItem */

		$modes = Array ('Reply', 'Bounce');
		$fields = Array ('Server', 'Port', 'Username', 'Password');
		$changed_fields = array_keys( $object->GetChangedFields() );

		foreach ($modes as $mode) {
			$set = true;
			$changed = false;

			foreach ($fields as $field) {
				$value = $object->GetDBField($mode . $field);

				if (strlen( trim($value) ) == 0) {
					$set = false;
					break;
				}

				if (!$changed && in_array($mode . $field, $changed_fields)) {
					$changed = true;
				}
			}

			if ($set && $changed) {
				// fields are set and at least on of them is changed
				$connection_info = Array ();

				foreach ($fields as $field) {
					$connection_info[ strtolower($field) ] = $object->GetDBField($mode . $field);
				}

				$pop3_helper =& $this->Application->makeClass('POP3Helper', Array ($connection_info, 10));
				/* @var $pop3_helper POP3Helper */

				switch ( $pop3_helper->initMailbox(true) ) {
					case 'socket':
						$object->SetError($mode . 'Server', 'connection_failed');
						break;

					case 'login':
						$object->SetError($mode . 'Username', 'login_failed');
						break;

					case 'list':
						$object->SetError($mode . 'Server', 'message_listing_failed');
						break;
				}
			}
		}

	}

	/**
	 * Makes email communication fields required, when form uses email communication
	 *
	 * @param kEvent $event
	 */
	function _setRequired(&$event)
	{
		$object =& $event->getObject();
		/* @var $object kDBItem */

		$required = $object->GetDBField('EnableEmailCommunication');
		$fields = Array (
			'ReplyFromName', 'ReplyFromEmail', 'ReplyServer', 'ReplyPort', 'ReplyUsername', 'ReplyPassword',
		);

		if ($required && $object->GetDBField('BounceEmail')) {
			$bounce_fields = Array ('BounceEmail', 'BounceServer', 'BouncePort', 'BounceUsername', 'BouncePassword');
			$fields = array_merge($fields, $bounce_fields);
		}

		$object->setRequired($fields, $required);
	}

	/**
	 * Don't use security image, when form requires login
	 *
	 * @param kEvent $event
	 */
	function _disableSecurityImage(&$event)
	{
		$object =& $event->getObject();
		/* @var $object kDBItem */

		if ($object->GetDBField('RequireLogin')) {
			$object->SetDBField('UseSecurityImage', 0);
		}
	}

	/**
	 * Queries pop3 server about new incoming mail
	 *
	 * @param kEvent $event
	 */
	function OnProcessReplies(&$event)
	{
		$this->_processMailbox($event, false);
	}

	/**
	 * Queries pop3 server about new incoming mail
	 *
	 * @param kEvent $event
	 */
	function OnProcessBouncedReplies(&$event)
	{
		$this->_processMailbox($event, true);
	}

	/**
	 * Queries pop3 server about new incoming mail
	 *
	 * @param kEvent $event
	 * @param bool $bounce_mode
	 */
	function _processMailbox(&$event, $bounce_mode = false)
	{
		$this->Application->SetVar('client_mode', 1);

		$id_field = $this->Application->getUnitOption($event->Prefix, 'IDField');
		$table_name = $this->Application->getUnitOption($event->Prefix, 'TableName');

		$sql = 'SELECT *
				FROM ' . $table_name . '
				WHERE EnableEmailCommunication = 1';
		$forms = $this->Conn->Query($sql, $id_field);

		$mailbox_helper =& $this->Application->recallObject('MailboxHelper');
		/* @var $mailbox_helper MailboxHelper */

		$field_prefix = $bounce_mode ? 'Bounce' : 'Reply';

		foreach ($forms as $form_id => $form_info) {
			$recipient_email = $bounce_mode ? $form_info['BounceEmail'] : $form_info['ReplyFromEmail'];

			if (!$recipient_email) {
				continue;
			}

			$mailbox_helper->process(
				Array (
					'server' => $form_info[$field_prefix . 'Server'],
					'port' => $form_info[$field_prefix . 'Port'],
					'username' => $form_info[$field_prefix . 'Username'],
					'password' => $form_info[$field_prefix . 'Password']
				),
				Array (&$this, 'isValidRecipient'),
				Array (&$this, 'processEmail'),
				Array (
					'recipient_email' => $recipient_email,
					'bounce_mode' => $bounce_mode,
					'form_info' => $form_info,
				)
			);
		}
	}

	function isValidRecipient($params)
	{
		$mailbox_helper =& $this->Application->recallObject('MailboxHelper');
		/* @var $mailbox_helper MailboxHelper */

		$recipients = $mailbox_helper->getRecipients();
		$recipient_email = $params['recipient_email'];

		$emails_found = preg_match_all('/((' . REGEX_EMAIL_USER . ')(@' . REGEX_EMAIL_DOMAIN . '))/i', $recipients, $all_emails);

		if (is_array($all_emails)) {
			for ($i = 0; $i < $emails_found; $i++) {
				if ($all_emails[1][$i] == $recipient_email) {
					// only read messages, that are addresses to submission reply email
					return true;
				}
			}
		}

		// If this is a forwarded message - we drop all the other aliases and deliver only to the x-forward to address;
		if (preg_match('/((' . REGEX_EMAIL_USER . ')(@' . REGEX_EMAIL_DOMAIN . '))/i', $mailbox_helper->headers['x-forward-to'], $get_to_email)) {
			if ($get_to_email[1] == $recipient_email) {
				// only read messages, that are addresses to submission reply email
				return true;
			}
		}

		return false;
	}

	function processEmail($params, &$fields_hash)
	{
		if ($params['bounce_mode']) {
			// mark original message as bounced

			$mailbox_helper =& $this->Application->recallObject('MailboxHelper');
			/* @var $mailbox_helper MailboxHelper */

			if (!array_key_exists('attachments', $mailbox_helper->parsedMessage)) {
				// for now only parse bounces based on attachments, skip other bounce types
				return false;
			}

			for ($i = 0; $i < count($mailbox_helper->parsedMessage['attachments']); $i++) {
				$attachment =& $mailbox_helper->parsedMessage['attachments'][$i];

				switch ($attachment['headers']['content-type']) {
					case 'message/delivery-status':
						// save as BounceInfo
						$mime_decode_helper =& $this->Application->recallObject('MimeDecodeHelper');
						/* @var $mime_decode_helper MimeDecodeHelper */

						$charset = $mailbox_helper->parsedMessage[ $fields_hash['MessageType'] ][0]['charset'];
						$fields_hash['Message'] = $mime_decode_helper->convertEncoding($charset, $attachment['data']);
						break;

					case 'message/rfc822':
						// undelivered message
						$fields_hash['Subject'] = $attachment['filename2'] ? $attachment['filename2'] : $attachment['filename'];
						break;
				}
			}
		}

		if (!preg_match('/^(.*) #verify(.*)$/', $fields_hash['Subject'], $regs)) {
			// incorrect subject, no verification code
			$form_info = $params['form_info'];

			if ($form_info['ProcessUnmatchedEmails'] && ($fields_hash['FromEmail'] != $params['recipient_email'])) {
				// it's requested to convert unmatched emails to new submissions
				$form_id = $form_info['FormId'];
				$this->Application->SetVar('form_id', $form_id);

				$sql = 'SELECT ' . $this->Application->getUnitOption('formsubs', 'IDField') . '
						FROM ' . $this->Application->getUnitOption('formsubs', 'TableName') . '
						WHERE MessageId = ' . $this->Conn->qstr($fields_hash['MessageId']);
				$found = $this->Conn->GetOne($sql);

				if ($found) {
					// don't process same message twice
					return false;
				}

				$sql = 'SELECT *
						FROM ' . TABLE_PREFIX . 'FormFields
						WHERE (FormId = ' . $form_info['FormId'] . ') AND (EmailCommunicationRole > 0)';
				$form_fields = $this->Conn->Query($sql, 'EmailCommunicationRole');

				// what roles are filled from what fields
				$role_mapping = Array (
					SubmissionFormField::COMMUNICATION_ROLE_EMAIL => 'FromEmail',
					SubmissionFormField::COMMUNICATION_ROLE_NAME => 'FromName',
					SubmissionFormField::COMMUNICATION_ROLE_SUBJECT => 'Subject',
					SubmissionFormField::COMMUNICATION_ROLE_BODY => 'Message',
				);

				$submission_fields = Array ();

				foreach ($role_mapping as $role => $email_field) {
					if (array_key_exists($role, $form_fields)) {
						$submission_fields[ 'fld_' . $form_fields[$role]['FormFieldId'] ] = $fields_hash[$email_field];
					}
				}

				if ($submission_fields) {
					// remove object, because it's linked to single form upon creation forever
					$this->Application->removeObject('formsubs.-item');

					$form_submission =& $this->Application->recallObject('formsubs.-item', null, Array ('skip_autoload' => true));
					/* @var $form_submission kDBItem */

					// in case that other non-role mapped fields are required
					$form_submission->IgnoreValidation = true;
					$form_submission->SetDBFieldsFromHash($submission_fields);
					$form_submission->SetDBField('FormId', $form_id);
					$form_submission->SetDBField('MessageId', $fields_hash['MessageId']);
					$form_submission->SetDBField('SubmissionTime_date', adodb_mktime());
					$form_submission->SetDBField('SubmissionTime_time', adodb_mktime());
					$form_submission->SetDBField('ReferrerURL', $this->Application->Phrase('la_Text_Email'));
					return $form_submission->Create();
				}
			}

			return false;
		}

		$sql = 'SELECT ' . $this->Application->getUnitOption('submission-log', 'IDField') . '
				FROM ' . $this->Application->getUnitOption('submission-log', 'TableName') . '
				WHERE MessageId = ' . $this->Conn->qstr($fields_hash['MessageId']);
		$found = $this->Conn->GetOne($sql);

		if ($found) {
			// don't process same message twice
			return false;
		}

		$reply_to =& $this->Application->recallObject('submission-log.-reply-to', null, Array ('skip_autoload' => true));
		/* @var $reply_to kDBItem */

		$reply_to->Load($regs[2], 'VerifyCode');
		if (!$reply_to->isLoaded()) {
			// fake verification code OR feedback, containing submission log was deleted
			return false;
		}

		if ($params['bounce_mode']) {
			// mark original message as bounced
			$reply_to->SetDBField('BounceInfo', $fields_hash['Message']);
			$reply_to->SetDBField('BounceDate_date', TIMENOW);
			$reply_to->SetDBField('BounceDate_time', TIMENOW);
			$reply_to->SetDBField('SentStatus', SUBMISSION_LOG_BOUNCE);
			$reply_to->Update();

			return true;
		}

		$reply =& $this->Application->recallObject('submission-log.-reply', null, Array ('skip_autoload' => true));
		/* @var $reply kDBItem */

		$reply->SetDBFieldsFromHash($fields_hash);
		$reply->SetDBField('ReplyTo', $reply_to->GetID());
		$reply->SetDBField('FormSubmissionId', $reply_to->GetDBField('FormSubmissionId'));
		$reply->SetDBField('ToEmail', $params['recipient_email']);
		$reply->SetDBField('Subject', $regs[1]); // save subject without verification code
		$reply->SetDBField('SentStatus', SUBMISSION_LOG_SENT);

		return $reply->Create();
	}
}