Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117751977
rcube_cache_shared.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
18 KB
Referenced Files
None
Subscribers
None
rcube_cache_shared.php
View Options
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2011-2013, The Roundcube Dev Team |
| Copyright (C) 2011-2013, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Shared (cross-user) caching engine |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing Roundcube shared cache
*
* @package Framework
* @subpackage Cache
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class
rcube_cache_shared
{
/**
* Instance of database handler
*
* @var rcube_db|Memcache|bool
*/
private
$db
;
private
$type
;
private
$prefix
;
private
$ttl
;
private
$packed
;
private
$index
;
private
$table
;
private
$debug
;
private
$index_changed
=
false
;
private
$cache
=
array
();
private
$cache_changes
=
array
();
private
$cache_sums
=
array
();
private
$max_packet
=
-
1
;
/**
* Object constructor.
*
* @param string $type Engine type ('db' or 'memcache' or 'apc')
* @param string $prefix Key name prefix
* @param string $ttl Expiration time of memcache/apc items
* @param bool $packed Enables/disabled data serialization.
* It's possible to disable data serialization if you're sure
* stored data will be always a safe string
*/
function
__construct
(
$type
,
$prefix
=
''
,
$ttl
=
0
,
$packed
=
true
)
{
$rcube
=
rcube
::
get_instance
();
$type
=
strtolower
(
$type
);
if
(
$type
==
'memcache'
)
{
$this
->
type
=
'memcache'
;
$this
->
db
=
$rcube
->
get_memcache
();
$this
->
debug
=
$rcube
->
config
->
get
(
'memcache_debug'
);
}
else
if
(
$type
==
'apc'
)
{
$this
->
type
=
'apc'
;
$this
->
db
=
function_exists
(
'apc_exists'
);
// APC 3.1.4 required
$this
->
debug
=
$rcube
->
config
->
get
(
'apc_debug'
);
}
else
{
$this
->
type
=
'db'
;
$this
->
db
=
$rcube
->
get_dbh
();
$this
->
table
=
$this
->
db
->
table_name
(
'cache_shared'
,
true
);
}
// convert ttl string to seconds
$ttl
=
get_offset_sec
(
$ttl
);
if
(
$ttl
>
2592000
)
$ttl
=
2592000
;
$this
->
ttl
=
$ttl
;
$this
->
packed
=
$packed
;
$this
->
prefix
=
$prefix
;
}
/**
* Returns cached value.
*
* @param string $key Cache key name
*
* @return mixed Cached value
*/
function
get
(
$key
)
{
if
(!
array_key_exists
(
$key
,
$this
->
cache
))
{
return
$this
->
read_record
(
$key
);
}
return
$this
->
cache
[
$key
];
}
/**
* Sets (add/update) value in cache.
*
* @param string $key Cache key name
* @param mixed $data Cache data
*/
function
set
(
$key
,
$data
)
{
$this
->
cache
[
$key
]
=
$data
;
$this
->
cache_changes
[
$key
]
=
true
;
}
/**
* Returns cached value without storing it in internal memory.
*
* @param string $key Cache key name
*
* @return mixed Cached value
*/
function
read
(
$key
)
{
if
(
array_key_exists
(
$key
,
$this
->
cache
))
{
return
$this
->
cache
[
$key
];
}
return
$this
->
read_record
(
$key
,
true
);
}
/**
* Sets (add/update) value in cache and immediately saves
* it in the backend, no internal memory will be used.
*
* @param string $key Cache key name
* @param mixed $data Cache data
*
* @param boolean True on success, False on failure
*/
function
write
(
$key
,
$data
)
{
return
$this
->
write_record
(
$key
,
$this
->
serialize
(
$data
));
}
/**
* Clears the cache.
*
* @param string $key Cache key name or pattern
* @param boolean $prefix_mode Enable it to clear all keys starting
* with prefix specified in $key
*/
function
remove
(
$key
=
null
,
$prefix_mode
=
false
)
{
// Remove all keys
if
(
$key
===
null
)
{
$this
->
cache
=
array
();
$this
->
cache_changes
=
array
();
$this
->
cache_sums
=
array
();
}
// Remove keys by name prefix
else
if
(
$prefix_mode
)
{
foreach
(
array_keys
(
$this
->
cache
)
as
$k
)
{
if
(
strpos
(
$k
,
$key
)
===
0
)
{
$this
->
cache
[
$k
]
=
null
;
$this
->
cache_changes
[
$k
]
=
false
;
unset
(
$this
->
cache_sums
[
$k
]);
}
}
}
// Remove one key by name
else
{
$this
->
cache
[
$key
]
=
null
;
$this
->
cache_changes
[
$key
]
=
false
;
unset
(
$this
->
cache_sums
[
$key
]);
}
// Remove record(s) from the backend
$this
->
remove_record
(
$key
,
$prefix_mode
);
}
/**
* Remove cache records older than ttl
*/
function
expunge
()
{
if
(
$this
->
type
==
'db'
&&
$this
->
db
&&
$this
->
ttl
)
{
$this
->
db
->
query
(
"DELETE FROM {$this->table}"
.
" WHERE `cache_key` LIKE ?"
.
" AND `expires` < "
.
$this
->
db
->
now
(),
$this
->
prefix
.
'.%'
);
}
}
/**
* Remove expired records of all caches
*/
static
function
gc
()
{
$rcube
=
rcube
::
get_instance
();
$db
=
$rcube
->
get_dbh
();
$db
->
query
(
"DELETE FROM "
.
$db
->
table_name
(
'cache_shared'
,
true
)
.
" WHERE `expires` < "
.
$db
->
now
());
}
/**
* Writes the cache back to the DB.
*/
function
close
()
{
foreach
(
$this
->
cache
as
$key
=>
$data
)
{
// The key has been used
if
(
$this
->
cache_changes
[
$key
])
{
// Make sure we're not going to write unchanged data
// by comparing current md5 sum with the sum calculated on DB read
$data
=
$this
->
serialize
(
$data
);
if
(!
$this
->
cache_sums
[
$key
]
||
$this
->
cache_sums
[
$key
]
!=
md5
(
$data
))
{
$this
->
write_record
(
$key
,
$data
);
}
}
}
if
(
$this
->
index_changed
)
{
$this
->
write_index
();
}
// reset internal cache index, thanks to this we can force index reload
$this
->
index
=
null
;
$this
->
index_changed
=
false
;
$this
->
cache
=
array
();
$this
->
cache_sums
=
array
();
$this
->
cache_changes
=
array
();
}
/**
* Reads cache entry.
*
* @param string $key Cache key name
* @param boolean $nostore Enable to skip in-memory store
*
* @return mixed Cached value
*/
private
function
read_record
(
$key
,
$nostore
=
false
)
{
if
(!
$this
->
db
)
{
return
null
;
}
if
(
$this
->
type
!=
'db'
)
{
$this
->
load_index
();
// Consistency check (#1490390)
if
(!
in_array
(
$key
,
$this
->
index
))
{
// we always check if the key exist in the index
// to have data in consistent state. Keeping the index consistent
// is needed for keys delete operation when we delete all keys or by prefix.
}
else
{
$ckey
=
$this
->
ckey
(
$key
);
if
(
$this
->
type
==
'memcache'
)
{
$data
=
$this
->
db
->
get
(
$ckey
);
}
else
if
(
$this
->
type
==
'apc'
)
{
$data
=
apc_fetch
(
$ckey
);
}
if
(
$this
->
debug
)
{
$this
->
debug
(
'get'
,
$ckey
,
$data
);
}
}
if
(
$data
!==
false
)
{
$md5sum
=
md5
(
$data
);
$data
=
$this
->
unserialize
(
$data
);
if
(
$nostore
)
{
return
$data
;
}
$this
->
cache_sums
[
$key
]
=
$md5sum
;
$this
->
cache
[
$key
]
=
$data
;
}
else
{
$this
->
cache
[
$key
]
=
null
;
}
}
else
{
$sql_result
=
$this
->
db
->
query
(
"SELECT `data`, `cache_key` FROM {$this->table}"
.
" WHERE `cache_key` = ?"
,
$this
->
prefix
.
'.'
.
$key
);
if
(
$sql_arr
=
$this
->
db
->
fetch_assoc
(
$sql_result
))
{
if
(
strlen
(
$sql_arr
[
'data'
])
>
0
)
{
$md5sum
=
md5
(
$sql_arr
[
'data'
]);
$data
=
$this
->
unserialize
(
$sql_arr
[
'data'
]);
}
$this
->
db
->
reset
();
if
(
$nostore
)
{
return
$data
;
}
$this
->
cache
[
$key
]
=
$data
;
$this
->
cache_sums
[
$key
]
=
$md5sum
;
}
else
{
$this
->
cache
[
$key
]
=
null
;
}
}
return
$this
->
cache
[
$key
];
}
/**
* Writes single cache record into DB.
*
* @param string $key Cache key name
* @param mixed $data Serialized cache data
*
* @param boolean True on success, False on failure
*/
private
function
write_record
(
$key
,
$data
)
{
if
(!
$this
->
db
)
{
return
false
;
}
// don't attempt to write too big data sets
if
(
strlen
(
$data
)
>
$this
->
max_packet_size
())
{
trigger_error
(
"rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write "
.
strlen
(
$data
)
.
" bytes"
,
E_USER_WARNING
);
return
false
;
}
if
(
$this
->
type
==
'memcache'
||
$this
->
type
==
'apc'
)
{
$result
=
$this
->
add_record
(
$this
->
ckey
(
$key
),
$data
);
// make sure index will be updated
if
(
$result
)
{
if
(!
array_key_exists
(
$key
,
$this
->
cache_sums
))
{
$this
->
cache_sums
[
$key
]
=
true
;
}
$this
->
load_index
();
if
(!
$this
->
index_changed
&&
!
in_array
(
$key
,
$this
->
index
))
{
$this
->
index_changed
=
true
;
}
}
return
$result
;
}
$db_key
=
$this
->
prefix
.
'.'
.
$key
;
// Remove NULL rows (here we don't need to check if the record exist)
if
(
$data
==
'N;'
)
{
$result
=
$this
->
db
->
query
(
"DELETE FROM {$this->table} WHERE `cache_key` = ?"
,
$db_key
);
return
!
$this
->
db
->
is_error
(
$result
);
}
$key_exists
=
array_key_exists
(
$key
,
$this
->
cache_sums
);
$expires
=
$this
->
ttl
?
$this
->
db
->
now
(
$this
->
ttl
)
:
'NULL'
;
if
(!
$key_exists
)
{
// Try INSERT temporarily ignoring "duplicate key" errors
$this
->
db
->
set_option
(
'ignore_key_errors'
,
true
);
$result
=
$this
->
db
->
query
(
"INSERT INTO {$this->table} (`expires`, `cache_key`, `data`)"
.
" VALUES ($expires, ?, ?)"
,
$db_key
,
$data
);
$this
->
db
->
set_option
(
'ignore_key_errors'
,
false
);
}
// otherwise try UPDATE
if
(!
isset
(
$result
)
||
!(
$count
=
$this
->
db
->
affected_rows
(
$result
)))
{
$result
=
$this
->
db
->
query
(
"UPDATE {$this->table} SET `expires` = $expires, `data` = ?"
.
" WHERE `cache_key` = ?"
,
$data
,
$db_key
);
$count
=
$this
->
db
->
affected_rows
(
$result
);
}
return
$count
>
0
;
}
/**
* Deletes the cache record(s).
*
* @param string $key Cache key name or pattern
* @param boolean $prefix_mode Enable it to clear all keys starting
* with prefix specified in $key
*/
private
function
remove_record
(
$key
=
null
,
$prefix_mode
=
false
)
{
if
(!
$this
->
db
)
{
return
;
}
if
(
$this
->
type
!=
'db'
)
{
$this
->
load_index
();
// Remove all keys
if
(
$key
===
null
)
{
foreach
(
$this
->
index
as
$key
)
{
$this
->
delete_record
(
$this
->
ckey
(
$key
));
}
$this
->
index
=
array
();
}
// Remove keys by name prefix
else
if
(
$prefix_mode
)
{
foreach
(
$this
->
index
as
$idx
=>
$k
)
{
if
(
strpos
(
$k
,
$key
)
===
0
)
{
$this
->
delete_record
(
$this
->
ckey
(
$k
));
unset
(
$this
->
index
[
$idx
]);
}
}
}
// Remove one key by name
else
{
$this
->
delete_record
(
$this
->
ckey
(
$key
));
if
((
$idx
=
array_search
(
$key
,
$this
->
index
))
!==
false
)
{
unset
(
$this
->
index
[
$idx
]);
}
}
$this
->
index_changed
=
true
;
return
;
}
// Remove all keys (in specified cache)
if
(
$key
===
null
)
{
$where
=
" WHERE `cache_key` LIKE "
.
$this
->
db
->
quote
(
$this
->
prefix
.
'.%'
);
}
// Remove keys by name prefix
else
if
(
$prefix_mode
)
{
$where
=
" WHERE `cache_key` LIKE "
.
$this
->
db
->
quote
(
$this
->
prefix
.
'.'
.
$key
.
'%'
);
}
// Remove one key by name
else
{
$where
=
" WHERE `cache_key` = "
.
$this
->
db
->
quote
(
$this
->
prefix
.
'.'
.
$key
);
}
$this
->
db
->
query
(
"DELETE FROM "
.
$this
->
table
.
$where
);
}
/**
* Adds entry into memcache/apc DB.
*
* @param string $key Cache internal key name
* @param mixed $data Serialized cache data
*
* @param boolean True on success, False on failure
*/
private
function
add_record
(
$key
,
$data
)
{
if
(
$this
->
type
==
'memcache'
)
{
$result
=
$this
->
db
->
replace
(
$key
,
$data
,
MEMCACHE_COMPRESSED
,
$this
->
ttl
);
if
(!
$result
)
{
$result
=
$this
->
db
->
set
(
$key
,
$data
,
MEMCACHE_COMPRESSED
,
$this
->
ttl
);
}
}
else
if
(
$this
->
type
==
'apc'
)
{
if
(
apc_exists
(
$key
))
{
apc_delete
(
$key
);
}
$result
=
apc_store
(
$key
,
$data
,
$this
->
ttl
);
}
if
(
$this
->
debug
)
{
$this
->
debug
(
'set'
,
$key
,
$data
,
$result
);
}
return
$result
;
}
/**
* Deletes entry from memcache/apc DB.
*
* @param string $key Cache internal key name
*
* @param boolean True on success, False on failure
*/
private
function
delete_record
(
$key
)
{
if
(
$this
->
type
==
'memcache'
)
{
// #1488592: use 2nd argument
$result
=
$this
->
db
->
delete
(
$key
,
0
);
}
else
{
$result
=
apc_delete
(
$key
);
}
if
(
$this
->
debug
)
{
$this
->
debug
(
'delete'
,
$key
,
null
,
$result
);
}
return
$result
;
}
/**
* Writes the index entry into memcache/apc DB.
*/
private
function
write_index
()
{
if
(!
$this
->
db
||
$this
->
type
==
'db'
)
{
return
;
}
$this
->
load_index
();
// Make sure index contains new keys
foreach
(
$this
->
cache
as
$key
=>
$value
)
{
if
(
$value
!==
null
&&
!
in_array
(
$key
,
$this
->
index
))
{
$this
->
index
[]
=
$key
;
}
}
// new keys added using self::write()
foreach
(
$this
->
cache_sums
as
$key
=>
$value
)
{
if
(
$value
===
true
&&
!
in_array
(
$key
,
$this
->
index
))
{
$this
->
index
[]
=
$key
;
}
}
$data
=
serialize
(
$this
->
index
);
$this
->
add_record
(
$this
->
ikey
(),
$data
);
}
/**
* Gets the index entry from memcache/apc DB.
*/
private
function
load_index
()
{
if
(!
$this
->
db
||
$this
->
type
==
'db'
)
{
return
;
}
if
(
$this
->
index
!==
null
)
{
return
;
}
$index_key
=
$this
->
ikey
();
if
(
$this
->
type
==
'memcache'
)
{
$data
=
$this
->
db
->
get
(
$index_key
);
}
else
if
(
$this
->
type
==
'apc'
)
{
$data
=
apc_fetch
(
$index_key
);
}
if
(
$this
->
debug
)
{
$this
->
debug
(
'get'
,
$index_key
,
$data
);
}
$this
->
index
=
$data
?
unserialize
(
$data
)
:
array
();
}
/**
* Creates cache key name (for memcache and apc)
*
* @param string $key Cache key name
*
* @return string Cache key
*/
private
function
ckey
(
$key
)
{
return
$this
->
prefix
.
':'
.
$key
;
}
/**
* Creates index cache key name (for memcache and apc)
*
* @return string Cache key
*/
private
function
ikey
()
{
// This way each cache will have its own index
return
$this
->
prefix
.
'INDEX'
;
}
/**
* Serializes data for storing
*/
private
function
serialize
(
$data
)
{
if
(
$this
->
type
==
'db'
)
{
return
$this
->
db
->
encode
(
$data
,
$this
->
packed
);
}
return
$this
->
packed
?
serialize
(
$data
)
:
$data
;
}
/**
* Unserializes serialized data
*/
private
function
unserialize
(
$data
)
{
if
(
$this
->
type
==
'db'
)
{
return
$this
->
db
->
decode
(
$data
,
$this
->
packed
);
}
return
$this
->
packed
?
@
unserialize
(
$data
)
:
$data
;
}
/**
* Determine the maximum size for cache data to be written
*/
private
function
max_packet_size
()
{
if
(
$this
->
max_packet
<
0
)
{
$this
->
max_packet
=
2097152
;
// default/max is 2 MB
if
(
$this
->
type
==
'db'
)
{
if
(
$value
=
$this
->
db
->
get_variable
(
'max_allowed_packet'
,
$this
->
max_packet
))
{
$this
->
max_packet
=
$value
;
}
$this
->
max_packet
-=
2000
;
}
else
{
$max_packet
=
rcube
::
get_instance
()->
config
->
get
(
$this
->
type
.
'_max_allowed_packet'
);
$this
->
max_packet
=
parse_bytes
(
$max_packet
)
?:
$this
->
max_packet
;
}
}
return
$this
->
max_packet
;
}
/**
* Write memcache/apc debug info to the log
*/
private
function
debug
(
$type
,
$key
,
$data
=
null
,
$result
=
null
)
{
$line
=
strtoupper
(
$type
)
.
' '
.
$key
;
if
(
$data
!==
null
)
{
$line
.=
' '
.
(
$this
->
packed
?
$data
:
serialize
(
$data
));
}
rcube
::
debug
(
$this
->
type
,
$line
,
$result
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, Apr 4, 3:39 AM (1 d, 18 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
4c/0c/26fc1a539ba0962a0098a6d46443
Default Alt Text
rcube_cache_shared.php (18 KB)
Attached To
Mode
R113 roundcubemail
Attached
Detach File
Event Timeline