1 <?php
2 /**
3 * @package Joomla.Platform
4 * @subpackage Cache
5 *
6 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 /**
13 * Memcache cache storage handler
14 *
15 * @link https://secure.php.net/manual/en/book.memcache.php
16 * @since 11.1
17 */
18 class JCacheStorageMemcache extends JCacheStorage
19 {
20 /**
21 * Memcache connection object
22 *
23 * @var Memcache
24 * @since 11.1
25 */
26 protected static $_db = null;
27
28 /**
29 * Payload compression level
30 *
31 * @var integer
32 * @since 11.1
33 */
34 protected $_compress = 0;
35
36 /**
37 * Constructor
38 *
39 * @param array $options Optional parameters.
40 *
41 * @since 11.1
42 */
43 public function __construct($options = array())
44 {
45 parent::__construct($options);
46
47 $this->_compress = JFactory::getConfig()->get('memcache_compress', false) ? MEMCACHE_COMPRESSED : 0;
48
49 if (static::$_db === null)
50 {
51 $this->getConnection();
52 }
53 }
54
55 /**
56 * Create the Memcache connection
57 *
58 * @return void
59 *
60 * @since 11.1
61 * @throws RuntimeException
62 */
63 protected function getConnection()
64 {
65 if (!static::isSupported())
66 {
67 throw new RuntimeException('Memcache Extension is not available');
68 }
69
70 $config = JFactory::getConfig();
71
72 $host = $config->get('memcache_server_host', 'localhost');
73 $port = $config->get('memcache_server_port', 11211);
74
75 // Create the memcache connection
76 static::$_db = new Memcache;
77
78 if ($config->get('memcache_persist', true))
79 {
80 $result = @static::$_db->pconnect($host, $port);
81 }
82 else
83 {
84 $result = @static::$_db->connect($host, $port);
85 }
86
87 if (!$result)
88 {
89 // Null out the connection to inform the constructor it will need to attempt to connect if this class is instantiated again
90 static::$_db = null;
91
92 throw new JCacheExceptionConnecting('Could not connect to memcache server');
93 }
94 }
95
96 /**
97 * Get a cache_id string from an id/group pair
98 *
99 * @param string $id The cache data id
100 * @param string $group The cache data group
101 *
102 * @return string The cache_id string
103 *
104 * @since 11.1
105 */
106 protected function _getCacheId($id, $group)
107 {
108 $prefix = JCache::getPlatformPrefix();
109 $length = strlen($prefix);
110 $cache_id = parent::_getCacheId($id, $group);
111
112 if ($length)
113 {
114 // Memcache use suffix instead of prefix
115 $cache_id = substr($cache_id, $length) . strrev($prefix);
116 }
117
118 return $cache_id;
119 }
120
121 /**
122 * Check if the cache contains data stored by ID and group
123 *
124 * @param string $id The cache data ID
125 * @param string $group The cache data group
126 *
127 * @return boolean
128 *
129 * @since 3.7.0
130 */
131 public function contains($id, $group)
132 {
133 return $this->get($id, $group) !== false;
134 }
135
136 /**
137 * Get cached data by ID and group
138 *
139 * @param string $id The cache data ID
140 * @param string $group The cache data group
141 * @param boolean $checkTime True to verify cache time expiration threshold
142 *
143 * @return mixed Boolean false on failure or a cached data object
144 *
145 * @since 11.1
146 */
147 public function get($id, $group, $checkTime = true)
148 {
149 return static::$_db->get($this->_getCacheId($id, $group));
150 }
151
152 /**
153 * Get all cached data
154 *
155 * @return mixed Boolean false on failure or a cached data object
156 *
157 * @since 11.1
158 */
159 public function getAll()
160 {
161 $keys = static::$_db->get($this->_hash . '-index');
162 $secret = $this->_hash;
163
164 $data = array();
165
166 if (is_array($keys))
167 {
168 foreach ($keys as $key)
169 {
170 if (empty($key))
171 {
172 continue;
173 }
174
175 $namearr = explode('-', $key->name);
176
177 if ($namearr !== false && $namearr[0] == $secret && $namearr[1] == 'cache')
178 {
179 $group = $namearr[2];
180
181 if (!isset($data[$group]))
182 {
183 $item = new JCacheStorageHelper($group);
184 }
185 else
186 {
187 $item = $data[$group];
188 }
189
190 $item->updateSize($key->size);
191
192 $data[$group] = $item;
193 }
194 }
195 }
196
197 return $data;
198 }
199
200 /**
201 * Store the data to cache by ID and group
202 *
203 * @param string $id The cache data ID
204 * @param string $group The cache data group
205 * @param string $data The data to store in cache
206 *
207 * @return boolean
208 *
209 * @since 11.1
210 */
211 public function store($id, $group, $data)
212 {
213 $cache_id = $this->_getCacheId($id, $group);
214
215 if (!$this->lockindex())
216 {
217 return false;
218 }
219
220 $index = static::$_db->get($this->_hash . '-index');
221
222 if (!is_array($index))
223 {
224 $index = array();
225 }
226
227 $tmparr = new stdClass;
228 $tmparr->name = $cache_id;
229 $tmparr->size = strlen($data);
230
231 $index[] = $tmparr;
232 static::$_db->set($this->_hash . '-index', $index, 0, 0);
233 $this->unlockindex();
234
235 static::$_db->set($cache_id, $data, $this->_compress, $this->_lifetime);
236
237 return true;
238 }
239
240 /**
241 * Remove a cached data entry by ID and group
242 *
243 * @param string $id The cache data ID
244 * @param string $group The cache data group
245 *
246 * @return boolean
247 *
248 * @since 11.1
249 */
250 public function remove($id, $group)
251 {
252 $cache_id = $this->_getCacheId($id, $group);
253
254 if (!$this->lockindex())
255 {
256 return false;
257 }
258
259 $index = static::$_db->get($this->_hash . '-index');
260
261 if (is_array($index))
262 {
263 foreach ($index as $key => $value)
264 {
265 if ($value->name == $cache_id)
266 {
267 unset($index[$key]);
268 static::$_db->set($this->_hash . '-index', $index, 0, 0);
269 break;
270 }
271 }
272 }
273
274 $this->unlockindex();
275
276 return static::$_db->delete($cache_id);
277 }
278
279 /**
280 * Clean cache for a group given a mode.
281 *
282 * group mode : cleans all cache in the group
283 * notgroup mode : cleans all cache not in the group
284 *
285 * @param string $group The cache data group
286 * @param string $mode The mode for cleaning cache [group|notgroup]
287 *
288 * @return boolean
289 *
290 * @since 11.1
291 */
292 public function clean($group, $mode = null)
293 {
294 if (!$this->lockindex())
295 {
296 return false;
297 }
298
299 $index = static::$_db->get($this->_hash . '-index');
300
301 if (is_array($index))
302 {
303 $prefix = $this->_hash . '-cache-' . $group . '-';
304
305 foreach ($index as $key => $value)
306 {
307 if (strpos($value->name, $prefix) === 0 xor $mode != 'group')
308 {
309 static::$_db->delete($value->name);
310 unset($index[$key]);
311 }
312 }
313
314 static::$_db->set($this->_hash . '-index', $index, 0, 0);
315 }
316
317 $this->unlockindex();
318
319 return true;
320 }
321
322 /**
323 * Flush all existing items in storage.
324 *
325 * @return boolean
326 *
327 * @since 3.6.3
328 */
329 public function flush()
330 {
331 if (!$this->lockindex())
332 {
333 return false;
334 }
335
336 return static::$_db->flush();
337 }
338
339 /**
340 * Test to see if the storage handler is available.
341 *
342 * @return boolean
343 *
344 * @since 12.1
345 */
346 public static function isSupported()
347 {
348 return extension_loaded('memcache') && class_exists('Memcache');
349 }
350
351 /**
352 * Lock cached item
353 *
354 * @param string $id The cache data ID
355 * @param string $group The cache data group
356 * @param integer $locktime Cached item max lock time
357 *
358 * @return mixed Boolean false if locking failed or an object containing properties lock and locklooped
359 *
360 * @since 11.1
361 */
362 public function lock($id, $group, $locktime)
363 {
364 $returning = new stdClass;
365 $returning->locklooped = false;
366
367 $looptime = $locktime * 10;
368
369 $cache_id = $this->_getCacheId($id, $group);
370
371 $data_lock = static::$_db->add($cache_id . '_lock', 1, 0, $locktime);
372
373 if ($data_lock === false)
374 {
375 $lock_counter = 0;
376
377 // Loop until you find that the lock has been released.
378 // That implies that data get from other thread has finished.
379 while ($data_lock === false)
380 {
381 if ($lock_counter > $looptime)
382 {
383 break;
384 }
385
386 usleep(100);
387 $data_lock = static::$_db->add($cache_id . '_lock', 1, 0, $locktime);
388 $lock_counter++;
389 }
390
391 $returning->locklooped = true;
392 }
393
394 $returning->locked = $data_lock;
395
396 return $returning;
397 }
398
399 /**
400 * Unlock cached item
401 *
402 * @param string $id The cache data ID
403 * @param string $group The cache data group
404 *
405 * @return boolean
406 *
407 * @since 11.1
408 */
409 public function unlock($id, $group = null)
410 {
411 $cache_id = $this->_getCacheId($id, $group) . '_lock';
412 return static::$_db->delete($cache_id);
413 }
414
415 /**
416 * Lock cache index
417 *
418 * @return boolean
419 *
420 * @since 11.1
421 */
422 protected function lockindex()
423 {
424 $looptime = 300;
425 $data_lock = static::$_db->add($this->_hash . '-index_lock', 1, 0, 30);
426
427 if ($data_lock === false)
428 {
429 $lock_counter = 0;
430
431 // Loop until you find that the lock has been released. that implies that data get from other thread has finished
432 while ($data_lock === false)
433 {
434 if ($lock_counter > $looptime)
435 {
436 return false;
437 }
438
439 usleep(100);
440 $data_lock = static::$_db->add($this->_hash . '-index_lock', 1, 0, 30);
441 $lock_counter++;
442 }
443 }
444
445 return true;
446 }
447
448 /**
449 * Unlock cache index
450 *
451 * @return boolean
452 *
453 * @since 11.1
454 */
455 protected function unlockindex()
456 {
457 return static::$_db->delete($this->_hash . '-index_lock');
458 }
459 }
460