<?php
/**
* @version	$Id: image_event_handler.php 16513 2017-01-20 14:10:53Z 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 ImageEventHandler extends kDBEventHandler {

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

		$permissions = Array (
			'OnCleanImages' => Array ('subitem' => true),
		);

		$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();	// ensure auto-adding of approve/decline and so on events

		$image_events = Array (
			'OnAfterCopyToTemp'=>'ImageAction',
			'OnBeforeDeleteFromLive'=>'ImageAction',
			'OnBeforeCopyToLive'=>'ImageAction',
			'OnBeforeItemDelete'=>'ImageAction',
			'OnAfterClone'=>'ImageAction',
		);

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

	/**
	 * Returns special of main item for linking with sub-item
	 *
	 * @param kEvent $event
	 * @return string
	 * @access protected
	 */
	protected function getMainSpecial(kEvent $event)
	{
		if ( $event->Special == 'list' && !$this->Application->isAdmin ) {
			// ListImages aggregated tag uses this special
			return '';
		}

		return parent::getMainSpecial($event);
	}

	/**
	 * Don't allow to delete primary category item image, when there are no more images
	 *
	 * @param kEvent $event
	 * @param string $type
	 * @return void
	 * @access protected
	 */
	protected function customProcessing(kEvent $event, $type)
	{
		/** @var kDBItem $object */
		$object = $event->getObject();

		if ( $event->Name == 'OnMassDelete' && $type == 'before' ) {
			$ids = $event->getEventParam('ids');

			$parent_info = $object->getLinkedInfo($event->Special);

			$sql = 'SELECT ImageId
					FROM ' . $object->TableName . '
					WHERE DefaultImg = 1 AND ' . $parent_info['ForeignKey'] . ' = ' . $parent_info['ParentId'];
			$primary_file_id = $this->Conn->GetOne($sql);

			if ( $primary_file_id ) {
				$file_id_index = array_search($primary_file_id, $ids);

				if ( $file_id_index ) {
					// allow deleting of primary product file, when there is another file to make primary
					$sql = 'SELECT COUNT(*)
							FROM ' . $object->TableName . '
							WHERE DefaultImg = 0 AND ' . $parent_info['ForeignKey'] . ' = ' . $parent_info['ParentId'];
					$non_primary_file_count = $this->Conn->GetOne($sql);

					if ( $non_primary_file_count ) {
						unset($ids[$file_id_index]);
					}
				}
			}

			$event->setEventParam('ids', $ids);
		}

		switch ($type) {
			case 'before' :
				// empty unused fields
				$object->SetDBField($object->GetDBField('LocalImage') ? 'Url' : 'LocalPath', '');
				$object->SetDBField($object->GetDBField('LocalThumb') ? 'ThumbUrl' : 'ThumbPath', '');

				if ( $object->GetDBField('SameImages') ) {
					$object->SetDBField('LocalImage', 1);
					$object->SetDBField('LocalPath', '');
					$object->SetDBField('Url', '');
				}
				break;

			case 'after':
				// make sure, that there is only one primary image for the item
				if ( $object->GetDBField('DefaultImg') ) {
					$sql = 'UPDATE ' . $object->TableName . '
							SET DefaultImg = 0
							WHERE ResourceId = ' . $object->GetDBField('ResourceId') . ' AND ImageId <> ' . $object->GetID();
					$this->Conn->Query($sql);
				}
				break;
		}
	}

	/**
	 * Performs temp-table related action on current image record
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function ImageAction($event)
	{
		$id = $event->getEventParam('id');

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

		if ( in_array($event->Name, Array ('OnBeforeDeleteFromLive', 'OnAfterClone')) ) {
			$object->SwitchToLive();
		}
		elseif ( $event->Name == 'OnBeforeItemDelete' ) {
			// keep current table
		}
		else {
			$object->SwitchToTemp();
		}

		$object->Load($id);

		/** @var FileHelper $file_helper */
		$file_helper = $this->Application->recallObject('FileHelper');

		$fields = Array ('LocalPath' => 'LocalImage', 'ThumbPath' => 'LocalThumb');

		foreach ($fields as $a_field => $mode_field) {
			$file = $object->GetDBField($a_field);

			if ( !$file ) {
				continue;
			}

			$source_file = FULL_PATH . $file;

			switch ($event->Name) {
				// Copy image files to pending dir and update corresponding fields in temp record
				// Checking for existing files and renaming if necessary - two users may upload same pending files at the same time!
				case 'OnAfterCopyToTemp':
					$file = preg_replace('/^' . preg_quote(IMAGES_PATH, '/') . '/', IMAGES_PENDING_PATH, $file, 1);
					$new_file = $file_helper->ensureUniqueFilename(FULL_PATH, $file);

					$dst_file = FULL_PATH . $new_file;
					copy($source_file, $dst_file);

					$object->SetFieldOption($a_field, 'skip_empty', false);
					$object->SetDBField($a_field, $new_file);
					break;

				// Copy image files to live dir (checking if file exists and renaming if necessary)
				// and update corresponding fields in temp record (which gets copied to live automatically)
				case 'OnBeforeCopyToLive':
					if ( $object->GetDBField($mode_field) ) {
						// if image is local -> rename file if it exists in live folder
						$file = preg_replace('/^' . preg_quote(IMAGES_PENDING_PATH, '/') . '/', IMAGES_PATH, $file, 1);
						$new_file = $file_helper->ensureUniqueFilename(FULL_PATH, $file);

						$dst_file = FULL_PATH . $new_file;
						rename($source_file, $dst_file);
					}
					else {
						// if image is remote url - remove local file (if any), update local file field with empty value
						if ( file_exists($source_file) ) {
							@unlink($source_file);
						}

						$new_file = '';
					}

					$object->SetFieldOption($a_field, 'skip_empty', false);
					$object->SetDBField($a_field, $new_file);
					break;

				case 'OnBeforeDeleteFromLive': // Delete image files from live folder before copying over from temp
				case 'OnBeforeItemDelete': // Delete image files when deleting Image object
					@unlink(FULL_PATH . $file);
					break;

				case 'OnAfterClone':
					// Copy files when cloning objects, renaming it on the fly
					$new_file = $file_helper->ensureUniqueFilename(FULL_PATH, $file);
					$dst_file = FULL_PATH . $new_file;
					copy($source_file, $dst_file);

					$object->SetFieldOption($a_field, 'skip_empty', false);
					$object->SetDBField($a_field, $new_file);
					break;
			}
		}

		if ( in_array($event->Name, Array ('OnAfterClone', 'OnBeforeCopyToLive', 'OnAfterCopyToTemp')) ) {
			$object->Update(null, null, true);
		}
	}

	/**
	 * Sets primary image of user/category/category item
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnSetPrimary($event)
	{
		/** @var kDBItem $object */
		$object = $event->getObject();

		$object->SetDBField('DefaultImg', 1);
		$object->Update();
	}

	/**
	 * Occurs before updating item
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnBeforeItemUpdate(kEvent $event)
	{
		parent::OnBeforeItemUpdate($event);

		$this->processImageStatus($event);
	}

	/**
	 * Occurs after creating item
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnAfterItemCreate(kEvent $event)
	{
		parent::OnAfterItemCreate($event);

		$this->processImageStatus($event);

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

		$object->Update();
	}

	/**
	 * Occurs before item changed
	 *
	 * @param kEvent $event
	 */
	function processImageStatus($event)
	{
		/** @var kDBItem $object */
		$object = $event->getObject();

		$id = $object->GetDBField('ResourceId');

		$sql = 'SELECT ImageId
				FROM ' . $object->TableName . '
				WHERE ResourceId = ' . $id . ' AND DefaultImg = 1';
		$primary_image_id = $this->Conn->GetOne($sql);

		if ( !$primary_image_id ) {
			$object->SetDBField('DefaultImg', 1);
		}

		if ( $object->GetDBField('DefaultImg') && $object->Validate() ) {
			$sql = 'UPDATE ' . $object->TableName . '
					SET DefaultImg = 0
					WHERE ResourceId = ' . $id . ' AND ImageId <> ' . $object->GetDBField('ImageId');
			$this->Conn->Query($sql);

			$object->SetDBField('Enabled', 1);
		}
	}

	/**
	 * Apply any custom changes to list's sql query
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 * @see kDBEventHandler::OnListBuild()
	 */
	protected function SetCustomQuery(kEvent $event)
	{
		parent::SetCustomQuery($event);

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

		if ( !$this->Application->isAdminUser ) {
			$object->addFilter('active', '%1$s.Enabled = 1');
		}

		$product_id = $event->getEventParam('product_id');

		if ( $product_id ) {
			$object->removeFilter('parent_filter');

			$sql = 'SELECT ResourceId
					FROM ' . $this->Application->getUnitOption('p', 'TableName') . '
					WHERE ProductId = ' . $product_id;
			$resource_id = (int)$this->Conn->GetOne($sql);

			$object->addFilter('product_images', '%1$s.ResourceId = ' . $resource_id);
		}

		/** @var kSearchHelper $search_helper */
		$search_helper = $this->Application->recallObject('SearchHelper');

		$types = $event->getEventParam('types');
		$except_types = $event->getEventParam('except');
		$type_clauses = $this->getTypeClauses($event);

		$search_helper->SetComplexFilter($event, $type_clauses, $types, $except_types);
	}

	/**
	 * Return type clauses for list bulding on front
	 *
	 * @param kEvent $event
	 * @return Array
	 */
	function getTypeClauses($event)
	{
		$type_clauses = Array ();

		$type_clauses['additional']['include'] = '%1$s.DefaultImg != 1';
		$type_clauses['additional']['except'] = '%1$s.DefaultImg = 1';
		$type_clauses['additional']['having_filter'] = false;

		return $type_clauses;
	}

	/**
	 * [SCHEDULED TASK] Remove unused images from "/system/images" and "/system/images/pending" folders
	 *
	 * @param kEvent $event
	 */
	function OnCleanImages($event)
	{
		// 1. get images, that are currently in use
		$active_images = $this->_getActiveImages( $this->Application->getUnitOption('img', 'TableName') );
		$active_images[] = 'noimage.gif';

		// 2. get images on disk
		$this->_deleteUnusedImages(FULL_PATH . IMAGES_PATH, $active_images);

		// 3. get images in use from "images/pending" folder
		$active_images = $this->_getPendingImages();

		// 4. get image on disk
		$this->_deleteUnusedImages(FULL_PATH . IMAGES_PENDING_PATH, $active_images);
	}

	/**
	 * Gets image filenames (no path) from given table
	 *
	 * @param string $image_table
	 * @return Array
	 */
	function _getActiveImages($image_table)
	{
		$sql = 'SELECT LocalPath, ThumbPath
				FROM ' . $image_table . '
				WHERE COALESCE(LocalPath, "") <> "" OR COALESCE(ThumbPath) <> ""';
		$images = $this->Conn->Query($sql);

		$active_images = Array ();
		foreach ($images as $image) {
			if ($image['LocalPath']) {
				$active_images[] = basename($image['LocalPath']);
			}

			if ($image['ThumbPath']) {
				$active_images[] = basename($image['ThumbPath']);
			}
		}

		return $active_images;
	}

	/**
	 * Gets active images, that are currently beeing edited inside temporary tables
	 *
	 * @return Array
	 */
	function _getPendingImages()
	{
		$tables = $this->Conn->GetCol('SHOW TABLES');
		$mask_edit_table = '/'.TABLE_PREFIX.'ses_(.*)_edit_' . TABLE_PREFIX . 'CatalogImages/';

		$active_images = Array ();

		foreach ($tables as $table) {
			if (!preg_match($mask_edit_table, $table)) {
				continue;
			}

			$active_images = array_unique( array_merge($active_images, $this->_getActiveImages($table)) );
		}

		return $active_images;
	}

	/**
	 * Deletes all files in given path, except of given $active_images
	 *
	 * @param string $path
	 * @param Array $active_images
	 */
	function _deleteUnusedImages($path, &$active_images)
	{
		$images = glob($path . '*.*');
		if ($images) {
			$images = array_map('basename', $images);

			// delete images, that are on disk, but are not mentioned in CatalogImages table
			$delete_images = array_diff($images, $active_images);
			foreach ($delete_images as $delete_image) {
				unlink($path . $delete_image);
			}
		}
	}

}
