Completed
Push — master ( 1f89ca...fd92ed )
by Lukas
12s
created

lib/cache/cache.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * @author Lukas Reschke <[email protected]>
5
 * @author Thomas Müller <[email protected]>
6
 *
7
 * Mail
8
 *
9
 * This code is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License, version 3,
11
 * as published by the Free Software Foundation.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU Affero General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public License, version 3,
19
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
20
 *
21
 */
22
23
namespace OCA\Mail\Cache;
24
25
use Horde_Imap_Client_Cache_Backend;
26
use Horde_Imap_Client_Exception;
27
use InvalidArgumentException;
28
29
/**
30
 * Class UserCache
31
 *
32
 * This class is inspired by Horde_Imap_Client_Cache_Backend_Cache of the Horde Project
33
 *
34
 * @package OCA\Mail\Cache
35
 */
36
class Cache extends Horde_Imap_Client_Cache_Backend
37
{
38
	/** Cache structure version. */
39
	const VERSION = 3;
40
41
	/**
42
	 * The cache object.
43
	 *
44
	 * @var \OCP\ICache
45
	 */
46
	protected $_cache;
47
48
	/**
49
	 * The working data for the current pageload.  All changes take place to
50
	 * this data.
51
	 *
52
	 * @var array
53
	 */
54
	protected $_data = array();
55
56
	/**
57
	 * The list of cache slices loaded.
58
	 *
59
	 * @var array
60
	 */
61
	protected $_loaded = array();
62
63
	/**
64
	 * The mapping of UIDs to slices.
65
	 *
66
	 * @var array
67
	 */
68
	protected $_slicemap = array();
69
70
	/**
71
	 * The list of items to update:
72
	 *   - add: (array) List of IDs that were added.
73
	 *   - slice: (array) List of slices that were modified.
74
	 *   - slicemap: (boolean) Was slicemap info changed?
75
	 *
76
	 * @var array
77
	 */
78
	protected $_update = array();
79
80
	/**
81
	 * Constructor.
82
	 *
83
	 * @param array $params  Configuration parameters:
84
	 */
85
	public function __construct(array $params = array())
86
	{
87
		// Default parameters.
88
		$params = array_merge(array(
89
			'lifetime' => 604800,
90
			'slicesize' => 50
91
		), array_filter($params));
92
93
		if (!isset($params['cacheob'])) {
94
			throw new InvalidArgumentException('Missing cacheob parameter.');
95
		}
96
97
		foreach (array('lifetime', 'slicesize') as $val) {
98
			$params[$val] = intval($params[$val]);
99
		}
100
101
		parent::__construct($params);
102
	}
103
104
	/**
105
	 * Initialization tasks.
106
	 */
107
	protected function _initOb()
108
	{
109
		$this->_cache = $this->_params['cacheob'];
110
		register_shutdown_function(array($this, 'save'));
111
	}
112
113
	/**
114
	 * Updates the cache.
115
	 */
116
	public function save()
117
	{
118
		$lifetime = $this->_params['lifetime'];
119
120
		foreach ($this->_update as $mbox => $val) {
121
			$s = &$this->_slicemap[$mbox];
122
123
			if (!empty($val['add'])) {
124
				if ($s['c'] <= $this->_params['slicesize']) {
125
					$val['slice'][] = $s['i'];
126
					$this->_loadSlice($mbox, $s['i']);
127
				}
128
				$val['slicemap'] = true;
129
130
				foreach (array_keys(array_flip($val['add'])) as $uid) {
131
					if ($s['c']++ > $this->_params['slicesize']) {
132
						$s['c'] = 0;
133
						$val['slice'][] = ++$s['i'];
134
						$this->_loadSlice($mbox, $s['i']);
135
					}
136
					$s['s'][$uid] = $s['i'];
137
				}
138
			}
139
140
			if (!empty($val['slice'])) {
141
				$d = &$this->_data[$mbox];
142
				$val['slicemap'] = true;
143
144
				foreach (array_keys(array_flip($val['slice'])) as $slice) {
145
					$data = array();
146
					foreach (array_keys($s['s'], $slice) as $uid) {
147
						$data[$uid] = is_array($d[$uid])
148
							? serialize($d[$uid])
149
							: $d[$uid];
150
					}
151
					$this->_cache->set($this->_getCid($mbox, $slice), serialize($data), $lifetime);
152
				}
153
			}
154
155
			if (!empty($val['slicemap'])) {
156
				$this->_cache->set($this->_getCid($mbox, 'slicemap'), serialize($s), $lifetime);
157
			}
158
		}
159
160
		$this->_update = array();
161
	}
162
163
	/** {@inheritDoc} */
164
	public function get($mailbox, $uids, $fields, $uidvalid)
165
	{
166
		$ret = array();
167
		$this->_loadUids($mailbox, $uids, $uidvalid);
168
169
		if (empty($this->_data[$mailbox])) {
170
			return $ret;
171
		}
172
173
		if (!empty($fields)) {
174
			$fields = array_flip($fields);
175
		}
176
		$ptr = &$this->_data[$mailbox];
177
178
		foreach (array_intersect($uids, array_keys($ptr)) as $val) {
179
			if (is_string($ptr[$val])) {
180
				$ptr[$val] = @unserialize($ptr[$val]);
181
			}
182
183
			$ret[$val] = (empty($fields) || empty($ptr[$val]))
184
				? $ptr[$val]
185
				: array_intersect_key($ptr[$val], $fields);
186
		}
187
188
		return $ret;
189
	}
190
191
	/** {@inheritDoc} */
192
	public function getCachedUids($mailbox, $uidvalid)
193
	{
194
		$this->_loadSliceMap($mailbox, $uidvalid);
195
		return array_unique(array_merge(
196
			array_keys($this->_slicemap[$mailbox]['s']),
197
			(isset($this->_update[$mailbox]) ? $this->_update[$mailbox]['add'] : array())
198
		));
199
	}
200
201
	/** {@inheritDoc} */
202
	public function set($mailbox, $data, $uidvalid)
203
	{
204
		$update = array_keys($data);
205
206
		try {
207
			$this->_loadUids($mailbox, $update, $uidvalid);
208
		} catch (Horde_Imap_Client_Exception $e) {
0 ignored issues
show
The class Horde_Imap_Client_Exception does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
209
			// Ignore invalidity - just start building the new cache
210
		}
211
212
		$d = &$this->_data[$mailbox];
213
		$s = &$this->_slicemap[$mailbox]['s'];
214
		$add = $updated = array();
215
216
		foreach ($data as $k => $v) {
217
			if (isset($d[$k])) {
218
				if (is_string($d[$k])) {
219
					$d[$k] = @unserialize($d[$k]);
220
				}
221
				$d[$k] = is_array($d[$k])
222
					? array_merge($d[$k], $v)
223
					: $v;
224
				if (isset($s[$k])) {
225
					$updated[$s[$k]] = true;
226
				}
227
			} else {
228
				$d[$k] = $v;
229
				$add[] = $k;
230
			}
231
		}
232
233
		$this->_toUpdate($mailbox, 'add', $add);
234
		$this->_toUpdate($mailbox, 'slice', array_keys($updated));
235
	}
236
237
	/** {@inheritDoc} */
238
	public function getMetaData($mailbox, $uidvalid, $entries)
239
	{
240
		$this->_loadSliceMap($mailbox, $uidvalid);
241
242
		return empty($entries)
243
			? $this->_slicemap[$mailbox]['d']
244
			: array_intersect_key($this->_slicemap[$mailbox]['d'], array_flip($entries));
245
	}
246
247
	/** {@inheritDoc} */
248
	public function setMetaData($mailbox, $data)
249
	{
250
		$this->_loadSliceMap($mailbox, isset($data['uidvalid']) ? $data['uidvalid'] : null);
251
		$this->_slicemap[$mailbox]['d'] = array_merge($this->_slicemap[$mailbox]['d'], $data);
252
		$this->_toUpdate($mailbox, 'slicemap', true);
253
	}
254
255
	/** {@inheritDoc} */
256
	public function deleteMsgs($mailbox, $uids)
257
	{
258
		$this->_loadSliceMap($mailbox);
259
260
		$slicemap = &$this->_slicemap[$mailbox];
261
		$deleted = array_intersect_key($slicemap['s'], array_flip($uids));
262
263
		if (isset($this->_update[$mailbox])) {
264
			$this->_update[$mailbox]['add'] = array_diff(
265
				$this->_update[$mailbox]['add'],
266
				$uids
267
			);
268
		}
269
270
		if (empty($deleted)) {
271
			return;
272
		}
273
274
		$this->_loadUids($mailbox, array_keys($deleted));
275
		$d = &$this->_data[$mailbox];
276
277
		foreach (array_keys($deleted) as $id) {
278
			unset($d[$id], $slicemap['s'][$id]);
279
		}
280
281
		foreach (array_unique($deleted) as $slice) {
282
			/* Get rid of slice if less than 10% of capacity. */
283
			if (($slice != $slicemap['i']) &&
284
				($slice_uids = array_keys($slicemap['s'], $slice)) &&
285
				($this->_params['slicesize'] * 0.1) > count($slice_uids)) {
286
				$this->_toUpdate($mailbox, 'add', $slice_uids);
287
				$this->_cache->remove($this->_getCid($mailbox, $slice));
288
				foreach ($slice_uids as $val) {
289
					unset($slicemap['s'][$val]);
290
				}
291
			} else {
292
				$this->_toUpdate($mailbox, 'slice', array($slice));
293
			}
294
		}
295
	}
296
297
	/** {@inheritDoc} */
298
	public function deleteMailbox($mailbox)
299
	{
300
		$this->_loadSliceMap($mailbox);
301
		$this->_deleteMailbox($mailbox);
302
	}
303
304
	/** {@inheritDoc} */
305
	public function clear($lifetime)
306
	{
307
		$this->_cache->clear();
308
		$this->_data = $this->_loaded = $this->_slicemap = $this->_update = array();
309
	}
310
311
	/**
312
	 * Create the unique ID used to store the data in the cache.
313
	 *
314
	 * @param string $mailbox  The mailbox to cache.
315
	 * @param string $slice    The cache slice.
316
	 *
317
	 * @return string  The cache ID.
318
	 */
319
	protected function _getCid($mailbox, $slice)
320
	{
321
		return implode('|', array(
322
			'horde_imap_client',
323
			$this->_params['username'],
324
			$mailbox,
325
			$this->_params['hostspec'],
326
			$this->_params['port'],
327
			$slice,
328
			self::VERSION
329
		));
330
	}
331
332
	/**
333
	 * Delete a mailbox from the cache.
334
	 *
335
	 * @param string $mbox  The mailbox to delete.
336
	 */
337
	protected function _deleteMailbox($mbox)
338
	{
339
		foreach (array_merge(array_keys(array_flip($this->_slicemap[$mbox]['s'])), array('slicemap')) as $slice) {
340
			$cid = $this->_getCid($mbox, $slice);
341
			$this->_cache->remove($cid);
342
			unset($this->_loaded[$cid]);
343
		}
344
345
		unset(
346
		$this->_data[$mbox],
347
		$this->_slicemap[$mbox],
348
		$this->_update[$mbox]
349
		);
350
	}
351
352
	/**
353
	 * Load UIDs by regenerating from the cache.
354
	 *
355
	 * @param string $mailbox    The mailbox to load.
356
	 * @param array $uids        The UIDs to load.
357
	 * @param integer $uidvalid  The IMAP uidvalidity value of the mailbox.
358
	 */
359
	protected function _loadUids($mailbox, $uids, $uidvalid = null)
360
	{
361
		if (!isset($this->_data[$mailbox])) {
362
			$this->_data[$mailbox] = array();
363
		}
364
365
		$this->_loadSliceMap($mailbox, $uidvalid);
366
367
		if (!empty($uids)) {
368
			foreach (array_unique(array_intersect_key($this->_slicemap[$mailbox]['s'], array_flip($uids))) as $slice) {
369
				$this->_loadSlice($mailbox, $slice);
370
			}
371
		}
372
	}
373
374
	/**
375
	 * Load UIDs from a cache slice.
376
	 *
377
	 * @param string $mailbox  The mailbox to load.
378
	 * @param integer $slice   The slice to load.
379
	 */
380
	protected function _loadSlice($mailbox, $slice)
381
	{
382
		$cache_id = $this->_getCid($mailbox, $slice);
383
384
		if (!empty($this->_loaded[$cache_id])) {
385
			return;
386
		}
387
388
		if ((($data = $this->_cache->get($cache_id, 0)) !== false) &&
389
			($data = @unserialize($data)) &&
390
			is_array($data)) {
391
			$this->_data[$mailbox] += $data;
392
			$this->_loaded[$cache_id] = true;
393
		} else {
394
			$ptr = &$this->_slicemap[$mailbox];
395
396
			// Slice data is corrupt; remove from slicemap.
397
			foreach (array_keys($ptr['s'], $slice) as $val) {
398
				unset($ptr['s'][$val]);
399
			}
400
401
			if ($slice == $ptr['i']) {
402
				$ptr['c'] = 0;
403
			}
404
		}
405
	}
406
407
	/**
408
	 * Load the slicemap for a given mailbox.  The slicemap contains
409
	 * the uidvalidity information, the UIDs->slice lookup table, and any
410
	 * metadata that needs to be saved for the mailbox.
411
	 *
412
	 * @param string $mailbox    The mailbox.
413
	 * @param integer $uidvalid  The IMAP uidvalidity value of the mailbox.
414
	 */
415
	protected function _loadSliceMap($mailbox, $uidvalid = null)
416
	{
417
		if (!isset($this->_slicemap[$mailbox]) &&
418
			(($data = $this->_cache->get($this->_getCid($mailbox, 'slicemap'), 0)) !== false) &&
419
			($slice = @unserialize($data)) &&
420
			is_array($slice)) {
421
			$this->_slicemap[$mailbox] = $slice;
422
		}
423
424
		if (isset($this->_slicemap[$mailbox])) {
425
			$ptr = &$this->_slicemap[$mailbox];
426
			if (is_null($ptr['d']['uidvalid'])) {
427
				$ptr['d']['uidvalid'] = $uidvalid;
428
				return;
429
			} elseif (!is_null($uidvalid) &&
430
				($ptr['d']['uidvalid'] != $uidvalid)) {
431
				$this->_deleteMailbox($mailbox);
432
			} else {
433
				return;
434
			}
435
		}
436
437
		$this->_slicemap[$mailbox] = array(
438
			// Tracking count for purposes of determining slices
439
			'c' => 0,
440
			// Metadata storage
441
			// By default includes UIDVALIDITY of mailbox.
442
			'd' => array('uidvalid' => $uidvalid),
443
			// The ID of the last slice.
444
			'i' => 0,
445
			// The slice list.
446
			's' => array()
447
		);
448
	}
449
450
	/**
451
	 * Add update entry for a mailbox.
452
	 *
453
	 * @param string $mailbox  The mailbox.
454
	 * @param string $type     'add', 'slice', or 'slicemap'.
455
	 * @param mixed $data      The data to update.
456
	 */
457
	protected function _toUpdate($mailbox, $type, $data)
458
	{
459
		if (!isset($this->_update[$mailbox])) {
460
			$this->_update[$mailbox] = array(
461
				'add' => array(),
462
				'slice' => array()
463
			);
464
		}
465
466
		$this->_update[$mailbox][$type] = ($type == 'slicemap')
467
			? $data
468
			: array_merge($this->_update[$mailbox][$type], $data);
469
	}
470
471
	/* Serializable methods. */
472
473
	/**
474
	 */
475
	public function serialize()
476
	{
477
		$this->save();
478
		return parent::serialize();
479
	}
480
481
}
482