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