Index: branches/5.1.x/core/kernel/nparser/nparser.php =================================================================== diff -u -N -r13086 -r13168 --- branches/5.1.x/core/kernel/nparser/nparser.php (.../nparser.php) (revision 13086) +++ branches/5.1.x/core/kernel/nparser/nparser.php (.../nparser.php) (revision 13168) @@ -1,6 +1,6 @@ _btnPhrases['design'] = $this->Application->Phrase('la_btn_EditDesign', false, true); $this->_btnPhrases['block'] = $this->Application->Phrase('la_btn_EditBlock', false, true); } + + $this->RewriteUrls = $this->Application->RewriteURLs(); + $this->UserLoggedIn = $this->Application->LoggedIn(); + + // cache only Front-End templated, when memory caching is available and template caching is enabled in configuration + $this->CachingEnabled = !$this->Application->isAdmin && $this->Application->ConfigValue('SystemTagCache') && $this->Application->isCachingType(CACHING_TYPE_MEMORY); } function Compile($pre_parsed, $template_name = 'unknown') @@ -383,14 +429,126 @@ $this->TemplateName = $t; $this->TempalteFullPath = $pre_parsed['tname']; - $output =& $this->Application->TemplatesCache->runTemplate($this, $pre_parsed); + if (!isset($backup_template) && $this->CachingEnabled && !$this->UserLoggedIn && !EDITING_MODE) { + // this is main page template -> check for page-based aggressive caching settings + $output =& $this->RunMainPage($pre_parsed); + } + else { + $output =& $this->Application->TemplatesCache->runTemplate($this, $pre_parsed); + } $this->TemplateName = $backup_template; $this->TempalteFullPath = $backup_fullpath; return $output; } + function &RunMainPage($pre_parsed) + { + $page =& $this->Application->recallObject('st.-virtual'); + /* @var $page kDBItem */ + + if ($page->isLoaded()) { + // page found in database + $debug_mode = $this->Application->isDebugMode(); // don't cache debug output + $template_path = preg_replace('/^' . preg_quote(FULL_PATH, '/') . '/', '', $this->TempalteFullPath, 1); + $element = ($debug_mode ? 'DEBUG_MODE:' : '') . 'file=' . $template_path; + $this->FullCachePage = $page->GetDBField('EnablePageCache'); + + if ($this->FullCachePage && $page->GetDBField('PageCacheKey')) { + // page caching enabled -> try to get from cache + $cache_key = $this->FormCacheKey($element, $page->GetDBField('PageCacheKey')); + $output = $this->getCache($cache_key); + + if ($output !== false) { + return $output; + } + } + + // page not cached OR cache expired + $output =& $this->Application->TemplatesCache->runTemplate($this, $pre_parsed); + $this->generatePageCacheKey($page); + + if ($this->FullCachePage && $page->GetDBField('PageCacheKey')) { + $cache_key = $this->FormCacheKey($element, $page->GetDBField('PageCacheKey')); + $this->setCache($cache_key, $output, (int)$page->GetDBField('PageExpiration')); + } + } + else { + // page not found in database + $output =& $this->Application->TemplatesCache->runTemplate($this, $pre_parsed); + } + + return $output; + } + + /** + * Generate page caching key based on prefixes used on it + prefix IDs passed in url + * + * @param kDBItem $page + */ + function generatePageCacheKey(&$page) + { + if (!$page->isLoaded() || $page->GetDBField('OverridePageCacheKey')) { + return ; + } + + $page_cache_key = Array (); + // nobody resets "m" prefix serial, don't count no user too + unset($this->PrefixesInUse['m'], $this->PrefixesInUse['u']); + + if (array_key_exists('st', $this->PrefixesInUse)) { + // prefix "st" serial will never be changed + unset($this->PrefixesInUse['st']); + $this->PrefixesInUse['c'] = 1; + } + + $prefix_ids = Array (); + $prefixes = array_keys($this->PrefixesInUse); + asort($prefixes); + + foreach ($prefixes as $index => $prefix) { + $id = $this->Application->GetVar($prefix . '_id'); + + if (is_numeric($id)) { + if (defined('DEBUG_MODE') && DEBUG_MODE && $this->Application->isDebugMode()) { + $this->Application->Debugger->appendHTML('Found: "' . $prefix . '_id" = ' . $id . ' during PageCacheKey forming.'); + } + + $prefix_ids[] = $prefix; + unset($prefixes[$index]); + } + } + + if ($prefix_ids) { + $page_cache_key[] = 'prefix_id:' . implode(',', $prefix_ids); + } + + if ($prefixes) { + $page_cache_key[] = 'prefix:' . implode(',', $prefixes); + } + + $page_cache_key = implode(';', $page_cache_key); + + if ($page_cache_key != $page->GetOriginalField('PageCacheKey')) { + if (defined('DEBUG_MODE') && DEBUG_MODE && $this->Application->isDebugMode()) { + $this->Application->Debugger->appendHTML('Canging PageCacheKey from "' . $page->GetOriginalField('PageCacheKey') . '" to "' . $page_cache_key . '".'); + } + + $page->SetDBField('PageCacheKey', $page_cache_key); + + // don't use kDBItem::Update(), because it will change ModifiedById to current front-end user + $sql = 'UPDATE ' . $page->TableName . ' + SET PageCacheKey = ' . $page->Conn->qstr($page_cache_key) . ' + WHERE ' . $page->IDField . ' = ' . $page->GetID(); + $page->Conn->Query($sql); + + // increment serial, because we issue direct sql above! + $this->Application->incrementCacheSerial('c'); + $this->Application->incrementCacheSerial('c', $page->GetID()); + } + } + function &GetProcessor($prefix) { static $Processors = array(); @@ -443,8 +601,12 @@ function ParseBlock($params, $pass_params=false) { - if (isset($params['cache_timeout']) && ($ret = $this->CacheGet($this->FormCacheKey('element_'.$params['name'])))) { - return $ret; + if (array_key_exists('cache_timeout', $params) && $params['cache_timeout']) { + $ret = $this->getCache( $this->FormCacheKey('element_' . $params['name']) ); + + if ($ret) { + return $ret; + } } if (substr($params['name'], 0, 5) == 'html:') { @@ -517,8 +679,9 @@ $this->DataExists = $data_exists_bak || $this->DataExists; - if (isset($original_params['cache_timeout'])) { - $this->CacheSet($this->FormCacheKey('element_'.$original_params['name']), $ret, $original_params['cache_timeout']); + if (array_key_exists('cache_timeout', $original_params) && $original_params['cache_timeout']) { + $cache_key = $this->FormCacheKey('element_' . $original_params['name']); + $this->setCache($cache_key, $ret, (int)$original_params['cache_timeout']); } if (array_key_exists('no_editing', $block_params) && $block_params['no_editing']) { @@ -563,9 +726,9 @@ if (!$decorate) { return $block_content; } - else { - $block_content = /*$prepend .*/ $block_content; - } + /*else { + $block_content = $prepend . $block_content; + }*/ $block_name = $block_params['name']; $function_name = $is_template ? $block_name : $this->Elements[$block_name]; @@ -629,9 +792,15 @@ function IncludeTemplate($params, $silent=null) { $t = is_array($params) ? $this->SelectParam($params, 't,template,block,name') : $params; + $cache_timeout = array_key_exists('cache_timeout', $params) ? $params['cache_timeout'] : false; - if (isset($params['cache_timeout']) && ($ret = $this->CacheGet('template:'.$t))) { - return $ret; + if ($cache_timeout) { + $cache_key = $this->FormCacheKey('template:' . $t); + $ret = $this->getCache($cache_key); + + if ($ret !== false) { + return $ret; + } } $t = preg_replace('/\.tpl$/', '', $t); @@ -654,8 +823,8 @@ $this->CheckNoData($ret, $params); $this->DataExists = $data_exists_bak || $this->DataExists; - if (isset($params['cache_timeout'])) { - $this->CacheSet('template:'.$t, $ret, $params['cache_timeout']); + if ($cache_timeout) { + $this->setCache($cache_key, $ret, (int)$cache_timeout); } return $ret; @@ -674,53 +843,216 @@ } } - function CacheGet($name) + function getCache($name) { - if (!$this->Application->ConfigValue('SystemTagCache')) return false; - return $this->Application->CacheGet($name); + if (!$this->CachingEnabled) { + return false; + } + + $ret = $this->Application->getCache($name, false); + + if (preg_match('/^\[DE_MARK:(.*?)\]$/', substr($ret, -11), $regs)) { + $this->DataExists = $regs[1] ? true : false; + $ret = substr($ret, 0, -11); + } + + return $ret; } - function CacheSet($name, $value, $expiration=0) + function setCache($name, $value, $expiration = 0) { - if (!$this->Application->ConfigValue('SystemTagCache')) return false; - return $this->Application->CacheSet($name, $value, $expiration); + if (!$this->CachingEnabled) { + return false; + } + + // remeber DataExists in cache, because after cache will be restored + // it will not be available naturally (no tags, that set it will be called) + $value .= '[DE_MARK:' . (int)$this->DataExists . ']'; + + return $this->Application->setCache($name, $value, $expiration); } - function FormCacheKey($element, $file=null, $add_prefixes=null) + function FormCacheKey($element, $key_string = '') { - if (!isset($file)) { - $file = str_replace(FULL_PATH, '', $this->TempalteFullPath).':'.$this->Application->GetVar('t'); + if (strpos($key_string, 'guest_only') !== false && $this->UserLoggedIn) { + // don't cache, when user is logged-in "guest_only" is specified in key + return ''; } - $parts = array( - 'file_'.$file.'('.filemtime($this->TempalteFullPath).')' => 'serials:file_ts', // theme + template timestamp - 'm_lang_'.$this->Application->GetVar('m_lang') => 'serials:lang_ts', - 'm_cat_id_'.$this->Application->GetVar('m_cat_id') => 'serials:cat_'.$this->Application->GetVar('m_cat_id').'_ts', - 'm_cat_page'.$this->Application->GetVar('m_cat_page') => false, - ); - if (isset($add_prefixes)) { - foreach ($add_prefixes as $prefix) { - $parts[$prefix.'_id_'.$this->Application->GetVar("{$prefix}_id")] = "serials:$prefix_".$this->Application->GetVar("{$prefix}_id").'_ts'; - $parts[$prefix.'_page_'.$this->Application->GetVar("{$prefix}_Page")] = false; + + $parts = Array (); + + // 1. replace INLINE variable (from request) into key parts + if (preg_match_all('/\(%(.*?)\)/', $key_string, $regs)) { + // parts in form "(%variable_name)" were found + foreach ($regs[1] as $variable_name) { + $variable_value = $this->Application->GetVar($variable_name); + $key_string = str_replace('(%' . $variable_name . ')', $variable_value, $key_string); } } - $key = ''; - foreach ($parts as $part => $ts_name) { - if ($ts_name) { - $ts = $this->Application->CacheGet($ts_name); - $key .= "$part($ts):"; + + // 2. replace INLINE serial numbers (they may not be related to any prefix at all) + // Serial number also could be composed of inline variables! + if (preg_match_all('/\[%(.*?)%\]/', $key_string, $regs)) { + // format "[%LangSerial%]" - prefix-wide serial in case of any change in "lang" prefix + // format "[%LangIDSerial:5%]" - one id-wide serial in case of data, associated with given id was changed + // format "[%CiIDSerial:ItemResourceId:5%]" - foreign key-based serial in case of data, associated with given foreign key was changed + foreach ($regs[1] as $serial_name) { + $serial_value = $this->Application->getCache('[%' . $serial_name . '%]'); + $key_string = str_replace('[%' . $serial_name . '%]', '[%' . $serial_name . '=' . $serial_value . '%]', $key_string); } + } + + /* + Always add: + =========== + * "var:m_lang" - show content on current language + * "var:t" - template from url, used to differ multiple pages using same physical template (like as design) + * "var:admin,editing_mode" - differ cached content when different editing modes are used + * "var:m_cat_id,m_cat_page" - pass current category + * "var:page,per_page,sort_by" - list pagination/sorting parameters + * "prefix:theme-file" - to be able to reset all cached templated using "Rebuild Theme Files" function + * "prefix:phrases" - use latest phrase translations + * "prefix:conf" - output could slighly differ based on configuration settings + */ + $key_string = rtrim('var:m_lang,t,admin,editing_mode,m_cat_id,m_cat_page,page,per_page,sort_by;prefix:theme-file,phrases,conf;' . $key_string, ';'); + + $keys = explode(';', $key_string); + + /* + Possible parts of a $key_string (all can have multiple occurencies): + ==================================================================== + * prefix:[,,] - include global serial for given prefix(-es) + * skip_prefix:[,,] - exclude global serial for given prefix(-es) + * prefix_id:[,,] - include id-based serial for given prefix(-es) + * skip_prefix_id:[,,] - exclude id-based serial for given prefix(-es) + * var:[,,] - include request variable value(-s) + * skip_var:[,,] - exclude request variable value(-s) + * (%variable_name) - include request variable value (only value without variable name ifself, like in "var:variable_name") + * [%SerialName%] - use to retrieve serial value in free form + */ + + // 3. get variable names, prefixes and prefix ids, that should be skipped + $skip_prefixes = $skip_prefix_ids = $skip_variables = Array (); + + foreach ($keys as $index => $key) { + if (preg_match('/^(skip_var|skip_prefix|skip_prefix_id):(.*?)$/i', $key, $regs)) { + unset($keys[$index]); + $tmp_parts = explode(',', $regs[2]); + + switch ($regs[1]) { + case 'skip_var': + $skip_variables = array_merge($skip_variables, $tmp_parts); + break; + + case 'skip_prefix': + $skip_prefixes = array_merge($skip_prefixes, $tmp_parts); + break; + + case 'skip_prefix_id': + $skip_prefix_ids = array_merge($skip_prefix_ids, $tmp_parts); + break; + } + } + } + + $skip_prefixes = array_unique($skip_prefixes); + $skip_variables = array_unique($skip_variables); + $skip_prefix_ids = array_unique($skip_prefix_ids); + + // 4. process keys + foreach ($keys as $key) { + if (preg_match('/^(var|prefix|prefix_id):(.*?)$/i', $key, $regs)) { + $tmp_parts = explode(',', $regs[2]); + + switch ($regs[1]) { + case 'var': + // format: "var:country_id" will become "country_id=" + $tmp_parts = array_diff($tmp_parts, $skip_variables); + + foreach ($tmp_parts as $variable_name) { + $variable_value = $this->Application->GetVar($variable_name); + + if ($variable_value !== false) { + $parts[] = $variable_name . '=' . $variable_value; + } + } + break; + + case 'prefix': + // format: "prefix:country" will become "[%CountrySerial%]" + $tmp_parts = array_diff($tmp_parts, $skip_prefixes); + + foreach ($tmp_parts as $prefix) { + $serial_name = $this->Application->incrementCacheSerial($prefix, null, false); + $parts[] = '[%' . $serial_name . '=' . $this->Application->getCache($serial_name) . '%]'; + + if (!$this->RewriteUrls) { + // add env-style page and per-page variable, when mod-rewrite is off + $prefix_variables = Array ($prefix . '_Page', $prefix . '_PerPage'); + foreach ($prefix_variables as $variable_name) { + $variable_value = $this->Application->GetVar($variable_name); + + if ($variable_value !== false) { + $parts[] = $variable_name . '=' . $variable_value; + } + } + } + } + break; + + case 'prefix_id': + // format: "id:country" will become "[%CountryIDSerial:5%]" + $tmp_parts = array_diff($tmp_parts, $skip_prefix_ids); + + foreach ($tmp_parts as $prefix_id) { + $id = $this->Application->GetVar($prefix_id . '_id'); + + if ($id !== false) { + $serial_name = $this->Application->incrementCacheSerial($prefix_id, $id, false); + $parts[] = '[%' . $serial_name . '=' . $this->Application->getCache($serial_name) . '%]'; + } + } + break; + } + } + elseif ($key == 'currency') { + // based on current currency + $parts[] = 'curr_iso=' . $this->Application->RecallVar('curr_iso'); + } + elseif ($key == 'groups') { + // based on logged-in user groups + $parts[] = 'groups=' . $this->Application->RecallVar('UserGroups'); + } + elseif ($key == 'guest_only') { + // we know this key, but process it at method beginning + } else { - $key .= "$part:"; + if ($this->Application->isDebugMode()) { + $this->Application->Debugger->appendTrace(); + } + + trigger_error('Unknown key part "' . $key . '" used in "key" parameter of tag.', E_USER_ERROR); } } - $key .= $element; - return crc32($key); + // 5. add unique given cache key identifier on this page + $parts[] = $element; + + $key = implode(':', $parts); + + if (defined('DEBUG_MODE') && DEBUG_MODE && $this->Application->isDebugMode()) { + $this->Application->Debugger->appendHTML('Parser Key: ' . $key); + } + + return 'parser_' . crc32($key); } - function PushPointer($pointer) + function PushPointer($pointer, $key) { - $this->CachePointers[++$this->CacheLevel] = $this->FormCacheKey('pointer:'.$pointer); + $cache_key = $this->FullCachePage || !$this->CachingEnabled ? '' : $this->FormCacheKey('pointer:' . $pointer, $key); + + $this->CachePointers[++$this->CacheLevel] = $cache_key; + return $this->CachePointers[$this->CacheLevel]; } @@ -729,21 +1061,45 @@ return $this->CachePointers[$this->CacheLevel--]; } - function CacheStart($pointer=null) + function CacheStart($pointer, $key) { - if ($ret = $this->CacheGet($this->PushPointer($pointer)) ) { - echo $ret; - $this->PopPointer(); - return true; + $pointer = $this->PushPointer($pointer, $key); + + if ($pointer) { + $ret = $this->getCache($pointer); + + $debug_mode = defined('DEBUG_MODE') && DEBUG_MODE && $this->Application->isDebugMode(); + + if ($ret !== false) { + echo $debug_mode ? '' . $ret . '' : $ret; + $this->PopPointer(); + + return true; + } + + if ($debug_mode) { + echo ''; + } } + ob_start(); + return false; } - function CacheEnd($elem=null) + function CacheEnd($expiration = 0) { $ret = ob_get_clean(); - $this->CacheSet($this->PopPointer(), $ret); // . ($this->CurrentKeyPart ? ':'.$this->CurrentKeyPart : '') + $pointer = $this->PopPointer(); + + if ($pointer) { + $res = $this->setCache($pointer, $ret, $expiration); + + if (defined('DEBUG_MODE') && DEBUG_MODE && $this->Application->isDebugMode()) { + echo ''; + } + } + echo $ret; } } \ No newline at end of file