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 = ''; /** * Creates class instance. * * @param integer $cat_id Category ID. * @param string $table_name Table name. */ public function __construct($cat_id, $table_name) { $this->CatId = $cat_id; $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 ''; } return 'INSERT INTO `' . $this->table . '` (CategoryId, PermId, ACL) VALUES ' . join(',', $values); } } 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 /** @var kMultiLanguageHelper $ml_helper */ $ml_helper = $this->Application->recallObject('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; if ( PHP_SAPI === 'cli' ) { // The progress bar isn't used. $this->progressTable = sprintf('permCacheUpdate_%s_%s', time(), SecurityGenerator::generateBytes(8)); $this->permCacheTable = sprintf( TABLE_PREFIX . 'CategoryPermissionsCache_%s_%s', time(), SecurityGenerator::generateBytes(8) ); } else { // Temporary table name must be the same between redirects for progress bar to work. $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(sprintf( 'CREATE TABLE `%1$s` (data LONGTEXT%2$s%3$s) %2$s%3$s', $this->progressTable, $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(); } }