<?php
/**
* @version	$Id: cache_updater.php 15390 2012-06-12 16:25:51Z 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 clsRecursionStack {

		var $Stack;

		function clsRecursionStack()
		{
			$this->Stack = Array();
		}

		function Push($values)
		{
			array_push($this->Stack, $values);
		}

		function Pop()
		{
			if ($this->Count() > 0) {
				return array_pop($this->Stack);
			}
			else {
				return false;
			}
		}

		function Get()
		{
			if ($this->Count() > 0) {
	//			return end($this->Stack);
				return $this->Stack[count($this->Stack)-1];
			}
			else {
				return false;
			}
		}

		function Update($values)
		{
			$this->Stack[count($this->Stack)-1] = $values;
		}

		function Count()
		{
			return count($this->Stack);
		}
	}

	class clsCachedPermissions {

		var $Allow = Array();
		var $Deny = Array();
		var $CatId;

		/**
		 * Table name used for inserting permissions
		 *
		 * @var string
		 */
		var $table = '';

		function clsCachedPermissions($CatId, $table_name)
		{
			$this->CatId = $CatId;
			$this->table = $table_name;
		}

		function SetCatId($CatId)
		{
			$this->CatId = $CatId;
		}

		function CheckPermArray($Perm)
		{
			if (!isset($this->Allow[$Perm])) {
				$this->Allow[$Perm] = array();
				$this->Deny[$Perm] = array();
			}
		}

		function AddAllow($Perm, $GroupId)
		{
			$this->CheckPermArray($Perm);
			if (!in_array($GroupId, $this->Allow[$Perm])) {
				array_push($this->Allow[$Perm], $GroupId);
				$this->RemoveDeny($Perm, $GroupId);
			}
		}

		function AddDeny($Perm, $GroupId)
		{
			$this->CheckPermArray($Perm);
			if (!in_array($GroupId, $this->Deny[$Perm])) {
				array_push($this->Deny[$Perm], $GroupId);
				$this->RemoveAllow($Perm, $GroupId);
			}
		}

		function RemoveDeny($Perm, $GroupId)
		{
			if (in_array($GroupId, $this->Deny[$Perm])) {
				array_splice($this->Deny[$Perm], array_search($GroupId, $this->Deny[$Perm]), 1);
			}
		}

		function RemoveAllow($Perm, $GroupId)
		{
			if (in_array($GroupId, $this->Allow[$Perm])) {
				array_splice($this->Allow[$Perm], array_search($GroupId, $this->Allow[$Perm]), 1);
			}
		}

		function GetInsertSQL()
		{
			$values = array();
			foreach ($this->Allow as $perm => $groups) {
				if (count($groups) > 0) {
					$values[] = '(' .$this->CatId. ', ' .$perm. ', "' .join(',', $groups). '")';
				}
			}
			if (!$values) return '';
			$sql = 'INSERT INTO '.$this->table.' (CategoryId, PermId, ACL) VALUES '.join(',', $values);
			return $sql;
		}
	}

	class kPermCacheUpdater extends kHelper {
		/**
		 * Holds Stack
		 *
		 * @var clsRecursionStack
		 */
		var $Stack;

		/**
		 * Rebuild process iteration
		 *
		 * @var int
		 */
		var $iteration;

		/**
		 * Categories count to process
		 *
		 * @var unknown_type
		 */
		var $totalCats = 0;

		/**
		 * Processed categories count
		 *
		 * @var int
		 */
		var $doneCats = 0;

		/**
		 * Temporary table name used for storing cache building progress
		 *
		 * @var string
		 */
		var $progressTable = '';

		/**
		 * Temporary table name used for storing not fully built permissions cache
		 * 1. preserves previous cache while new cache is building
		 * 2. when rebuild process fails allows previous cache (in live table) is used
		 *
		 * @var string
		 */
		var $permCacheTable = '';

		var $primaryLanguageId = 0;

		var $languages = Array ();

		var $root_prefixes = Array();

		/**
		 * Update cache only for requested categories and it's parent categories
		 *
		 * @var bool
		 */
		var $StrictPath = false;

		/**
		 * Returns instance of perm cache updater
		 *
		 * @param int $continuing
		 * @param mixed $strict_path
		 */
		public function __construct($continuing = null, $strict_path = null)
		{
			parent::__construct();

			if ( isset($strict_path) ) {
				if ( $strict_path && !is_array($strict_path) ) {
					$strict_path = explode('|', trim($strict_path, '|'));
				}

				$this->StrictPath = $strict_path;
			}

			// cache widely used values to speed up process
			$ml_helper = $this->Application->recallObject('kMultiLanguageHelper');
			/* @var $ml_helper kMultiLanguageHelper */

			$this->languages = $ml_helper->getLanguages();
			$this->primaryLanguageId = $this->Application->GetDefaultLanguageId();

			foreach ($this->Application->ModuleInfo as $module_name => $module_info) {
				$this->root_prefixes[ $module_info['RootCat'] ] = $module_info['Var'];
			}

			$this->iteration = 0;
			$this->progressTable = $this->Application->GetTempName('permCacheUpdate');
			$this->permCacheTable = $this->Application->GetTempName(TABLE_PREFIX.'CategoryPermissionsCache');

			if (!isset($continuing) || $continuing == 1) {
				$this->InitUpdater();
			}
			elseif ($continuing == 2) {
				$this->getData();
			}
		}

		function InitUpdater()
		{
			$this->Stack = new clsRecursionStack();
			$this->initData();
		}

		function getDonePercent()
		{
			if (!$this->totalCats) {
				return 0;
			}
			return min(100, intval( floor( $this->doneCats / $this->totalCats * 100 ) ));
		}

		function getData()
		{
			$tmp = $this->Conn->GetOne('SELECT data FROM '.$this->progressTable);
			if ($tmp) $tmp = unserialize($tmp);

			$this->totalCats = isset($tmp['totalCats']) ? $tmp['totalCats'] : 0;
			$this->doneCats = isset($tmp['doneCats']) ? $tmp['doneCats'] : 0;
			if (isset($tmp['stack'])) {
				$this->Stack = $tmp['stack'];
			}
			else {
				$this->Stack = new clsRecursionStack();
			}
		}

		function setData()
		{
			$tmp = Array (
				'totalCats'	=>	$this->totalCats,
				'doneCats'	=>	$this->doneCats,
				'stack'		=>	$this->Stack,
			);

			$this->Conn->Query('DELETE FROM '.$this->progressTable);

			$fields_hash = Array('data' => serialize($tmp));
			$this->Conn->doInsert($fields_hash, $this->progressTable);
		}

		function initData()
		{
			$this->clearData(); // drop table before starting anyway

			// 1. create table for rebuilding permissions cache
			$this->Conn->Query('CREATE TABLE '.$this->permCacheTable.' LIKE '.TABLE_PREFIX.'CategoryPermissionsCache');

			if ($this->StrictPath) {
				// when using strict path leave all other cache intact
				$sql = 'INSERT INTO '.$this->permCacheTable.'
						SELECT *
						FROM '.TABLE_PREFIX.'CategoryPermissionsCache';
				$this->Conn->Query($sql);

				// delete only cache related to categories in path
				$sql = 'DELETE FROM '.$this->permCacheTable.'
						WHERE CategoryId IN ('.implode(',', $this->StrictPath).')';
				$this->Conn->Query($sql);
			}

			$add_charset = defined(SQL_CHARSET)? ' CHARACTER SET '.SQL_CHARSET.' ' : '';
			$add_collation = defined(SQL_COLLATION)? ' COLLATE '.SQL_COLLATION.' ' : '';

			$this->Conn->Query('CREATE TABLE '.$this->progressTable.'(data LONGTEXT'.$add_charset.$add_collation.') '.$add_charset.$add_collation);

			$this->totalCats = (int)$this->Conn->GetOne('SELECT COUNT(*) FROM '.TABLE_PREFIX.'Categories');
			$this->doneCats = 0;
		}

		function clearData()
		{
			// some templates use this
			$sql = 'UPDATE ' . TABLE_PREFIX . 'SystemSettings
					SET VariableValue = VariableValue + 1
					WHERE VariableName = "CategoriesRebuildSerial"';
			$this->Conn->Query($sql);

			// always drop temporary tables
			$this->Conn->Query('DROP TABLE IF EXISTS '.$this->progressTable);
			$this->Conn->Query('DROP TABLE IF EXISTS '.$this->permCacheTable);
			$this->Application->deleteDBCache('ForcePermCacheUpdate');
		}

		function SaveData()
		{
			// copy data from temp permission cache table back to live
			$this->Conn->Query('TRUNCATE '.TABLE_PREFIX.'CategoryPermissionsCache');

			$sql = 'INSERT INTO '.TABLE_PREFIX.'CategoryPermissionsCache
					SELECT *
					FROM '.$this->permCacheTable;
			$this->Conn->Query($sql);
			$this->clearData();

			$this->Application->incrementCacheSerial('c');
		}

		function DoTheJob()
		{
			$data = $this->Stack->Get();
			if ($data === false) { //If Stack is empty
				$data['current_id']			=	0;
				$data['titles']				=	Array();
				$data['parent_path']		=	Array();
				$data['named_path']			=	Array();
				$data['file_name']			=	'';
				$data['template']	=	''; // design
				$data['item_template']		=	'';
				$data['children_count'] = 0;
				$data['left'] = 0;
				$data['right'] = 2;
				$data['debug_title'] = 'ROOT';
				$this->Stack->Push($data);
			}

			if (!isset($data['queried'])) {
				$this->QueryTitle($data);
				$this->QueryChildren($data);
				$data['children_count'] = count($data['children']);
				$this->QueryPermissions($data);
				$data['queried'] = 1;
				$data['right'] = $data['left']+1;

				$sql = $data['perms']->GetInsertSQL();

				if ( $sql ) {
					$this->Conn->Query($sql);
					// $this->doneCats++;  // moved to the place where it pops out of the stack by Kostja
				}
				$this->iteration++;
			}

			// start with first child if we haven't started yet
			if (!isset($data['current_child'])) $data['current_child'] = 0;

			// if we have more children on CURRENT LEVEL
			if (isset($data['children'][$data['current_child']])) {
				if ($this->StrictPath) {
					while ( isset($data['children'][ $data['current_child'] ]) && !in_array($data['children'][ $data['current_child'] ], $this->StrictPath) ) {
						$data['current_child']++;
						continue;
					}
					if (!isset($data['children'][ $data['current_child'] ])) return false; //error
				}
				$next_data = Array();
				$next_data['titles']			=	$data['titles'];
				$next_data['parent_path']		=	$data['parent_path'];
				$next_data['named_path']		=	$data['named_path'];
				$next_data['template']			=	$data['template'];
				$next_data['item_template']		=	$data['item_template'];
				$next_data['current_id']		=	$data['children'][ $data['current_child'] ]; //next iteration should process child
				$next_data['perms']				=	clone $data['perms']; // copy permissions to child - inheritance
				$next_data['perms']->SetCatId($next_data['current_id']);
				$next_data['left'] = $data['right'];
				$data['current_child']++;
				$this->Stack->Update($data); //we need to update ourself for the iteration after the next (or further) return to next child
				$this->Stack->Push($next_data); //next iteration should process this child
				return true;
			}
			else {
				$this->Stack->Update($data);
				$prev_data = $this->Stack->Pop(); //remove ourself from stack if we have finished all the childs (or there are none)
				$data['right'] = $prev_data['right'];
				$this->UpdateCachedPath($data);
				// we are getting here if we finished with current level, so check if it's first level - then bail out.

				$this->doneCats++; // moved by Kostja from above, seems to fix the prob
				$has_more = $this->Stack->Count() > 0;
				if ($has_more) {
					$next_data = $this->Stack->Get();
					$next_data['right'] = $data['right']+1;
					$next_data['children_count'] += $data['children_count'];
					$this->Stack->Update($next_data);
				}
				return $has_more;
			}
		}

		function UpdateCachedPath(&$data)
		{
			if ( $data['current_id'] == 0 ) {
				// don't update non-existing "Home" category
				return;
			}

			// allow old fashion system templates to work (maybe this is no longer needed, since no such urls after upgrade from 4.3.1)
			$named_parent_path = strpos($data['file_name'], '/') !== false ? $data['file_name'] : implode('/', $data['named_path']);

			$fields_hash = Array (
				'ParentPath' => '|' . implode('|', $data['parent_path']) . '|',
				'NamedParentPath' => $named_parent_path, // url component for a category page
				'NamedParentPathHash' => kUtil::crc32(mb_strtolower(preg_replace('/^Content\//i', '', $named_parent_path))),
				'CachedTemplate' => $data['template'], // actual template to use when category is visited
				'CachedTemplateHash' => kUtil::crc32(mb_strtolower($data['template'])),
				'CachedDescendantCatsQty' => $data['children_count'],
				'TreeLeft' => $data['left'],
				'TreeRight' => $data['right'],
			);

			foreach ($this->languages as $language_id) {
				$fields_hash['l' . $language_id . '_CachedNavbar'] = implode('&|&', $data['titles'][$language_id]);
			}

			$this->Conn->doUpdate($fields_hash, TABLE_PREFIX . 'Categories', 'CategoryId = ' . $data['current_id']);

			if ( $this->Conn->getAffectedRows() > 0 ) {
				$this->Application->incrementCacheSerial('c', $data['current_id']);
			}
		}

		function QueryTitle(&$data)
		{
			$category_id = $data['current_id'];
			$sql = 'SELECT *
					FROM '.TABLE_PREFIX.'Categories
					WHERE CategoryId = '.$category_id;

			$record = $this->Conn->GetRow($sql);
			if ($record) {

				foreach ($this->languages as $language_id) {
					$data['titles'][$language_id][] = $record['l'.$language_id.'_Name'] ? $record['l'.$language_id.'_Name'] : $record['l'.$this->primaryLanguageId.'_Name'];
				}

				$data['debug_title'] = $record['l1_Name'];

				$data['parent_path'][] = $category_id;
				$data['named_path'][] = preg_replace('/^Content\\//', '', $record['Filename']);
				$data['file_name'] = $record['Filename'];

				// it is one of the modules root category
				/*$root_prefix = isset($this->root_prefixes[$category_id]) ? $this->root_prefixes[$category_id] : false;
				if ( $root_prefix ) {
					$fields_hash = Array ();
					if ( !$record['Template'] ) {
						$record['Template'] = $this->Application->ConfigValue($root_prefix . '_CategoryTemplate');
						$fields_hash['Template'] = $record['Template'];
					}

					$this->Conn->doUpdate($fields_hash, TABLE_PREFIX . 'Categories', 'CategoryId = ' . $category_id);
				}*/

				// if explicitly set, then use it; use parent template otherwise
				if ($record['Template'] && ($record['Template'] != CATEGORY_TEMPLATE_INHERIT)) {
					$data['template'] = $record['Template'];
				}
			}

		}

		function QueryChildren(&$data)
		{
			$sql = 'SELECT CategoryId
					FROM '.TABLE_PREFIX.'Categories
					WHERE ParentId = '.$data['current_id'];
			 $data['children'] = $this->Conn->GetCol($sql);
		}

		function QueryPermissions(&$data)
		{
			// don't search for section "view" permissions here :)
			$sql = 'SELECT ipc.PermissionConfigId, ip.GroupId, ip.PermissionValue
					FROM '.TABLE_PREFIX.'Permissions AS ip
					LEFT JOIN '.TABLE_PREFIX.'CategoryPermissionsConfig AS ipc ON ipc.PermissionName = ip.Permission
					WHERE (CatId = '.$data['current_id'].') AND (Permission LIKE "%.VIEW") AND (ip.Type = 0)';

			$records = $this->Conn->Query($sql);

			//create permissions array only if we don't have it yet (set by parent)
			if (!isset($data['perms'])) {
				$data['perms'] = new clsCachedPermissions($data['current_id'], $this->permCacheTable);
			}

			foreach ($records as $record) {
				if ($record['PermissionValue'] == 1) {
					$data['perms']->AddAllow($record['PermissionConfigId'], $record['GroupId']);
				}
				else {
					$data['perms']->AddDeny($record['PermissionConfigId'], $record['GroupId']);
				}
			}
		}

		/**
		 * Rebuild all cache in one step
		 *
		 * @param string $path
		 * @return void
		 */
		function OneStepRun($path = '')
		{
			$this->InitUpdater();
			$needs_more = true;

			while ($needs_more) {
				// until proceeded in this step category count exceeds category per step limit
				$needs_more = $this->DoTheJob();
			}

			$this->SaveData();
		}
	}