1<?php 2 3 4/* 5 * TODO: 6 * - add flush on logout? 7 * - scrutinizer fixes 8 * - redis sessions 9 */ 10 11/** 12 * Cache structures 13 * @package framework 14 * @subpackage cache 15 */ 16 17/** 18 * Helper struct to provide data sources the don't track messages read or flagged state 19 * (like POP3 or RSS) with an alternative. 20 * @package framework 21 * @subpackage cache 22 */ 23trait Hm_Uid_Cache { 24 25 /* UID list */ 26 private static $read = array(); 27 private static $unread = array(); 28 29 /* Load UIDs from an outside source 30 * @param array $data list of uids 31 * @return void 32 */ 33 public static function load($data) { 34 if (!is_array($data) || count($data) != 2) { 35 return; 36 } 37 if (count($data[0]) > 0) { 38 self::update_count($data, 'read', 0); 39 } 40 if (count($data[1]) > 0) { 41 self::update_count($data, 'unread', 1); 42 } 43 } 44 45 /** 46 * @param array $data uids to merge 47 * @param string $type uid type (read or unread) 48 * @param integer $pos position in the $data array 49 * @return void 50 */ 51 private static function update_count($data, $type, $pos) { 52 self::$$type = array_combine($data[$pos], array_fill(0, count($data[$pos]), 0)); 53 } 54 55 /** 56 * Determine if a UID has been unread 57 * @param string $uid UID to search for 58 * @return bool true if te UID exists 59 */ 60 public static function is_unread($uid) { 61 return array_key_exists($uid, self::$unread); 62 } 63 64 /** 65 * Determine if a UID has been read 66 * @param string $uid UID to search for 67 * @return bool true if te UID exists 68 */ 69 public static function is_read($uid) { 70 return array_key_exists($uid, self::$read); 71 } 72 73 /** 74 * Return all the UIDs 75 * @return array list of known UIDs 76 */ 77 public static function dump() { 78 return array(array_keys(self::$read), array_keys(self::$unread)); 79 } 80 81 /** 82 * Add a UID to the unread list 83 * @param string $uid uid to add 84 */ 85 public static function unread($uid) { 86 self::$unread[$uid] = 0; 87 if (array_key_exists($uid, self::$read)) { 88 unset(self::$read[$uid]); 89 } 90 } 91 92 /** 93 * Add a UID to the read list 94 * @param string $uid uid to add 95 */ 96 public static function read($uid) { 97 self::$read[$uid] = 0; 98 if (array_key_exists($uid, self::$unread)) { 99 unset(self::$unread[$uid]); 100 } 101 } 102} 103 104/** 105 * Shared utils for Redis and Memcached 106 * @package framework 107 * @subpackage cache 108 */ 109trait Hm_Cache_Base { 110 111 public $supported; 112 private $enabled; 113 private $server; 114 private $config; 115 private $port; 116 private $cache_con; 117 118 /** 119 * @return boolean 120 */ 121 abstract protected function connect(); 122 123 /* 124 * @return boolean 125 */ 126 public function is_active() { 127 if (!$this->enabled) { 128 return false; 129 } 130 elseif (!$this->configured()) { 131 return false; 132 } 133 elseif (!$this->cache_con) { 134 return $this->connect(); 135 } 136 return true; 137 } 138 139 /** 140 * @param string $key cache key to delete 141 */ 142 public function del($key) { 143 if (!$this->is_active()) { 144 return false; 145 } 146 return $this->cache_con->delete($key); 147 } 148 149 /** 150 * @param string $key key to set 151 * @param string|string $val value to set 152 * @param integer $lifetime lifetime of the cache entry 153 * @param string $crypt_key encryption key 154 * @return boolean 155 */ 156 public function set($key, $val, $lifetime=600, $crypt_key='') { 157 if (!$this->is_active()) { 158 return false; 159 } 160 return $this->cache_con->set($key, $this->prep_in($val, $crypt_key), $lifetime); 161 } 162 163 /** 164 * @param string $key name of value to fetch 165 * @param string $crypt_key encryption key 166 * @return false|array|string 167 */ 168 public function get($key, $crypt_key='') { 169 if (!$this->is_active()) { 170 return false; 171 } 172 return $this->prep_out($this->cache_con->get($key), $crypt_key); 173 } 174 175 /** 176 * @param array|string $data data to prep 177 * @param string $crypt_key encryption key 178 * @return string|array 179 */ 180 private function prep_in($data, $crypt_key) { 181 if ($crypt_key) { 182 return Hm_Crypt::ciphertext(Hm_transform::stringify($data), $crypt_key); 183 } 184 return $data; 185 } 186 187 /** 188 * @param array $data data to prep 189 * @param string $crypt_key encryption key 190 * @return false|array|string 191 */ 192 private function prep_out($data, $crypt_key) { 193 if ($crypt_key && is_string($data) && trim($data)) { 194 return Hm_transform::unstringify(Hm_Crypt::plaintext($data, $crypt_key), 'base64_decode', true); 195 } 196 return $data; 197 } 198 199 /** 200 * @return boolean 201 */ 202 private function configured() { 203 if (!$this->server || !$this->port) { 204 Hm_Debug::add(sprintf('%s enabled but no server or port found', $this->type)); 205 return false; 206 } 207 if (!$this->supported) { 208 Hm_Debug::add(sprintf('%s enabled but not supported by PHP', $this->type)); 209 return false; 210 } 211 return true; 212 } 213} 214 215/** 216 * Redis cache 217 * @package framework 218 * @subpackage cache 219 */ 220class Hm_Redis { 221 222 use Hm_Cache_Base; 223 private $type = 'Redis'; 224 private $db_index; 225 226 /** 227 * @param Hm_Config $config site config object 228 */ 229 public function __construct($config) { 230 $this->server = $config->get('redis_server', false); 231 $this->port = $config->get('redis_port', false); 232 $this->enabled = $config->get('enable_redis', false); 233 $this->db_index = $config->get('redis_index', 0); 234 $this->socket = $config->get('redis_socket', ''); 235 $this->supported = Hm_Functions::class_exists('Redis'); 236 $this->config = $config; 237 } 238 239 /** 240 * @return boolean 241 */ 242 private function connect() { 243 $this->cache_con = Hm_Functions::redis(); 244 try { 245 if ($this->socket) { 246 $con = $this->cache_con->connect($this->socket); 247 } 248 else { 249 $con = $this->cache_con->connect($this->server, $this->port); 250 } 251 if ($con) { 252 $this->auth(); 253 $this->cache_con->select($this->db_index); 254 return true; 255 } 256 else { 257 $this->cache_con = false; 258 return false; 259 } 260 } 261 catch (Exception $oops) { 262 Hm_Debug::add('Redis connect failed'); 263 $this->cache_con = false; 264 return false; 265 } 266 } 267 268 /** 269 * @return void 270 */ 271 private function auth() { 272 if ($this->config->get('redis_pass')) { 273 $this->cache_con->auth($this->config->get('redis_pass')); 274 } 275 } 276 277 /** 278 * @return boolean 279 */ 280 public function close() { 281 if (!$this->is_active()) { 282 return false; 283 } 284 return $this->cache_con->close(); 285 } 286} 287 288/** 289 * Memcached cache 290 * @package framework 291 * @subpackage cache 292 */ 293class Hm_Memcached { 294 295 use Hm_Cache_Base; 296 private $type = 'Memcached'; 297 298 /** 299 * @param Hm_Config $config site config object 300 */ 301 public function __construct($config) { 302 $this->server = $config->get('memcached_server', false); 303 $this->port = $config->get('memcached_port', false); 304 $this->enabled = $config->get('enable_memcached', false); 305 $this->supported = Hm_Functions::class_exists('Memcached'); 306 $this->config = $config; 307 } 308 309 /** 310 * @return void 311 */ 312 private function auth() { 313 if ($this->config->get('memcached_auth')) { 314 $this->cache_con->setOption(Memcached::OPT_BINARY_PROTOCOL, true); 315 $this->cache_con->setSaslAuthData($this->config->get('memcached_user'), 316 $this->config->get('memcached_pass')); 317 } 318 } 319 320 /* 321 * @return boolean 322 */ 323 private function connect() { 324 $this->cache_con = Hm_Functions::memcached(); 325 $this->auth(); 326 if (!$this->cache_con->addServer($this->server, $this->port)) { 327 Hm_Debug::add('Memcached addServer failed'); 328 $this->cache_con = false; 329 return false; 330 } 331 return true; 332 } 333 334 /** 335 * @return mixed 336 */ 337 public function last_err() { 338 if (!$this->is_active()) { 339 return false; 340 } 341 return $this->cache_con->getResultCode(); 342 } 343 344 /** 345 * @return boolean 346 */ 347 public function close() { 348 if (!$this->is_active()) { 349 return false; 350 } 351 return $this->cache_con->quit(); 352 } 353} 354 355/** 356 * @package framework 357 * @subpackage cache 358 */ 359class Hm_Noop_Cache { 360 361 public function del($key) { 362 return true; 363 } 364 public function set($key, $val, $lifetime, $crypt_key) { 365 return false; 366 } 367} 368 369/** 370 * Generic cache 371 * @package framework 372 * @subpackage cache 373 */ 374class Hm_Cache { 375 376 private $backend; 377 private $session; 378 public $type; 379 380 /** 381 * @param Hm_Config $config site config object 382 * @param object $session session object 383 * @return void 384 */ 385 public function __construct($config, $session) { 386 $this->session = $session; 387 if (!$this->check_redis($config) && !$this->check_memcache($config)) { 388 $this->check_session($config); 389 } 390 Hm_Debug::add(sprintf('CACHE backend using: %s', $this->type)); 391 } 392 393 /** 394 * @param Hm_Config $config site config object 395 * @return void 396 */ 397 private function check_session($config) { 398 $this->type = 'noop'; 399 $this->backend = new Hm_Noop_Cache(); 400 if ($config->get('allow_session_cache')) { 401 $this->type = 'session'; 402 } 403 } 404 405 /** 406 * @param Hm_Config $config site config object 407 * @return boolean 408 */ 409 private function check_redis($config) { 410 if ($config->get('enable_redis', false)) { 411 $backend = new Hm_Redis($config); 412 if ($backend->is_active()) { 413 $this->type = 'redis'; 414 $this->backend = $backend; 415 return true; 416 } 417 } 418 return false; 419 } 420 421 /** 422 * @param Hm_Config $config site config object 423 * @return boolean 424 */ 425 private function check_memcache($config) { 426 if ($config->get('enable_memcached', false)) { 427 $backend = new Hm_Memcached($config); 428 if ($backend->is_active()) { 429 $this->type = 'memcache'; 430 $this->backend = $backend; 431 return true; 432 } 433 } 434 return false; 435 } 436 437 /** 438 * @param string $key key name 439 * @param string $msg_type log message 440 * @return void 441 */ 442 private function log($key, $msg_type) { 443 switch ($msg_type) { 444 case 'save': 445 Hm_Debug::add(sprintf('CACHE: saving "%s" using %s', $key, $this->type)); 446 break; 447 case 'hit': 448 Hm_Debug::add(sprintf('CACHE: hit for "%s" using %s', $key, $this->type)); 449 break; 450 case 'miss': 451 Hm_Debug::add(sprintf('CACHE: miss for "%s" using %s', $key, $this->type)); 452 break; 453 case 'del': 454 Hm_Debug::add(sprintf('CACHE: deleting "%s" using %s', $key, $this->type)); 455 break; 456 } 457 } 458 459 /** 460 * @param string $key name of value to cache 461 * @param mixed $val value to cache 462 * @param integer $lifetime how long to cache (if applicable for the backend) 463 * @param boolean $session store in the session instead of the enabled cache 464 * @return boolean 465 */ 466 public function set($key, $val, $lifetime=600, $session=false) { 467 if ($session || $this->type == 'session') { 468 return $this->session_set($key, $val, false); 469 } 470 return $this->generic_set($key, $val, $lifetime); 471 } 472 473 /** 474 * @param string $key name of value to fetch 475 * @param mixed $default value to return if not found 476 * @param boolean $session fetch from the session instead of the enabled cache 477 * @return mixed 478 */ 479 public function get($key, $default=false, $session=false) { 480 if ($session || $this->type == 'session') { 481 return $this->session_get($key, $default); 482 } 483 return $this->{$this->type.'_get'}($key, $default); 484 } 485 486 /** 487 * @param string $key name to delete 488 * @param boolean $session fetch from the session instead of the enabled cache 489 * @return boolean 490 */ 491 public function del($key, $session=false) { 492 if ($session || $this->type == 'session') { 493 return $this->session_del($key); 494 } 495 return $this->generic_del($key); 496 } 497 498 /** 499 * @param string $key name of value to fetch 500 * @param mixed $default value to return if not found 501 * @return mixed 502 */ 503 private function redis_get($key, $default) { 504 $res = $this->backend->get($this->key_hash($key), $this->session->enc_key); 505 if (!$res) { 506 $this->log($key, 'miss'); 507 return $default; 508 } 509 $this->log($key, 'hit'); 510 return $res; 511 } 512 513 /** 514 * @param string $key name of value to fetch 515 * @param mixed $default value to return if not found 516 * @return mixed 517 */ 518 private function memcache_get($key, $default) { 519 $res = $this->backend->get($this->key_hash($key), $this->session->enc_key); 520 if (!$res && $this->backend->last_err() == Memcached::RES_NOTFOUND) { 521 $this->log($key, 'miss'); 522 return $default; 523 } 524 $this->log($key, 'hit'); 525 return $res; 526 } 527 528 /* 529 * @param string $key name of value to cache 530 * @param mixed $val value to cache 531 * @param integer $lifetime how long to cache (if applicable for the backend) 532 * @return boolean 533 */ 534 private function session_set($key, $val, $lifetime) { 535 $this->log($key, 'save'); 536 $this->session->set($this->key_hash($key), $val); 537 return true; 538 } 539 540 /** 541 * @param string $key name of value to fetch 542 * @param mixed $default value to return if not found 543 * @return mixed 544 */ 545 private function session_get($key, $default) { 546 $res = $this->session->get($this->key_hash($key), $default); 547 if ($res === $default) { 548 $this->log($key, 'miss'); 549 return $default; 550 } 551 $this->log($key, 'hit'); 552 return $res; 553 } 554 555 /** 556 * @param string $key name to delete 557 * @return boolean 558 */ 559 private function session_del($key) { 560 $this->log($key, 'del'); 561 return $this->session->del($this->key_hash($key)); 562 } 563 564 /** 565 * @param string $key name of value to fetch 566 * @param mixed $default value to return if not found 567 * @return mixed 568 */ 569 private function noop_get($key, $default) { 570 return $default; 571 } 572 573 /* 574 * @param string $key key to make the hash unique 575 * @return string 576 */ 577 private function key_hash($key) { 578 return sprintf('hm_cache_%s', hash('sha256', (sprintf('%s%s%s%s', $key, SITE_ID, 579 $this->session->get('fingerprint'), $this->session->get('username'))))); 580 } 581 582 /** 583 * @param string $key name to delete 584 * @return boolean 585 */ 586 private function generic_del($key) { 587 $this->log($key, 'del'); 588 return $this->backend->del($this->key_hash($key)); 589 } 590 591 /** 592 * @param string $key name of value to cache 593 * @param mixed $val value to cache 594 * @param integer $lifetime how long to cache (if applicable for the backend) 595 * @return boolean 596 */ 597 private function generic_set($key, $val, $lifetime) { 598 $this->log($key, 'save'); 599 return $this->backend->set($this->key_hash($key), $val, $lifetime, $this->session->enc_key); 600 } 601} 602