<?php
/**
* @version	$Id: link_validation_eh.php 16523 2017-01-20 20:31:57Z alex $
* @package	In-Link
* @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 LinkValidationEventHandler extends kDBEventHandler {

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

			$permissions = Array (
				'OnResetValidationStatus' => Array ('self' => 'advanced:reset',),
				'OnRestartValidation' => Array ('self' => 'advanced:restart',),
				'OnContinueValidation' => Array ('self' => 'advanced:continue',),
				'OnValidateSelected' => Array ('self' => 'advanced:validate',),
				'OnValidateProgress' => Array ('self' => 'advanced:validate|advanced:continue|advanced:restart|advanced:reset',),
				'OnCancelValidation' => Array ('self' => 'advanced:validate|advanced:continue|advanced:restart|advanced:reset',),
				'OnCronValidation' => Array ('self' => 'advanced:validate|advanced:continue|advanced:restart|advanced:reset',),
			);

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

		/**
		 * Define alternative event processing method names
		 *
		 * @return void
		 * @see kEventHandler::$eventMethods
		 * @access protected
		 */
		protected function mapEvents()
		{
			parent::mapEvents();

			$events_map = Array (
				'OnApproveLinks' => 'iterateItems',
				'OnDeclineLinks' => 'iterateItems',
			);

			$this->eventMethods = array_merge($this->eventMethods, $events_map);
		}

		/**
		 * Checks user permission to execute given $event
		 *
		 * @param kEvent $event
		 * @return bool
		 * @access public
		 */
		public function CheckPermission(kEvent $event)
		{
			$check_events = Array ('OnApproveLinks', 'OnDeclineLinks', 'OnDeleteLinks');

			if ( in_array($event->Name, $check_events) ) {
				$ids = $this->_getSelectedIds($event);

				$perm_value = true;
				if ( $ids ) {
					/** @var kPermissionsHelper $perm_helper */
					$perm_helper = $this->Application->recallObject('PermissionsHelper');

					$items = $perm_helper->GetCategoryItemData('l', $ids);
					$check_method = $event->Name == 'OnDeleteLinks' ? 'DeleteCheckPermission' : 'ModifyCheckPermission';
					foreach ($items as $item_id => $item_data) {
						if ( $perm_helper->$check_method($item_data['CreatedById'], $item_data['CategoryId'], 'l') == 0 ) {
							// one of items selected has no permission
							$perm_value = false;
							break;
						}
					}

					if ( !$perm_value ) {
						$event->status = kEvent::erPERM_FAIL;
					}
				}

				return $perm_value;
			}

			return parent::CheckPermission($event);
		}

		/**
		 * Adds calculates fields for category name
		 *
		 * @param kDBItem|kDBList $object
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function prepareObject(&$object, kEvent $event)
		{
			parent::prepareObject($object, $event);

			$object->addCalculatedField('CachedNavbar', 'c.l' . $this->Application->GetVar('m_lang') . '_CachedNavbar');
		}

		/**
		 * Allows to show only invalid links
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 * @see kDBEventHandler::OnListBuild()
		 */
		protected function SetCustomQuery(kEvent $event)
		{
			parent::SetCustomQuery($event);

			/** @var kDBList $object */
			$object = $event->getObject();

			$object->addFilter('primary_category_filter', 'ci.PrimaryCat = 1');

			if ( $event->Special == 'invalid' ) {
				$object->addFilter('status_filter', '%1$s.ValidationStatus = ' . LINK_VALIDATION_INVALID);
			}
		}

		/**
		 * Restarts link validation process
		 *
		 * @param kEvent $event
		 */
		function OnRestartValidation($event)
		{
			$this->_resetValidation($event);

			$this->OnContinueValidation($event);
		}

		/**
		 * Restarts link validation process
		 *
		 * @param kEvent $event
		 */
		function _resetValidation($event)
		{
			$config = $event->getUnitConfig();

			// 1. delete previous validation results
			$sql = 'SELECT ' . $config->getIDField() . '
					FROM ' . $config->getTableName();
			$ids = $this->Conn->GetCol($sql);

			if ( $ids ) {
				/** @var kTempTablesHandler $temp_handler */
				$temp_handler = $this->Application->recallObject($event->getPrefixSpecial() . '_TempHandler', 'kTempTablesHandler');

				$temp_handler->DeleteItems($event->Prefix, $event->Special, $ids);
			}
		}

		/**
		 * Validates only selected links
		 *
		 * @param kEvent $event
		 */
		function OnValidateSelected($event)
		{
			$link_ids = $this->_getSelectedIds($event);
			if (!$link_ids) {
				return ;
			}

			$validation_data = Array (
				'processed' => 0,
				'total' => count($link_ids),
				'items' => $link_ids,
			);
			$this->Application->StoreVar($event->Prefix . '_status', serialize($validation_data));

			$event->redirect = $this->Application->GetVar('progress_template');
		}

		/**
		 * Validates only links, that were not previously validated
		 *
		 * @param kEvent $event
		 */
		function OnContinueValidation($event)
		{
			$have_data = $this->_prepareValidation($event);
			if ($have_data) {
				$event->redirect = $this->Application->GetVar('progress_template');
			}
		}

		/**
		 * Performs validation
		 *
		 * @param kEvent $event
		 * @param bool $from_ajax
		 */
		function _validate($event, $from_ajax = true)
		{
			$validation_data = unserialize( $this->Application->RecallVar($event->Prefix . '_status') );

			$i = 0;
			$link_ids = $validation_data['items'];
			$per_page = count($link_ids) >= LINK_VALIDATION_PER_PAGE ? LINK_VALIDATION_PER_PAGE : count($link_ids);

			while ($i < $per_page) {
				$this->_validateLink($link_ids[$i]);
				$i++;
			}

			// remove processed links from array
			array_splice($link_ids, 0, LINK_VALIDATION_PER_PAGE);

			// store validation progress
			$validation_data['processed'] += $i;
			$validation_data['items'] = $link_ids;

			if ($validation_data['processed'] >= $validation_data['total']) {
				// finished
				$this->Application->emailAdmin('LINK.VALIDATION.RESULTS');

				$this->Application->RemoveVar($event->Prefix . '_status');
				return true;
			}

			// show progress, proceed to next step
			$this->Application->StoreVar($event->Prefix . '_status', serialize($validation_data));

			if ($from_ajax) {
				echo $validation_data['processed'] / $validation_data['total'] * 100;
				$event->status = kEvent::erSTOP;
			}

			return false;
		}

		/**
		 * Performs validation of links (called from AjaxProgressBar)
		 *
		 * @param kEvent $event
		 */
		function OnValidateProgress($event)
		{
			$done = $this->_validate($event, true);

			if ($done) {
				$this->Application->Redirect( $this->Application->GetVar('finish_template') );
			}
		}

		/**
		 * Returns categories, that are located inside recycle bin category
		 *
		 * @return Array
		 */
		function _getRecycleBinCategories()
		{
			$recycle_bin = $this->Application->ConfigValue('RecycleBinFolder');

			if ( !is_numeric($recycle_bin) ) {
				return Array ();
			}

			$recycle_categories = $this->Application->RecallVar('recycle_categories');

			if ( $recycle_categories === false ) {
				$categories_config = $this->Application->getUnitConfig('c');
				$tree_indexes = $this->Application->getTreeIndex($recycle_bin);

				$sql = 'SELECT ' . $categories_config->getIDField() . '
						FROM ' . $categories_config->getTableName() . '
						WHERE TreeLeft BETWEEN ' . $tree_indexes['TreeLeft'] . ' AND ' . $tree_indexes['TreeRight'];
				$recycle_categories = serialize($this->Conn->GetCol($sql));

				// store recycle bin categories in session to prevent query below happening on each link validation step
				$this->Application->StoreVar('recycle_categories', $recycle_categories);
			}

			return unserialize($recycle_categories);

		}

		/**
		 * Checks, that link is located in one of RecycleBin subcategories
		 *
		 * @param unknown_type $resource_id
		 * @return unknown
		 */
		function _inRecycleBin($resource_id)
		{
			static $recycle_bin = null;

			if ( !isset($recycle_bin) ) {
				$recycle_bin = $this->_getRecycleBinCategories();
			}

			if ( !$recycle_bin ) {
				// Recycle Bin not used in system -> link is 100% not there
				return false;
			}

			$sql = 'SELECT CategoryId
					FROM ' . $this->Application->getUnitConfig('l-ci')->getTableName() . '
					WHERE ItemResourceId = ' . $resource_id . ' AND PrimaryCat = 1';

			return in_array($this->Conn->GetOne($sql), $recycle_bin);
		}

		function _validateLink($link_id)
		{
			/** @var kCurlHelper $curl_helper */
			$curl_helper = $this->Application->recallObject('CurlHelper');

			$link_config = $this->Application->getUnitConfig('l');

			$sql = 'SELECT Url, ResourceId
					FROM ' . $link_config->getTableName() . '
					WHERE ' . $link_config->getIDField() . ' = ' . $link_id;
			$link_data = $this->Conn->GetRow($sql);

			if ( !preg_match('/^(http|https):\/\/(.*)/U', $link_data['Url']) || $this->_inRecycleBin($link_data['ResourceId']) ) {
				return;
			}

			$curl_helper->timeout = LINK_VALIDATION_TIMEOUT;

			$result = $curl_helper->Send($link_data['Url']);
			if ( $result === false || $curl_helper->lastErrorMsg != '' ) {
				$curl_helper->lastHTTPCode = 500;
			}

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

			$link_validation->Load($link_id, 'LinkId');

			$now = time();

			$fields_hash = Array (
				'LinkId' => $link_id,
				'ValidationTime_date' => $now,
				'ValidationTime_time' => $now,
				'ValidationCode' => $curl_helper->lastHTTPCode,
				'ValidationStatus' => $curl_helper->lastHTTPCode < 400 ? LINK_VALIDATION_VALID : LINK_VALIDATION_INVALID,
			);

			$link_validation->SetDBFieldsFromHash($fields_hash);

			return $link_validation->isLoaded() ? $link_validation->Update() : $link_validation->Create();
		}

		/**
		 * Cancels validation (from validation progress bar)
		 *
		 * @param kEvent $event
		 */
		function OnCancelValidation($event)
		{
			$this->Application->RemoveVar($event->Prefix . '_status');
		}

		/**
		 * Resets validation status for selected
		 *
		 * @param kEvent $event
		 */
		function OnResetValidationStatus($event)
		{
			$ids = $this->_getSelectedIds($event, true);
			if (!$ids) {
				return ;
			}

			/** @var kTempTablesHandler $temp_handler */
			$temp_handler = $this->Application->recallObject($event->getPrefixSpecial().'_TempHandler', 'kTempTablesHandler', Array ('parent_event' => $event));

			$temp_handler->DeleteItems($event->Prefix, $event->Special, $ids);
		}

		/**
		 * Returns ids, that user has checked in grid
		 *
		 * @param kEvent $event
		 * @param bool $transform convert link ids to link validation ids
		 * @return Array
		 */
		function _getSelectedIds($event, $transform = false)
		{
			$ids = Array ();

			$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));

			if ( $items_info ) {
				foreach ($items_info as $id => $field_values) {
					if ( getArrayValue($field_values, 'ForeignLinkId') ) {
						// we are not gathering ids by unit idfield here!
						array_push($ids, $id);
					}
				}
			}

			if ( $transform && $ids ) {
				$config = $event->getUnitConfig();

				$sql = 'SELECT ' . $config->getIDField() . '
						FROM ' . $config->getTableName() . '
						WHERE LinkId IN (' . implode(',', $ids) . ')';
				$ids = $this->Conn->GetCol($sql);
			}

			return $ids;
		}

		/**
		 * Approves/declines selected links
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function iterateItems(kEvent $event)
		{
			if ( $this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1) ) {
				$event->status = kEvent::erFAIL;
				return;
			}

			$ids = $this->_getSelectedIds($event);
			if ( !$ids ) {
				return;
			}

			/** @var kCatDBItem $object */
			$object = $this->Application->recallObject('l.-item', null, Array ('skip_autoload' => true));

			foreach ($ids as $id) {
				$object->Load($id);

				switch ( $event->Name ) {
					case 'OnApproveLinks':
						$object->ApproveChanges();
						break;

					case 'OnDeclineLinks':
						$object->DeclineChanges();
						break;
				}
			}
		}

		/**
		 * Deletes selected links
		 *
		 * @param kEvent $event
		 */
		function OnDeleteLinks($event)
		{
			if ($this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1)) {
				$event->status = kEvent::erFAIL;
				return;
			}

			$ids = $this->_getSelectedIds($event);
			if (!$ids) {
				return ;
			}

			/** @var kTempTablesHandler $temp_handler */
			$temp_handler = $this->Application->recallObject('l_TempHandler', 'kTempTablesHandler', Array ('parent_event' => $event));

			$temp_handler->DeleteItems('l', '', $ids);
		}

		/**
		 * [HOOK] Allows to edit links, used in selected link validation records
		 *
		 * @param kEvent $event
		 */
		function OnPrepareLinkEditing($event)
		{
			// hook to OnAfterConfigRead instead of OnEdit, because fake ids should be available in CheckPermission
			if ( $this->Application->GetVar('l_event') != 'OnEdit' ) {
				return;
			}

			$items_info = Array ();
			$ids = $this->_getSelectedIds($event);
			$id_field = $this->Application->getUnitConfig('l')->getIDField();

			foreach ($ids as $id) {
				$items_info[$id][$id_field] = 'on';
			}

			$this->Application->SetVar('l', $items_info);
		}

		/**
		 * Gets all links, that are not yet validated and prepare data
		 *
		 * @param kEvent $event
		 *
		 * @return bool
		 */
		function _prepareValidation($event)
		{
			// 2. get ids of all links and put them into validation queue
			$links_config = $this->Application->getUnitConfig('l');

			$sql = 'SELECT ' . $links_config->getIDField() . '
					FROM ' . $links_config->getTableName() . '
					WHERE LinkId NOT IN (SELECT LinkId FROM ' . $event->getUnitConfig()->getTableName() . ')';
			$link_ids = $this->Conn->GetCol($sql);

			if ( $link_ids ) {
				$validation_data = Array (
					'processed' => 0,
					'total' => count($link_ids),
					'items' => $link_ids,
				);

				$this->Application->StoreVar($event->Prefix . '_status', serialize($validation_data)); // 4K links will be 78KB serialized

				return true;
			}

			return false;
		}

		/**
		 * [SCHEDULED TASK] Performs link validation through cron
		 *
		 * @param kEvent $event
		 */
		function OnCronValidation($event)
		{
			$this->_resetValidation($event); // remove this for continuing to non validated before links

			$have_data = $this->_prepareValidation($event);
			if ($have_data) {
				do {
					$done = $this->_validate($event, false);
				} while (!$done);
			}
		}

		/**
		 * Makes calculated fields to go to multilingual link fields
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterConfigRead(kEvent $event)
		{
			parent::OnAfterConfigRead($event);

			$event->getUnitConfig()->addCalculatedFieldsBySpecial('', Array (
				'LinkName' => 'l.l' . $this->Application->GetVar('m_lang') . '_Name',
			));
		}

	}