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 RSS) with an alternative.
20: * @package framework
21: * @subpackage cache
22: */
23: trait Hm_Uid_Cache {
24:
25: /* UID list */
26: private static $read = [];
27: private static $unread = [];
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_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: */
109: trait 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), 'warning');
205: return false;
206: }
207: if (!$this->supported) {
208: Hm_Debug::add(sprintf('%s enabled but not supported by PHP', $this->type), 'warning');
209: return false;
210: }
211: return true;
212: }
213: }
214:
215: /**
216: * Redis cache
217: * @package framework
218: * @subpackage cache
219: */
220: class Hm_Redis {
221:
222: use Hm_Cache_Base;
223: private $type = 'Redis';
224: private $db_index;
225: private $socket;
226:
227: /**
228: * @param Hm_Config $config site config object
229: */
230: public function __construct($config) {
231: $this->server = $config->get('redis_server', false);
232: $this->port = $config->get('redis_port', false);
233: $this->enabled = $config->get('enable_redis', false);
234: $this->db_index = $config->get('redis_index', 0);
235: $this->socket = $config->get('redis_socket', '');
236: $this->supported = Hm_Functions::class_exists('Redis');
237: $this->config = $config;
238: }
239:
240: /**
241: * @return boolean
242: */
243: private function connect() {
244: $this->cache_con = Hm_Functions::redis();
245: try {
246: if ($this->socket) {
247: $con = $this->cache_con->connect($this->socket);
248: }
249: else {
250: $con = $this->cache_con->connect($this->server, $this->port);
251: }
252: if ($con) {
253: $this->auth();
254: $this->cache_con->select($this->db_index);
255: return true;
256: }
257: else {
258: $this->cache_con = false;
259: return false;
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 boolean
270: */
271: public function reconnect() {
272: return $this->connect();
273: }
274:
275: /**
276: * @return void
277: */
278: private function auth() {
279: if ($this->config->get('redis_pass')) {
280: $this->cache_con->auth($this->config->get('redis_pass'));
281: }
282: }
283:
284: /**
285: * @param string $key cache key to delete
286: */
287: public function del($key) {
288: if (!$this->is_active()) {
289: return false;
290: }
291: return $this->cache_con->del($key);
292: }
293:
294:
295: /**
296: * @return boolean
297: */
298: public function close() {
299: if (!$this->is_active()) {
300: return false;
301: }
302: return $this->cache_con->close();
303: }
304: }
305:
306: /**
307: * Memcached cache
308: * @package framework
309: * @subpackage cache
310: */
311: class Hm_Memcached {
312:
313: use Hm_Cache_Base;
314: private $type = 'Memcached';
315:
316: /**
317: * @param Hm_Config $config site config object
318: */
319: public function __construct($config) {
320: $this->server = $config->get('memcached_server', false);
321: $this->port = $config->get('memcached_port', false);
322: $this->enabled = $config->get('enable_memcached', false);
323: $this->supported = Hm_Functions::class_exists('Memcached');
324: $this->config = $config;
325: }
326:
327: /**
328: * @return void
329: */
330: private function auth() {
331: if ($this->config->get('memcached_auth')) {
332: $this->cache_con->setOption(Memcached::OPT_BINARY_PROTOCOL, true);
333: $this->cache_con->setSaslAuthData($this->config->get('memcached_user'),
334: $this->config->get('memcached_pass'));
335: }
336: }
337:
338: /*
339: * @return boolean
340: */
341: private function connect() {
342: $this->cache_con = Hm_Functions::memcached();
343: $this->auth();
344: if (!$this->cache_con->addServer($this->server, $this->port)) {
345: Hm_Debug::add('Memcached addServer failed');
346: $this->cache_con = false;
347: return false;
348: }
349: return true;
350: }
351:
352: /**
353: * @return boolean
354: */
355: public function reconnect() {
356: return $this->connect();
357: }
358:
359: /**
360: * @return mixed
361: */
362: public function last_err() {
363: if (!$this->is_active()) {
364: return false;
365: }
366: return $this->cache_con->getResultCode();
367: }
368:
369: /**
370: * @return boolean
371: */
372: public function close() {
373: if (!$this->is_active()) {
374: return false;
375: }
376: return $this->cache_con->quit();
377: }
378: }
379:
380: /**
381: * @package framework
382: * @subpackage cache
383: */
384: class Hm_Noop_Cache {
385:
386: public function del($key) {
387: return true;
388: }
389:
390: public function set($key, $val, $lifetime, $crypt_key) {
391: return false;
392: }
393:
394: /**
395: * @return boolean
396: */
397: public function reconnect(){
398: return true;
399: }
400: }
401:
402: /**
403: * Generic cache
404: * @package framework
405: * @subpackage cache
406: */
407: class Hm_Cache {
408:
409: private $backend;
410: private $session;
411: public $type;
412:
413: /**
414: * @param Hm_Config $config site config object
415: * @param object $session session object
416: * @return void
417: */
418: public function __construct($config, $session) {
419: $this->session = $session;
420: if (!$this->check_redis($config) && !$this->check_memcache($config)) {
421: $this->check_session($config);
422: }
423: Hm_Debug::add(sprintf('CACHE backend using: %s', $this->type), 'info');
424: }
425:
426: /**
427: * @param Hm_Config $config site config object
428: * @return void
429: */
430: protected function check_session($config) {
431: $this->type = 'noop';
432: $this->backend = new Hm_Noop_Cache();
433: if ($config->get('allow_session_cache')) {
434: $this->type = 'session';
435: }
436: }
437:
438: /**
439: * @param Hm_Config $config site config object
440: * @return boolean
441: */
442: protected function check_redis($config) {
443: if ($config->get('enable_redis', false)) {
444: $backend = new Hm_Redis($config);
445: if ($backend->is_active()) {
446: $this->type = 'redis';
447: $this->backend = $backend;
448: return true;
449: }
450: }
451: return false;
452: }
453:
454: /**
455: * @param Hm_Config $config site config object
456: * @return boolean
457: */
458: protected function check_memcache($config) {
459: if ($config->get('enable_memcached', false)) {
460: $backend = new Hm_Memcached($config);
461: if ($backend->is_active()) {
462: $this->type = 'memcache';
463: $this->backend = $backend;
464: return true;
465: }
466: }
467: return false;
468: }
469:
470: /**
471: * @param string $key key name
472: * @param string $msg_type log message
473: * @return void
474: */
475: protected function log($key, $msg_type) {
476: switch ($msg_type) {
477: case 'save':
478: Hm_Debug::add(sprintf('CACHE: saving "%s" using %s', $key, $this->type), 'info');
479: break;
480: case 'hit':
481: Hm_Debug::add(sprintf('CACHE: hit for "%s" using %s', $key, $this->type), 'info');
482: break;
483: case 'miss':
484: Hm_Debug::add(sprintf('CACHE: miss for "%s" using %s', $key, $this->type), 'warning');
485: break;
486: case 'del':
487: Hm_Debug::add(sprintf('CACHE: deleting "%s" using %s', $key, $this->type), 'info');
488: break;
489: }
490: }
491:
492: /**
493: * @param string $key name of value to cache
494: * @param mixed $val value to cache
495: * @param integer $lifetime how long to cache (if applicable for the backend)
496: * @param boolean $session store in the session instead of the enabled cache
497: * @return boolean
498: */
499: public function set($key, $val, $lifetime = 600, $session = false) {
500: if ($session || $this->type == 'session') {
501: return $this->session_set($key, $val, false);
502: }
503: return $this->generic_set($key, $val, $lifetime);
504: }
505:
506: /**
507: * @param string $key name of value to fetch
508: * @param mixed $default value to return if not found
509: * @param boolean $session fetch from the session instead of the enabled cache
510: * @return mixed
511: */
512: public function get($key, $default = false, $session = false) {
513: if ($session || $this->type == 'session') {
514: return $this->session_get($key, $default);
515: }
516: return $this->{$this->type.'_get'}($key, $default);
517: }
518:
519: /**
520: * @param string $key name to delete
521: * @param boolean $session fetch from the session instead of the enabled cache
522: * @return boolean
523: */
524: public function del($key, $session = false) {
525: if ($session || $this->type == 'session') {
526: return $this->session_del($key);
527: }
528: return $this->generic_del($key);
529: }
530:
531: /**
532: * @param string $key name of value to fetch
533: * @param mixed $default value to return if not found
534: * @return mixed
535: */
536: protected function redis_get($key, $default) {
537: $res = $this->backend->get($this->key_hash($key), $this->session->enc_key);
538: if (!$res) {
539: $this->log($key, 'miss');
540: return $default;
541: }
542: $this->log($key, 'hit');
543: return $res;
544: }
545:
546: /**
547: * @param string $key name of value to fetch
548: * @param mixed $default value to return if not found
549: * @return mixed
550: */
551: protected function memcache_get($key, $default) {
552: $res = $this->backend->get($this->key_hash($key), $this->session->enc_key);
553: if (!$res && $this->backend->last_err() == Memcached::RES_NOTFOUND) {
554: $this->log($key, 'miss');
555: return $default;
556: }
557: $this->log($key, 'hit');
558: return $res;
559: }
560:
561: /*
562: * @param string $key name of value to cache
563: * @param mixed $val value to cache
564: * @param integer $lifetime how long to cache (if applicable for the backend)
565: * @return boolean
566: */
567: protected function session_set($key, $val, $lifetime) {
568: $this->log($key, 'save');
569: $this->session->set($this->key_hash($key), $val);
570: return true;
571: }
572:
573: /**
574: * @param string $key name of value to fetch
575: * @param mixed $default value to return if not found
576: * @return mixed
577: */
578: protected function session_get($key, $default) {
579: $res = $this->session->get($this->key_hash($key), $default);
580: if ($res === $default) {
581: $this->log($key, 'miss');
582: return $default;
583: }
584: $this->log($key, 'hit');
585: return $res;
586: }
587:
588: /**
589: * @param string $key name to delete
590: * @return boolean
591: */
592: protected function session_del($key) {
593: $this->log($key, 'del');
594: return $this->session->del($this->key_hash($key));
595: }
596:
597: /**
598: * @param string $key name of value to fetch
599: * @param mixed $default value to return if not found
600: * @return mixed
601: */
602: protected function noop_get($key, $default) {
603: return $default;
604: }
605:
606: /*
607: * @param string $key key to make the hash unique
608: * @return string
609: */
610: protected function key_hash($key) {
611: return sprintf('hm_cache_%s', hash('sha256', (sprintf('%s%s%s%s', $key, SITE_ID,
612: $this->session->get('fingerprint'), $this->session->get('username')))));
613: }
614:
615: /**
616: * @param string $key name to delete
617: * @return boolean
618: */
619: protected function generic_del($key) {
620: $this->log($key, 'del');
621: return $this->backend->del($this->key_hash($key));
622: }
623:
624: /**
625: * @param string $key name of value to cache
626: * @param mixed $val value to cache
627: * @param integer $lifetime how long to cache (if applicable for the backend)
628: * @return boolean
629: */
630: protected function generic_set($key, $val, $lifetime) {
631: $this->log($key, 'save');
632: return $this->backend->set($this->key_hash($key), $val, $lifetime, $this->session->enc_key);
633: }
634:
635: /**
636: * Manually reconnect to cache service
637: * @return boolean
638: */
639: public function reconnect() {
640: return $this->backend->reconnect();
641: }
642: }
643:
644: /**
645: * Setup the cache
646: */
647: class Hm_Cache_Setup {
648:
649: private $config;
650: private $session;
651:
652: /**
653: * @param object $config site configuration
654: */
655: public function __construct($config, $session) {
656: $this->config = $config;
657: $this->session = $session;
658: }
659:
660: /**
661: * @return object
662: */
663: public function setup_cache() {
664: $cache_class = $this->get_cache_class();
665: Hm_Debug::add(sprintf('Using %s for cache', $cache_class), 'info');
666: return new $cache_class($this->config, $this->session);
667: }
668:
669: /**
670: * @return string
671: */
672: private function get_cache_class() {
673: $custom_cache_class = $this->config->get('cache_class', 'Custom_Cache');
674: $cache_class = 'Hm_Cache';
675: if (class_exists($custom_cache_class)) {
676: $cache_class = $custom_cache_class;
677: }
678: return $cache_class;
679: }
680: }
681: