1<?php
2
3/**
4 * Observium
5 *
6 *   This file is part of Observium.
7 *
8 *   These functions perform rewrites on strings and numbers.
9 *
10 * @package    observium
11 * @subpackage cache
12 * @copyright  (C) 2006-2013 Adam Armstrong, (C) 2013-2019 Observium Limited
13 *
14 */
15
16// Please see phpFastCache documentation and examples here:
17// https://github.com/PHPSocialNetwork/phpfastcache/wiki
18// https://github.com/PHPSocialNetwork/phpfastcache/tree/final/examples
19// NOTE, static functions used only for compatibility with older php versions (5.4 and less).
20//       For more cache functionality needed OOP style for use..
21
22if (version_compare(PHP_VERSION, '5.5.0', '<'))
23{
24  // Disable phpFastCache 5.x for PHP less than 5.5, since it unsupported
25  $config['cache']['enable'] = FALSE;
26}
27
28/**
29 * Cache functions.
30 * Leave it before initialize phpFastCache, since here used check for php version.
31 */
32
33/**
34 * Prepend cache key with username/level
35 *
36 * @param string $key Identificator name
37 * @param boolean $global Set to TRUE for do not prepend keys
38 * @return string Prepended key identificator (ie 'data' -> 'mysql|mike|10|data')
39 */
40function get_cache_key($key, $global = FALSE)
41{
42  if ($global || is_cli()) { return $key; } // CLI used global key
43
44  if ($_SESSION['authenticated'])
45  {
46    if ($_SESSION['userlevel'] >= 10)
47    {
48      // Use common cache for Global Administrators
49      $key = '__admin|' . $key;
50    } else {
51      // All other users use unique keys!
52      $key = $_SESSION['auth_mechanism'] . '|' . $_SESSION['username'] . '|' . $_SESSION['userlevel'] . '|' . $key;
53    }
54  } else {
55    // Just "protect" anonymous requests from read/write to global cache
56    $key = '__anonymous|' . $key;
57  }
58
59  return $key;
60}
61
62/**
63 * Call to getItem() method for retrieve an Item for cached object
64 *
65 * @param string $key Item identifier (key), for WUI key auto prepended with current user identificator (by get_cache_key())
66 * @return object Object with cache Item
67 */
68function get_cache_item($key)
69{
70  if ($GLOBALS[OBS_CACHE_LINK] === (object)$GLOBALS[OBS_CACHE_LINK])
71  {
72    $observium_cache = $GLOBALS[OBS_CACHE_LINK];
73  } else {
74    // Cache not enabled
75    return;
76  }
77
78  $observium_cache_key  = get_cache_key($key); // User specific key
79
80  return $observium_cache->getItem($observium_cache_key);
81}
82
83/**
84 * Call to isHit() method for check if your cache Item exists and is still valid
85 *
86 * @param object $item Cache Item object
87 * @return bool TRUE if cache exist and not expired
88 */
89function ishit_cache_item($item)
90{
91  if ($GLOBALS[OBS_CACHE_LINK] !== (object)$GLOBALS[OBS_CACHE_LINK])
92  {
93    // Cache not enabled
94    return;
95  }
96
97  return $item->isHit();
98}
99
100/**
101 * Call to get() method for retrieve cache data for used Item
102 *
103 * @param object $item Cache Item object
104 * @return mixed Cached data
105 */
106function get_cache_data($item)
107{
108  if ($GLOBALS[OBS_CACHE_LINK] !== (object)$GLOBALS[OBS_CACHE_LINK])
109  {
110    // Cache not enabled
111    return;
112  }
113
114  $start = microtime(TRUE);
115  $data = $item->get();
116  $cache_time = microtime(TRUE) - $start;
117
118  if (OBS_DEBUG || OBS_CACHE_DEBUG)
119  {
120    print_warning('<span class="text-success">READ FROM CACHE</span> // TTL: '.$item->getTtl().'s // Expiration: <strong>' . $item->getExpirationDate()->format(Datetime::RFC2822) . '</strong><br />' .
121                  'Key: <strong>' . $item->getKey() . '</strong> // Tags: <strong>' . $item->getTagsAsString() . '</strong><br />' .
122                  'Driver: <strong>'.str_replace(array('phpFastCache\\Drivers\\', '\\Item'), '', get_class($item)).'</strong> // Read time: ' . sprintf("%.7f", $cache_time) . ' ms');
123    //print_vars($item->getTags());
124  }
125  return $data;
126}
127
128/**
129 * Call to set() and save() methods for store data in cache
130 *
131 * @param object $item Cache Item object
132 * @param mixed $data Data for store in cache
133 * @param array $params Additional options for cache entry
134 * @return int Unix timestamp when cache item will expired
135 */
136function set_cache_item($item, $data, $params = array())
137{
138  if ($GLOBALS[OBS_CACHE_LINK] === (object)$GLOBALS[OBS_CACHE_LINK])
139  {
140    $observium_cache = $GLOBALS[OBS_CACHE_LINK];
141  } else {
142    // Cache not enabled
143    return;
144  }
145
146  // Item tags (for search/cleanup cache later)
147  if (is_cli())
148  {
149    $tags = array('__cli');
150  } else {
151    $tags = array('__wui');
152    if ($_SESSION['authenticated'])
153    {
154      if ($_SESSION['userlevel'] >= 10)
155      {
156        $tags[] = '__admin';
157      } else {
158        $tags[] = '__username=' . $_SESSION['username'];
159      }
160    } else {
161      $tags[] = '__anonymous';
162    }
163  }
164  if (isset($params['tags']))
165  {
166    $tags = array_merge($tags, (array)$params['tags']);
167  }
168
169  // TTL for cache entry in seconds
170  if (isset($params['ttl']) && is_numeric($params['ttl']))
171  {
172    $ttl = intval($params['ttl']);
173  }
174  else if (is_numeric($_GLOBALS['config']['cache']['ttl']))
175  {
176    $ttl = $_GLOBALS['config']['cache']['ttl'];
177  } else {
178    // Default TTL (5 min)
179    $ttl = 300;
180  }
181
182  $start = microtime(TRUE);
183  // Add data to cache
184  $item->set($data);
185  // Set expiration TTL
186  $item->expiresAfter($ttl);
187  // Add tags
188  $item->addTags($tags);
189  // Commit
190  $observium_cache->save($item);
191  $cache_time = microtime(TRUE) - $start;
192
193  if (OBS_DEBUG || OBS_CACHE_DEBUG)
194  {
195    print_warning('<span class="text-info">WROTE TO CACHE</span> // TTL: '.$item->getTtl().'s // Expiration: <strong>' . $item->getExpirationDate()->format(Datetime::RFC2822) . '</strong><br />' .
196                  'Key: <strong>' . $item->getKey() . '</strong> // Tags: <strong>' . $item->getTagsAsString() . '</strong><br />' .
197                  'Driver: <strong>'.str_replace(array('phpFastCache\\Drivers\\', '\\Item'), '', get_class($item)).'</strong> // Write time: ' . sprintf("%.7f", $cache_time) . ' ms');
198    //print_vars($item->getTags());
199  }
200  return $item->getExpirationDate()->getTimestamp();
201}
202
203/**
204 * Get cache Items by single tag
205 *
206 * @param string $tag Tag name for Get Items
207 */
208function get_cache_items($tag)
209{
210  if ($GLOBALS[OBS_CACHE_LINK] === (object)$GLOBALS[OBS_CACHE_LINK])
211  {
212    $observium_cache = $GLOBALS[OBS_CACHE_LINK];
213  } else {
214    // Cache not enabled
215    return;
216  }
217
218  return $observium_cache->getItemsByTag($tag);
219}
220
221/**
222 * Delete cache Items by tags.
223 * See session_logout() for example.
224 *
225 * @param array $tags Array of tags for deletion
226 */
227function del_cache_items($tags)
228{
229  if ($GLOBALS[OBS_CACHE_LINK] === (object)$GLOBALS[OBS_CACHE_LINK])
230  {
231    $observium_cache = $GLOBALS[OBS_CACHE_LINK];
232  } else {
233    // Cache not enabled
234    return;
235  }
236
237  return $observium_cache->deleteItemsByTags($tags);
238}
239
240/**
241 * Delete expired Items.
242 * Used "workaround" as described here:
243 * https://github.com/PHPSocialNetwork/phpfastcache/issues/413#issuecomment-270692658
244 *
245 * @param array $tags Array of tags for clear
246 * @return int Unixtime when last expired cache cleared
247 */
248function del_cache_expired($tag = '')
249{
250  if ($GLOBALS[OBS_CACHE_LINK] === (object)$GLOBALS[OBS_CACHE_LINK])
251  {
252    $observium_cache = $GLOBALS[OBS_CACHE_LINK];
253  } else {
254    // Cache not enabled
255    return;
256  }
257
258  $item = $observium_cache->getItem('__cache_last_clear_expired');
259  if (!$item->isHit())
260  {
261    // Here our default tags, see set_cache_item()
262    if (empty($tag))
263    {
264      if (is_cli())
265      {
266        $tag = '__cli';
267      } else {
268        $tag = '__wui';
269      }
270    }
271
272    // Touch items for clear expired
273    get_cache_items($tag);
274
275    $clear_expired = time();
276
277    // Store last clean time for 24 hours
278    $item->set($clear_expired)->expiresAfter(86400);
279    // Commit
280    $observium_cache->save($item);
281
282    if (OBS_DEBUG || OBS_CACHE_DEBUG)
283    {
284      print_warning('<span class="text-success">CLEAR EXPIRED CACHE</span> // Time: <strong>' . format_unixtime($clear_expired) . '</strong>');
285    }
286  } else {
287    $clear_expired = $item->get();
288    if (OBS_DEBUG || OBS_CACHE_DEBUG)
289    {
290      print_warning('<span class="text-info">LAST CLEAR CACHE TIME</span> // Time: <strong>' . format_unixtime($clear_expired) . '</strong>');
291    }
292  }
293
294  return $clear_expired;
295}
296
297/**
298 * Add clear cache attrib, this will request for clering cache in next request.
299 *
300 * @param string $target Clear cache target: wui or cli (default if wui)
301 */
302/*
303function set_cache_clear($target = 'wui')
304{
305  if (OBS_DEBUG || OBS_CACHE_DEBUG)
306  {
307    print_error('<span class="text-warning">CACHE CLEAR SET.</span> Cache clear set.');
308  }
309  if (!$GLOBALS['config']['cache']['enable'])
310  {
311    // Cache not enabled
312    return;
313  }
314
315  switch (strtolower($target))
316  {
317    case 'cli':
318      // Add clear CLI cache attrib. Currently not used
319      set_obs_attrib('cache_cli_clear', get_request_id());
320      break;
321    default:
322      // Add clear WUI cache attrib
323      set_obs_attrib('cache_wui_clear', get_request_id());
324  }
325}
326*/
327
328/**
329 * Return total cache size in bytes.
330 * Note, this is not user/session specific size, but total for cache system
331 *
332 * @return int Total cache size in bytes
333 */
334function get_cache_stats()
335{
336  if ($GLOBALS[OBS_CACHE_LINK] === (object)$GLOBALS[OBS_CACHE_LINK])
337  {
338    $observium_cache = $GLOBALS[OBS_CACHE_LINK];
339  } else {
340    // Cache not enabled
341    return array('enabled' => FALSE, 'size' => 0);
342  }
343
344  try
345  {
346    //$_s = $observium_cache->getStats();
347    $size = $observium_cache->getStats()->getSize();
348  }
349  catch (Exception $e)
350  {
351    $size = 0;
352  }
353  //r($_s->getInfo());
354  $stats = array('enabled' => TRUE,
355                 'driver'  => str_replace(array('phpFastCache\\Drivers\\', '\\Driver'), '', get_class($observium_cache)),
356                 'size'    => $size,
357                );
358
359  return $stats;
360}
361
362/////////////////////////////////////////////////////////
363//  NO FUNCTION DEFINITIONS FOR CACHE AFTER THIS LINE! //
364/////////////////////////////////////////////////////////
365//               YES, THAT MEANS YOU                   //
366/////////////////////////////////////////////////////////
367
368define('OBS_CACHE_DEBUG', isset($_SERVER['PATH_INFO']) && strpos($_SERVER['PATH_INFO'], 'cache_info') !== FALSE);
369
370// Do not load phpFastCache classes if caching mechanism not enabled or not supported
371if (!$config['cache']['enable'])
372{
373  if (OBS_DEBUG || OBS_CACHE_DEBUG)
374  {
375    if (version_compare(PHP_VERSION, '5.5.0', '<'))
376    {
377      print_error('<span class="text-danger">CACHE DISABLED.</span> You use too old php version, see <a href="' . OBSERVIUM_URL . '/docs/software_requirements/">minimum software requirements</a>.');
378    } else {
379      print_error('<span class="text-danger">CACHE DISABLED.</span> Disabled in config.');
380    }
381  }
382  return;
383}
384
385/**
386 * Temporary hardcoded caching in files, will improved later with other providers
387 */
388
389define('OBS_CACHE_LINK', 'observium_cache'); // Variable name for call to cache class
390
391// Call the phpFastCache
392use phpFastCache\CacheManager;
393use phpFastCache\Core\phpFastCache;
394
395// Setup File Path and other basic options
396CacheManager::setDefaultConfig(array(
397  'path'                => $config['cache_dir'],
398  'securityKey'         => is_cli() ? 'cli' : 'wui', // do not use $_SERVER['hostname'] as key
399  'ignoreSymfonyNotice' => TRUE,
400));
401
402$cache_driver = 'files'; // If other drivers not detected, use files as fallback
403if (str_istarts($config['cache']['driver'], 'auto'))
404{
405  // Detect avialable drivers,
406  // NOTE order from fastest to slowest!
407  //$detect_driver = array('zendshm', 'apcu', 'xcache', 'sqlite', 'files');
408  $detect_driver = array('zendshm', 'apcu', 'sqlite', 'files');
409} else {
410  $detect_driver = array(strtolower($config['cache']['driver']));
411}
412// Basic detect if extension/driver available
413$cache_driver = 'files';
414foreach ($detect_driver as $entry)
415{
416  switch($entry)
417  {
418    case 'zendshm':
419      if (extension_loaded('Zend Data Cache') && function_exists('zend_shm_cache_store'))
420      {
421        $cache_driver = 'zendshm';
422        break 2;
423      }
424      break;
425    case '!memcached':
426      // Also need connection test
427      try
428      {
429        $mc = new Memcached();
430      } catch (Exception $e) {}
431      if (class_exists('Memcached'))
432      {
433        $cache_driver = 'memcached';
434        break 2;
435      }
436      break;
437    case 'apcu':
438      if (extension_loaded('apcu') && ini_get('apc.enabled'))
439      {
440        $cache_driver = 'apcu';
441        break 2;
442      }
443      break;
444    /* XCache RIP
445    case 'xcache':
446      if (extension_loaded('xcache') && function_exists('xcache_get'))
447      {
448        $cache_driver = 'xcache';
449        break 2;
450      }
451      break;
452    */
453    case 'sqlite':
454      if (extension_loaded('pdo_sqlite'))
455      {
456        $cache_driver = 'sqlite';
457        break 2;
458      }
459      break;
460    case 'files':
461    default:
462      //$cache_driver = 'files';
463      break;
464  }
465}
466
467switch($cache_driver)
468{
469  case 'sqlite':
470    // Create base cache dir if not exist
471    if (!is_dir($config['cache_dir']))
472    {
473      mkdir($config['cache_dir'], 0777, TRUE);
474      chmod($config['cache_dir'], 0777);
475    }
476    // Do not break here!
477
478  case 'zendshm':
479  case 'memcached':
480  case 'apcu':
481  //case 'xcache':
482    try
483    {
484      $GLOBALS[OBS_CACHE_LINK] = CacheManager::getInstance($cache_driver);
485    }
486    catch (Exception $e)
487    {
488      print_debug('Cache driver '.ucfirst($cache_driver).' not functional. Caching disabled!');
489      $GLOBALS[OBS_CACHE_LINK] = CacheManager::getInstance('Devfalse'); // disable caching
490    }
491    break;
492
493  case 'files':
494  default:
495    // Create base cache dir if not exist
496    if (!is_dir($config['cache_dir']))
497    {
498      mkdir($config['cache_dir'], 0777, TRUE);
499      chmod($config['cache_dir'], 0777);
500    }
501
502    try
503    {
504      $GLOBALS[OBS_CACHE_LINK] = CacheManager::getInstance('files');
505    }
506    catch (Exeption $e)
507    {
508      print_debug('Cache driver Files not functional. Caching disabled!');
509      $GLOBALS[OBS_CACHE_LINK] = CacheManager::getInstance('Devfalse'); // disable caching
510    }
511}
512
513// Clear cache totally by requested attrib
514if (is_cli())
515{
516  if (get_obs_attrib('cache_cli_clear'))
517  {
518    $GLOBALS[OBS_CACHE_LINK]->clear();
519    del_obs_attrib('cache_cli_clear');
520    if (OBS_DEBUG || OBS_CACHE_DEBUG)
521    {
522      print_error('<span class="text-warning">CACHE CLEARED.</span> Cache clear requested.');
523    }
524  }
525}
526else if ($request_id = get_obs_attrib('cache_wui_clear'))
527{
528  if ($request_id !== get_request_id()) // Skip cache clear inside same page request
529  {
530    $GLOBALS[OBS_CACHE_LINK]->clear();
531    del_obs_attrib('cache_wui_clear');
532    if (OBS_DEBUG || OBS_CACHE_DEBUG)
533    {
534      print_error('<span class="text-warning">CACHE CLEARED.</span> Cache clear requested.');
535    }
536  }
537}
538
539// Clean
540unset($detect_driver, $cache_driver, $entry);
541
542// EOF
543