Page MenuHomePhorge

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/SQL/oracle.initial.sql b/SQL/oracle.initial.sql
new file mode 100644
index 000000000..84f7a1f16
--- /dev/null
+++ b/SQL/oracle.initial.sql
@@ -0,0 +1,221 @@
+-- Roundcube Webmail initial database structure
+-- This was tested with Oracle 11g
+
+CREATE TABLE "users" (
+ "user_id" integer PRIMARY KEY,
+ "username" varchar(128) NOT NULL,
+ "mail_host" varchar(128) NOT NULL,
+ "created" timestamp with time zone DEFAULT current_timestamp NOT NULL,
+ "last_login" timestamp with time zone DEFAULT NULL,
+ "language" varchar(5),
+ "preferences" long DEFAULT NULL,
+ CONSTRAINT "users_username_key" UNIQUE ("username", "mail_host")
+);
+
+CREATE SEQUENCE "users_seq"
+ START WITH 1 INCREMENT BY 1 NOMAXVALUE;
+
+CREATE TRIGGER "users_seq_trig"
+BEFORE INSERT ON "users" FOR EACH ROW
+BEGIN
+ :NEW."user_id" := "users_seq".nextval;
+END;
+
+
+CREATE TABLE "session" (
+ "sess_id" varchar(128) NOT NULL PRIMARY KEY,
+ "created" timestamp with time zone DEFAULT current_timestamp NOT NULL,
+ "changed" timestamp with time zone DEFAULT current_timestamp NOT NULL,
+ "ip" varchar(41) NOT NULL,
+ "vars" long NOT NULL
+);
+
+CREATE INDEX "session_changed_idx" ON "session" ("changed");
+
+
+CREATE TABLE "identities" (
+ "identity_id" integer PRIMARY KEY,
+ "user_id" integer NOT NULL
+ REFERENCES "users" ("user_id") ON DELETE CASCADE,
+ "changed" timestamp with time zone DEFAULT current_timestamp NOT NULL,
+ "del" smallint DEFAULT 0 NOT NULL,
+ "standard" smallint DEFAULT 0 NOT NULL,
+ "name" varchar(128) NOT NULL,
+ "organization" varchar(128),
+ "email" varchar(128) NOT NULL,
+ "reply-to" varchar(128),
+ "bcc" varchar(128),
+ "signature" long,
+ "html_signature" integer DEFAULT 0 NOT NULL
+);
+
+CREATE INDEX "identities_user_id_idx" ON "identities" ("user_id", "del");
+CREATE INDEX "identities_email_idx" ON "identities" ("email", "del");
+
+CREATE SEQUENCE "identities_seq"
+ START WITH 1 INCREMENT BY 1 NOMAXVALUE;
+
+CREATE TRIGGER "identities_seq_trig"
+BEFORE INSERT ON "identities" FOR EACH ROW
+BEGIN
+ :NEW."identity_id" := "identities_seq".nextval;
+END;
+
+
+CREATE TABLE "contacts" (
+ "contact_id" integer PRIMARY KEY,
+ "user_id" integer NOT NULL
+ REFERENCES "users" ("user_id") ON DELETE CASCADE,
+ "changed" timestamp with time zone DEFAULT current_timestamp NOT NULL,
+ "del" smallint DEFAULT 0 NOT NULL,
+ "name" varchar(128) DEFAULT NULL,
+ "email" varchar(4000) DEFAULT NULL,
+ "firstname" varchar(128) DEFAULT NULL,
+ "surname" varchar(128) DEFAULT NULL,
+ "vcard" long,
+ "words" varchar(4000)
+);
+
+CREATE INDEX "contacts_user_id_idx" ON "contacts" ("user_id", "del");
+
+CREATE SEQUENCE "contacts_seq"
+ START WITH 1 INCREMENT BY 1 NOMAXVALUE;
+
+CREATE TRIGGER "contacts_seq_trig"
+BEFORE INSERT ON "contacts" FOR EACH ROW
+BEGIN
+ :NEW."contact_id" := "contacts_seq".nextval;
+END;
+
+
+CREATE TABLE "contactgroups" (
+ "contactgroup_id" integer PRIMARY KEY,
+ "user_id" integer NOT NULL
+ REFERENCES "users" ("user_id") ON DELETE CASCADE,
+ "changed" timestamp with time zone DEFAULT current_timestamp NOT NULL,
+ "del" smallint DEFAULT 0 NOT NULL,
+ "name" varchar(128) NOT NULL
+);
+
+CREATE INDEX "contactgroups_user_id_idx" ON "contactgroups" ("user_id", "del");
+
+CREATE SEQUENCE "contactgroups_seq"
+ START WITH 1 INCREMENT BY 1 NOMAXVALUE;
+
+CREATE TRIGGER "contactgroups_seq_trig"
+BEFORE INSERT ON "contactgroups" FOR EACH ROW
+BEGIN
+ :NEW."contactgroup_id" := "contactgroups_seq".nextval;
+END;
+
+
+CREATE TABLE "contactgroupmembers" (
+ "contactgroup_id" integer NOT NULL
+ REFERENCES "contactgroups" ("contactgroup_id") ON DELETE CASCADE,
+ "contact_id" integer NOT NULL
+ REFERENCES "contacts" ("contact_id") ON DELETE CASCADE,
+ "created" timestamp with time zone DEFAULT current_timestamp NOT NULL,
+ PRIMARY KEY ("contactgroup_id", "contact_id")
+);
+
+CREATE INDEX "contactgroupmembers_idx" ON "contactgroupmembers" ("contact_id");
+
+
+CREATE TABLE "cache" (
+ "user_id" integer NOT NULL
+ REFERENCES "users" ("user_id") ON DELETE CASCADE,
+ "cache_key" varchar(128) NOT NULL,
+ "created" timestamp with time zone DEFAULT current_timestamp NOT NULL,
+ "expires" timestamp with time zone DEFAULT NULL,
+ "data" long NOT NULL
+);
+
+CREATE INDEX "cache_user_id_idx" ON "cache" ("user_id", "cache_key");
+CREATE INDEX "cache_expires_idx" ON "cache" ("expires");
+
+
+CREATE TABLE "cache_shared" (
+ "cache_key" varchar(255) NOT NULL,
+ "created" timestamp with time zone DEFAULT current_timestamp NOT NULL,
+ "expires" timestamp with time zone DEFAULT NULL,
+ "data" long NOT NULL
+);
+
+CREATE INDEX "cache_shared_cache_key_idx" ON "cache_shared" ("cache_key");
+CREATE INDEX "cache_shared_expires_idx" ON "cache_shared" ("expires");
+
+
+CREATE TABLE "cache_index" (
+ "user_id" integer NOT NULL
+ REFERENCES "users" ("user_id") ON DELETE CASCADE,
+ "mailbox" varchar(255) NOT NULL,
+ "expires" timestamp with time zone DEFAULT NULL,
+ "valid" smallint DEFAULT 0 NOT NULL,
+ "data" long NOT NULL,
+ PRIMARY KEY ("user_id", "mailbox")
+);
+
+CREATE INDEX "cache_index_expires_idx" ON "cache_index" ("expires");
+
+
+CREATE TABLE "cache_thread" (
+ "user_id" integer NOT NULL
+ REFERENCES "users" ("user_id") ON DELETE CASCADE,
+ "mailbox" varchar(255) NOT NULL,
+ "expires" timestamp with time zone DEFAULT NULL,
+ "data" long NOT NULL,
+ PRIMARY KEY ("user_id", "mailbox")
+);
+
+CREATE INDEX "cache_thread_expires_idx" ON "cache_thread" ("expires");
+
+
+CREATE TABLE "cache_messages" (
+ "user_id" integer NOT NULL
+ REFERENCES "users" ("user_id") ON DELETE CASCADE,
+ "mailbox" varchar(255) NOT NULL,
+ "uid" integer NOT NULL,
+ "expires" timestamp with time zone DEFAULT NULL,
+ "data" long NOT NULL,
+ "flags" integer DEFAULT 0 NOT NULL,
+ PRIMARY KEY ("user_id", "mailbox", "uid")
+);
+
+CREATE INDEX "cache_messages_expires_idx" ON "cache_messages" ("expires");
+
+
+CREATE TABLE "dictionary" (
+ "user_id" integer DEFAULT NULL
+ REFERENCES "users" ("user_id") ON DELETE CASCADE,
+ "language" varchar(5) NOT NULL,
+ "data" long DEFAULT NULL,
+ CONSTRAINT "dictionary_user_id_lang_key" UNIQUE ("user_id", "language")
+);
+
+
+CREATE TABLE "searches" (
+ "search_id" integer PRIMARY KEY,
+ "user_id" integer NOT NULL
+ REFERENCES "users" ("user_id") ON DELETE CASCADE,
+ "type" smallint DEFAULT 0 NOT NULL,
+ "name" varchar(128) NOT NULL,
+ "data" long NOT NULL,
+ CONSTRAINT "searches_user_id_key" UNIQUE ("user_id", "type", "name")
+);
+
+CREATE SEQUENCE "searches_seq"
+ START WITH 1 INCREMENT BY 1 NOMAXVALUE;
+
+CREATE TRIGGER "searches_seq_trig"
+BEFORE INSERT ON "searches" FOR EACH ROW
+BEGIN
+ :NEW."search_id" := "searches_seq".nextval;
+END;
+
+
+CREATE TABLE "system" (
+ "name" varchar(64) NOT NULL PRIMARY KEY,
+ "value" long
+);
+
+INSERT INTO "system" ("name", "value") VALUES ('roundcube-version', '2014042900');
diff --git a/bin/cleandb.sh b/bin/cleandb.sh
index 165d33f38..9edfeec91 100755
--- a/bin/cleandb.sh
+++ b/bin/cleandb.sh
@@ -1,78 +1,78 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
| bin/cleandb.sh |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2010, The Roundcube Dev Team |
| |
| 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: |
| Finally remove all db records marked as deleted some time ago |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(dirname(__FILE__) . '/..') . '/' );
require INSTALL_PATH.'program/include/clisetup.php';
// mapping for table name => primary key
$primary_keys = array(
'contacts' => "contact_id",
'contactgroups' => "contactgroup_id",
);
// connect to DB
$RCMAIL = rcube::get_instance();
$db = $RCMAIL->get_dbh();
$db->db_connect('w');
if (!$db->is_connected() || $db->is_error()) {
rcube::raise_error("No DB connection", false, true);
}
if (!empty($_SERVER['argv'][1]))
$days = intval($_SERVER['argv'][1]);
else
$days = 7;
// remove all deleted records older than two days
$threshold = date('Y-m-d 00:00:00', time() - $days * 86400);
foreach (array('contacts','contactgroups','identities') as $table) {
- $sqltable = $db->table_name($table);
+ $sqltable = $db->table_name($table, true);
// also delete linked records
// could be skipped for databases which respect foreign key constraints
if ($db->db_provider == 'sqlite'
&& ($table == 'contacts' || $table == 'contactgroups')
) {
$pk = $primary_keys[$table];
$memberstable = $db->table_name('contactgroupmembers');
$db->query(
- "DELETE FROM $memberstable".
- " WHERE $pk IN (".
- "SELECT $pk FROM $sqltable".
- " WHERE del=1 AND changed < ?".
+ "DELETE FROM " . $db->quote_identifier($memberstable).
+ " WHERE `$pk` IN (".
+ "SELECT `$pk` FROM $sqltable".
+ " WHERE `del` = 1 AND `changed` < ?".
")",
$threshold);
echo $db->affected_rows() . " records deleted from '$memberstable'\n";
}
// delete outdated records
- $db->query("DELETE FROM $sqltable WHERE del=1 AND changed < ?", $threshold);
+ $db->query("DELETE FROM $sqltable WHERE `del` = 1 AND `changed` < ?", $threshold);
echo $db->affected_rows() . " records deleted from '$table'\n";
}
?>
diff --git a/bin/deluser.sh b/bin/deluser.sh
index f12ec9032..9504d5b43 100755
--- a/bin/deluser.sh
+++ b/bin/deluser.sh
@@ -1,103 +1,103 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
| bin/deluser.sh |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2014, The Roundcube Dev Team |
| |
| 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: |
| Utility script to remove all data related to a certain user |
| from the local database. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <thomas@roundcube.net> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(dirname(__FILE__) . '/..') . '/' );
require_once INSTALL_PATH . 'program/include/clisetup.php';
function print_usage()
{
print "Usage: deluser.sh [--host=mail_host] username\n";
print "--host=HOST The IMAP hostname or IP the given user is related to\n";
}
function _die($msg, $usage=false)
{
fputs(STDERR, $msg . "\n");
if ($usage) print_usage();
exit(1);
}
$rcmail = rcmail::get_instance();
// get arguments
$args = rcube_utils::get_opt(array('h' => 'host'));
$username = trim($args[0]);
if (empty($username)) {
_die("Missing required parameters", true);
}
if (empty($args['host'])) {
$hosts = $rcmail->config->get('default_host', '');
if (is_string($hosts)) {
$args['host'] = $hosts;
}
else if (is_array($hosts) && count($hosts) == 1) {
$args['host'] = reset($hosts);
}
else {
_die("Specify a host name", true);
}
// host can be a URL like tls://192.168.12.44
$host_url = parse_url($args['host']);
if ($host_url['host']) {
$args['host'] = $host_url['host'];
}
}
// connect to DB
$db = $rcmail->get_dbh();
$db->db_connect('w');
if (!$db->is_connected() || $db->is_error()) {
_die("No DB connection\n" . $db->is_error());
}
// find user in loca database
$user = rcube_user::query($username, $args['host']);
if (!$user) {
die("User not found.\n");
}
// let plugins cleanup their own user-related data
$plugin = $rcmail->plugins->exec_hook('user_delete', array('user' => $user, 'username' => $username, 'host' => $args['host']));
if ($plugin['abort']) {
_die("User deletion aborted by plugin");
}
// deleting the user record should be sufficient due to ON DELETE CASCADE foreign key references
// but not all database backends actually support this so let's do it by hand
foreach (array('identities','contacts','contactgroups','dictionaries','cache','cache_index','cache_messages','cache_thread','searches','users') as $table) {
- $db->query('DELETE FROM ' . $db->table_name($table) . ' WHERE user_id=?', $user->ID);
+ $db->query('DELETE FROM ' . $db->table_name($table, true) . ' WHERE `user_id` = ?', $user->ID);
}
if ($db->is_error()) {
_die("DB error occurred: " . $db->is_error());
}
else {
echo "Successfully deleted user $user->ID\n";
}
diff --git a/bin/indexcontacts.sh b/bin/indexcontacts.sh
index df403807c..9509dc06a 100755
--- a/bin/indexcontacts.sh
+++ b/bin/indexcontacts.sh
@@ -1,54 +1,54 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
| bin/indexcontacts.sh |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2011, The Roundcube Dev Team |
| |
| 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: |
| Update the fulltext index for all contacts of the internal |
| address book. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(dirname(__FILE__) . '/..') . '/' );
require_once INSTALL_PATH.'program/include/clisetup.php';
ini_set('memory_limit', -1);
// connect to DB
$RCMAIL = rcube::get_instance();
$db = $RCMAIL->get_dbh();
$db->db_connect('w');
if (!$db->is_connected() || $db->is_error()) {
rcube::raise_error("No DB connection", false, true);
}
// iterate over all users
-$sql_result = $db->query("SELECT user_id FROM " . $db->table_name('users') . " ORDER BY user_id");
+$sql_result = $db->query("SELECT `user_id` FROM " . $db->table_name('users', true) . " ORDER BY `user_id`");
while ($sql_result && ($sql_arr = $db->fetch_assoc($sql_result))) {
echo "Indexing contacts for user " . $sql_arr['user_id'] . "...";
$contacts = new rcube_contacts($db, $sql_arr['user_id']);
$contacts->set_pagesize(9999);
$result = $contacts->list_records();
while ($result->count && ($row = $result->next())) {
unset($row['words']);
$contacts->update($row['ID'], $row);
}
echo "done.\n";
}
?>
diff --git a/bin/moduserprefs.sh b/bin/moduserprefs.sh
index 8a9725fa4..e892b1f4d 100755
--- a/bin/moduserprefs.sh
+++ b/bin/moduserprefs.sh
@@ -1,82 +1,82 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
| bin/moduserprefs.sh |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2012, The Roundcube Dev Team |
| |
| 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: |
| Bulk-change settings stored in user preferences |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(dirname(__FILE__) . '/..') . '/' );
require_once INSTALL_PATH.'program/include/clisetup.php';
function print_usage()
{
print "Usage: moduserprefs.sh [--user=user-id] pref-name [pref-value|--delete]\n";
print "--user User ID in local database\n";
print "--delete Unset the given preference\n";
}
// get arguments
$args = rcube_utils::get_opt(array('u' => 'user', 'd' => 'delete'));
if ($_SERVER['argv'][1] == 'help') {
print_usage();
exit;
}
else if (empty($args[0]) || (!isset($args[1]) && !$args['delete'])) {
print "Missing required parameters.\n";
print_usage();
exit;
}
$pref_name = trim($args[0]);
$pref_value = $args['delete'] ? null : trim($args[1]);
// connect to DB
$rcmail = rcube::get_instance();
$db = $rcmail->get_dbh();
$db->db_connect('w');
if (!$db->is_connected() || $db->is_error())
die("No DB connection\n" . $db->is_error());
$query = '1=1';
if ($args['user'])
- $query = 'user_id=' . intval($args['user']);
+ $query = '`user_id` = ' . intval($args['user']);
// iterate over all users
-$sql_result = $db->query("SELECT * FROM " . $db->table_name('users') . " WHERE $query");
+$sql_result = $db->query("SELECT * FROM " . $db->table_name('users', true) . " WHERE $query");
while ($sql_result && ($sql_arr = $db->fetch_assoc($sql_result))) {
echo "Updating prefs for user " . $sql_arr['user_id'] . "...";
$user = new rcube_user($sql_arr['user_id'], $sql_arr);
$prefs = $old_prefs = $user->get_prefs();
$prefs[$pref_name] = $pref_value;
if ($prefs != $old_prefs) {
$user->save_prefs($prefs);
echo "saved.\n";
}
else {
echo "nothing changed.\n";
}
}
?>
diff --git a/bin/updatedb.sh b/bin/updatedb.sh
index 964bc184c..e9818074d 100755
--- a/bin/updatedb.sh
+++ b/bin/updatedb.sh
@@ -1,179 +1,178 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
| bin/updatedb.sh |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2010-2012, The Roundcube Dev Team |
| Copyright (C) 2010-2012, 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: |
| Update database schema |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(dirname(__FILE__) . '/..') . '/' );
require_once INSTALL_PATH . 'program/include/clisetup.php';
// get arguments
$opts = rcube_utils::get_opt(array(
'v' => 'version',
'd' => 'dir',
'p' => 'package',
));
if (empty($opts['dir'])) {
rcube::raise_error("Database schema directory not specified (--dir).", false, true);
}
if (empty($opts['package'])) {
rcube::raise_error("Database schema package name not specified (--package).", false, true);
}
// Check if directory exists
if (!file_exists($opts['dir'])) {
rcube::raise_error("Specified database schema directory doesn't exist.", false, true);
}
$RC = rcube::get_instance();
$DB = rcube_db::factory($RC->config->get('db_dsnw'));
$DB->set_debug((bool)$RC->config->get('sql_debug'));
// Connect to database
$DB->db_connect('w');
if (!$DB->is_connected()) {
rcube::raise_error("Error connecting to database: " . $DB->is_error(), false, true);
}
// Read DB schema version from database (if 'system' table exists)
if (in_array($DB->table_name('system'), (array)$DB->list_tables())) {
- $DB->query("SELECT " . $DB->quote_identifier('value')
- ." FROM " . $DB->quote_identifier($DB->table_name('system'))
- ." WHERE " . $DB->quote_identifier('name') ." = ?",
+ $DB->query("SELECT `value`"
+ ." FROM " . $DB->table_name('system', true)
+ ." WHERE `name` = ?",
$opts['package'] . '-version');
$row = $DB->fetch_array();
$version = preg_replace('/[^0-9]/', '', $row[0]);
}
// DB version not found, but release version is specified
if (!$version && $opts['version']) {
// Map old release version string to DB schema version
// Note: This is for backward compat. only, do not need to be updated
$map = array(
'0.1-stable' => 1,
'0.1.1' => 2008030300,
'0.2-alpha' => 2008040500,
'0.2-beta' => 2008060900,
'0.2-stable' => 2008092100,
'0.2.1' => 2008092100,
'0.2.2' => 2008092100,
'0.3-stable' => 2008092100,
'0.3.1' => 2009090400,
'0.4-beta' => 2009103100,
'0.4' => 2010042300,
'0.4.1' => 2010042300,
'0.4.2' => 2010042300,
'0.5-beta' => 2010100600,
'0.5' => 2010100600,
'0.5.1' => 2010100600,
'0.5.2' => 2010100600,
'0.5.3' => 2010100600,
'0.5.4' => 2010100600,
'0.6-beta' => 2011011200,
'0.6' => 2011011200,
'0.7-beta' => 2011092800,
'0.7' => 2011111600,
'0.7.1' => 2011111600,
'0.7.2' => 2011111600,
'0.7.3' => 2011111600,
'0.7.4' => 2011111600,
'0.8-beta' => 2011121400,
'0.8-rc' => 2011121400,
'0.8.0' => 2011121400,
'0.8.1' => 2011121400,
'0.8.2' => 2011121400,
'0.8.3' => 2011121400,
'0.8.4' => 2011121400,
'0.8.5' => 2011121400,
'0.8.6' => 2011121400,
'0.9-beta' => 2012080700,
);
$version = $map[$opts['version']];
}
// Assume last version before the 'system' table was added
if (empty($version)) {
$version = 2012080700;
}
$dir = $opts['dir'] . '/' . $DB->db_provider;
if (!file_exists($dir)) {
rcube::raise_error("DDL Upgrade files for " . $DB->db_provider . " driver not found.", false, true);
}
$dh = opendir($dir);
$result = array();
while ($file = readdir($dh)) {
if (preg_match('/^([0-9]+)\.sql$/', $file, $m) && $m[1] > $version) {
$result[] = $m[1];
}
}
sort($result, SORT_NUMERIC);
foreach ($result as $v) {
echo "Updating database schema ($v)... ";
$error = update_db_schema($opts['package'], $v, "$dir/$v.sql");
if ($error) {
echo "[FAILED]\n";
rcube::raise_error("Error in DDL upgrade $v: $error", false, true);
}
echo "[OK]\n";
}
function update_db_schema($package, $version, $file)
{
global $DB;
// read DDL file
if ($sql = file_get_contents($file)) {
if (!$DB->exec_script($sql)) {
return $DB->is_error();
}
}
// escape if 'system' table does not exist
if ($version < 2013011000) {
return;
}
- $system_table = $DB->quote_identifier($DB->table_name('system'));
+ $system_table = $DB->table_name('system', true);
$DB->query("UPDATE " . $system_table
- ." SET " . $DB->quote_identifier('value') . " = ?"
- ." WHERE " . $DB->quote_identifier('name') . " = ?",
+ ." SET `value` = ?"
+ ." WHERE `name` = ?",
$version, $package . '-version');
if (!$DB->is_error() && !$DB->affected_rows()) {
$DB->query("INSERT INTO " . $system_table
- ." (" . $DB->quote_identifier('name') . ", " . $DB->quote_identifier('value') . ")"
- ." VALUES (?, ?)",
+ ." (`name`, `value`) VALUES (?, ?)",
$package . '-version', $version);
}
return $DB->is_error();
}
?>
diff --git a/installer/test.php b/installer/test.php
index 72c7a1f76..988451e9f 100644
--- a/installer/test.php
+++ b/installer/test.php
@@ -1,457 +1,459 @@
<?php
if (!class_exists('rcmail_install', false) || !is_object($RCI)) {
die("Not allowed! Please open installer/index.php instead.");
}
?>
<form action="index.php?_step=3" method="post">
<h3>Check config file</h3>
<?php
if ($read_config = is_readable(RCUBE_CONFIG_DIR . 'defaults.inc.php')) {
$config = $RCI->load_config_file(RCUBE_CONFIG_DIR . 'defaults.inc.php');
if (!empty($config)) {
$RCI->pass('defaults.inc.php');
}
else {
$RCI->fail('defaults.inc.php', 'Syntax error');
}
}
else {
$RCI->fail('defaults.inc.php', 'Unable to read default config file?');
}
echo '<br />';
if ($read_config = is_readable(RCUBE_CONFIG_DIR . 'config.inc.php')) {
$config = $RCI->load_config_file(RCUBE_CONFIG_DIR . 'config.inc.php');
if (!empty($config)) {
$RCI->pass('config.inc.php');
}
else {
$RCI->fail('config.inc.php', 'Syntax error');
}
}
else {
$RCI->fail('config.inc.php', 'Unable to read file. Did you create the config file?');
}
echo '<br />';
if ($RCI->configured && ($messages = $RCI->check_config())) {
if (is_array($messages['replaced'])) {
echo '<h3 class="warning">Replaced config options</h3>';
echo '<p class="hint">The following config options have been replaced or renamed. ';
echo 'Please update them accordingly in your config files.</p>';
echo '<ul class="configwarings">';
foreach ($messages['replaced'] as $msg) {
echo html::tag('li', null, html::span('propname', $msg['prop']) .
' was replaced by ' . html::span('propname', $msg['replacement']));
}
echo '</ul>';
}
if (is_array($messages['obsolete'])) {
echo '<h3>Obsolete config options</h3>';
echo '<p class="hint">You still have some obsolete or inexistent properties set. This isn\'t a problem but should be noticed.</p>';
echo '<ul class="configwarings">';
foreach ($messages['obsolete'] as $msg) {
echo html::tag('li', null, html::span('propname', $msg['prop']) . ($msg['name'] ? ':&nbsp;' . $msg['name'] : ''));
}
echo '</ul>';
}
echo '<p class="suggestion">OK, lazy people can download the updated config file here: ';
echo html::a(array('href' => './?_mergeconfig=1'), 'config.inc.php') . ' &nbsp;';
echo "</p>";
if (is_array($messages['dependencies'])) {
echo '<h3 class="warning">Dependency check failed</h3>';
echo '<p class="hint">Some of your configuration settings require other options to be configured or additional PHP modules to be installed</p>';
echo '<ul class="configwarings">';
foreach ($messages['dependencies'] as $msg) {
echo html::tag('li', null, html::span('propname', $msg['prop']) . ': ' . $msg['explain']);
}
echo '</ul>';
}
}
?>
<h3>Check if directories are writable</h3>
<p>Roundcube may need to write/save files into these directories</p>
<?php
$dirs[] = $RCI->config['temp_dir'] ? $RCI->config['temp_dir'] : 'temp';
if ($RCI->config['log_driver'] != 'syslog')
$dirs[] = $RCI->config['log_dir'] ? $RCI->config['log_dir'] : 'logs';
foreach ($dirs as $dir) {
$dirpath = rcube_utils::is_absolute_path($dir) ? $dir : INSTALL_PATH . $dir;
if (is_writable(realpath($dirpath))) {
$RCI->pass($dir);
$pass = true;
}
else {
$RCI->fail($dir, 'not writeable for the webserver');
}
echo '<br />';
}
if (!$pass) {
echo '<p class="hint">Use <tt>chmod</tt> or <tt>chown</tt> to grant write privileges to the webserver</p>';
}
?>
<h3>Check DB config</h3>
<?php
$db_working = false;
if ($RCI->configured) {
if (!empty($RCI->config['db_dsnw'])) {
$DB = rcube_db::factory($RCI->config['db_dsnw'], '', false);
$DB->set_debug((bool)$RCI->config['sql_debug']);
$DB->db_connect('w');
if (!($db_error_msg = $DB->is_error())) {
$RCI->pass('DSN (write)');
echo '<br />';
$db_working = true;
}
else {
$RCI->fail('DSN (write)', $db_error_msg);
echo '<p class="hint">Make sure that the configured database exists and that the user has write privileges<br />';
echo 'DSN: ' . $RCI->config['db_dsnw'] . '</p>';
}
}
else {
$RCI->fail('DSN (write)', 'not set');
}
}
else {
$RCI->fail('DSN (write)', 'Could not read config file');
}
// initialize db with schema found in /SQL/*
if ($db_working && $_POST['initdb']) {
if (!($success = $RCI->init_db($DB))) {
$db_working = false;
echo '<p class="warning">Please try to inizialize the database manually as described in the INSTALL guide.
Make sure that the configured database extists and that the user as write privileges</p>';
}
}
else if ($db_working && $_POST['updatedb']) {
if (!($success = $RCI->update_db($_POST['version']))) {
echo '<p class="warning">Database schema update failed.</p>';
}
}
// test database
if ($db_working) {
- $db_read = $DB->query("SELECT count(*) FROM {$RCI->config['db_prefix']}users");
+ $db_read = $DB->query("SELECT count(*) FROM " . $DB->quote_identifier($RCI->config['db_prefix'] . 'users'));
if ($DB->is_error()) {
$RCI->fail('DB Schema', "Database not initialized");
echo '<p><input type="submit" name="initdb" value="Initialize database" /></p>';
$db_working = false;
}
else if ($err = $RCI->db_schema_check($DB, $update = !empty($_POST['updatedb']))) {
$RCI->fail('DB Schema', "Database schema differs");
echo '<ul style="margin:0"><li>' . join("</li>\n<li>", $err) . "</li></ul>";
$select = $RCI->versions_select(array('name' => 'version'));
$select->add('0.9 or newer', '');
echo '<p class="suggestion">You should run the update queries to get the schema fixed.<br/><br/>Version to update from: ' . $select->show() . '&nbsp;<input type="submit" name="updatedb" value="Update" /></p>';
$db_working = false;
}
else {
$RCI->pass('DB Schema');
echo '<br />';
}
}
// more database tests
if ($db_working) {
// write test
$insert_id = md5(uniqid());
- $db_write = $DB->query("INSERT INTO {$RCI->config['db_prefix']}session (sess_id, created, ip, vars) VALUES (?, ".$DB->now().", '127.0.0.1', 'foo')", $insert_id);
+ $db_write = $DB->query("INSERT INTO " . $DB->quote_identifier($RCI->config['db_prefix'] . 'session')
+ . " (`sess_id`, `created`, `ip`, `vars`) VALUES (?, ".$DB->now().", '127.0.0.1', 'foo')", $insert_id);
if ($db_write) {
$RCI->pass('DB Write');
- $DB->query("DELETE FROM {$RCI->config['db_prefix']}session WHERE sess_id=?", $insert_id);
+ $DB->query("DELETE FROM " . $DB->quote_identifier($RCI->config['db_prefix'] . 'session')
+ . " WHERE `sess_id` = ?", $insert_id);
}
else {
$RCI->fail('DB Write', $RCI->get_error());
}
echo '<br />';
-
+
// check timezone settings
$tz_db = 'SELECT ' . $DB->unixtimestamp($DB->now()) . ' AS tz_db';
$tz_db = $DB->query($tz_db);
$tz_db = $DB->fetch_assoc($tz_db);
$tz_db = (int) $tz_db['tz_db'];
$tz_local = (int) time();
$tz_diff = $tz_local - $tz_db;
// sometimes db and web servers are on separate hosts, so allow a 30 minutes delta
if (abs($tz_diff) > 1800) {
$RCI->fail('DB Time', "Database time differs {$td_ziff}s from PHP time");
}
else {
$RCI->pass('DB Time');
}
}
?>
<h3>Test filetype detection</h3>
<?php
if ($errors = $RCI->check_mime_detection()) {
$RCI->fail('Fileinfo/mime_content_type configuration');
if (!empty($RCI->config['mime_magic'])) {
echo '<p class="hint">Try setting the <tt>mime_magic</tt> config option to <tt>null</tt>.</p>';
}
else {
echo '<p class="hint">Check the <a href="http://www.php.net/manual/en/function.finfo-open.php">Fileinfo functions</a> of your PHP installation.<br/>';
echo 'The path to the magic.mime file can be set using the <tt>mime_magic</tt> config option in Roundcube.</p>';
}
}
else {
$RCI->pass('Fileinfo/mime_content_type configuration');
echo "<br/>";
}
if ($errors = $RCI->check_mime_extensions()) {
$RCI->fail('Mimetype to file extension mapping');
echo '<p class="hint">Please set a valid path to your webserver\'s mime.types file to the <tt>mime_types</tt> config option.<br/>';
echo 'If you can\'t find such a file, download it from <a href="http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types">svn.apache.org</a>.</p>';
}
else {
$RCI->pass('Mimetype to file extension mapping');
echo "<br/>";
}
?>
<h3>Test SMTP config</h3>
<p>
Server: <?php echo rcube_parse_host($RCI->getprop('smtp_server', 'PHP mail()')); ?><br />
Port: <?php echo $RCI->getprop('smtp_port'); ?><br />
<?php
if ($RCI->getprop('smtp_server')) {
$user = $RCI->getprop('smtp_user', '(none)');
$pass = $RCI->getprop('smtp_pass', '(none)');
if ($user == '%u') {
$user_field = new html_inputfield(array('name' => '_smtp_user'));
$user = $user_field->show($_POST['_smtp_user']);
}
if ($pass == '%p') {
$pass_field = new html_passwordfield(array('name' => '_smtp_pass'));
$pass = $pass_field->show();
}
echo "User: $user<br />";
echo "Password: $pass<br />";
}
$from_field = new html_inputfield(array('name' => '_from', 'id' => 'sendmailfrom'));
$to_field = new html_inputfield(array('name' => '_to', 'id' => 'sendmailto'));
?>
</p>
<?php
if (isset($_POST['sendmail'])) {
echo '<p>Trying to send email...<br />';
$from = idn_to_ascii(trim($_POST['_from']));
$to = idn_to_ascii(trim($_POST['_to']));
if (preg_match('/^' . $RCI->email_pattern . '$/i', $from) &&
preg_match('/^' . $RCI->email_pattern . '$/i', $to)
) {
$headers = array(
'From' => $from,
'To' => $to,
'Subject' => 'Test message from Roundcube',
);
$body = 'This is a test to confirm that Roundcube can send email.';
$smtp_response = array();
// send mail using configured SMTP server
if ($RCI->getprop('smtp_server')) {
$CONFIG = $RCI->config;
if (!empty($_POST['_smtp_user'])) {
$CONFIG['smtp_user'] = $_POST['_smtp_user'];
}
if (!empty($_POST['_smtp_pass'])) {
$CONFIG['smtp_pass'] = $_POST['_smtp_pass'];
}
$mail_object = new Mail_mime();
$send_headers = $mail_object->headers($headers);
$SMTP = new rcube_smtp();
$SMTP->connect(rcube_parse_host($RCI->getprop('smtp_server')),
$RCI->getprop('smtp_port'), $CONFIG['smtp_user'], $CONFIG['smtp_pass']);
$status = $SMTP->send_mail($headers['From'], $headers['To'],
($foo = $mail_object->txtHeaders($send_headers)), $body);
$smtp_response = $SMTP->get_response();
}
else { // use mail()
$header_str = 'From: ' . $headers['From'];
if (ini_get('safe_mode'))
$status = mail($headers['To'], $headers['Subject'], $body, $header_str);
else
$status = mail($headers['To'], $headers['Subject'], $body, $header_str, '-f'.$headers['From']);
if (!$status)
$smtp_response[] = 'Mail delivery with mail() failed. Check your error logs for details';
}
if ($status) {
$RCI->pass('SMTP send');
}
else {
$RCI->fail('SMTP send', join('; ', $smtp_response));
}
}
else {
$RCI->fail('SMTP send', 'Invalid sender or recipient');
}
echo '</p>';
}
?>
<table>
<tbody>
<tr>
<td><label for="sendmailfrom">Sender</label></td>
<td><?php echo $from_field->show($_POST['_from']); ?></td>
</tr>
<tr>
<td><label for="sendmailto">Recipient</label></td>
<td><?php echo $to_field->show($_POST['_to']); ?></td>
</tr>
</tbody>
</table>
<p><input type="submit" name="sendmail" value="Send test mail" /></p>
<h3>Test IMAP config</h3>
<?php
$default_hosts = $RCI->get_hostlist();
if (!empty($default_hosts)) {
$host_field = new html_select(array('name' => '_host', 'id' => 'imaphost'));
$host_field->add($default_hosts);
}
else {
$host_field = new html_inputfield(array('name' => '_host', 'id' => 'imaphost'));
}
$user_field = new html_inputfield(array('name' => '_user', 'id' => 'imapuser'));
$pass_field = new html_passwordfield(array('name' => '_pass', 'id' => 'imappass'));
?>
<table>
<tbody>
<tr>
<td><label for="imaphost">Server</label></td>
<td><?php echo $host_field->show($_POST['_host']); ?></td>
</tr>
<tr>
<td>Port</td>
<td><?php echo $RCI->getprop('default_port'); ?></td>
</tr>
<tr>
<td><label for="imapuser">Username</label></td>
<td><?php echo $user_field->show($_POST['_user']); ?></td>
</tr>
<tr>
<td><label for="imappass">Password</label></td>
<td><?php echo $pass_field->show(); ?></td>
</tr>
</tbody>
</table>
<?php
if (isset($_POST['imaptest']) && !empty($_POST['_host']) && !empty($_POST['_user'])) {
echo '<p>Connecting to ' . Q($_POST['_host']) . '...<br />';
$imap_host = trim($_POST['_host']);
$imap_port = $RCI->getprop('default_port');
$a_host = parse_url($imap_host);
if ($a_host['host']) {
$imap_host = $a_host['host'];
$imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
if (isset($a_host['port']))
$imap_port = $a_host['port'];
else if ($imap_ssl && $imap_ssl != 'tls' && (!$imap_port || $imap_port == 143))
$imap_port = 993;
}
$imap_host = idn_to_ascii($imap_host);
$imap_user = idn_to_ascii($_POST['_user']);
$imap = new rcube_imap(null);
$imap->set_options(array(
'auth_type' => $RCI->getprop('imap_auth_type'),
'debug' => $RCI->getprop('imap_debug'),
));
if ($imap->connect($imap_host, $imap_user, $_POST['_pass'], $imap_port, $imap_ssl)) {
$RCI->pass('IMAP connect', 'SORT capability: ' . ($imap->get_capability('SORT') ? 'yes' : 'no'));
$imap->close();
}
else {
$RCI->fail('IMAP connect', $RCI->get_error());
}
}
?>
<p><input type="submit" name="imaptest" value="Check login" /></p>
</form>
<hr />
<p class="warning">
After completing the installation and the final tests please <b>remove</b> the whole
installer folder from the document root of the webserver or make sure that
<tt>enable_installer</tt> option in <tt>config.inc.php</tt> is disabled.<br />
<br />
These files may expose sensitive configuration data like server passwords and encryption keys
to the public. Make sure you cannot access this installer from your browser.
</p>
diff --git a/plugins/squirrelmail_usercopy/squirrelmail_usercopy.php b/plugins/squirrelmail_usercopy/squirrelmail_usercopy.php
index e882a2f37..7f378678e 100644
--- a/plugins/squirrelmail_usercopy/squirrelmail_usercopy.php
+++ b/plugins/squirrelmail_usercopy/squirrelmail_usercopy.php
@@ -1,190 +1,191 @@
<?php
/**
* Copy a new users identity and settings from a nearby Squirrelmail installation
*
* @version 1.4
* @author Thomas Bruederli, Johannes Hessellund, pommi, Thomas Lueder
*/
class squirrelmail_usercopy extends rcube_plugin
{
public $task = 'login';
private $prefs = null;
private $identities_level = 0;
private $abook = array();
public function init()
{
$this->add_hook('user_create', array($this, 'create_user'));
$this->add_hook('identity_create', array($this, 'create_identity'));
}
public function create_user($p)
{
$rcmail = rcmail::get_instance();
// Read plugin's config
$this->initialize();
// read prefs and add email address
$this->read_squirrel_prefs($p['user']);
if (($this->identities_level == 0 || $this->identities_level == 2) && $rcmail->config->get('squirrelmail_set_alias') && $this->prefs['email_address'])
$p['user_email'] = $this->prefs['email_address'];
return $p;
}
public function create_identity($p)
{
$rcmail = rcmail::get_instance();
// prefs are set in create_user()
if ($this->prefs) {
if ($this->prefs['full_name'])
$p['record']['name'] = $this->prefs['full_name'];
if (($this->identities_level == 0 || $this->identities_level == 2) && $this->prefs['email_address'])
$p['record']['email'] = $this->prefs['email_address'];
if ($this->prefs['___signature___'])
$p['record']['signature'] = $this->prefs['___signature___'];
if ($this->prefs['reply_to'])
$p['record']['reply-to'] = $this->prefs['reply_to'];
if (($this->identities_level == 0 || $this->identities_level == 1) && isset($this->prefs['identities']) && $this->prefs['identities'] > 1) {
for ($i=1; $i < $this->prefs['identities']; $i++) {
unset($ident_data);
$ident_data = array('name' => '', 'email' => ''); // required data
if ($this->prefs['full_name'.$i])
$ident_data['name'] = $this->prefs['full_name'.$i];
if ($this->identities_level == 0 && $this->prefs['email_address'.$i])
$ident_data['email'] = $this->prefs['email_address'.$i];
else
$ident_data['email'] = $p['record']['email'];
if ($this->prefs['reply_to'.$i])
$ident_data['reply-to'] = $this->prefs['reply_to'.$i];
if ($this->prefs['___sig'.$i.'___'])
$ident_data['signature'] = $this->prefs['___sig'.$i.'___'];
// insert identity
$rcmail->user->insert_identity($ident_data);
}
}
// copy address book
$contacts = $rcmail->get_address_book(null, true);
if ($contacts && count($this->abook)) {
foreach ($this->abook as $rec) {
// #1487096 handle multi-address and/or too long items
$rec['email'] = array_shift(explode(';', $rec['email']));
if (rcube_utils::check_email(rcube_utils::idn_to_ascii($rec['email']))) {
$rec['email'] = rcube_utils::idn_to_utf8($rec['email']);
$contacts->insert($rec, true);
}
}
}
// mark identity as complete for following hooks
$p['complete'] = true;
}
return $p;
}
private function initialize()
{
$rcmail = rcmail::get_instance();
// Load plugin's config file
$this->load_config();
// Set identities_level for operations of this plugin
$ilevel = $rcmail->config->get('squirrelmail_identities_level');
if ($ilevel === null)
$ilevel = $rcmail->config->get('identities_level', 0);
$this->identities_level = intval($ilevel);
}
private function read_squirrel_prefs($uname)
{
$rcmail = rcmail::get_instance();
/**** File based backend ****/
if ($rcmail->config->get('squirrelmail_driver') == 'file' && ($srcdir = $rcmail->config->get('squirrelmail_data_dir'))) {
if (($hash_level = $rcmail->config->get('squirrelmail_data_dir_hash_level')) > 0)
$srcdir = slashify($srcdir).chunk_split(substr(base_convert(crc32($uname), 10, 16), 0, $hash_level), 1, '/');
$prefsfile = slashify($srcdir) . $uname . '.pref';
$abookfile = slashify($srcdir) . $uname . '.abook';
$sigfile = slashify($srcdir) . $uname . '.sig';
$sigbase = slashify($srcdir) . $uname . '.si';
if (is_readable($prefsfile)) {
$this->prefs = array();
foreach (file($prefsfile) as $line) {
list($key, $value) = explode('=', $line);
$this->prefs[$key] = utf8_encode(rtrim($value));
}
// also read signature file if exists
if (is_readable($sigfile)) {
$this->prefs['___signature___'] = utf8_encode(file_get_contents($sigfile));
}
if (isset($this->prefs['identities']) && $this->prefs['identities'] > 1) {
for ($i=1; $i < $this->prefs['identities']; $i++) {
// read signature file if exists
if (is_readable($sigbase.$i)) {
$this->prefs['___sig'.$i.'___'] = utf8_encode(file_get_contents($sigbase.$i));
}
}
}
// parse addres book file
if (filesize($abookfile)) {
foreach(file($abookfile) as $line) {
list($rec['name'], $rec['firstname'], $rec['surname'], $rec['email']) = explode('|', utf8_encode(rtrim($line)));
if ($rec['name'] && $rec['email'])
$this->abook[] = $rec;
}
}
}
}
/**** Database backend ****/
else if ($rcmail->config->get('squirrelmail_driver') == 'sql') {
$this->prefs = array();
/* connect to squirrelmail database */
$db = rcube_db::factory($rcmail->config->get('squirrelmail_dsn'));
$db->set_debug($rcmail->config->get('sql_debug'));
$db->db_connect('r'); // connect in read mode
/* retrieve prefs */
$userprefs_table = $rcmail->config->get('squirrelmail_userprefs_table');
$address_table = $rcmail->config->get('squirrelmail_address_table');
$db_charset = $rcmail->config->get('squirrelmail_db_charset');
if ($db_charset)
$db->query('SET NAMES '.$db_charset);
- $sql_result = $db->query('SELECT * FROM '.$userprefs_table.' WHERE user=?', $uname); // ? is replaced with emailaddress
+ $sql_result = $db->query('SELECT * FROM ' . $db->quote_identifier($userprefs_table)
+ .' WHERE `user` = ?', $uname); // ? is replaced with emailaddress
while ($sql_array = $db->fetch_assoc($sql_result) ) { // fetch one row from result
$this->prefs[$sql_array['prefkey']] = rcube_charset::convert(rtrim($sql_array['prefval']), $db_charset);
}
/* retrieve address table data */
- $sql_result = $db->query('SELECT * FROM '.$address_table.' WHERE owner=?', $uname); // ? is replaced with emailaddress
+ $sql_result = $db->query('SELECT * FROM ' . $db->quote_identifier($address_table)
+ .' WHERE `owner` = ?', $uname); // ? is replaced with emailaddress
// parse addres book
while ($sql_array = $db->fetch_assoc($sql_result) ) { // fetch one row from result
$rec['name'] = rcube_charset::convert(rtrim($sql_array['nickname']), $db_charset);
$rec['firstname'] = rcube_charset::convert(rtrim($sql_array['firstname']), $db_charset);
$rec['surname'] = rcube_charset::convert(rtrim($sql_array['lastname']), $db_charset);
$rec['email'] = rcube_charset::convert(rtrim($sql_array['email']), $db_charset);
$rec['notes'] = rcube_charset::convert(rtrim($sql_array['label']), $db_charset);
if ($rec['name'] && $rec['email'])
$this->abook[] = $rec;
}
} // end if 'sql'-driver
}
-
}
diff --git a/program/lib/Roundcube/rcube_cache.php b/program/lib/Roundcube/rcube_cache.php
index 0017dcacc..7210ce645 100644
--- a/program/lib/Roundcube/rcube_cache.php
+++ b/program/lib/Roundcube/rcube_cache.php
@@ -1,627 +1,624 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2011, The Roundcube Dev Team |
| Copyright (C) 2011, 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: |
| Caching engine |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing Roundcube cache
*
* @package Framework
* @subpackage Cache
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_cache
{
/**
* Instance of database handler
*
* @var rcube_db|Memcache|bool
*/
private $db;
private $type;
private $userid;
private $prefix;
private $table;
private $ttl;
private $packed;
private $index;
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 int $userid User identifier
* @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, $userid, $prefix='', $ttl=0, $packed=true)
{
$rcube = rcube::get_instance();
$type = strtolower($type);
if ($type == 'memcache') {
$this->type = 'memcache';
$this->db = $rcube->get_memcache();
}
else if ($type == 'apc') {
$this->type = 'apc';
$this->db = function_exists('apc_exists'); // APC 3.1.4 required
}
else {
$this->type = 'db';
$this->db = $rcube->get_dbh();
- $this->table = $this->db->table_name('cache');
+ $this->table = $this->db->table_name('cache', true);
}
// convert ttl string to seconds
$ttl = get_offset_sec($ttl);
if ($ttl > 2592000) $ttl = 2592000;
$this->userid = (int) $userid;
$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_changed = true;
$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_changed = false;
$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 user_id = ?".
- " AND cache_key LIKE ?".
- " AND expires < " . $this->db->now(),
+ "DELETE FROM {$this->table}".
+ " WHERE `user_id` = ?".
+ " AND `cache_key` LIKE ?".
+ " AND `expires` < " . $this->db->now(),
$this->userid,
$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') . " WHERE expires < " . $db->now());
+ $db->query("DELETE FROM " . $db->table_name('cache', true) . " WHERE `expires` < " . $db->now());
}
/**
* Writes the cache back to the DB.
*/
function close()
{
if (!$this->cache_changed) {
return;
}
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);
}
}
}
$this->write_index();
}
/**
* 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') {
if ($this->type == 'memcache') {
$data = $this->db->get($this->ckey($key));
}
else if ($this->type == 'apc') {
$data = apc_fetch($this->ckey($key));
}
if ($data) {
$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->limitquery(
- "SELECT data, cache_key".
- " FROM " . $this->table.
- " WHERE user_id = ?".
- " AND cache_key = ?".
+ "SELECT `data`, `cache_key`".
+ " FROM {$this->table}".
+ " WHERE `user_id` = ? AND `cache_key` = ?".
// for better performance we allow more records for one key
// get the newer one
- " ORDER BY created DESC",
+ " ORDER BY `created` DESC",
0, 1, $this->userid, $this->prefix.'.'.$key);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$key = substr($sql_arr['cache_key'], strlen($this->prefix)+1);
$md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null;
if ($sql_arr['data']) {
$data = $this->unserialize($sql_arr['data']);
}
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') {
return $this->add_record($this->ckey($key), $data);
}
$key_exists = array_key_exists($key, $this->cache_sums);
$key = $this->prefix . '.' . $key;
// Remove NULL rows (here we don't need to check if the record exist)
if ($data == 'N;') {
$this->db->query(
- "DELETE FROM " . $this->table.
- " WHERE user_id = ?".
- " AND cache_key = ?",
+ "DELETE FROM {$this->table}".
+ " WHERE `user_id` = ? AND `cache_key` = ?",
$this->userid, $key);
return true;
}
// update existing cache record
if ($key_exists) {
$result = $this->db->query(
- "UPDATE " . $this->table.
- " SET created = " . $this->db->now().
- ", expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL').
- ", data = ?".
- " WHERE user_id = ?".
- " AND cache_key = ?",
+ "UPDATE {$this->table}".
+ " SET `created` = " . $this->db->now().
+ ", `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL').
+ ", `data` = ?".
+ " WHERE `user_id` = ?".
+ " AND `cache_key` = ?",
$data, $this->userid, $key);
}
// add new cache record
else {
// for better performance we allow more records for one key
// so, no need to check if record exist (see rcube_cache::read_record())
$result = $this->db->query(
- "INSERT INTO " . $this->table.
- " (created, expires, user_id, cache_key, data)".
+ "INSERT INTO {$this->table}".
+ " (`created`, `expires`, `user_id`, `cache_key`, `data`)".
" VALUES (" . $this->db->now() . ", " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?, ?, ?)",
$this->userid, $key, $data);
}
return $this->db->affected_rows($result);
}
/**
* 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($key, false);
}
$this->index = array();
}
// Remove keys by name prefix
else if ($prefix_mode) {
foreach ($this->index as $k) {
if (strpos($k, $key) === 0) {
$this->delete_record($k);
}
}
}
// Remove one key by name
else {
$this->delete_record($key);
}
return;
}
// Remove all keys (in specified cache)
if ($key === null) {
- $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.%');
+ $where = " AND `cache_key` LIKE " . $this->db->quote($this->prefix.'.%');
}
// Remove keys by name prefix
else if ($prefix_mode) {
- $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
+ $where = " AND `cache_key` LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
}
// Remove one key by name
else {
- $where = " AND cache_key = " . $this->db->quote($this->prefix.'.'.$key);
+ $where = " AND `cache_key` = " . $this->db->quote($this->prefix.'.'.$key);
}
$this->db->query(
- "DELETE FROM " . $this->table.
- " WHERE user_id = ?" . $where,
+ "DELETE FROM {$this->table} WHERE `user_id` = ?" . $where,
$this->userid);
}
/**
* Adds entry into memcache/apc DB.
*
* @param string $key Cache key name
* @param mxied $data Serialized cache data
* @param bollean $index Enables immediate index update
*
* @param boolean True on success, False on failure
*/
private function add_record($key, $data, $index=false)
{
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);
}
// Update index
if ($index && $result) {
$this->load_index();
if (array_search($key, $this->index) === false) {
$this->index[] = $key;
$data = serialize($this->index);
$this->add_record($this->ikey(), $data);
}
}
return $result;
}
/**
* Deletes entry from memcache/apc DB.
*/
private function delete_record($key, $index=true)
{
if ($this->type == 'memcache') {
// #1488592: use 2nd argument
$this->db->delete($this->ckey($key), 0);
}
else {
apc_delete($this->ckey($key));
}
if ($index) {
if (($idx = array_search($key, $this->index)) !== false) {
unset($this->index[$idx]);
}
}
}
/**
* Writes the index entry into memcache/apc DB.
*/
private function write_index()
{
if (!$this->db) {
return;
}
if ($this->type == 'db') {
return;
}
$this->load_index();
// Make sure index contains new keys
foreach ($this->cache as $key => $value) {
if ($value !== null) {
if (array_search($key, $this->index) === false) {
$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) {
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);
}
$this->index = $data ? unserialize($data) : array();
}
/**
* Creates per-user cache key name (for memcache and apc)
*
* @param string $key Cache key name
*
* @return string Cache key
*/
private function ckey($key)
{
return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key);
}
/**
* Creates per-user 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 sprintf('%d:%s%s', $this->userid, $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') {
$value = $this->db->get_variable('max_allowed_packet', 1048500);
$this->max_packet = min($value, $this->max_packet) - 2000;
}
else if ($this->type == 'memcache') {
$stats = $this->db->getStats();
$remaining = $stats['limit_maxbytes'] - $stats['bytes'];
$this->max_packet = min($remaining / 5, $this->max_packet);
}
else if ($this->type == 'apc' && function_exists('apc_sma_info')) {
$stats = apc_sma_info();
$this->max_packet = min($stats['avail_mem'] / 5, $this->max_packet);
}
}
return $this->max_packet;
}
}
diff --git a/program/lib/Roundcube/rcube_cache_shared.php b/program/lib/Roundcube/rcube_cache_shared.php
index 8f2574046..a2bf09208 100644
--- a/program/lib/Roundcube/rcube_cache_shared.php
+++ b/program/lib/Roundcube/rcube_cache_shared.php
@@ -1,581 +1,581 @@
<?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 $cache = array();
private $cache_changes = array();
private $cache_sums = array();
/**
* 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();
}
else if ($type == 'apc') {
$this->type = 'apc';
$this->db = function_exists('apc_exists'); // APC 3.1.4 required
}
else {
$this->type = 'db';
$this->db = $rcube->get_dbh();
- $this->table = $this->db->table_name('cache_shared');
+ $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_changed = true;
$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_changed = false;
$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(),
+ "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') . " WHERE expires < " . $db->now());
+ $db->query("DELETE FROM " . $db->table_name('cache_shared', true) . " WHERE `expires` < " . $db->now());
}
/**
* Writes the cache back to the DB.
*/
function close()
{
if (!$this->cache_changed) {
return;
}
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);
}
}
}
$this->write_index();
}
/**
* 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') {
if ($this->type == 'memcache') {
$data = $this->db->get($this->ckey($key));
}
else if ($this->type == 'apc') {
$data = apc_fetch($this->ckey($key));
}
if ($data) {
$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->limitquery(
- "SELECT data, cache_key".
- " FROM " . $this->table .
- " WHERE cache_key = ?".
+ "SELECT `data`, `cache_key`".
+ " FROM {$this->table}" .
+ " WHERE `cache_key` = ?".
// for better performance we allow more records for one key
// get the newer one
- " ORDER BY created DESC",
+ " ORDER BY `created` DESC",
0, 1, $this->prefix . '.' . $key);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null;
if ($sql_arr['data']) {
$data = $this->unserialize($sql_arr['data']);
}
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 mxied $data Serialized cache data
*
* @param boolean True on success, False on failure
*/
private function write_record($key, $data)
{
if (!$this->db) {
return false;
}
if ($this->type == 'memcache' || $this->type == 'apc') {
return $this->add_record($this->ckey($key), $data);
}
$key_exists = array_key_exists($key, $this->cache_sums);
$key = $this->prefix . '.' . $key;
// Remove NULL rows (here we don't need to check if the record exist)
if ($data == 'N;') {
- $this->db->query("DELETE FROM " . $this->table . " WHERE cache_key = ?", $key);
+ $this->db->query("DELETE FROM {$this->table} WHERE `cache_key` = ?", $key);
return true;
}
// update existing cache record
if ($key_exists) {
$result = $this->db->query(
- "UPDATE " . $this->table .
- " SET created = " . $this->db->now() .
- ", expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') .
- ", data = ?".
- " WHERE cache_key = ?",
+ "UPDATE {$this->table}" .
+ " SET `created` = " . $this->db->now() .
+ ", `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') .
+ ", `data` = ?".
+ " WHERE `cache_key` = ?",
$data, $key);
}
// add new cache record
else {
// for better performance we allow more records for one key
// so, no need to check if record exist (see rcube_cache::read_record())
$result = $this->db->query(
- "INSERT INTO ".$this->table.
- " (created, expires, cache_key, data)".
+ "INSERT INTO {$this->table}".
+ " (`created`, `expires`, `cache_key`, `data`)".
" VALUES (".$this->db->now().", " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?, ?)",
$key, $data);
}
return $this->db->affected_rows($result);
}
/**
* 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($key, false);
}
$this->index = array();
}
// Remove keys by name prefix
else if ($prefix_mode) {
foreach ($this->index as $k) {
if (strpos($k, $key) === 0) {
$this->delete_record($k);
}
}
}
// Remove one key by name
else {
$this->delete_record($key);
}
return;
}
// Remove all keys (in specified cache)
if ($key === null) {
- $where = " WHERE cache_key LIKE " . $this->db->quote($this->prefix.'.%');
+ $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.'%');
+ $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);
+ $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 key name
* @param mxied $data Serialized cache data
* @param bollean $index Enables immediate index update
*
* @param boolean True on success, False on failure
*/
private function add_record($key, $data, $index=false)
{
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);
}
// Update index
if ($index && $result) {
$this->load_index();
if (array_search($key, $this->index) === false) {
$this->index[] = $key;
$data = serialize($this->index);
$this->add_record($this->ikey(), $data);
}
}
return $result;
}
/**
* Deletes entry from memcache/apc DB.
*/
private function delete_record($key, $index=true)
{
if ($this->type == 'memcache') {
// #1488592: use 2nd argument
$this->db->delete($this->ckey($key), 0);
}
else {
apc_delete($this->ckey($key));
}
if ($index) {
if (($idx = array_search($key, $this->index)) !== false) {
unset($this->index[$idx]);
}
}
}
/**
* Writes the index entry into memcache/apc DB.
*/
private function write_index()
{
if (!$this->db) {
return;
}
if ($this->type == 'db') {
return;
}
$this->load_index();
// Make sure index contains new keys
foreach ($this->cache as $key => $value) {
if ($value !== null) {
if (array_search($key, $this->index) === false) {
$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) {
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);
}
$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;
}
}
diff --git a/program/lib/Roundcube/rcube_contacts.php b/program/lib/Roundcube/rcube_contacts.php
index 5e1a40e5b..bd3a3f82b 100644
--- a/program/lib/Roundcube/rcube_contacts.php
+++ b/program/lib/Roundcube/rcube_contacts.php
@@ -1,1018 +1,1018 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2006-2012, The Roundcube Dev Team |
| |
| 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: |
| Interface to the local address book database |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Model class for the local address book database
*
* @package Framework
* @subpackage Addressbook
*/
class rcube_contacts extends rcube_addressbook
{
// protected for backward compat. with some plugins
protected $db_name = 'contacts';
protected $db_groups = 'contactgroups';
protected $db_groupmembers = 'contactgroupmembers';
protected $vcard_fieldmap = array();
/**
* Store database connection.
*
* @var rcube_db
*/
private $db = null;
private $user_id = 0;
private $filter = null;
private $result = null;
private $cache;
private $table_cols = array('name', 'email', 'firstname', 'surname');
private $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'nickname',
'jobtitle', 'organization', 'department', 'maidenname', 'email', 'phone',
'address', 'street', 'locality', 'zipcode', 'region', 'country', 'website', 'im', 'notes');
// public properties
public $primary_key = 'contact_id';
public $name;
public $readonly = false;
public $groups = true;
public $undelete = true;
public $list_page = 1;
public $page_size = 10;
public $group_id = 0;
public $ready = false;
public $coltypes = array('name', 'firstname', 'surname', 'middlename', 'prefix', 'suffix', 'nickname',
'jobtitle', 'organization', 'department', 'assistant', 'manager',
'gender', 'maidenname', 'spouse', 'email', 'phone', 'address',
'birthday', 'anniversary', 'website', 'im', 'notes', 'photo');
public $date_cols = array('birthday', 'anniversary');
const SEPARATOR = ',';
/**
* Object constructor
*
* @param object Instance of the rcube_db class
* @param integer User-ID
*/
function __construct($dbconn, $user)
{
$this->db = $dbconn;
$this->user_id = $user;
$this->ready = $this->db && !$this->db->is_error();
}
/**
* Returns addressbook name
*/
function get_name()
{
return $this->name;
}
/**
* Save a search string for future listings
*
* @param string SQL params to use in listing method
*/
function set_search_set($filter)
{
$this->filter = $filter;
$this->cache = null;
}
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
function get_search_set()
{
return $this->filter;
}
/**
* Setter for the current group
* (empty, has to be re-implemented by extending class)
*/
function set_group($gid)
{
$this->group_id = $gid;
$this->cache = null;
}
/**
* Reset all saved results and search parameters
*/
function reset()
{
$this->result = null;
$this->filter = null;
$this->cache = null;
}
/**
* List all active contact groups of this source
*
* @param string Search string to match group name
* @param int Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
*
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null, $mode = 0)
{
$results = array();
if (!$this->groups)
return $results;
if ($search) {
switch (intval($mode)) {
case 1:
$sql_filter = $this->db->ilike('name', $search);
break;
case 2:
$sql_filter = $this->db->ilike('name', $search . '%');
break;
default:
$sql_filter = $this->db->ilike('name', '%' . $search . '%');
}
$sql_filter = " AND $sql_filter";
}
$sql_result = $this->db->query(
- "SELECT * FROM ".$this->db->table_name($this->db_groups).
- " WHERE del<>1".
- " AND user_id=?".
- $sql_filter.
- " ORDER BY name",
+ "SELECT * FROM " . $this->db->table_name($this->db_groups, true)
+ . " WHERE `del` <> 1 AND `user_id` = ?" . $sql_filter
+ . " ORDER BY `name`",
$this->user_id);
while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$sql_arr['ID'] = $sql_arr['contactgroup_id'];
$results[] = $sql_arr;
}
return $results;
}
/**
* Get group properties such as name and email address(es)
*
* @param string Group identifier
* @return array Group properties as hash array
*/
function get_group($group_id)
{
$sql_result = $this->db->query(
- "SELECT * FROM ".$this->db->table_name($this->db_groups).
- " WHERE del<>1".
- " AND contactgroup_id=?".
- " AND user_id=?",
+ "SELECT * FROM " . $this->db->table_name($this->db_groups, true)
+ . " WHERE `del` <> 1 AND `contactgroup_id` = ? AND `user_id` = ?",
$group_id, $this->user_id);
if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$sql_arr['ID'] = $sql_arr['contactgroup_id'];
return $sql_arr;
}
return null;
}
/**
* List the current set of contact records
*
* @param array List of cols to show, Null means all
* @param int Only return this number of records, use negative values for tail
* @param boolean True to skip the count query (select only)
* @return array Indexed list of contact records, each a hash array
*/
function list_records($cols=null, $subset=0, $nocount=false)
{
if ($nocount || $this->list_page <= 1) {
// create dummy result, we don't need a count now
$this->result = new rcube_result_set();
} else {
// count all records
$this->result = $this->count();
}
$start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
$length = $subset != 0 ? abs($subset) : $this->page_size;
if ($this->group_id)
- $join = " LEFT JOIN ".$this->db->table_name($this->db_groupmembers)." AS m".
- " ON (m.contact_id = c.".$this->primary_key.")";
+ $join = " LEFT JOIN " . $this->db->table_name($this->db_groupmembers, true) . " AS m".
+ " ON (m.`contact_id` = c.`".$this->primary_key."`)";
$order_col = (in_array($this->sort_col, $this->table_cols) ? $this->sort_col : 'name');
- $order_cols = array('c.'.$order_col);
+ $order_cols = array("c.`$order_col`");
if ($order_col == 'firstname')
- $order_cols[] = 'c.surname';
+ $order_cols[] = 'c.`surname`';
else if ($order_col == 'surname')
- $order_cols[] = 'c.firstname';
+ $order_cols[] = 'c.`firstname`';
if ($order_col != 'name')
- $order_cols[] = 'c.name';
- $order_cols[] = 'c.email';
+ $order_cols[] = 'c.`name`';
+ $order_cols[] = 'c.`email`';
$sql_result = $this->db->limitquery(
- "SELECT * FROM ".$this->db->table_name($this->db_name)." AS c" .
+ "SELECT * FROM " . $this->db->table_name($this->db_name, true) . " AS c" .
$join .
- " WHERE c.del<>1" .
- " AND c.user_id=?" .
- ($this->group_id ? " AND m.contactgroup_id=?" : "").
+ " WHERE c.`del` <> 1" .
+ " AND c.`user_id` = ?" .
+ ($this->group_id ? " AND m.`contactgroup_id` = ?" : "").
($this->filter ? " AND (".$this->filter.")" : "") .
" ORDER BY ". $this->db->concat($order_cols) .
" " . $this->sort_order,
$start_row,
$length,
$this->user_id,
$this->group_id);
// determine whether we have to parse the vcard or if only db cols are requested
$read_vcard = !$cols || count(array_intersect($cols, $this->table_cols)) < count($cols);
while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$sql_arr['ID'] = $sql_arr[$this->primary_key];
if ($read_vcard)
$sql_arr = $this->convert_db_data($sql_arr);
else {
$sql_arr['email'] = $sql_arr['email'] ? explode(self::SEPARATOR, $sql_arr['email']) : array();
$sql_arr['email'] = array_map('trim', $sql_arr['email']);
}
$this->result->add($sql_arr);
}
$cnt = count($this->result->records);
// update counter
if ($nocount)
$this->result->count = $cnt;
else if ($this->list_page <= 1) {
if ($cnt < $this->page_size && $subset == 0)
$this->result->count = $cnt;
else if (isset($this->cache['count']))
$this->result->count = $this->cache['count'];
else
$this->result->count = $this->_count();
}
return $this->result;
}
/**
* Search contacts
*
* @param mixed $fields The field name of array of field names to search in
* @param mixed $value Search value (or array of values when $fields is array)
* @param int $mode Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
* @param boolean $select True if results are requested, False if count only
* @param boolean $nocount True to skip the count query (select only)
* @param array $required List of fields that cannot be empty
*
* @return object rcube_result_set Contact records and 'count' value
*/
function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
{
if (!is_array($fields))
$fields = array($fields);
if (!is_array($required) && !empty($required))
$required = array($required);
$where = $and_where = array();
$mode = intval($mode);
$WS = ' ';
$AS = self::SEPARATOR;
foreach ($fields as $idx => $col) {
// direct ID search
if ($col == 'ID' || $col == $this->primary_key) {
$ids = !is_array($value) ? explode(self::SEPARATOR, $value) : $value;
$ids = $this->db->array2list($ids, 'integer');
$where[] = 'c.' . $this->primary_key.' IN ('.$ids.')';
continue;
}
// fulltext search in all fields
else if ($col == '*') {
$words = array();
foreach (explode($WS, rcube_utils::normalize_string($value)) as $word) {
switch ($mode) {
case 1: // strict
$words[] = '(' . $this->db->ilike('words', $word . '%')
. ' OR ' . $this->db->ilike('words', '%' . $WS . $word . $WS . '%')
. ' OR ' . $this->db->ilike('words', '%' . $WS . $word) . ')';
break;
case 2: // prefix
$words[] = '(' . $this->db->ilike('words', $word . '%')
. ' OR ' . $this->db->ilike('words', '%' . $WS . $word . '%') . ')';
break;
default: // partial
$words[] = $this->db->ilike('words', '%' . $word . '%');
}
}
$where[] = '(' . join(' AND ', $words) . ')';
}
else {
$val = is_array($value) ? $value[$idx] : $value;
// table column
if (in_array($col, $this->table_cols)) {
switch ($mode) {
case 1: // strict
$where[] = '(' . $this->db->quote_identifier($col) . ' = ' . $this->db->quote($val)
. ' OR ' . $this->db->ilike($col, $val . $AS . '%')
. ' OR ' . $this->db->ilike($col, '%' . $AS . $val . $AS . '%')
. ' OR ' . $this->db->ilike($col, '%' . $AS . $val) . ')';
break;
case 2: // prefix
$where[] = '(' . $this->db->ilike($col, $val . '%')
. ' OR ' . $this->db->ilike($col, $AS . $val . '%') . ')';
break;
default: // partial
$where[] = $this->db->ilike($col, '%' . $val . '%');
}
}
// vCard field
else {
if (in_array($col, $this->fulltext_cols)) {
foreach (rcube_utils::normalize_string($val, true) as $word) {
switch ($mode) {
case 1: // strict
$words[] = '(' . $this->db->ilike('words', $word . $WS . '%')
. ' OR ' . $this->db->ilike('words', '%' . $AS . $word . $WS .'%')
. ' OR ' . $this->db->ilike('words', '%' . $AS . $word) . ')';
break;
case 2: // prefix
$words[] = '(' . $this->db->ilike('words', $word . '%')
. ' OR ' . $this->db->ilike('words', $AS . $word . '%') . ')';
break;
default: // partial
$words[] = $this->db->ilike('words', '%' . $word . '%');
}
}
$where[] = '(' . join(' AND ', $words) . ')';
}
if (is_array($value))
$post_search[$col] = mb_strtolower($val);
}
}
}
foreach (array_intersect($required, $this->table_cols) as $col) {
$and_where[] = $this->db->quote_identifier($col).' <> '.$this->db->quote('');
}
if (!empty($where)) {
// use AND operator for advanced searches
$where = join(is_array($value) ? ' AND ' : ' OR ', $where);
}
if (!empty($and_where))
$where = ($where ? "($where) AND " : '') . join(' AND ', $and_where);
// Post-searching in vCard data fields
// we will search in all records and then build a where clause for their IDs
if (!empty($post_search)) {
$ids = array(0);
// build key name regexp
$regexp = '/^(' . implode(array_keys($post_search), '|') . ')(?:.*)$/';
// use initial WHERE clause, to limit records number if possible
if (!empty($where))
$this->set_search_set($where);
// count result pages
$cnt = $this->count();
$pages = ceil($cnt / $this->page_size);
$scnt = count($post_search);
// get (paged) result
for ($i=0; $i<$pages; $i++) {
$this->list_records(null, $i, true);
while ($row = $this->result->next()) {
$id = $row[$this->primary_key];
$found = array();
foreach (preg_grep($regexp, array_keys($row)) as $col) {
$pos = strpos($col, ':');
$colname = $pos ? substr($col, 0, $pos) : $col;
$search = $post_search[$colname];
foreach ((array)$row[$col] as $value) {
if ($this->compare_search_value($colname, $value, $search, $mode)) {
$found[$colname] = true;
break 2;
}
}
}
// all fields match
if (count($found) >= $scnt) {
$ids[] = $id;
}
}
}
// build WHERE clause
$ids = $this->db->array2list($ids, 'integer');
- $where = 'c.' . $this->primary_key.' IN ('.$ids.')';
+ $where = 'c.`' . $this->primary_key.'` IN ('.$ids.')';
// reset counter
unset($this->cache['count']);
// when we know we have an empty result
if ($ids == '0') {
$this->set_search_set($where);
return ($this->result = new rcube_result_set(0, 0));
}
}
if (!empty($where)) {
$this->set_search_set($where);
if ($select)
$this->list_records(null, 0, $nocount);
else
$this->result = $this->count();
}
return $this->result;
}
/**
* Count number of available contacts in database
*
* @return rcube_result_set Result object
*/
function count()
{
$count = isset($this->cache['count']) ? $this->cache['count'] : $this->_count();
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
/**
* Count number of available contacts in database
*
* @return int Contacts count
*/
private function _count()
{
if ($this->group_id)
- $join = " LEFT JOIN ".$this->db->table_name($this->db_groupmembers)." AS m".
- " ON (m.contact_id=c.".$this->primary_key.")";
+ $join = " LEFT JOIN " . $this->db->table_name($this->db_groupmembers, true) . " AS m".
+ " ON (m.`contact_id` = c.`".$this->primary_key."`)";
// count contacts for this user
$sql_result = $this->db->query(
- "SELECT COUNT(c.contact_id) AS rows".
- " FROM ".$this->db->table_name($this->db_name)." AS c".
+ "SELECT COUNT(c.`contact_id`) AS rows".
+ " FROM " . $this->db->table_name($this->db_name, true) . " AS c".
$join.
- " WHERE c.del<>1".
- " AND c.user_id=?".
- ($this->group_id ? " AND m.contactgroup_id=?" : "").
+ " WHERE c.`del` <> 1".
+ " AND c.`user_id` = ?".
+ ($this->group_id ? " AND m.`contactgroup_id` = ?" : "").
($this->filter ? " AND (".$this->filter.")" : ""),
$this->user_id,
$this->group_id
);
$sql_arr = $this->db->fetch_assoc($sql_result);
$this->cache['count'] = (int) $sql_arr['rows'];
return $this->cache['count'];
}
/**
* Return the last result set
*
* @return mixed Result array or NULL if nothing selected yet
*/
function get_result()
{
return $this->result;
}
/**
* Get a specific contact record
*
* @param mixed record identifier(s)
* @return mixed Result object with all record fields or False if not found
*/
function get_record($id, $assoc=false)
{
// return cached result
if ($this->result && ($first = $this->result->first()) && $first[$this->primary_key] == $id)
return $assoc ? $first : $this->result;
$this->db->query(
- "SELECT * FROM ".$this->db->table_name($this->db_name).
- " WHERE contact_id=?".
- " AND user_id=?".
- " AND del<>1",
+ "SELECT * FROM " . $this->db->table_name($this->db_name, true).
+ " WHERE `contact_id` = ?".
+ " AND `user_id` = ?".
+ " AND `del` <> 1",
$id,
$this->user_id
);
if ($sql_arr = $this->db->fetch_assoc()) {
$record = $this->convert_db_data($sql_arr);
$this->result = new rcube_result_set(1);
$this->result->add($record);
}
return $assoc && $record ? $record : $this->result;
}
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
* @return array List of assigned groups as ID=>Name pairs
*/
function get_record_groups($id)
{
$results = array();
if (!$this->groups)
return $results;
$sql_result = $this->db->query(
- "SELECT cgm.contactgroup_id, cg.name FROM " . $this->db->table_name($this->db_groupmembers) . " AS cgm" .
- " LEFT JOIN " . $this->db->table_name($this->db_groups) . " AS cg ON (cgm.contactgroup_id = cg.contactgroup_id AND cg.del<>1)" .
- " WHERE cgm.contact_id=?",
+ "SELECT cgm.`contactgroup_id`, cg.`name` "
+ . " FROM " . $this->db->table_name($this->db_groupmembers, true) . " AS cgm"
+ . " LEFT JOIN " . $this->db->table_name($this->db_groups, true) . " AS cg"
+ . " ON (cgm.`contactgroup_id` = cg.`contactgroup_id` AND cg.`del` <> 1)"
+ . " WHERE cgm.`contact_id` = ?",
$id
);
while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$results[$sql_arr['contactgroup_id']] = $sql_arr['name'];
}
return $results;
}
/**
* Check the given data before saving.
* If input not valid, the message to display can be fetched using get_error()
*
* @param array Assoziative array with data to save
* @param boolean Try to fix/complete record automatically
* @return boolean True if input is valid, False if not.
*/
public function validate(&$save_data, $autofix = false)
{
// validate e-mail addresses
$valid = parent::validate($save_data, $autofix);
// require at least one email address or a name
if ($valid && !strlen($save_data['firstname'].$save_data['surname'].$save_data['name']) && !array_filter($this->get_col_values('email', $save_data, true))) {
$this->set_error(self::ERROR_VALIDATE, 'noemailwarning');
$valid = false;
}
return $valid;
}
/**
* Create a new contact record
*
* @param array Associative array with save data
* @return integer|boolean The created record ID on success, False on error
*/
function insert($save_data, $check=false)
{
if (!is_array($save_data))
return false;
$insert_id = $existing = false;
if ($check) {
foreach ($save_data as $col => $values) {
if (strpos($col, 'email') === 0) {
foreach ((array)$values as $email) {
if ($existing = $this->search('email', $email, false, false))
break 2;
}
}
}
}
$save_data = $this->convert_save_data($save_data);
$a_insert_cols = $a_insert_values = array();
foreach ($save_data as $col => $value) {
$a_insert_cols[] = $this->db->quote_identifier($col);
$a_insert_values[] = $this->db->quote($value);
}
if (!$existing->count && !empty($a_insert_cols)) {
$this->db->query(
- "INSERT INTO ".$this->db->table_name($this->db_name).
- " (user_id, changed, del, ".join(', ', $a_insert_cols).")".
+ "INSERT INTO " . $this->db->table_name($this->db_name, true).
+ " (`user_id`, `changed`, `del`, ".join(', ', $a_insert_cols).")".
" VALUES (".intval($this->user_id).", ".$this->db->now().", 0, ".join(', ', $a_insert_values).")"
);
$insert_id = $this->db->insert_id($this->db_name);
}
$this->cache = null;
return $insert_id;
}
/**
* Update a specific contact record
*
* @param mixed Record identifier
* @param array Assoziative array with save data
*
* @return boolean True on success, False on error
*/
function update($id, $save_cols)
{
$updated = false;
$write_sql = array();
$record = $this->get_record($id, true);
$save_cols = $this->convert_save_data($save_cols, $record);
foreach ($save_cols as $col => $value) {
$write_sql[] = sprintf("%s=%s", $this->db->quote_identifier($col), $this->db->quote($value));
}
if (!empty($write_sql)) {
$this->db->query(
- "UPDATE ".$this->db->table_name($this->db_name).
- " SET changed=".$this->db->now().", ".join(', ', $write_sql).
- " WHERE contact_id=?".
- " AND user_id=?".
- " AND del<>1",
+ "UPDATE " . $this->db->table_name($this->db_name, true).
+ " SET `changed` = ".$this->db->now().", ".join(', ', $write_sql).
+ " WHERE `contact_id` = ?".
+ " AND `user_id` = ?".
+ " AND `del` <> 1",
$id,
$this->user_id
);
$updated = $this->db->affected_rows();
$this->result = null; // clear current result (from get_record())
}
return $updated ? true : false;
}
private function convert_db_data($sql_arr)
{
$record = array();
$record['ID'] = $sql_arr[$this->primary_key];
if ($sql_arr['vcard']) {
unset($sql_arr['email']);
$vcard = new rcube_vcard($sql_arr['vcard'], RCUBE_CHARSET, false, $this->vcard_fieldmap);
$record += $vcard->get_assoc() + $sql_arr;
}
else {
$record += $sql_arr;
$record['email'] = explode(self::SEPARATOR, $record['email']);
$record['email'] = array_map('trim', $record['email']);
}
return $record;
}
private function convert_save_data($save_data, $record = array())
{
$out = array();
$words = '';
// copy values into vcard object
$vcard = new rcube_vcard($record['vcard'] ? $record['vcard'] : $save_data['vcard'], RCUBE_CHARSET, false, $this->vcard_fieldmap);
$vcard->reset();
foreach ($save_data as $key => $values) {
list($field, $section) = explode(':', $key);
$fulltext = in_array($field, $this->fulltext_cols);
// avoid casting DateTime objects to array
if (is_object($values) && is_a($values, 'DateTime')) {
$values = array(0 => $values);
}
foreach ((array)$values as $value) {
if (isset($value))
$vcard->set($field, $value, $section);
if ($fulltext && is_array($value))
$words .= ' ' . rcube_utils::normalize_string(join(" ", $value));
else if ($fulltext && strlen($value) >= 3)
$words .= ' ' . rcube_utils::normalize_string($value);
}
}
$out['vcard'] = $vcard->export(false);
foreach ($this->table_cols as $col) {
$key = $col;
if (!isset($save_data[$key]))
$key .= ':home';
if (isset($save_data[$key])) {
if (is_array($save_data[$key]))
$out[$col] = join(self::SEPARATOR, $save_data[$key]);
else
$out[$col] = $save_data[$key];
}
}
// save all e-mails in database column
$out['email'] = join(self::SEPARATOR, $vcard->email);
// join words for fulltext search
$out['words'] = join(" ", array_unique(explode(" ", $words)));
return $out;
}
/**
* Mark one or more contact records as deleted
*
* @param array Record identifiers
* @param boolean Remove record(s) irreversible (unsupported)
*/
function delete($ids, $force=true)
{
if (!is_array($ids))
$ids = explode(self::SEPARATOR, $ids);
$ids = $this->db->array2list($ids, 'integer');
// flag record as deleted (always)
$this->db->query(
- "UPDATE ".$this->db->table_name($this->db_name).
- " SET del=1, changed=".$this->db->now().
- " WHERE user_id=?".
- " AND contact_id IN ($ids)",
+ "UPDATE " . $this->db->table_name($this->db_name, true).
+ " SET `del` = 1, `changed` = ".$this->db->now().
+ " WHERE `user_id` = ?".
+ " AND `contact_id` IN ($ids)",
$this->user_id
);
$this->cache = null;
return $this->db->affected_rows();
}
/**
* Undelete one or more contact records
*
* @param array Record identifiers
*/
function undelete($ids)
{
if (!is_array($ids))
$ids = explode(self::SEPARATOR, $ids);
$ids = $this->db->array2list($ids, 'integer');
// clear deleted flag
$this->db->query(
- "UPDATE ".$this->db->table_name($this->db_name).
- " SET del=0, changed=".$this->db->now().
- " WHERE user_id=?".
- " AND contact_id IN ($ids)",
+ "UPDATE " . $this->db->table_name($this->db_name, true).
+ " SET `del` = 0, `changed` = ".$this->db->now().
+ " WHERE `user_id` = ?".
+ " AND `contact_id` IN ($ids)",
$this->user_id
);
$this->cache = null;
return $this->db->affected_rows();
}
/**
* Remove all records from the database
*
* @param bool $with_groups Remove also groups
*
* @return int Number of removed records
*/
function delete_all($with_groups = false)
{
$this->cache = null;
- $this->db->query("UPDATE " . $this->db->table_name($this->db_name)
- . " SET del = 1, changed = " . $this->db->now()
- . " WHERE user_id = ?", $this->user_id);
+ $now = $this->db->now();
+
+ $this->db->query("UPDATE " . $this->db->table_name($this->db_name, true)
+ . " SET `del` = 1, `changed` = $now"
+ . " WHERE `user_id` = ?", $this->user_id);
$count = $this->db->affected_rows();
if ($with_groups) {
- $this->db->query("UPDATE " . $this->db->table_name($this->db_groups)
- . " SET del = 1, changed = " . $this->db->now()
- . " WHERE user_id = ?", $this->user_id);
+ $this->db->query("UPDATE " . $this->db->table_name($this->db_groups, true)
+ . " SET `del` = 1, `changed` = $now"
+ . " WHERE `user_id` = ?", $this->user_id);
$count += $this->db->affected_rows();
}
return $count;
}
/**
* Create a contact group with the given name
*
* @param string The group name
* @return mixed False on error, array with record props in success
*/
function create_group($name)
{
$result = false;
// make sure we have a unique name
$name = $this->unique_groupname($name);
$this->db->query(
- "INSERT INTO ".$this->db->table_name($this->db_groups).
- " (user_id, changed, name)".
+ "INSERT INTO " . $this->db->table_name($this->db_groups, true).
+ " (`user_id`, `changed`, `name`)".
" VALUES (".intval($this->user_id).", ".$this->db->now().", ".$this->db->quote($name).")"
);
- if ($insert_id = $this->db->insert_id($this->db_groups))
+ if ($insert_id = $this->db->insert_id($this->db_groups)) {
$result = array('id' => $insert_id, 'name' => $name);
+ }
return $result;
}
/**
* Delete the given group (and all linked group members)
*
* @param string Group identifier
* @return boolean True on success, false if no data was changed
*/
function delete_group($gid)
{
// flag group record as deleted
$this->db->query(
- "UPDATE " . $this->db->table_name($this->db_groups)
- . " SET del = 1, changed = " . $this->db->now()
- . " WHERE contactgroup_id = ?"
- . " AND user_id = ?",
+ "UPDATE " . $this->db->table_name($this->db_groups, true)
+ . " SET `del` = 1, `changed` = " . $this->db->now()
+ . " WHERE `contactgroup_id` = ?"
+ . " AND `user_id` = ?",
$gid, $this->user_id
);
$this->cache = null;
return $this->db->affected_rows();
}
/**
* Rename a specific contact group
*
* @param string Group identifier
* @param string New name to set for this group
* @return boolean New name on success, false if no data was changed
*/
function rename_group($gid, $newname, &$new_gid)
{
// make sure we have a unique name
$name = $this->unique_groupname($newname);
$sql_result = $this->db->query(
- "UPDATE ".$this->db->table_name($this->db_groups).
- " SET name=?, changed=".$this->db->now().
- " WHERE contactgroup_id=?".
- " AND user_id=?",
+ "UPDATE " . $this->db->table_name($this->db_groups, true).
+ " SET `name` = ?, `changed` = ".$this->db->now().
+ " WHERE `contactgroup_id` = ?".
+ " AND `user_id` = ?",
$name, $gid, $this->user_id
);
return $this->db->affected_rows() ? $name : false;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array|string List of contact identifiers to be added
*
* @return int Number of contacts added
*/
function add_to_group($group_id, $ids)
{
if (!is_array($ids))
$ids = explode(self::SEPARATOR, $ids);
$added = 0;
$exists = array();
// get existing assignments ...
$sql_result = $this->db->query(
- "SELECT contact_id FROM ".$this->db->table_name($this->db_groupmembers).
- " WHERE contactgroup_id=?".
- " AND contact_id IN (".$this->db->array2list($ids, 'integer').")",
+ "SELECT `contact_id` FROM " . $this->db->table_name($this->db_groupmembers, true).
+ " WHERE `contactgroup_id` = ?".
+ " AND `contact_id` IN (".$this->db->array2list($ids, 'integer').")",
$group_id
);
while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$exists[] = $sql_arr['contact_id'];
}
// ... and remove them from the list
$ids = array_diff($ids, $exists);
foreach ($ids as $contact_id) {
$this->db->query(
- "INSERT INTO ".$this->db->table_name($this->db_groupmembers).
- " (contactgroup_id, contact_id, created)".
+ "INSERT INTO " . $this->db->table_name($this->db_groupmembers, true).
+ " (`contactgroup_id`, `contact_id`, `created`)".
" VALUES (?, ?, ".$this->db->now().")",
$group_id,
$contact_id
);
if ($error = $this->db->is_error())
$this->set_error(self::ERROR_SAVING, $error);
else
$added++;
}
return $added;
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array|string List of contact identifiers to be removed
*
* @return int Number of deleted group members
*/
function remove_from_group($group_id, $ids)
{
if (!is_array($ids))
$ids = explode(self::SEPARATOR, $ids);
$ids = $this->db->array2list($ids, 'integer');
$sql_result = $this->db->query(
- "DELETE FROM ".$this->db->table_name($this->db_groupmembers).
- " WHERE contactgroup_id=?".
- " AND contact_id IN ($ids)",
+ "DELETE FROM " . $this->db->table_name($this->db_groupmembers, true).
+ " WHERE `contactgroup_id` = ?".
+ " AND `contact_id` IN ($ids)",
$group_id
);
return $this->db->affected_rows();
}
/**
* Check for existing groups with the same name
*
* @param string Name to check
* @return string A group name which is unique for the current use
*/
private function unique_groupname($name)
{
$checkname = $name;
$num = 2; $hit = false;
do {
$sql_result = $this->db->query(
- "SELECT 1 FROM ".$this->db->table_name($this->db_groups).
- " WHERE del<>1".
- " AND user_id=?".
- " AND name=?",
+ "SELECT 1 FROM " . $this->db->table_name($this->db_groups, true).
+ " WHERE `del` <> 1".
+ " AND `user_id` = ?".
+ " AND `name` = ?",
$this->user_id,
$checkname);
// append number to make name unique
if ($hit = $this->db->fetch_array($sql_result)) {
$checkname = $name . ' ' . $num++;
}
} while ($hit);
return $checkname;
}
-
}
diff --git a/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php
index 100afd530..b12c99d0e 100644
--- a/program/lib/Roundcube/rcube_db.php
+++ b/program/lib/Roundcube/rcube_db.php
@@ -1,1186 +1,1250 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2012, The Roundcube Dev Team |
| |
| 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: |
| Database wrapper class that implements PHP PDO functions |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Database independent query interface.
* This is a wrapper for the PHP PDO.
*
* @package Framework
* @subpackage Database
*/
class rcube_db
{
public $db_provider;
protected $db_dsnw; // DSN for write operations
protected $db_dsnr; // DSN for read operations
protected $db_connected = false; // Already connected ?
protected $db_mode; // Connection mode
protected $dbh; // Connection handle
protected $dbhs = array();
protected $table_connections = array();
protected $db_error = false;
protected $db_error_msg = '';
protected $conn_failure = false;
protected $db_index = 0;
protected $last_result;
protected $tables;
protected $variables;
protected $options = array(
// column/table quotes
'identifier_start' => '"',
'identifier_end' => '"',
);
const DEBUG_LINE_LENGTH = 4096;
+ const DEFAULT_QUOTE = '`';
/**
* Factory, returns driver-specific instance of the class
*
* @param string $db_dsnw DSN for read/write operations
* @param string $db_dsnr Optional DSN for read only operations
* @param bool $pconn Enables persistent connections
*
* @return rcube_db Object instance
*/
public static function factory($db_dsnw, $db_dsnr = '', $pconn = false)
{
$driver = strtolower(substr($db_dsnw, 0, strpos($db_dsnw, ':')));
$driver_map = array(
'sqlite2' => 'sqlite',
'sybase' => 'mssql',
'dblib' => 'mssql',
'mysqli' => 'mysql',
'oci' => 'oracle',
);
$driver = isset($driver_map[$driver]) ? $driver_map[$driver] : $driver;
$class = "rcube_db_$driver";
if (!$driver || !class_exists($class)) {
rcube::raise_error(array('code' => 600, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => "Configuration error. Unsupported database driver: $driver"),
true, true);
}
return new $class($db_dsnw, $db_dsnr, $pconn);
}
/**
* Object constructor
*
* @param string $db_dsnw DSN for read/write operations
* @param string $db_dsnr Optional DSN for read only operations
* @param bool $pconn Enables persistent connections
*/
public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
{
if (empty($db_dsnr)) {
$db_dsnr = $db_dsnw;
}
$this->db_dsnw = $db_dsnw;
$this->db_dsnr = $db_dsnr;
$this->db_pconn = $pconn;
$this->db_dsnw_array = self::parse_dsn($db_dsnw);
$this->db_dsnr_array = self::parse_dsn($db_dsnr);
$config = rcube::get_instance()->config;
$this->options['table_prefix'] = $config->get('db_prefix');
$this->options['dsnw_noread'] = $config->get('db_dsnw_noread', false);
$this->options['table_dsn_map'] = array_map(array($this, 'table_name'), $config->get('db_table_dsn', array()));
}
/**
* Connect to specific database
*
* @param array $dsn DSN for DB connections
* @param string $mode Connection mode (r|w)
*/
protected function dsn_connect($dsn, $mode)
{
$this->db_error = false;
$this->db_error_msg = null;
// return existing handle
if ($this->dbhs[$mode]) {
$this->dbh = $this->dbhs[$mode];
$this->db_mode = $mode;
return $this->dbh;
}
// Get database specific connection options
$dsn_string = $this->dsn_string($dsn);
$dsn_options = $this->dsn_options($dsn);
if ($this->db_pconn) {
$dsn_options[PDO::ATTR_PERSISTENT] = true;
}
// Connect
try {
// with this check we skip fatal error on PDO object creation
if (!class_exists('PDO', false)) {
throw new Exception('PDO extension not loaded. See http://php.net/manual/en/intro.pdo.php');
}
$this->conn_prepare($dsn);
$dbh = new PDO($dsn_string, $dsn['username'], $dsn['password'], $dsn_options);
// don't throw exceptions or warnings
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
}
catch (Exception $e) {
$this->db_error = true;
$this->db_error_msg = $e->getMessage();
rcube::raise_error(array('code' => 500, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => $this->db_error_msg), true, false);
return null;
}
$this->dbh = $dbh;
$this->dbhs[$mode] = $dbh;
$this->db_mode = $mode;
$this->db_connected = true;
$this->conn_configure($dsn, $dbh);
}
/**
* Driver-specific preparation of database connection
*
* @param array $dsn DSN for DB connections
*/
protected function conn_prepare($dsn)
{
}
/**
* Driver-specific configuration of database connection
*
* @param array $dsn DSN for DB connections
* @param PDO $dbh Connection handler
*/
protected function conn_configure($dsn, $dbh)
{
}
/**
* Connect to appropriate database depending on the operation
*
* @param string $mode Connection mode (r|w)
* @param boolean $force Enforce using the given mode
*/
public function db_connect($mode, $force = false)
{
// previous connection failed, don't attempt to connect again
if ($this->conn_failure) {
return;
}
// no replication
if ($this->db_dsnw == $this->db_dsnr) {
$mode = 'w';
}
// Already connected
if ($this->db_connected) {
// connected to db with the same or "higher" mode (if allowed)
if ($this->db_mode == $mode || $this->db_mode == 'w' && !$force && !$this->options['dsnw_noread']) {
return;
}
}
$dsn = ($mode == 'r') ? $this->db_dsnr_array : $this->db_dsnw_array;
$this->dsn_connect($dsn, $mode);
// use write-master when read-only fails
if (!$this->db_connected && $mode == 'r' && $this->is_replicated()) {
$this->dsn_connect($this->db_dsnw_array, 'w');
}
$this->conn_failure = !$this->db_connected;
}
/**
* Analyze the given SQL statement and select the appropriate connection to use
*/
protected function dsn_select($query)
{
// no replication
if ($this->db_dsnw == $this->db_dsnr) {
return 'w';
}
// Read or write ?
$mode = preg_match('/^(select|show|set)/i', $query) ? 'r' : 'w';
+ $start = '[' . $this->options['identifier_start'] . self::DEFAULT_QUOTE . ']';
+ $end = '[' . $this->options['identifier_end'] . self::DEFAULT_QUOTE . ']';
+ $regex = '/(?:^|\s)(from|update|into|join)\s+'.$start.'?([a-z0-9._]+)'.$end.'?\s+/i';
+
// find tables involved in this query
- if (preg_match_all('/(?:^|\s)(from|update|into|join)\s+'.$this->options['identifier_start'].'?([a-z0-9._]+)'.$this->options['identifier_end'].'?\s+/i', $query, $matches, PREG_SET_ORDER)) {
+ if (preg_match_all($regex, $query, $matches, PREG_SET_ORDER)) {
foreach ($matches as $m) {
$table = $m[2];
// always use direct mapping
if ($this->options['table_dsn_map'][$table]) {
$mode = $this->options['table_dsn_map'][$table];
break; // primary table rules
}
else if ($mode == 'r') {
// connected to db with the same or "higher" mode for this table
$db_mode = $this->table_connections[$table];
if ($db_mode == 'w' && !$this->options['dsnw_noread']) {
$mode = $db_mode;
}
}
}
// remember mode chosen (for primary table)
$table = $matches[0][2];
$this->table_connections[$table] = $mode;
}
return $mode;
}
/**
* Activate/deactivate debug mode
*
* @param boolean $dbg True if SQL queries should be logged
*/
public function set_debug($dbg = true)
{
$this->options['debug_mode'] = $dbg;
}
/**
* Writes debug information/query to 'sql' log file
*
* @param string $query SQL query
*/
protected function debug($query)
{
if ($this->options['debug_mode']) {
if (($len = strlen($query)) > self::DEBUG_LINE_LENGTH) {
$diff = $len - self::DEBUG_LINE_LENGTH;
$query = substr($query, 0, self::DEBUG_LINE_LENGTH)
. "... [truncated $diff bytes]";
}
rcube::write_log('sql', '[' . (++$this->db_index) . '] ' . $query . ';');
}
}
/**
* Getter for error state
*
* @param mixed $result Optional query result
*
* @return string Error message
*/
public function is_error($result = null)
{
if ($result !== null) {
return $result === false ? $this->db_error_msg : null;
}
return $this->db_error ? $this->db_error_msg : null;
}
/**
* Connection state checker
*
* @return boolean True if in connected state
*/
public function is_connected()
{
return !is_object($this->dbh) ? false : $this->db_connected;
}
/**
* Is database replication configured?
*
* @return bool Returns true if dsnw != dsnr
*/
public function is_replicated()
{
return !empty($this->db_dsnr) && $this->db_dsnw != $this->db_dsnr;
}
/**
* Get database runtime variables
*
* @param string $varname Variable name
* @param mixed $default Default value if variable is not set
*
* @return mixed Variable value or default
*/
public function get_variable($varname, $default = null)
{
// to be implemented by driver class
return $default;
}
/**
* Execute a SQL query
*
* @param string SQL query to execute
* @param mixed Values to be inserted in query
*
* @return number Query handle identifier
*/
public function query()
{
$params = func_get_args();
$query = array_shift($params);
// Support one argument of type array, instead of n arguments
if (count($params) == 1 && is_array($params[0])) {
$params = $params[0];
}
return $this->_query($query, 0, 0, $params);
}
/**
* Execute a SQL query with limits
*
* @param string SQL query to execute
* @param int Offset for LIMIT statement
* @param int Number of rows for LIMIT statement
* @param mixed Values to be inserted in query
*
* @return PDOStatement|bool Query handle or False on error
*/
public function limitquery()
{
$params = func_get_args();
$query = array_shift($params);
$offset = array_shift($params);
$numrows = array_shift($params);
return $this->_query($query, $offset, $numrows, $params);
}
/**
* Execute a SQL query with limits
*
* @param string $query SQL query to execute
* @param int $offset Offset for LIMIT statement
* @param int $numrows Number of rows for LIMIT statement
* @param array $params Values to be inserted in query
*
* @return PDOStatement|bool Query handle or False on error
*/
protected function _query($query, $offset, $numrows, $params)
{
$query = ltrim($query);
$this->db_connect($this->dsn_select($query), true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
if ($numrows || $offset) {
$query = $this->set_limit($query, $numrows, $offset);
}
+ // replace self::DEFAULT_QUOTE with driver-specific quoting
+ $query = $this->query_parse($query);
+
// Because in Roundcube we mostly use queries that are
// executed only once, we will not use prepared queries
$pos = 0;
$idx = 0;
if (count($params)) {
while ($pos = strpos($query, '?', $pos)) {
if ($query[$pos+1] == '?') { // skip escaped '?'
$pos += 2;
}
else {
$val = $this->quote($params[$idx++]);
unset($params[$idx-1]);
$query = substr_replace($query, $val, $pos, 1);
$pos += strlen($val);
}
}
}
// replace escaped '?' back to normal, see self::quote()
$query = str_replace('??', '?', $query);
$query = rtrim($query, " \t\n\r\0\x0B;");
+ // log query
$this->debug($query);
// destroy reference to previous result, required for SQLite driver (#1488874)
$this->last_result = null;
$this->db_error_msg = null;
// send query
$result = $this->dbh->query($query);
if ($result === false) {
$result = $this->handle_error($query);
}
$this->last_result = $result;
return $result;
}
+ /**
+ * Parse SQL query and replace identifier quoting
+ *
+ * @param string $query SQL query
+ *
+ * @return string SQL query
+ */
+ protected function query_parse($query)
+ {
+ $start = $this->options['identifier_start'];
+ $end = $this->options['identifier_end'];
+ $quote = self::DEFAULT_QUOTE;
+
+ if ($start == $quote) {
+ return $query;
+ }
+
+ $pos = 0;
+ $in = false;
+
+ while ($pos = strpos($query, $quote, $pos)) {
+ if ($query[$pos+1] == $quote) { // skip escaped quote
+ $pos += 2;
+ }
+ else {
+ if ($in) {
+ $q = $end;
+ $in = false;
+ }
+ else {
+ $q = $start;
+ $in = true;
+ }
+
+ $query = substr_replace($query, $q, $pos, 1);
+ $pos++;
+ }
+ }
+
+ // replace escaped quote back to normal, see self::quote()
+ $query = str_replace($quote.$quote, $quote, $query);
+
+ return $query;
+ }
+
/**
* Helper method to handle DB errors.
* This by default logs the error but could be overriden by a driver implementation
*
* @param string Query that triggered the error
* @return mixed Result to be stored and returned
*/
protected function handle_error($query)
{
$error = $this->dbh->errorInfo();
if (empty($this->options['ignore_key_errors']) || !in_array($error[0], array('23000', '23505'))) {
$this->db_error = true;
$this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
rcube::raise_error(array('code' => 500, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => $this->db_error_msg . " (SQL Query: $query)"
), true, false);
}
return false;
}
/**
* Get number of affected rows for the last query
*
* @param mixed $result Optional query handle
*
* @return int Number of (matching) rows
*/
public function affected_rows($result = null)
{
if ($result || ($result === null && ($result = $this->last_result))) {
return $result->rowCount();
}
return 0;
}
/**
* Get number of rows for a SQL query
* If no query handle is specified, the last query will be taken as reference
*
* @param mixed $result Optional query handle
* @return mixed Number of rows or false on failure
* @deprecated This method shows very poor performance and should be avoided.
*/
public function num_rows($result = null)
{
if ($result || ($result === null && ($result = $this->last_result))) {
// repeat query with SELECT COUNT(*) ...
if (preg_match('/^SELECT\s+(?:ALL\s+|DISTINCT\s+)?(?:.*?)\s+FROM\s+(.*)$/ims', $result->queryString, $m)) {
$query = $this->dbh->query('SELECT COUNT(*) FROM ' . $m[1], PDO::FETCH_NUM);
return $query ? intval($query->fetchColumn(0)) : false;
}
else {
$num = count($result->fetchAll());
$result->execute(); // re-execute query because there's no seek(0)
return $num;
}
}
return false;
}
/**
* Get last inserted record ID
*
* @param string $table Table name (to find the incremented sequence)
*
* @return mixed ID or false on failure
*/
public function insert_id($table = '')
{
if (!$this->db_connected || $this->db_mode == 'r') {
return false;
}
if ($table) {
// resolve table name
$table = $this->table_name($table);
}
$id = $this->dbh->lastInsertId($table);
return $id;
}
/**
* Get an associative array for one row
* If no query handle is specified, the last query will be taken as reference
*
* @param mixed $result Optional query handle
*
* @return mixed Array with col values or false on failure
*/
public function fetch_assoc($result = null)
{
return $this->_fetch_row($result, PDO::FETCH_ASSOC);
}
/**
* Get an index array for one row
* If no query handle is specified, the last query will be taken as reference
*
* @param mixed $result Optional query handle
*
* @return mixed Array with col values or false on failure
*/
public function fetch_array($result = null)
{
return $this->_fetch_row($result, PDO::FETCH_NUM);
}
/**
* Get col values for a result row
*
* @param mixed $result Optional query handle
* @param int $mode Fetch mode identifier
*
* @return mixed Array with col values or false on failure
*/
protected function _fetch_row($result, $mode)
{
if ($result || ($result === null && ($result = $this->last_result))) {
return $result->fetch($mode);
}
return false;
}
/**
* Adds LIMIT,OFFSET clauses to the query
*
* @param string $query SQL query
* @param int $limit Number of rows
* @param int $offset Offset
*
* @return string SQL query
*/
protected function set_limit($query, $limit = 0, $offset = 0)
{
if ($limit) {
$query .= ' LIMIT ' . intval($limit);
}
if ($offset) {
$query .= ' OFFSET ' . intval($offset);
}
return $query;
}
/**
* Returns list of tables in a database
*
* @return array List of all tables of the current database
*/
public function list_tables()
{
// get tables if not cached
if ($this->tables === null) {
$q = $this->query('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES ORDER BY TABLE_NAME');
if ($q) {
$this->tables = $q->fetchAll(PDO::FETCH_COLUMN, 0);
}
else {
$this->tables = array();
}
}
return $this->tables;
}
/**
* Returns list of columns in database table
*
* @param string $table Table name
*
* @return array List of table cols
*/
public function list_cols($table)
{
$q = $this->query('SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?',
array($table));
if ($q) {
return $q->fetchAll(PDO::FETCH_COLUMN, 0);
}
return array();
}
/**
* Formats input so it can be safely used in a query
*
* @param mixed $input Value to quote
* @param string $type Type of data (integer, bool, ident)
*
* @return string Quoted/converted string for use in query
*/
public function quote($input, $type = null)
{
// handle int directly for better performance
if ($type == 'integer' || $type == 'int') {
return intval($input);
}
if (is_null($input)) {
return 'NULL';
}
if ($type == 'ident') {
return $this->quote_identifier($input);
}
// create DB handle if not available
if (!$this->dbh) {
$this->db_connect('r');
}
if ($this->dbh) {
$map = array(
'bool' => PDO::PARAM_BOOL,
'integer' => PDO::PARAM_INT,
);
+
$type = isset($map[$type]) ? $map[$type] : PDO::PARAM_STR;
- return strtr($this->dbh->quote($input, $type), array('?' => '??')); // escape ?
+
+ return strtr($this->dbh->quote($input, $type),
+ // escape ? and `
+ array('?' => '??', self::DEFAULT_QUOTE => self::DEFAULT_QUOTE.self::DEFAULT_QUOTE)
+ );
}
return 'NULL';
}
/**
* Escapes a string so it can be safely used in a query
*
* @param string $str A string to escape
*
* @return string Escaped string for use in a query
*/
public function escape($str)
{
if (is_null($str)) {
return 'NULL';
}
return substr($this->quote($str), 1, -1);
}
/**
* Quotes a string so it can be safely used as a table or column name
*
* @param string $str Value to quote
*
* @return string Quoted string for use in query
* @deprecated Replaced by rcube_db::quote_identifier
* @see rcube_db::quote_identifier
*/
public function quoteIdentifier($str)
{
return $this->quote_identifier($str);
}
/**
* Escapes a string so it can be safely used in a query
*
* @param string $str A string to escape
*
* @return string Escaped string for use in a query
* @deprecated Replaced by rcube_db::escape
* @see rcube_db::escape
*/
public function escapeSimple($str)
{
return $this->escape($str);
}
/**
* Quotes a string so it can be safely used as a table or column name
*
* @param string $str Value to quote
*
* @return string Quoted string for use in query
*/
public function quote_identifier($str)
{
$start = $this->options['identifier_start'];
$end = $this->options['identifier_end'];
$name = array();
foreach (explode('.', $str) as $elem) {
$elem = str_replace(array($start, $end), '', $elem);
$name[] = $start . $elem . $end;
}
return implode($name, '.');
}
/**
* Return SQL function for current time and date
*
* @param int $interval Optional interval (in seconds) to add/subtract
*
* @return string SQL function to use in query
*/
public function now($interval = 0)
{
if ($interval) {
$add = ' ' . ($interval > 0 ? '+' : '-') . ' INTERVAL ';
$add .= $interval > 0 ? intval($interval) : intval($interval) * -1;
$add .= ' SECOND';
}
return "now()" . $add;
}
/**
* Return list of elements for use with SQL's IN clause
*
* @param array $arr Input array
* @param string $type Type of data (integer, bool, ident)
*
* @return string Comma-separated list of quoted values for use in query
*/
public function array2list($arr, $type = null)
{
if (!is_array($arr)) {
return $this->quote($arr, $type);
}
foreach ($arr as $idx => $item) {
$arr[$idx] = $this->quote($item, $type);
}
return implode(',', $arr);
}
/**
* Return SQL statement to convert a field value into a unix timestamp
*
* This method is deprecated and should not be used anymore due to limitations
* of timestamp functions in Mysql (year 2038 problem)
*
* @param string $field Field name
*
* @return string SQL statement to use in query
* @deprecated
*/
public function unixtimestamp($field)
{
return "UNIX_TIMESTAMP($field)";
}
/**
* Return SQL statement to convert from a unix timestamp
*
* @param int $timestamp Unix timestamp
*
* @return string Date string in db-specific format
*/
public function fromunixtime($timestamp)
{
return date("'Y-m-d H:i:s'", $timestamp);
}
/**
* Return SQL statement for case insensitive LIKE
*
* @param string $column Field name
* @param string $value Search value
*
* @return string SQL statement to use in query
*/
public function ilike($column, $value)
{
return $this->quote_identifier($column).' LIKE '.$this->quote($value);
}
/**
* Abstract SQL statement for value concatenation
*
* @return string SQL statement to be used in query
*/
public function concat(/* col1, col2, ... */)
{
$args = func_get_args();
if (is_array($args[0])) {
$args = $args[0];
}
return '(' . join(' || ', $args) . ')';
}
/**
* Encodes non-UTF-8 characters in string/array/object (recursive)
*
* @param mixed $input Data to fix
* @param bool $serialized Enable serialization
*
* @return mixed Properly UTF-8 encoded data
*/
public static function encode($input, $serialized = false)
{
// use Base64 encoding to workaround issues with invalid
// or null characters in serialized string (#1489142)
if ($serialized) {
return base64_encode(serialize($input));
}
if (is_object($input)) {
foreach (get_object_vars($input) as $idx => $value) {
$input->$idx = self::encode($value);
}
return $input;
}
else if (is_array($input)) {
foreach ($input as $idx => $value) {
$input[$idx] = self::encode($value);
}
return $input;
}
return utf8_encode($input);
}
/**
* Decodes encoded UTF-8 string/object/array (recursive)
*
* @param mixed $input Input data
* @param bool $serialized Enable serialization
*
* @return mixed Decoded data
*/
public static function decode($input, $serialized = false)
{
// use Base64 encoding to workaround issues with invalid
// or null characters in serialized string (#1489142)
if ($serialized) {
// Keep backward compatybility where base64 wasn't used
if (strpos(substr($input, 0, 16), ':') !== false) {
return self::decode(@unserialize($input));
}
return @unserialize(base64_decode($input));
}
if (is_object($input)) {
foreach (get_object_vars($input) as $idx => $value) {
$input->$idx = self::decode($value);
}
return $input;
}
else if (is_array($input)) {
foreach ($input as $idx => $value) {
$input[$idx] = self::decode($value);
}
return $input;
}
return utf8_decode($input);
}
/**
* Return correct name for a specific database table
*
- * @param string $table Table name
+ * @param string $table Table name
+ * @param bool $quoted Quote table identifier
*
* @return string Translated table name
*/
- public function table_name($table)
+ public function table_name($table, $quoted = false)
{
// add prefix to the table name if configured
if (($prefix = $this->options['table_prefix']) && strpos($table, $prefix) !== 0) {
- return $prefix . $table;
+ $table = $prefix . $table;
+ }
+
+ if ($quoted) {
+ $table = $this->quote_identifier($table);
}
return $table;
}
/**
* Set class option value
*
* @param string $name Option name
* @param mixed $value Option value
*/
public function set_option($name, $value)
{
$this->options[$name] = $value;
}
/**
* Set DSN connection to be used for the given table
*
* @param string Table name
* @param string DSN connection ('r' or 'w') to be used
*/
public function set_table_dsn($table, $mode)
{
$this->options['table_dsn_map'][$this->table_name($table)] = $mode;
}
/**
* MDB2 DSN string parser
*
* @param string $sequence Secuence name
*
* @return array DSN parameters
*/
public static function parse_dsn($dsn)
{
if (empty($dsn)) {
return null;
}
// Find phptype and dbsyntax
if (($pos = strpos($dsn, '://')) !== false) {
$str = substr($dsn, 0, $pos);
$dsn = substr($dsn, $pos + 3);
}
else {
$str = $dsn;
$dsn = null;
}
// Get phptype and dbsyntax
// $str => phptype(dbsyntax)
if (preg_match('|^(.+?)\((.*?)\)$|', $str, $arr)) {
$parsed['phptype'] = $arr[1];
$parsed['dbsyntax'] = !$arr[2] ? $arr[1] : $arr[2];
}
else {
$parsed['phptype'] = $str;
$parsed['dbsyntax'] = $str;
}
if (empty($dsn)) {
return $parsed;
}
// Get (if found): username and password
// $dsn => username:password@protocol+hostspec/database
if (($at = strrpos($dsn,'@')) !== false) {
$str = substr($dsn, 0, $at);
$dsn = substr($dsn, $at + 1);
if (($pos = strpos($str, ':')) !== false) {
$parsed['username'] = rawurldecode(substr($str, 0, $pos));
$parsed['password'] = rawurldecode(substr($str, $pos + 1));
}
else {
$parsed['username'] = rawurldecode($str);
}
}
// Find protocol and hostspec
// $dsn => proto(proto_opts)/database
if (preg_match('|^([^(]+)\((.*?)\)/?(.*?)$|', $dsn, $match)) {
$proto = $match[1];
$proto_opts = $match[2] ? $match[2] : false;
$dsn = $match[3];
}
// $dsn => protocol+hostspec/database (old format)
else {
if (strpos($dsn, '+') !== false) {
list($proto, $dsn) = explode('+', $dsn, 2);
}
if ( strpos($dsn, '//') === 0
&& strpos($dsn, '/', 2) !== false
&& $parsed['phptype'] == 'oci8'
) {
//oracle's "Easy Connect" syntax:
//"username/password@[//]host[:port][/service_name]"
//e.g. "scott/tiger@//mymachine:1521/oracle"
$proto_opts = $dsn;
$pos = strrpos($proto_opts, '/');
$dsn = substr($proto_opts, $pos + 1);
$proto_opts = substr($proto_opts, 0, $pos);
}
else if (strpos($dsn, '/') !== false) {
list($proto_opts, $dsn) = explode('/', $dsn, 2);
}
else {
$proto_opts = $dsn;
$dsn = null;
}
}
// process the different protocol options
$parsed['protocol'] = (!empty($proto)) ? $proto : 'tcp';
$proto_opts = rawurldecode($proto_opts);
if (strpos($proto_opts, ':') !== false) {
list($proto_opts, $parsed['port']) = explode(':', $proto_opts);
}
if ($parsed['protocol'] == 'tcp') {
$parsed['hostspec'] = $proto_opts;
}
else if ($parsed['protocol'] == 'unix') {
$parsed['socket'] = $proto_opts;
}
// Get dabase if any
// $dsn => database
if ($dsn) {
// /database
if (($pos = strpos($dsn, '?')) === false) {
$parsed['database'] = rawurldecode($dsn);
// /database?param1=value1&param2=value2
}
else {
$parsed['database'] = rawurldecode(substr($dsn, 0, $pos));
$dsn = substr($dsn, $pos + 1);
if (strpos($dsn, '&') !== false) {
$opts = explode('&', $dsn);
}
else { // database?param1=value1
$opts = array($dsn);
}
foreach ($opts as $opt) {
list($key, $value) = explode('=', $opt);
if (!array_key_exists($key, $parsed) || false === $parsed[$key]) {
// don't allow params overwrite
$parsed[$key] = rawurldecode($value);
}
}
}
}
return $parsed;
}
/**
* Returns PDO DSN string from DSN array
*
* @param array $dsn DSN parameters
*
* @return string DSN string
*/
protected function dsn_string($dsn)
{
$params = array();
$result = $dsn['phptype'] . ':';
if ($dsn['hostspec']) {
$params[] = 'host=' . $dsn['hostspec'];
}
if ($dsn['port']) {
$params[] = 'port=' . $dsn['port'];
}
if ($dsn['database']) {
$params[] = 'dbname=' . $dsn['database'];
}
if (!empty($params)) {
$result .= implode(';', $params);
}
return $result;
}
/**
* Returns driver-specific connection options
*
* @param array $dsn DSN parameters
*
* @return array Connection options
*/
protected function dsn_options($dsn)
{
$result = array();
return $result;
}
/**
* Execute the given SQL script
*
* @param string SQL queries to execute
*
* @return boolen True on success, False on error
*/
public function exec_script($sql)
{
$sql = $this->fix_table_names($sql);
$buff = '';
foreach (explode("\n", $sql) as $line) {
if (preg_match('/^--/', $line) || trim($line) == '')
continue;
$buff .= $line . "\n";
if (preg_match('/(;|^GO)$/', trim($line))) {
$this->query($buff);
$buff = '';
if ($this->db_error) {
break;
}
}
}
return !$this->db_error;
}
/**
* Parse SQL file and fix table names according to table prefix
*/
protected function fix_table_names($sql)
{
if (!$this->options['table_prefix']) {
return $sql;
}
$sql = preg_replace_callback(
'/((TABLE|TRUNCATE|(?<!ON )UPDATE|INSERT INTO|FROM'
. '| ON(?! (DELETE|UPDATE))|REFERENCES|CONSTRAINT|FOREIGN KEY|INDEX)'
. '\s+(IF (NOT )?EXISTS )?[`"]*)([^`"\( \r\n]+)/',
array($this, 'fix_table_names_callback'),
$sql
);
return $sql;
}
/**
* Preg_replace callback for fix_table_names()
*/
protected function fix_table_names_callback($matches)
{
return $matches[1] . $this->options['table_prefix'] . $matches[count($matches)-1];
}
}
diff --git a/program/lib/Roundcube/rcube_db_oracle.php b/program/lib/Roundcube/rcube_db_oracle.php
new file mode 100644
index 000000000..ddd351ec3
--- /dev/null
+++ b/program/lib/Roundcube/rcube_db_oracle.php
@@ -0,0 +1,263 @@
+<?php
+
+/**
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) 2011-2014, 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: |
+ | Database wrapper class that implements PHP PDO functions |
+ | for Oracle database |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Database independent query interface
+ * This is a wrapper for the PHP PDO
+ *
+ * @package Framework
+ * @subpackage Database
+ */
+class rcube_db_oracle extends rcube_db
+{
+ public $db_provider = 'oracle';
+
+ /**
+ * Driver-specific configuration of database connection
+ *
+ * @param array $dsn DSN for DB connections
+ * @param PDO $dbh Connection handler
+ */
+ protected function conn_configure($dsn, $dbh)
+ {
+ $dbh->query("ALTER SESSION SET nls_date_format = 'YYYY-MM-DD'");
+ $dbh->query("ALTER SESSION SET nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'");
+ }
+
+ /**
+ * Get last inserted record ID
+ *
+ * @param string $table Table name (to find the incremented sequence)
+ *
+ * @return mixed ID or false on failure
+ */
+ public function insert_id($table = null)
+ {
+ if (!$this->db_connected || $this->db_mode == 'r' || empty($table)) {
+ return false;
+ }
+
+ $sequence = $this->quote_identifier($this->sequence_name($table));
+ $result = $dbh->query("SELECT $sequence.currval FROM dual");
+
+ return $result ? $result->fetchColumn() : false;
+ }
+
+ /**
+ * Formats input so it can be safely used in a query
+ * PDO_OCI does not implement quote() method
+ *
+ * @param mixed $input Value to quote
+ * @param string $type Type of data (integer, bool, ident)
+ *
+ * @return string Quoted/converted string for use in query
+ */
+ public function quote($input, $type = null)
+ {
+ // handle int directly for better performance
+ if ($type == 'integer' || $type == 'int') {
+ return intval($input);
+ }
+
+ if (is_null($input)) {
+ return 'NULL';
+ }
+
+ if ($type == 'ident') {
+ return $this->quote_identifier($input);
+ }
+
+ switch ($type) {
+ case 'bool':
+ case 'integer':
+ return intval($input);
+ default:
+ return "'" . strtr($input, array(
+ '?' => '??',
+ "'" => "''",
+ rcube_db::DEFAULT_QUOTE => rcube_db::DEFAULT_QUOTE . rcube_db::DEFAULT_QUOTE
+ )) . "'";
+ }
+ }
+
+ /**
+ * Return correct name for a specific database sequence
+ *
+ * @param string $table Table name
+ *
+ * @return string Translated sequence name
+ */
+ protected function sequence_name($table)
+ {
+ // Note: we support only one sequence per table
+ // Note: The sequence name must be <table_name>_seq
+ $sequence = $table . '_seq';
+
+ // modify sequence name if prefix is configured
+ if ($prefix = $this->options['table_prefix']) {
+ return $prefix . $sequence;
+ }
+
+ return $sequence;
+ }
+
+ /**
+ * Return SQL statement for case insensitive LIKE
+ *
+ * @param string $column Field name
+ * @param string $value Search value
+ *
+ * @return string SQL statement to use in query
+ */
+ public function ilike($column, $value)
+ {
+ return 'UPPER(' . $this->quote_identifier($column) . ') LIKE UPPER(' . $this->quote($value) . ')';
+ }
+
+ /**
+ * Return SQL function for current time and date
+ *
+ * @param int $interval Optional interval (in seconds) to add/subtract
+ *
+ * @return string SQL function to use in query
+ */
+ public function now($interval = 0)
+ {
+ if ($interval) {
+ $interval = intval($interval);
+ return "current_timestamp + INTERVAL '$interval' SECOND";
+ }
+
+ return "current_timestamp";
+ }
+
+ /**
+ * Return SQL statement to convert a field value into a unix timestamp
+ *
+ * @param string $field Field name
+ *
+ * @return string SQL statement to use in query
+ * @deprecated
+ */
+ public function unixtimestamp($field)
+ {
+ return "(($field - to_date('1970-01-01','YYYY-MM-DD')) * 60 * 60 * 24)";
+ }
+
+ /**
+ * Adds TOP (LIMIT,OFFSET) clause to the query
+ *
+ * @param string $query SQL query
+ * @param int $limit Number of rows
+ * @param int $offset Offset
+ *
+ * @return string SQL query
+ */
+ protected function set_limit($query, $limit = 0, $offset = 0)
+ {
+ $limit = intval($limit);
+ $offset = intval($offset);
+ $end = $offset + $limit;
+
+ // @TODO: Oracle 12g has better OFFSET support
+
+ $orderby = stristr($query, 'ORDER BY');
+ $select = substr($query, 0, stripos($query, 'FROM'));
+ $offset += 1;
+
+ if ($orderby !== false) {
+ $query = trim(substr($query, 0, -1 * strlen($orderby)));
+ }
+ else {
+ // it shouldn't happen, paging without sorting has not much sense
+ // @FIXME: I don't know how to build paging query without ORDER BY
+ $orderby = "ORDER BY 1";
+ }
+
+ $query = preg_replace('/^SELECT\s/i', '', $query);
+ $query = "$select FROM (SELECT ROW_NUMBER() OVER ($orderby) AS row_number, $query)"
+ . " WHERE row_number BETWEEN $offset AND $end";
+
+ return $query;
+ }
+
+ /**
+ * Parse SQL file and fix table names according to table prefix
+ */
+ protected function fix_table_names($sql)
+ {
+ if (!$this->options['table_prefix']) {
+ return $sql;
+ }
+
+ $sql = parent::fix_table_names($sql);
+
+ // replace sequence names, and other Oracle-specific commands
+ $sql = preg_replace_callback('/((SEQUENCE ["]?)([^" \r\n]+)/',
+ array($this, 'fix_table_names_callback'),
+ $sql
+ );
+
+ $sql = preg_replace_callback(
+ '/([ \r\n]+["]?)([^"\' \r\n\.]+)(["]?\.nextval)/',
+ array($this, 'fix_table_names_seq_callback'),
+ $sql
+ );
+
+ return $sql;
+ }
+
+ /**
+ * Preg_replace callback for fix_table_names()
+ */
+ protected function fix_table_names_seq_callback($matches)
+ {
+ return $matches[1] . $this->options['table_prefix'] . $matches[2] . $matches[3];
+ }
+
+ /**
+ * Returns PDO DSN string from DSN array
+ */
+ protected function dsn_string($dsn)
+ {
+ $params = array();
+ $result = 'oci:';
+
+ if ($dsn['hostspec']) {
+ $host = $dsn['hostspec'];
+ if ($dsn['port']) {
+ $host .= ':' . $dsn['port'];
+ }
+
+ $dsn['database'] = $host . '/' . $dsn['database'];
+ }
+
+ if ($dsn['database']) {
+ $params[] = 'dbname=' . $dsn['database'];
+ }
+
+ $params['charset'] = 'UTF8';
+
+ if (!empty($params)) {
+ $result .= implode(';', $params);
+ }
+
+ return $result;
+ }
+}
diff --git a/program/lib/Roundcube/rcube_imap_cache.php b/program/lib/Roundcube/rcube_imap_cache.php
index e49e77803..519132126 100644
--- a/program/lib/Roundcube/rcube_imap_cache.php
+++ b/program/lib/Roundcube/rcube_imap_cache.php
@@ -1,1309 +1,1317 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2012, The Roundcube Dev Team |
| |
| 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: |
| Caching of IMAP folder contents (messages and index) |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing Roundcube messages cache
*
* @package Framework
* @subpackage Storage
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_imap_cache
{
const MODE_INDEX = 1;
const MODE_MESSAGE = 2;
/**
* Instance of rcube_imap
*
* @var rcube_imap
*/
private $imap;
/**
* Instance of rcube_db
*
* @var rcube_db
*/
private $db;
/**
* User ID
*
* @var int
*/
private $userid;
/**
* Expiration time in seconds
*
* @var int
*/
private $ttl;
/**
* Maximum cached message size
*
* @var int
*/
private $threshold;
/**
* Internal (in-memory) cache
*
* @var array
*/
private $icache = array();
private $skip_deleted = false;
private $mode;
/**
* List of known flags. Thanks to this we can handle flag changes
* with good performance. Bad thing is we need to know used flags.
*/
public $flags = array(
1 => 'SEEN', // RFC3501
2 => 'DELETED', // RFC3501
4 => 'ANSWERED', // RFC3501
8 => 'FLAGGED', // RFC3501
16 => 'DRAFT', // RFC3501
32 => 'MDNSENT', // RFC3503
64 => 'FORWARDED', // RFC5550
128 => 'SUBMITPENDING', // RFC5550
256 => 'SUBMITTED', // RFC5550
512 => 'JUNK',
1024 => 'NONJUNK',
2048 => 'LABEL1',
4096 => 'LABEL2',
8192 => 'LABEL3',
16384 => 'LABEL4',
32768 => 'LABEL5',
);
/**
* Object constructor.
*
* @param rcube_db $db DB handler
* @param rcube_imap $imap IMAP handler
* @param int $userid User identifier
* @param bool $skip_deleted skip_deleted flag
* @param string $ttl Expiration time of memcache/apc items
* @param int $threshold Maximum cached message size
*/
function __construct($db, $imap, $userid, $skip_deleted, $ttl=0, $threshold=0)
{
// convert ttl string to seconds
$ttl = get_offset_sec($ttl);
if ($ttl > 2592000) $ttl = 2592000;
$this->db = $db;
$this->imap = $imap;
$this->userid = $userid;
$this->skip_deleted = $skip_deleted;
$this->ttl = $ttl;
$this->threshold = $threshold;
// cache all possible information by default
$this->mode = self::MODE_INDEX | self::MODE_MESSAGE;
+
+ // database tables
+ $this->index_table = $db->table_name('cache_index', true);
+ $this->thread_table = $db->table_name('cache_thread', true);
+ $this->messages_table = $db->table_name('cache_messages', true);
}
/**
* Cleanup actions (on shutdown).
*/
public function close()
{
$this->save_icache();
$this->icache = null;
}
/**
* Set cache mode
*
* @param int $mode Cache mode
*/
public function set_mode($mode)
{
$this->mode = $mode;
}
/**
* Return (sorted) messages index (UIDs).
* If index doesn't exist or is invalid, will be updated.
*
* @param string $mailbox Folder name
* @param string $sort_field Sorting column
* @param string $sort_order Sorting order (ASC|DESC)
* @param bool $exiting Skip index initialization if it doesn't exist in DB
*
* @return array Messages index
*/
function get_index($mailbox, $sort_field = null, $sort_order = null, $existing = false)
{
if (empty($this->icache[$mailbox])) {
$this->icache[$mailbox] = array();
}
$sort_order = strtoupper($sort_order) == 'ASC' ? 'ASC' : 'DESC';
// Seek in internal cache
if (array_key_exists('index', $this->icache[$mailbox])) {
// The index was fetched from database already, but not validated yet
if (empty($this->icache[$mailbox]['index']['validated'])) {
$index = $this->icache[$mailbox]['index'];
}
// We've got a valid index
else if ($sort_field == 'ANY' || $this->icache[$mailbox]['index']['sort_field'] == $sort_field) {
$result = $this->icache[$mailbox]['index']['object'];
if ($result->get_parameters('ORDER') != $sort_order) {
$result->revert();
}
return $result;
}
}
// Get index from DB (if DB wasn't already queried)
if (empty($index) && empty($this->icache[$mailbox]['index_queried'])) {
$index = $this->get_index_row($mailbox);
// set the flag that DB was already queried for index
// this way we'll be able to skip one SELECT, when
// get_index() is called more than once
$this->icache[$mailbox]['index_queried'] = true;
}
$data = null;
// @TODO: Think about skipping validation checks.
// If we could check only every 10 minutes, we would be able to skip
// expensive checks, mailbox selection or even IMAP connection, this would require
// additional logic to force cache invalidation in some cases
// and many rcube_imap changes to connect when needed
// Entry exists, check cache status
if (!empty($index)) {
$exists = true;
if ($sort_field == 'ANY') {
$sort_field = $index['sort_field'];
}
if ($sort_field != $index['sort_field']) {
$is_valid = false;
}
else {
$is_valid = $this->validate($mailbox, $index, $exists);
}
if ($is_valid) {
$data = $index['object'];
// revert the order if needed
if ($data->get_parameters('ORDER') != $sort_order) {
$data->revert();
}
}
}
else {
if ($existing) {
return null;
}
else if ($sort_field == 'ANY') {
$sort_field = '';
}
// Got it in internal cache, so the row already exist
$exists = array_key_exists('index', $this->icache[$mailbox]);
}
// Index not found, not valid or sort field changed, get index from IMAP server
if ($data === null) {
// Get mailbox data (UIDVALIDITY, counters, etc.) for status check
$mbox_data = $this->imap->folder_data($mailbox);
$data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
// insert/update
$this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists, $index['modseq']);
}
$this->icache[$mailbox]['index'] = array(
'validated' => true,
'object' => $data,
'sort_field' => $sort_field,
'modseq' => !empty($index['modseq']) ? $index['modseq'] : $mbox_data['HIGHESTMODSEQ']
);
return $data;
}
/**
* Return messages thread.
* If threaded index doesn't exist or is invalid, will be updated.
*
* @param string $mailbox Folder name
*
* @return array Messages threaded index
*/
function get_thread($mailbox)
{
if (empty($this->icache[$mailbox])) {
$this->icache[$mailbox] = array();
}
// Seek in internal cache
if (array_key_exists('thread', $this->icache[$mailbox])) {
return $this->icache[$mailbox]['thread']['object'];
}
// Get thread from DB (if DB wasn't already queried)
if (empty($this->icache[$mailbox]['thread_queried'])) {
$index = $this->get_thread_row($mailbox);
// set the flag that DB was already queried for thread
// this way we'll be able to skip one SELECT, when
// get_thread() is called more than once or after clear()
$this->icache[$mailbox]['thread_queried'] = true;
}
// Entry exist, check cache status
if (!empty($index)) {
$exists = true;
$is_valid = $this->validate($mailbox, $index, $exists);
if (!$is_valid) {
$index = null;
}
}
// Index not found or not valid, get index from IMAP server
if ($index === null) {
// Get mailbox data (UIDVALIDITY, counters, etc.) for status check
$mbox_data = $this->imap->folder_data($mailbox);
// Get THREADS result
$index['object'] = $this->get_thread_data($mailbox, $mbox_data);
// insert/update
$this->add_thread_row($mailbox, $index['object'], $mbox_data, $exists);
}
$this->icache[$mailbox]['thread'] = $index;
return $index['object'];
}
/**
* Returns list of messages (headers). See rcube_imap::fetch_headers().
*
* @param string $mailbox Folder name
* @param array $msgs Message UIDs
*
* @return array The list of messages (rcube_message_header) indexed by UID
*/
function get_messages($mailbox, $msgs = array())
{
if (empty($msgs)) {
return array();
}
$result = array();
if ($this->mode & self::MODE_MESSAGE) {
// Fetch messages from cache
$sql_result = $this->db->query(
- "SELECT uid, data, flags"
- ." FROM ".$this->db->table_name('cache_messages')
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- ." AND uid IN (".$this->db->array2list($msgs, 'integer').")",
+ "SELECT `uid`, `data`, `flags`"
+ ." FROM {$this->messages_table}"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?"
+ ." AND `uid` IN (".$this->db->array2list($msgs, 'integer').")",
$this->userid, $mailbox);
$msgs = array_flip($msgs);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$uid = intval($sql_arr['uid']);
$result[$uid] = $this->build_message($sql_arr);
if (!empty($result[$uid])) {
// save memory, we don't need message body here (?)
$result[$uid]->body = null;
unset($msgs[$uid]);
}
}
$msgs = array_flip($msgs);
}
// Fetch not found messages from IMAP server
if (!empty($msgs)) {
$messages = $this->imap->fetch_headers($mailbox, $msgs, false, true);
// Insert to DB and add to result list
if (!empty($messages)) {
foreach ($messages as $msg) {
if ($this->mode & self::MODE_MESSAGE) {
$this->add_message($mailbox, $msg, !array_key_exists($msg->uid, $result));
}
$result[$msg->uid] = $msg;
}
}
}
return $result;
}
/**
* Returns message data.
*
* @param string $mailbox Folder name
* @param int $uid Message UID
* @param bool $update If message doesn't exists in cache it will be fetched
* from IMAP server
* @param bool $no_cache Enables internal cache usage
*
* @return rcube_message_header Message data
*/
function get_message($mailbox, $uid, $update = true, $cache = true)
{
// Check internal cache
if ($this->icache['__message']
&& $this->icache['__message']['mailbox'] == $mailbox
&& $this->icache['__message']['object']->uid == $uid
) {
return $this->icache['__message']['object'];
}
if ($this->mode & self::MODE_MESSAGE) {
$sql_result = $this->db->query(
- "SELECT flags, data"
- ." FROM ".$this->db->table_name('cache_messages')
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- ." AND uid = ?",
+ "SELECT `flags`, `data`"
+ ." FROM {$this->messages_table}"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?"
+ ." AND `uid` = ?",
$this->userid, $mailbox, (int)$uid);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$message = $this->build_message($sql_arr);
$found = true;
}
}
// Get the message from IMAP server
if (empty($message) && $update) {
$message = $this->imap->get_message_headers($uid, $mailbox, true);
// cache will be updated in close(), see below
}
if (!($this->mode & self::MODE_MESSAGE)) {
return $message;
}
// Save the message in internal cache, will be written to DB in close()
// Common scenario: user opens unseen message
// - get message (SELECT)
// - set message headers/structure (INSERT or UPDATE)
// - set \Seen flag (UPDATE)
// This way we can skip one UPDATE
if (!empty($message) && $cache) {
// Save current message from internal cache
$this->save_icache();
$this->icache['__message'] = array(
'object' => $message,
'mailbox' => $mailbox,
'exists' => $found,
'md5sum' => md5(serialize($message)),
);
}
return $message;
}
/**
* Saves the message in cache.
*
* @param string $mailbox Folder name
* @param rcube_message_header $message Message data
* @param bool $force Skips message in-cache existance check
*/
function add_message($mailbox, $message, $force = false)
{
if (!is_object($message) || empty($message->uid)) {
return;
}
if (!($this->mode & self::MODE_MESSAGE)) {
return;
}
$flags = 0;
$msg = clone $message;
if (!empty($message->flags)) {
foreach ($this->flags as $idx => $flag) {
if (!empty($message->flags[$flag])) {
$flags += $idx;
}
}
}
unset($msg->flags);
$msg = $this->db->encode($msg, true);
// update cache record (even if it exists, the update
// here will work as select, assume row exist if affected_rows=0)
if (!$force) {
$res = $this->db->query(
- "UPDATE ".$this->db->table_name('cache_messages')
- ." SET flags = ?, data = ?, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- ." AND uid = ?",
+ "UPDATE {$this->messages_table}"
+ ." SET `flags` = ?, `data` = ?, `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?"
+ ." AND `uid` = ?",
$flags, $msg, $this->userid, $mailbox, (int) $message->uid);
if ($this->db->affected_rows($res)) {
return;
}
}
$this->db->set_option('ignore_key_errors', true);
// insert new record
$res = $this->db->query(
- "INSERT INTO ".$this->db->table_name('cache_messages')
- ." (user_id, mailbox, uid, flags, expires, data)"
+ "INSERT INTO {$this->messages_table}"
+ ." (`user_id`, `mailbox`, `uid`, `flags`, `expires`, `data`)"
." VALUES (?, ?, ?, ?, ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?)",
$this->userid, $mailbox, (int) $message->uid, $flags, $msg);
// race-condition, insert failed so try update (#1489146)
// thanks to ignore_key_errors "duplicate row" errors will be ignored
if ($force && !$res && !$this->db->is_error($res)) {
$this->db->query(
- "UPDATE ".$this->db->table_name('cache_messages')
- ." SET expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
- .", flags = ?, data = ?"
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- ." AND uid = ?",
+ "UPDATE {$this->messages_table}"
+ ." SET `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+ .", `flags` = ?, `data` = ?"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?"
+ ." AND `uid` = ?",
$flags, $msg, $this->userid, $mailbox, (int) $message->uid);
}
$this->db->set_option('ignore_key_errors', false);
}
/**
* Sets the flag for specified message.
*
* @param string $mailbox Folder name
* @param array $uids Message UIDs or null to change flag
* of all messages in a folder
* @param string $flag The name of the flag
* @param bool $enabled Flag state
*/
function change_flag($mailbox, $uids, $flag, $enabled = false)
{
if (empty($uids)) {
return;
}
if (!($this->mode & self::MODE_MESSAGE)) {
return;
}
$flag = strtoupper($flag);
$idx = (int) array_search($flag, $this->flags);
$uids = (array) $uids;
if (!$idx) {
return;
}
// Internal cache update
if (($message = $this->icache['__message'])
&& $message['mailbox'] === $mailbox
&& in_array($message['object']->uid, $uids)
) {
$message['object']->flags[$flag] = $enabled;
if (count($uids) == 1) {
return;
}
}
$this->db->query(
- "UPDATE ".$this->db->table_name('cache_messages')
- ." SET expires = ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
- .", flags = flags ".($enabled ? "+ $idx" : "- $idx")
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : "")
- ." AND (flags & $idx) ".($enabled ? "= 0" : "= $idx"),
+ "UPDATE {$this->messages_table}"
+ ." SET `expires` = ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+ .", `flags` = `flags` ".($enabled ? "+ $idx" : "- $idx")
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?"
+ .(!empty($uids) ? " AND `uid` IN (".$this->db->array2list($uids, 'integer').")" : "")
+ ." AND (`flags` & $idx) ".($enabled ? "= 0" : "= $idx"),
$this->userid, $mailbox);
}
/**
* Removes message(s) from cache.
*
* @param string $mailbox Folder name
* @param array $uids Message UIDs, NULL removes all messages
*/
function remove_message($mailbox = null, $uids = null)
{
if (!($this->mode & self::MODE_MESSAGE)) {
return;
}
if (!strlen($mailbox)) {
$this->db->query(
- "DELETE FROM ".$this->db->table_name('cache_messages')
- ." WHERE user_id = ?",
+ "DELETE FROM {$this->messages_table}"
+ ." WHERE `user_id` = ?",
$this->userid);
}
else {
// Remove the message from internal cache
if (!empty($uids) && ($message = $this->icache['__message'])
&& $message['mailbox'] === $mailbox
&& in_array($message['object']->uid, (array)$uids)
) {
$this->icache['__message'] = null;
}
$this->db->query(
- "DELETE FROM ".$this->db->table_name('cache_messages')
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- .($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : ""),
+ "DELETE FROM {$this->messages_table}"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?"
+ .($uids !== null ? " AND `uid` IN (".$this->db->array2list((array)$uids, 'integer').")" : ""),
$this->userid, $mailbox);
}
}
/**
* Clears index cache.
*
* @param string $mailbox Folder name
* @param bool $remove Enable to remove the DB row
*/
function remove_index($mailbox = null, $remove = false)
{
// The index should be only removed from database when
// UIDVALIDITY was detected or the mailbox is empty
// otherwise use 'valid' flag to not loose HIGHESTMODSEQ value
if ($remove) {
$this->db->query(
- "DELETE FROM ".$this->db->table_name('cache_index')
- ." WHERE user_id = ?"
- .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
+ "DELETE FROM {$this->index_table}"
+ ." WHERE `user_id` = ?"
+ .(strlen($mailbox) ? " AND `mailbox` = ".$this->db->quote($mailbox) : ""),
$this->userid
);
}
else {
$this->db->query(
- "UPDATE ".$this->db->table_name('cache_index')
- ." SET valid = 0"
- ." WHERE user_id = ?"
- .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
+ "UPDATE {$this->index_table}"
+ ." SET `valid` = 0"
+ ." WHERE `user_id` = ?"
+ .(strlen($mailbox) ? " AND `mailbox` = ".$this->db->quote($mailbox) : ""),
$this->userid
);
}
if (strlen($mailbox)) {
unset($this->icache[$mailbox]['index']);
// Index removed, set flag to skip SELECT query in get_index()
$this->icache[$mailbox]['index_queried'] = true;
}
else {
$this->icache = array();
}
}
/**
* Clears thread cache.
*
* @param string $mailbox Folder name
*/
function remove_thread($mailbox = null)
{
$this->db->query(
- "DELETE FROM ".$this->db->table_name('cache_thread')
- ." WHERE user_id = ?"
- .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
+ "DELETE FROM {$this->thread_table}"
+ ." WHERE `user_id` = ?"
+ .(strlen($mailbox) ? " AND `mailbox` = ".$this->db->quote($mailbox) : ""),
$this->userid
);
if (strlen($mailbox)) {
unset($this->icache[$mailbox]['thread']);
// Thread data removed, set flag to skip SELECT query in get_thread()
$this->icache[$mailbox]['thread_queried'] = true;
}
else {
$this->icache = array();
}
}
/**
* Clears the cache.
*
* @param string $mailbox Folder name
* @param array $uids Message UIDs, NULL removes all messages in a folder
*/
function clear($mailbox = null, $uids = null)
{
$this->remove_index($mailbox, true);
$this->remove_thread($mailbox);
$this->remove_message($mailbox, $uids);
}
/**
* Delete expired cache entries
*/
static function gc()
{
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
+ $now = $db->now();
- $db->query("DELETE FROM ".$db->table_name('cache_messages')
- ." WHERE expires < " . $db->now());
+ $db->query("DELETE FROM " . $db->table_name('cache_messages', true)
+ ." WHERE `expires` < $now");
- $db->query("DELETE FROM ".$db->table_name('cache_index')
- ." WHERE expires < " . $db->now());
+ $db->query("DELETE FROM " . $db->table_name('cache_index', true)
+ ." WHERE `expires` < $now");
- $db->query("DELETE FROM ".$db->table_name('cache_thread')
- ." WHERE expires < " . $db->now());
+ $db->query("DELETE FROM ".$db->table_name('cache_thread', true)
+ ." WHERE `expires` < $now");
}
/**
* Fetches index data from database
*/
private function get_index_row($mailbox)
{
// Get index from DB
$sql_result = $this->db->query(
- "SELECT data, valid"
- ." FROM ".$this->db->table_name('cache_index')
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
+ "SELECT `data`, `valid`"
+ ." FROM {$this->index_table}"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?",
$this->userid, $mailbox);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$data = explode('@', $sql_arr['data']);
$index = $this->db->decode($data[0], true);
unset($data[0]);
if (empty($index)) {
$index = new rcube_result_index($mailbox);
}
return array(
'valid' => $sql_arr['valid'],
'object' => $index,
'sort_field' => $data[1],
'deleted' => $data[2],
'validity' => $data[3],
'uidnext' => $data[4],
'modseq' => $data[5],
);
}
return null;
}
/**
* Fetches thread data from database
*/
private function get_thread_row($mailbox)
{
// Get thread from DB
$sql_result = $this->db->query(
- "SELECT data"
- ." FROM ".$this->db->table_name('cache_thread')
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
+ "SELECT `data`"
+ ." FROM {$this->thread_table}"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?",
$this->userid, $mailbox);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$data = explode('@', $sql_arr['data']);
$thread = $this->db->decode($data[0], true);
unset($data[0]);
if (empty($thread)) {
$thread = new rcube_result_thread($mailbox);
}
return array(
'object' => $thread,
'deleted' => $data[1],
'validity' => $data[2],
'uidnext' => $data[3],
);
}
return null;
}
/**
* Saves index data into database
*/
private function add_index_row($mailbox, $sort_field,
$data, $mbox_data = array(), $exists = false, $modseq = null)
{
$data = array(
$this->db->encode($data, true),
$sort_field,
(int) $this->skip_deleted,
(int) $mbox_data['UIDVALIDITY'],
(int) $mbox_data['UIDNEXT'],
$modseq ? $modseq : $mbox_data['HIGHESTMODSEQ'],
);
- $data = implode('@', $data);
+
+ $data = implode('@', $data);
+ $expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL';
if ($exists) {
$res = $this->db->query(
- "UPDATE ".$this->db->table_name('cache_index')
- ." SET data = ?, valid = 1, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
+ "UPDATE {$this->index_table}"
+ ." SET `data` = ?, `valid` = 1, `expires` = $expires"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
if ($this->db->affected_rows($res)) {
return;
}
}
$this->db->set_option('ignore_key_errors', true);
$res = $this->db->query(
- "INSERT INTO ".$this->db->table_name('cache_index')
- ." (user_id, mailbox, valid, expires, data)"
- ." VALUES (?, ?, 1, ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') .", ?)",
+ "INSERT INTO {$this->index_table}"
+ ." (`user_id`, `mailbox`, `valid`, `expires`, `data`)"
+ ." VALUES (?, ?, 1, $expires, ?)",
$this->userid, $mailbox, $data);
// race-condition, insert failed so try update (#1489146)
// thanks to ignore_key_errors "duplicate row" errors will be ignored
if (!$exists && !$res && !$this->db->is_error($res)) {
$res = $this->db->query(
- "UPDATE ".$this->db->table_name('cache_index')
- ." SET data = ?, valid = 1, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
+ "UPDATE {$this->index_table}"
+ ." SET `data` = ?, `valid` = 1, `expires` = $expires"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
}
$this->db->set_option('ignore_key_errors', false);
}
/**
* Saves thread data into database
*/
private function add_thread_row($mailbox, $data, $mbox_data = array(), $exists = false)
{
$data = array(
$this->db->encode($data, true),
(int) $this->skip_deleted,
(int) $mbox_data['UIDVALIDITY'],
(int) $mbox_data['UIDNEXT'],
);
- $data = implode('@', $data);
- $expires = ($this->ttl ? $this->db->now($this->ttl) : 'NULL');
+ $data = implode('@', $data);
+ $expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL';
if ($exists) {
$res = $this->db->query(
- "UPDATE ".$this->db->table_name('cache_thread')
- ." SET data = ?, expires = $expires"
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
+ "UPDATE {$this->thread_table}"
+ ." SET `data` = ?, `expires` = $expires"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
if ($this->db->affected_rows($res)) {
return;
}
}
$this->db->set_option('ignore_key_errors', true);
$res = $this->db->query(
- "INSERT INTO ".$this->db->table_name('cache_thread')
- ." (user_id, mailbox, expires, data)"
+ "INSERT INTO {$this->thread_table}"
+ ." (`user_id`, `mailbox`, `expires`, `data`)"
." VALUES (?, ?, $expires, ?)",
$this->userid, $mailbox, $data);
// race-condition, insert failed so try update (#1489146)
// thanks to ignore_key_errors "duplicate row" errors will be ignored
if (!$exists && !$res && !$this->db->is_error($res)) {
$this->db->query(
- "UPDATE ".$this->db->table_name('cache_thread')
- ." SET expires = $expires, data = ?"
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
+ "UPDATE {$this->thread_table}"
+ ." SET `expires` = $expires, `data` = ?"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
}
$this->db->set_option('ignore_key_errors', false);
}
/**
* Checks index/thread validity
*/
private function validate($mailbox, $index, &$exists = true)
{
$object = $index['object'];
$is_thread = is_a($object, 'rcube_result_thread');
// sanity check
if (empty($object)) {
return false;
}
$index['validated'] = true;
// Get mailbox data (UIDVALIDITY, counters, etc.) for status check
$mbox_data = $this->imap->folder_data($mailbox);
// @TODO: Think about skipping validation checks.
// If we could check only every 10 minutes, we would be able to skip
// expensive checks, mailbox selection or even IMAP connection, this would require
// additional logic to force cache invalidation in some cases
// and many rcube_imap changes to connect when needed
// Check UIDVALIDITY
if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
$this->clear($mailbox);
$exists = false;
return false;
}
// Folder is empty but cache isn't
if (empty($mbox_data['EXISTS'])) {
if (!$object->is_empty()) {
$this->clear($mailbox);
$exists = false;
return false;
}
}
// Folder is not empty but cache is
else if ($object->is_empty()) {
unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
return false;
}
// Validation flag
if (!$is_thread && empty($index['valid'])) {
unset($this->icache[$mailbox]['index']);
return false;
}
// Index was created with different skip_deleted setting
if ($this->skip_deleted != $index['deleted']) {
return false;
}
// Check HIGHESTMODSEQ
if (!empty($index['modseq']) && !empty($mbox_data['HIGHESTMODSEQ'])
&& $index['modseq'] == $mbox_data['HIGHESTMODSEQ']
) {
return true;
}
// Check UIDNEXT
if ($index['uidnext'] != $mbox_data['UIDNEXT']) {
unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
return false;
}
// @TODO: find better validity check for threaded index
if ($is_thread) {
// check messages number...
if (!$this->skip_deleted && $mbox_data['EXISTS'] != $object->count_messages()) {
return false;
}
return true;
}
// The rest of checks, more expensive
if (!empty($this->skip_deleted)) {
// compare counts if available
if (!empty($mbox_data['UNDELETED'])
&& $mbox_data['UNDELETED']->count() != $object->count()
) {
return false;
}
// compare UID sets
if (!empty($mbox_data['UNDELETED'])) {
$uids_new = $mbox_data['UNDELETED']->get();
$uids_old = $object->get();
if (count($uids_new) != count($uids_old)) {
return false;
}
sort($uids_new, SORT_NUMERIC);
sort($uids_old, SORT_NUMERIC);
if ($uids_old != $uids_new)
return false;
}
else {
// get all undeleted messages excluding cached UIDs
$ids = $this->imap->search_once($mailbox, 'ALL UNDELETED NOT UID '.
rcube_imap_generic::compressMessageSet($object->get()));
if (!$ids->is_empty()) {
return false;
}
}
}
else {
// check messages number...
if ($mbox_data['EXISTS'] != $object->count()) {
return false;
}
// ... and max UID
if ($object->max() != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox, true)) {
return false;
}
}
return true;
}
/**
* Synchronizes the mailbox.
*
* @param string $mailbox Folder name
*/
function synchronize($mailbox)
{
// RFC4549: Synchronization Operations for Disconnected IMAP4 Clients
// RFC4551: IMAP Extension for Conditional STORE Operation
// or Quick Flag Changes Resynchronization
// RFC5162: IMAP Extensions for Quick Mailbox Resynchronization
// @TODO: synchronize with other methods?
$qresync = $this->imap->get_capability('QRESYNC');
$condstore = $qresync ? true : $this->imap->get_capability('CONDSTORE');
if (!$qresync && !$condstore) {
return;
}
// Get stored index
$index = $this->get_index_row($mailbox);
// database is empty
if (empty($index)) {
// set the flag that DB was already queried for index
// this way we'll be able to skip one SELECT in get_index()
$this->icache[$mailbox]['index_queried'] = true;
return;
}
$this->icache[$mailbox]['index'] = $index;
// no last HIGHESTMODSEQ value
if (empty($index['modseq'])) {
return;
}
if (!$this->imap->check_connection()) {
return;
}
// Enable QRESYNC
$res = $this->imap->conn->enable($qresync ? 'QRESYNC' : 'CONDSTORE');
if ($res === false) {
return;
}
// Close mailbox if already selected to get most recent data
if ($this->imap->conn->selected == $mailbox) {
$this->imap->conn->close();
}
// Get mailbox data (UIDVALIDITY, HIGHESTMODSEQ, counters, etc.)
$mbox_data = $this->imap->folder_data($mailbox);
if (empty($mbox_data)) {
return;
}
// Check UIDVALIDITY
if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
$this->clear($mailbox);
return;
}
// QRESYNC not supported on specified mailbox
if (!empty($mbox_data['NOMODSEQ']) || empty($mbox_data['HIGHESTMODSEQ'])) {
return;
}
// Nothing new
if ($mbox_data['HIGHESTMODSEQ'] == $index['modseq']) {
return;
}
$uids = array();
$removed = array();
// Get known UIDs
if ($this->mode & self::MODE_MESSAGE) {
$sql_result = $this->db->query(
- "SELECT uid"
- ." FROM ".$this->db->table_name('cache_messages')
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
+ "SELECT `uid`"
+ ." FROM {$this->messages_table}"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?",
$this->userid, $mailbox);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$uids[] = $sql_arr['uid'];
}
}
// Synchronize messages data
if (!empty($uids)) {
// Get modified flags and vanished messages
// UID FETCH 1:* (FLAGS) (CHANGEDSINCE 0123456789 VANISHED)
$result = $this->imap->conn->fetch($mailbox,
$uids, true, array('FLAGS'), $index['modseq'], $qresync);
if (!empty($result)) {
foreach ($result as $msg) {
$uid = $msg->uid;
// Remove deleted message
if ($this->skip_deleted && !empty($msg->flags['DELETED'])) {
$removed[] = $uid;
// Invalidate index
$index['valid'] = false;
continue;
}
$flags = 0;
if (!empty($msg->flags)) {
foreach ($this->flags as $idx => $flag) {
if (!empty($msg->flags[$flag])) {
$flags += $idx;
}
}
}
$this->db->query(
- "UPDATE ".$this->db->table_name('cache_messages')
- ." SET flags = ?, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- ." AND uid = ?"
- ." AND flags <> ?",
+ "UPDATE {$this->messages_table}"
+ ." SET `flags` = ?, `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?"
+ ." AND `uid` = ?"
+ ." AND `flags` <> ?",
$flags, $this->userid, $mailbox, $uid, $flags);
}
}
// VANISHED found?
if ($qresync) {
$mbox_data = $this->imap->folder_data($mailbox);
// Removed messages found
$uids = rcube_imap_generic::uncompressMessageSet($mbox_data['VANISHED']);
if (!empty($uids)) {
$removed = array_merge($removed, $uids);
// Invalidate index
$index['valid'] = false;
}
}
// remove messages from database
if (!empty($removed)) {
$this->remove_message($mailbox, $removed);
}
}
$sort_field = $index['sort_field'];
$sort_order = $index['object']->get_parameters('ORDER');
$exists = true;
// Validate index
if (!$this->validate($mailbox, $index, $exists)) {
// Invalidate (remove) thread index
// if $exists=false it was already removed in validate()
if ($exists) {
$this->remove_thread($mailbox);
}
// Update index
$data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
}
else {
$data = $index['object'];
}
// update index and/or HIGHESTMODSEQ value
$this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists);
// update internal cache for get_index()
$this->icache[$mailbox]['index']['object'] = $data;
}
/**
* Converts cache row into message object.
*
* @param array $sql_arr Message row data
*
* @return rcube_message_header Message object
*/
private function build_message($sql_arr)
{
$message = $this->db->decode($sql_arr['data'], true);
if ($message) {
$message->flags = array();
foreach ($this->flags as $idx => $flag) {
if (($sql_arr['flags'] & $idx) == $idx) {
$message->flags[$flag] = true;
}
}
}
return $message;
}
/**
* Saves message stored in internal cache
*/
private function save_icache()
{
// Save current message from internal cache
if ($message = $this->icache['__message']) {
// clean up some object's data
$this->message_object_prepare($message['object']);
// calculate current md5 sum
$md5sum = md5(serialize($message['object']));
if ($message['md5sum'] != $md5sum) {
$this->add_message($message['mailbox'], $message['object'], !$message['exists']);
}
$this->icache['__message']['md5sum'] = $md5sum;
}
}
/**
* Prepares message object to be stored in database.
*
* @param rcube_message_header|rcube_message_part
*/
private function message_object_prepare(&$msg, &$size = 0)
{
// Remove body too big
if ($msg->body && ($length = strlen($msg->body))) {
$size += $length;
if ($size > $this->threshold * 1024) {
$size -= $length;
unset($msg->body);
}
}
// Fix mimetype which might be broken by some code when message is displayed
// Another solution would be to use object's copy in rcube_message class
// to prevent related issues, however I'm not sure which is better
if ($msg->mimetype) {
list($msg->ctype_primary, $msg->ctype_secondary) = explode('/', $msg->mimetype);
}
unset($msg->replaces);
if (is_object($msg->structure)) {
$this->message_object_prepare($msg->structure, $size);
}
if (is_array($msg->parts)) {
foreach ($msg->parts as $part) {
$this->message_object_prepare($part, $size);
}
}
}
/**
* Fetches index data from IMAP server
*/
private function get_index_data($mailbox, $sort_field, $sort_order, $mbox_data = array())
{
if (empty($mbox_data)) {
$mbox_data = $this->imap->folder_data($mailbox);
}
if ($mbox_data['EXISTS']) {
// fetch sorted sequence numbers
$index = $this->imap->index_direct($mailbox, $sort_field, $sort_order);
}
else {
$index = new rcube_result_index($mailbox, '* SORT');
}
return $index;
}
/**
* Fetches thread data from IMAP server
*/
private function get_thread_data($mailbox, $mbox_data = array())
{
if (empty($mbox_data)) {
$mbox_data = $this->imap->folder_data($mailbox);
}
if ($mbox_data['EXISTS']) {
// get all threads (default sort order)
return $this->imap->threads_direct($mailbox);
}
return new rcube_result_thread($mailbox, '* THREAD');
}
}
// for backward compat.
class rcube_mail_header extends rcube_message_header { }
diff --git a/program/lib/Roundcube/rcube_session.php b/program/lib/Roundcube/rcube_session.php
index 26f78433a..8306a0687 100644
--- a/program/lib/Roundcube/rcube_session.php
+++ b/program/lib/Roundcube/rcube_session.php
@@ -1,807 +1,807 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2014, The Roundcube Dev Team |
| Copyright (C) 2011, 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: |
| Provide database supported session management |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class to provide database supported session storage
*
* @package Framework
* @subpackage Core
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_session
{
private $db;
private $ip;
private $start;
private $changed;
private $time_diff = 0;
private $reloaded = false;
private $appends = array();
private $unsets = array();
private $gc_handlers = array();
private $cookiename = 'roundcube_sessauth';
private $vars;
private $key;
private $now;
private $secret = '';
private $ip_check = false;
private $logging = false;
private $storage;
private $memcache;
/**
* Blocks session data from being written to database.
* Can be used if write-race conditions are to be expected
* @var boolean
*/
public $nowrite = false;
/**
* Default constructor
*/
public function __construct($db, $config)
{
$this->db = $db;
$this->start = microtime(true);
$this->ip = rcube_utils::remote_addr();
$this->logging = $config->get('log_session', false);
$lifetime = $config->get('session_lifetime', 1) * 60;
$this->set_lifetime($lifetime);
// use memcache backend
$this->storage = $config->get('session_storage', 'db');
if ($this->storage == 'memcache') {
$this->memcache = rcube::get_instance()->get_memcache();
// set custom functions for PHP session management if memcache is available
if ($this->memcache) {
ini_set('session.serialize_handler', 'php');
session_set_save_handler(
array($this, 'open'),
array($this, 'close'),
array($this, 'mc_read'),
array($this, 'mc_write'),
array($this, 'mc_destroy'),
array($this, 'gc'));
}
else {
rcube::raise_error(array('code' => 604, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => "Failed to connect to memcached. Please check configuration"),
true, true);
}
}
else if ($this->storage != 'php') {
ini_set('session.serialize_handler', 'php');
// set custom functions for PHP session management
session_set_save_handler(
array($this, 'open'),
array($this, 'close'),
array($this, 'db_read'),
array($this, 'db_write'),
array($this, 'db_destroy'),
array($this, 'gc'));
+
+ $this->table_name = $this->db->table_name('session', true);
}
}
/**
* Wrapper for session_start()
*/
public function start()
{
session_start();
// copy some session properties to object vars
if ($this->storage == 'php') {
$this->key = session_id();
$this->ip = $_SESSION['__IP'];
$this->changed = $_SESSION['__MTIME'];
}
}
public function open($save_path, $session_name)
{
return true;
}
public function close()
{
return true;
}
/**
* Delete session data for the given key
*
* @param string Session ID
*/
public function destroy($key)
{
return $this->memcache ? $this->mc_destroy($key) : $this->db_destroy($key);
}
/**
* Wrapper for session_write_close()
*/
public function write_close()
{
if ($this->storage == 'php') {
$_SESSION['__IP'] = $this->ip;
$_SESSION['__MTIME'] = time();
}
session_write_close();
// write_close() is called on script shutdown, see rcube::shutdown()
// execute cleanup functionality if enabled by session gc handler
// we do this after closing the session for better performance
$this->gc_shutdown();
}
/**
* Read session data from database
*
* @param string Session ID
*
* @return string Session vars
*/
public function db_read($key)
{
$sql_result = $this->db->query(
- "SELECT vars, ip, changed, " . $this->db->now() . " AS ts"
- . " FROM " . $this->db->table_name('session')
- . " WHERE sess_id = ?", $key);
+ "SELECT `vars`, `ip`, `changed`, " . $this->db->now() . " AS ts"
+ . " FROM {$this->table_name} WHERE `sess_id` = ?", $key);
if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$this->time_diff = time() - strtotime($sql_arr['ts']);
$this->changed = strtotime($sql_arr['changed']);
$this->ip = $sql_arr['ip'];
$this->vars = base64_decode($sql_arr['vars']);
$this->key = $key;
return !empty($this->vars) ? (string) $this->vars : '';
}
return null;
}
/**
* Save session data.
* handler for session_read()
*
* @param string Session ID
* @param string Serialized session vars
*
* @return boolean True on success
*/
public function db_write($key, $vars)
{
- $now = $this->db->now();
- $table = $this->db->table_name('session');
- $ts = microtime(true);
+ $now = $this->db->now();
+ $ts = microtime(true);
if ($this->nowrite)
return true;
// no session row in DB (db_read() returns false)
if (!$this->key) {
$oldvars = null;
}
// use internal data from read() for fast requests (up to 0.5 sec.)
else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) {
$oldvars = $this->vars;
}
else { // else read data again from DB
$oldvars = $this->db_read($key);
}
if ($oldvars !== null) {
$newvars = $this->_fixvars($vars, $oldvars);
if ($newvars !== $oldvars) {
- $this->db->query("UPDATE $table "
- . "SET changed = $now, vars = ? WHERE sess_id = ?",
+ $this->db->query("UPDATE {$this->table_name} "
+ . "SET `changed` = $now, `vars` = ? WHERE `sess_id` = ?",
base64_encode($newvars), $key);
}
else if ($ts - $this->changed + $this->time_diff > $this->lifetime / 2) {
- $this->db->query("UPDATE $table SET changed = $now"
- . " WHERE sess_id = ?", $key);
+ $this->db->query("UPDATE {$this->table_name} SET `changed` = $now"
+ . " WHERE `sess_id` = ?", $key);
}
}
else {
- $this->db->query("INSERT INTO $table (sess_id, vars, ip, created, changed)"
+ $this->db->query("INSERT INTO {$this->table_name}"
+ . " (`sess_id`, `vars`, `ip`, `created`, `changed`)"
. " VALUES (?, ?, ?, $now, $now)",
$key, base64_encode($vars), (string)$this->ip);
}
return true;
}
/**
* Merge vars with old vars and apply unsets
*/
private function _fixvars($vars, $oldvars)
{
if ($oldvars !== null) {
$a_oldvars = $this->unserialize($oldvars);
if (is_array($a_oldvars)) {
// remove unset keys on oldvars
foreach ((array)$this->unsets as $var) {
if (isset($a_oldvars[$var])) {
unset($a_oldvars[$var]);
}
else {
$path = explode('.', $var);
$k = array_pop($path);
$node = &$this->get_node($path, $a_oldvars);
unset($node[$k]);
}
}
$newvars = $this->serialize(array_merge(
(array)$a_oldvars, (array)$this->unserialize($vars)));
}
else {
$newvars = $vars;
}
}
$this->unsets = array();
return $newvars;
}
/**
* Handler for session_destroy()
*
* @param string Session ID
*
* @return boolean True on success
*/
public function db_destroy($key)
{
if ($key) {
- $this->db->query(sprintf("DELETE FROM %s WHERE sess_id = ?",
- $this->db->table_name('session')), $key);
+ $this->db->query("DELETE FROM {$this->table_name} WHERE `sess_id` = ?", $key);
}
return true;
}
/**
* Read session data from memcache
*
* @param string Session ID
* @return string Session vars
*/
public function mc_read($key)
{
if ($value = $this->memcache->get($key)) {
$arr = unserialize($value);
$this->changed = $arr['changed'];
$this->ip = $arr['ip'];
$this->vars = $arr['vars'];
$this->key = $key;
return !empty($this->vars) ? (string) $this->vars : '';
}
return null;
}
/**
* Save session data.
* handler for session_read()
*
* @param string Session ID
* @param string Serialized session vars
*
* @return boolean True on success
*/
public function mc_write($key, $vars)
{
$ts = microtime(true);
// no session data in cache (mc_read() returns false)
if (!$this->key)
$oldvars = null;
// use internal data for fast requests (up to 0.5 sec.)
else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5))
$oldvars = $this->vars;
else // else read data again
$oldvars = $this->mc_read($key);
$newvars = $oldvars !== null ? $this->_fixvars($vars, $oldvars) : $vars;
if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 3) {
return $this->memcache->set($key, serialize(array('changed' => time(), 'ip' => $this->ip, 'vars' => $newvars)),
MEMCACHE_COMPRESSED, $this->lifetime + 60);
}
return true;
}
/**
* Handler for session_destroy() with memcache backend
*
* @param string Session ID
*
* @return boolean True on success
*/
public function mc_destroy($key)
{
if ($key) {
// #1488592: use 2nd argument
$this->memcache->delete($key, 0);
}
return true;
}
/**
* Execute registered garbage collector routines
*/
public function gc($maxlifetime)
{
// move gc execution to the script shutdown function
// see rcube::shutdown() and rcube_session::write_close()
return $this->gc_enabled = $maxlifetime;
}
/**
* Register additional garbage collector functions
*
* @param mixed Callback function
*/
public function register_gc_handler($func)
{
foreach ($this->gc_handlers as $handler) {
if ($handler == $func) {
return;
}
}
$this->gc_handlers[] = $func;
}
/**
* Garbage collector handler to run on script shutdown
*/
protected function gc_shutdown()
{
if ($this->gc_enabled) {
// just delete all expired sessions
if ($this->storage == 'db') {
- $this->db->query("DELETE FROM " . $this->db->table_name('session')
- . " WHERE changed < " . $this->db->now(-$this->gc_enabled));
+ $this->db->query("DELETE FROM {$this->table_name}"
+ . " WHERE `changed` < " . $this->db->now(-$this->gc_enabled));
}
foreach ($this->gc_handlers as $fct) {
call_user_func($fct);
}
}
}
/**
* Generate and set new session id
*
* @param boolean $destroy If enabled the current session will be destroyed
*/
public function regenerate_id($destroy=true)
{
session_regenerate_id($destroy);
$this->vars = null;
$this->key = session_id();
return true;
}
/**
* Append the given value to the certain node in the session data array
*
* @param string Path denoting the session variable where to append the value
* @param string Key name under which to append the new value (use null for appending to an indexed list)
* @param mixed Value to append to the session data array
*/
public function append($path, $key, $value)
{
// re-read session data from DB because it might be outdated
if (!$this->reloaded && microtime(true) - $this->start > 0.5) {
$this->reload();
$this->reloaded = true;
$this->start = microtime(true);
}
$node = &$this->get_node(explode('.', $path), $_SESSION);
if ($key !== null) {
$node[$key] = $value;
$path .= '.' . $key;
}
else {
$node[] = $value;
}
$this->appends[] = $path;
// when overwriting a previously unset variable
if ($this->unsets[$path])
unset($this->unsets[$path]);
}
/**
* Unset a session variable
*
* @param string Variable name (can be a path denoting a certain node in the session array, e.g. compose.attachments.5)
* @return boolean True on success
*/
public function remove($var=null)
{
if (empty($var)) {
return $this->destroy(session_id());
}
$this->unsets[] = $var;
if (isset($_SESSION[$var])) {
unset($_SESSION[$var]);
}
else {
$path = explode('.', $var);
$key = array_pop($path);
$node = &$this->get_node($path, $_SESSION);
unset($node[$key]);
}
return true;
}
/**
* Kill this session
*/
public function kill()
{
$this->vars = null;
$this->ip = rcube_utils::remote_addr(); // update IP (might have changed)
$this->destroy(session_id());
rcube_utils::setcookie($this->cookiename, '-del-', time() - 60);
}
/**
* Re-read session data from storage backend
*/
public function reload()
{
// collect updated data from previous appends
$merge_data = array();
foreach ((array)$this->appends as $var) {
$path = explode('.', $var);
$value = $this->get_node($path, $_SESSION);
$k = array_pop($path);
$node = &$this->get_node($path, $merge_data);
$node[$k] = $value;
}
if ($this->key && $this->memcache)
$data = $this->mc_read($this->key);
else if ($this->key)
$data = $this->db_read($this->key);
if ($data) {
session_decode($data);
// apply appends and unsets to reloaded data
$_SESSION = array_merge_recursive($_SESSION, $merge_data);
foreach ((array)$this->unsets as $var) {
if (isset($_SESSION[$var])) {
unset($_SESSION[$var]);
}
else {
$path = explode('.', $var);
$k = array_pop($path);
$node = &$this->get_node($path, $_SESSION);
unset($node[$k]);
}
}
}
}
/**
* Returns a reference to the node in data array referenced by the given path.
* e.g. ['compose','attachments'] will return $_SESSION['compose']['attachments']
*/
private function &get_node($path, &$data_arr)
{
$node = &$data_arr;
if (!empty($path)) {
foreach ((array)$path as $key) {
if (!isset($node[$key]))
$node[$key] = array();
$node = &$node[$key];
}
}
return $node;
}
/**
* Serialize session data
*/
private function serialize($vars)
{
$data = '';
if (is_array($vars)) {
foreach ($vars as $var=>$value)
$data .= $var.'|'.serialize($value);
}
else {
$data = 'b:0;';
}
return $data;
}
/**
* Unserialize session data
* http://www.php.net/manual/en/function.session-decode.php#56106
*/
private function unserialize($str)
{
$str = (string)$str;
$endptr = strlen($str);
$p = 0;
$serialized = '';
$items = 0;
$level = 0;
while ($p < $endptr) {
$q = $p;
while ($str[$q] != '|')
if (++$q >= $endptr)
break 2;
if ($str[$p] == '!') {
$p++;
$has_value = false;
}
else {
$has_value = true;
}
$name = substr($str, $p, $q - $p);
$q++;
$serialized .= 's:' . strlen($name) . ':"' . $name . '";';
if ($has_value) {
for (;;) {
$p = $q;
switch (strtolower($str[$q])) {
case 'n': // null
case 'b': // boolean
case 'i': // integer
case 'd': // decimal
do $q++;
while ( ($q < $endptr) && ($str[$q] != ';') );
$q++;
$serialized .= substr($str, $p, $q - $p);
if ($level == 0)
break 2;
break;
case 'r': // reference
$q+= 2;
for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++)
$id .= $str[$q];
$q++;
// increment pointer because of outer array
$serialized .= 'R:' . ($id + 1) . ';';
if ($level == 0)
break 2;
break;
case 's': // string
$q+=2;
for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++)
$length .= $str[$q];
$q+=2;
$q+= (int)$length + 2;
$serialized .= substr($str, $p, $q - $p);
if ($level == 0)
break 2;
break;
case 'a': // array
case 'o': // object
do $q++;
while ($q < $endptr && $str[$q] != '{');
$q++;
$level++;
$serialized .= substr($str, $p, $q - $p);
break;
case '}': // end of array|object
$q++;
$serialized .= substr($str, $p, $q - $p);
if (--$level == 0)
break 2;
break;
default:
return false;
}
}
}
else {
$serialized .= 'N;';
$q += 2;
}
$items++;
$p = $q;
}
return unserialize( 'a:' . $items . ':{' . $serialized . '}' );
}
/**
* Setter for session lifetime
*/
public function set_lifetime($lifetime)
{
$this->lifetime = max(120, $lifetime);
// valid time range is now - 1/2 lifetime to now + 1/2 lifetime
$now = time();
$this->now = $now - ($now % ($this->lifetime / 2));
}
/**
* Getter for remote IP saved with this session
*/
public function get_ip()
{
return $this->ip;
}
/**
* Setter for cookie encryption secret
*/
function set_secret($secret)
{
$this->secret = $secret;
}
/**
* Enable/disable IP check
*/
function set_ip_check($check)
{
$this->ip_check = $check;
}
/**
* Setter for the cookie name used for session cookie
*/
function set_cookiename($cookiename)
{
if ($cookiename) {
$this->cookiename = $cookiename;
}
}
/**
* Check session authentication cookie
*
* @return boolean True if valid, False if not
*/
function check_auth()
{
$this->cookie = $_COOKIE[$this->cookiename];
$result = $this->ip_check ? rcube_utils::remote_addr() == $this->ip : true;
if (!$result) {
$this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . rcube_utils::remote_addr());
}
if ($result && $this->_mkcookie($this->now) != $this->cookie) {
$this->log("Session auth check failed for " . $this->key . "; timeslot = " . date('Y-m-d H:i:s', $this->now));
$result = false;
// Check if using id from a previous time slot
for ($i = 1; $i <= 2; $i++) {
$prev = $this->now - ($this->lifetime / 2) * $i;
if ($this->_mkcookie($prev) == $this->cookie) {
$this->log("Send new auth cookie for " . $this->key . ": " . $this->cookie);
$this->set_auth_cookie();
$result = true;
}
}
}
if (!$result) {
$this->log("Session authentication failed for " . $this->key
. "; invalid auth cookie sent; timeslot = " . date('Y-m-d H:i:s', $prev));
}
return $result;
}
/**
* Set session authentication cookie
*/
function set_auth_cookie()
{
$this->cookie = $this->_mkcookie($this->now);
rcube_utils::setcookie($this->cookiename, $this->cookie, 0);
$_COOKIE[$this->cookiename] = $this->cookie;
}
/**
* Create session cookie from session data
*
* @param int Time slot to use
*/
function _mkcookie($timeslot)
{
$auth_string = "$this->key,$this->secret,$timeslot";
return "S" . (function_exists('sha1') ? sha1($auth_string) : md5($auth_string));
}
/**
* Writes debug information to the log
*/
function log($line)
{
if ($this->logging) {
rcube::write_log('session', $line);
}
}
}
diff --git a/program/lib/Roundcube/rcube_spellchecker.php b/program/lib/Roundcube/rcube_spellchecker.php
index 43bab08c4..062780720 100644
--- a/program/lib/Roundcube/rcube_spellchecker.php
+++ b/program/lib/Roundcube/rcube_spellchecker.php
@@ -1,431 +1,430 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2011-2013, Kolab Systems AG |
| Copyright (C) 2008-2013, The Roundcube Dev Team |
| |
| 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: |
| Spellchecking using different backends |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Helper class for spellchecking with Googielspell and PSpell support.
*
* @package Framework
* @subpackage Utils
*/
class rcube_spellchecker
{
private $matches = array();
private $engine;
private $backend;
private $lang;
private $rc;
private $error;
private $options = array();
private $dict;
private $have_dict;
/**
* Constructor
*
* @param string $lang Language code
*/
function __construct($lang = 'en')
{
$this->rc = rcube::get_instance();
$this->engine = $this->rc->config->get('spellcheck_engine', 'googie');
$this->lang = $lang ? $lang : 'en';
$this->options = array(
'ignore_syms' => $this->rc->config->get('spellcheck_ignore_syms'),
'ignore_nums' => $this->rc->config->get('spellcheck_ignore_nums'),
'ignore_caps' => $this->rc->config->get('spellcheck_ignore_caps'),
'dictionary' => $this->rc->config->get('spellcheck_dictionary'),
);
$cls = 'rcube_spellcheck_' . $this->engine;
if (class_exists($cls)) {
$this->backend = new $cls($this, $this->lang);
$this->backend->options = $this->options;
}
else {
$this->error = "Unknown spellcheck engine '$this->engine'";
}
}
/**
* Return a list of supported languages
*/
function languages()
{
// trust configuration
$configured = $this->rc->config->get('spellcheck_languages');
if (!empty($configured) && is_array($configured) && !$configured[0]) {
return $configured;
}
else if (!empty($configured)) {
$langs = (array)$configured;
}
else if ($this->backend) {
$langs = $this->backend->languages();
}
// load index
@include(RCUBE_LOCALIZATION_DIR . 'index.inc');
// add correct labels
$languages = array();
foreach ($langs as $lang) {
$langc = strtolower(substr($lang, 0, 2));
$alias = $rcube_language_aliases[$langc];
if (!$alias) {
$alias = $langc.'_'.strtoupper($langc);
}
if ($rcube_languages[$lang]) {
$languages[$lang] = $rcube_languages[$lang];
}
else if ($rcube_languages[$alias]) {
$languages[$lang] = $rcube_languages[$alias];
}
else {
$languages[$lang] = ucfirst($lang);
}
}
// remove possible duplicates (#1489395)
$languages = array_unique($languages);
asort($languages);
return $languages;
}
/**
* Set content and check spelling
*
* @param string $text Text content for spellchecking
* @param bool $is_html Enables HTML-to-Text conversion
*
* @return bool True when no mispelling found, otherwise false
*/
function check($text, $is_html = false)
{
// convert to plain text
if ($is_html) {
$this->content = $this->html2text($text);
}
else {
$this->content = $text;
}
if ($this->backend) {
$this->matches = $this->backend->check($this->content);
}
return $this->found() == 0;
}
/**
* Number of mispellings found (after check)
*
* @return int Number of mispellings
*/
function found()
{
return count($this->matches);
}
/**
* Returns suggestions for the specified word
*
* @param string $word The word
*
* @return array Suggestions list
*/
function get_suggestions($word)
{
if ($this->backend) {
return $this->backend->get_suggestions($word);
}
return array();
}
/**
* Returns misspelled words
*
* @param string $text The content for spellchecking. If empty content
* used for check() method will be used.
*
* @return array List of misspelled words
*/
function get_words($text = null, $is_html=false)
{
if ($is_html) {
$text = $this->html2text($text);
}
if ($this->backend) {
return $this->backend->get_words($text);
}
return array();
}
/**
* Returns checking result in XML (Googiespell) format
*
* @return string XML content
*/
function get_xml()
{
// send output
$out = '<?xml version="1.0" encoding="'.RCUBE_CHARSET.'"?><spellresult charschecked="'.mb_strlen($this->content).'">';
foreach ((array)$this->matches as $item) {
$out .= '<c o="'.$item[1].'" l="'.$item[2].'">';
$out .= is_array($item[4]) ? implode("\t", $item[4]) : $item[4];
$out .= '</c>';
}
$out .= '</spellresult>';
return $out;
}
/**
* Returns checking result (misspelled words with suggestions)
*
* @return array Spellchecking result. An array indexed by word.
*/
function get()
{
$result = array();
foreach ((array)$this->matches as $item) {
if ($this->engine == 'pspell') {
$word = $item[0];
}
else {
$word = mb_substr($this->content, $item[1], $item[2], RCUBE_CHARSET);
}
if (is_array($item[4])) {
$suggestions = $item[4];
}
else if (empty($item[4])) {
$suggestions = array();
}
else {
$suggestions = explode("\t", $item[4]);
}
$result[$word] = $suggestions;
}
return $result;
}
/**
* Returns error message
*
* @return string Error message
*/
function error()
{
return $this->error ? $this->error : ($this->backend ? $this->backend->error() : false);
}
private function html2text($text)
{
$h2t = new rcube_html2text($text, false, true, 0);
return $h2t->get_text();
}
/**
* Check if the specified word is an exception accoring to
* spellcheck options.
*
* @param string $word The word
*
* @return bool True if the word is an exception, False otherwise
*/
public function is_exception($word)
{
// Contain only symbols (e.g. "+9,0", "2:2")
if (!$word || preg_match('/^[0-9@#$%^&_+~*<>=:;?!,.-]+$/', $word))
return true;
// Contain symbols (e.g. "g@@gle"), all symbols excluding separators
if (!empty($this->options['ignore_syms']) && preg_match('/[@#$%^&_+~*=-]/', $word))
return true;
// Contain numbers (e.g. "g00g13")
if (!empty($this->options['ignore_nums']) && preg_match('/[0-9]/', $word))
return true;
// Blocked caps (e.g. "GOOGLE")
if (!empty($this->options['ignore_caps']) && $word == mb_strtoupper($word))
return true;
// Use exceptions from dictionary
if (!empty($this->options['dictionary'])) {
$this->load_dict();
// @TODO: should dictionary be case-insensitive?
if (!empty($this->dict) && in_array($word, $this->dict))
return true;
}
return false;
}
/**
* Add a word to dictionary
*
* @param string $word The word to add
*/
public function add_word($word)
{
$this->load_dict();
foreach (explode(' ', $word) as $word) {
// sanity check
if (strlen($word) < 512) {
$this->dict[] = $word;
$valid = true;
}
}
if ($valid) {
$this->dict = array_unique($this->dict);
$this->update_dict();
}
}
/**
* Remove a word from dictionary
*
* @param string $word The word to remove
*/
public function remove_word($word)
{
$this->load_dict();
if (($key = array_search($word, $this->dict)) !== false) {
unset($this->dict[$key]);
$this->update_dict();
}
}
/**
* Update dictionary row in DB
*/
private function update_dict()
{
if (strcasecmp($this->options['dictionary'], 'shared') != 0) {
$userid = $this->rc->get_user_id();
}
$plugin = $this->rc->plugins->exec_hook('spell_dictionary_save', array(
'userid' => $userid, 'language' => $this->lang, 'dictionary' => $this->dict));
if (!empty($plugin['abort'])) {
return;
}
if ($this->have_dict) {
if (!empty($this->dict)) {
$this->rc->db->query(
- "UPDATE ".$this->rc->db->table_name('dictionary')
- ." SET data = ?"
- ." WHERE user_id " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
- ." AND " . $this->rc->db->quote_identifier('language') . " = ?",
+ "UPDATE " . $this->rc->db->table_name('dictionary', true)
+ ." SET `data` = ?"
+ ." WHERE `user_id` " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
+ ." AND `language` = ?",
implode(' ', $plugin['dictionary']), $plugin['language']);
}
// don't store empty dict
else {
$this->rc->db->query(
- "DELETE FROM " . $this->rc->db->table_name('dictionary')
- ." WHERE user_id " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
- ." AND " . $this->rc->db->quote_identifier('language') . " = ?",
+ "DELETE FROM " . $this->rc->db->table_name('dictionary', true)
+ ." WHERE `user_id` " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
+ ." AND `language` = ?",
$plugin['language']);
}
}
else if (!empty($this->dict)) {
$this->rc->db->query(
- "INSERT INTO " .$this->rc->db->table_name('dictionary')
- ." (user_id, " . $this->rc->db->quote_identifier('language') . ", data) VALUES (?, ?, ?)",
+ "INSERT INTO " . $this->rc->db->table_name('dictionary', true)
+ ." (`user_id`, `language`, `data`) VALUES (?, ?, ?)",
$plugin['userid'], $plugin['language'], implode(' ', $plugin['dictionary']));
}
}
/**
* Get dictionary from DB
*/
private function load_dict()
{
if (is_array($this->dict)) {
return $this->dict;
}
if (strcasecmp($this->options['dictionary'], 'shared') != 0) {
$userid = $this->rc->get_user_id();
}
$plugin = $this->rc->plugins->exec_hook('spell_dictionary_get', array(
'userid' => $userid, 'language' => $this->lang, 'dictionary' => array()));
if (empty($plugin['abort'])) {
$dict = array();
$sql_result = $this->rc->db->query(
- "SELECT data FROM ".$this->rc->db->table_name('dictionary')
- ." WHERE user_id ". ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
- ." AND " . $this->rc->db->quote_identifier('language') . " = ?",
+ "SELECT `data` FROM " . $this->rc->db->table_name('dictionary', true)
+ ." WHERE `user_id` ". ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
+ ." AND `language` = ?",
$plugin['language']);
if ($sql_arr = $this->rc->db->fetch_assoc($sql_result)) {
$this->have_dict = true;
if (!empty($sql_arr['data'])) {
$dict = explode(' ', $sql_arr['data']);
}
}
$plugin['dictionary'] = array_merge((array)$plugin['dictionary'], $dict);
}
if (!empty($plugin['dictionary']) && is_array($plugin['dictionary'])) {
$this->dict = $plugin['dictionary'];
}
else {
$this->dict = array();
}
return $this->dict;
}
-
}
diff --git a/program/lib/Roundcube/rcube_user.php b/program/lib/Roundcube/rcube_user.php
index 739b6f2a0..b2110df8b 100644
--- a/program/lib/Roundcube/rcube_user.php
+++ b/program/lib/Roundcube/rcube_user.php
@@ -1,755 +1,746 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2012, The Roundcube Dev Team |
| |
| 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: |
| This class represents a system user linked and provides access |
| to the related database records. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class representing a system user
*
* @package Framework
* @subpackage Core
*/
class rcube_user
{
public $ID;
public $data;
public $language;
/**
* Holds database connection.
*
* @var rcube_db
*/
private $db;
/**
* Framework object.
*
* @var rcube
*/
private $rc;
/**
* Internal identities cache
*
* @var array
*/
private $identities = array();
const SEARCH_ADDRESSBOOK = 1;
const SEARCH_MAIL = 2;
/**
* Object constructor
*
* @param int $id User id
* @param array $sql_arr SQL result set
*/
function __construct($id = null, $sql_arr = null)
{
$this->rc = rcube::get_instance();
$this->db = $this->rc->get_dbh();
if ($id && !$sql_arr) {
$sql_result = $this->db->query(
- "SELECT * FROM ".$this->db->table_name('users')." WHERE user_id = ?", $id);
+ "SELECT * FROM " . $this->db->table_name('users', true)
+ . " WHERE `user_id` = ?", $id);
$sql_arr = $this->db->fetch_assoc($sql_result);
}
if (!empty($sql_arr)) {
$this->ID = $sql_arr['user_id'];
$this->data = $sql_arr;
$this->language = $sql_arr['language'];
}
}
/**
* Build a user name string (as e-mail address)
*
* @param string $part Username part (empty or 'local' or 'domain', 'mail')
* @return string Full user name or its part
*/
function get_username($part = null)
{
if ($this->data['username']) {
// return real name
if (!$part) {
return $this->data['username'];
}
list($local, $domain) = explode('@', $this->data['username']);
// at least we should always have the local part
if ($part == 'local') {
return $local;
}
// if no domain was provided...
if (empty($domain)) {
$domain = $this->rc->config->mail_domain($this->data['mail_host']);
}
if ($part == 'domain') {
return $domain;
}
if (!empty($domain))
return $local . '@' . $domain;
else
return $local;
}
return false;
}
/**
* Get the preferences saved for this user
*
* @return array Hash array with prefs
*/
function get_prefs()
{
$prefs = array();
if (!empty($this->language))
$prefs['language'] = $this->language;
if ($this->ID) {
// Preferences from session (write-master is unavailable)
if (!empty($_SESSION['preferences'])) {
// Check last write attempt time, try to write again (every 5 minutes)
if ($_SESSION['preferences_time'] < time() - 5 * 60) {
$saved_prefs = unserialize($_SESSION['preferences']);
$this->rc->session->remove('preferences');
$this->rc->session->remove('preferences_time');
$this->save_prefs($saved_prefs);
}
else {
$this->data['preferences'] = $_SESSION['preferences'];
}
}
if ($this->data['preferences']) {
$prefs += (array)unserialize($this->data['preferences']);
}
}
return $prefs;
}
/**
* Write the given user prefs to the user's record
*
* @param array $a_user_prefs User prefs to save
* @return boolean True on success, False on failure
*/
function save_prefs($a_user_prefs)
{
if (!$this->ID)
return false;
$plugin = $this->rc->plugins->exec_hook('preferences_update', array(
'userid' => $this->ID, 'prefs' => $a_user_prefs, 'old' => (array)$this->get_prefs()));
if (!empty($plugin['abort'])) {
return;
}
$a_user_prefs = $plugin['prefs'];
$old_prefs = $plugin['old'];
$config = $this->rc->config;
// merge (partial) prefs array with existing settings
$save_prefs = $a_user_prefs + $old_prefs;
unset($save_prefs['language']);
// don't save prefs with default values if they haven't been changed yet
foreach ($a_user_prefs as $key => $value) {
if ($value === null || (!isset($old_prefs[$key]) && ($value == $config->get($key))))
unset($save_prefs[$key]);
}
$save_prefs = serialize($save_prefs);
$this->db->query(
- "UPDATE ".$this->db->table_name('users').
- " SET preferences = ?".
- ", language = ?".
- " WHERE user_id = ?",
+ "UPDATE ".$this->db->table_name('users', true).
+ " SET `preferences` = ?, `language` = ?".
+ " WHERE `user_id` = ?",
$save_prefs,
$_SESSION['language'],
$this->ID);
$this->language = $_SESSION['language'];
// Update success
if ($this->db->affected_rows() !== false) {
$config->set_user_prefs($a_user_prefs);
$this->data['preferences'] = $save_prefs;
if (isset($_SESSION['preferences'])) {
$this->rc->session->remove('preferences');
$this->rc->session->remove('preferences_time');
}
return true;
}
// Update error, but we are using replication (we have read-only DB connection)
// and we are storing session not in the SQL database
// we can store preferences in session and try to write later (see get_prefs())
else if ($this->db->is_replicated() && $config->get('session_storage', 'db') != 'db') {
$_SESSION['preferences'] = $save_prefs;
$_SESSION['preferences_time'] = time();
$config->set_user_prefs($a_user_prefs);
$this->data['preferences'] = $save_prefs;
}
return false;
}
/**
* Generate a unique hash to identify this user which
*/
function get_hash()
{
$key = substr($this->rc->config->get('des_key'), 1, 4);
return md5($this->data['user_id'] . $key . $this->data['username'] . '@' . $this->data['mail_host']);
}
/**
* Get default identity of this user
*
* @param int $id Identity ID. If empty, the default identity is returned
* @return array Hash array with all cols of the identity record
*/
function get_identity($id = null)
{
$id = (int)$id;
// cache identities for better performance
if (!array_key_exists($id, $this->identities)) {
- $result = $this->list_identities($id ? 'AND identity_id = ' . $id : '');
+ $result = $this->list_identities($id ? "AND `identity_id` = $id" : '');
$this->identities[$id] = $result[0];
}
return $this->identities[$id];
}
/**
* Return a list of all identities linked with this user
*
* @param string $sql_add Optional WHERE clauses
* @param bool $formatted Format identity email and name
*
* @return array List of identities
*/
function list_identities($sql_add = '', $formatted = false)
{
$result = array();
$sql_result = $this->db->query(
- "SELECT * FROM ".$this->db->table_name('identities').
- " WHERE del <> 1 AND user_id = ?".
+ "SELECT * FROM ".$this->db->table_name('identities', true).
+ " WHERE `del` <> 1 AND `user_id` = ?".
($sql_add ? " ".$sql_add : "").
- " ORDER BY ". $this->db->quote_identifier('standard') . " DESC, "
- . $this->db->quote_identifier('name') . " ASC, "
- . $this->db->quote_identifier('email') . " ASC, "
- . $this->db->quote_identifier('identity_id') . " ASC",
+ " ORDER BY `standard` DESC, `name` ASC, `email` ASC, `identity_id` ASC",
$this->ID);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
if ($formatted) {
$ascii_email = format_email($sql_arr['email']);
$utf8_email = format_email(rcube_utils::idn_to_utf8($ascii_email));
$sql_arr['email_ascii'] = $ascii_email;
$sql_arr['email'] = $utf8_email;
$sql_arr['ident'] = format_email_recipient($ascii_email, $sql_arr['name']);
}
$result[] = $sql_arr;
}
return $result;
}
/**
* Update a specific identity record
*
* @param int $iid Identity ID
* @param array $data Hash array with col->value pairs to save
* @return boolean True if saved successfully, false if nothing changed
*/
function update_identity($iid, $data)
{
if (!$this->ID)
return false;
$query_cols = $query_params = array();
foreach ((array)$data as $col => $value) {
$query_cols[] = $this->db->quote_identifier($col) . ' = ?';
$query_params[] = $value;
}
$query_params[] = $iid;
$query_params[] = $this->ID;
- $sql = "UPDATE ".$this->db->table_name('identities').
- " SET changed = ".$this->db->now().", ".join(', ', $query_cols).
- " WHERE identity_id = ?".
- " AND user_id = ?".
- " AND del <> 1";
+ $sql = "UPDATE ".$this->db->table_name('identities', true).
+ " SET `changed` = ".$this->db->now().", ".join(', ', $query_cols).
+ " WHERE `identity_id` = ?".
+ " AND `user_id` = ?".
+ " AND `del` <> 1";
call_user_func_array(array($this->db, 'query'),
array_merge(array($sql), $query_params));
$this->identities = array();
return $this->db->affected_rows();
}
/**
* Create a new identity record linked with this user
*
* @param array $data Hash array with col->value pairs to save
* @return int The inserted identity ID or false on error
*/
function insert_identity($data)
{
if (!$this->ID)
return false;
unset($data['user_id']);
$insert_cols = $insert_values = array();
foreach ((array)$data as $col => $value) {
$insert_cols[] = $this->db->quote_identifier($col);
$insert_values[] = $value;
}
- $insert_cols[] = 'user_id';
+ $insert_cols[] = $this->db->quote_identifier('user_id');
$insert_values[] = $this->ID;
- $sql = "INSERT INTO ".$this->db->table_name('identities').
- " (changed, ".join(', ', $insert_cols).")".
+ $sql = "INSERT INTO ".$this->db->table_name('identities', true).
+ " (`changed`, ".join(', ', $insert_cols).")".
" VALUES (".$this->db->now().", ".join(', ', array_pad(array(), sizeof($insert_values), '?')).")";
call_user_func_array(array($this->db, 'query'),
array_merge(array($sql), $insert_values));
$this->identities = array();
return $this->db->insert_id('identities');
}
/**
* Mark the given identity as deleted
*
* @param int $iid Identity ID
* @return boolean True if deleted successfully, false if nothing changed
*/
function delete_identity($iid)
{
if (!$this->ID)
return false;
$sql_result = $this->db->query(
- "SELECT count(*) AS ident_count FROM ".$this->db->table_name('identities').
- " WHERE user_id = ? AND del <> 1",
+ "SELECT count(*) AS ident_count FROM ".$this->db->table_name('identities', true).
+ " WHERE `user_id` = ? AND `del` <> 1",
$this->ID);
$sql_arr = $this->db->fetch_assoc($sql_result);
// we'll not delete last identity
if ($sql_arr['ident_count'] <= 1)
return -1;
$this->db->query(
- "UPDATE ".$this->db->table_name('identities').
- " SET del = 1, changed = ".$this->db->now().
- " WHERE user_id = ?".
- " AND identity_id = ?",
+ "UPDATE ".$this->db->table_name('identities', true).
+ " SET `del` = 1, `changed` = ".$this->db->now().
+ " WHERE `user_id` = ?".
+ " AND `identity_id` = ?",
$this->ID,
$iid);
$this->identities = array();
return $this->db->affected_rows();
}
/**
* Make this identity the default one for this user
*
* @param int $iid The identity ID
*/
function set_default($iid)
{
if ($this->ID && $iid) {
$this->db->query(
- "UPDATE ".$this->db->table_name('identities').
- " SET ".$this->db->quote_identifier('standard')." = '0'".
- " WHERE user_id = ?".
- " AND identity_id <> ?".
- " AND del <> 1",
+ "UPDATE ".$this->db->table_name('identities', true).
+ " SET `standard` = '0'".
+ " WHERE `user_id` = ? AND `identity_id` <> ?",
$this->ID,
$iid);
unset($this->identities[0]);
}
}
/**
* Update user's last_login timestamp
*/
function touch()
{
if ($this->ID) {
$this->db->query(
- "UPDATE ".$this->db->table_name('users').
- " SET last_login = ".$this->db->now().
- " WHERE user_id = ?",
+ "UPDATE ".$this->db->table_name('users', true).
+ " SET `last_login` = ".$this->db->now().
+ " WHERE `user_id` = ?",
$this->ID);
}
}
/**
* Clear the saved object state
*/
function reset()
{
$this->ID = null;
$this->data = null;
}
/**
* Find a user record matching the given name and host
*
* @param string $user IMAP user name
* @param string $host IMAP host name
* @return rcube_user New user instance
*/
static function query($user, $host)
{
$dbh = rcube::get_instance()->get_dbh();
$config = rcube::get_instance()->config;
// query for matching user name
- $sql_result = $dbh->query("SELECT * FROM " . $dbh->table_name('users')
- ." WHERE mail_host = ? AND username = ?", $host, $user);
+ $sql_result = $dbh->query("SELECT * FROM " . $dbh->table_name('users', true)
+ ." WHERE `mail_host` = ? AND `username` = ?", $host, $user);
$sql_arr = $dbh->fetch_assoc($sql_result);
// username not found, try aliases from identities
if (empty($sql_arr) && $config->get('user_aliases') && strpos($user, '@')) {
$sql_result = $dbh->limitquery("SELECT u.*"
- ." FROM " . $dbh->table_name('users') . " u"
- ." JOIN " . $dbh->table_name('identities') . " i ON (i.user_id = u.user_id)"
- ." WHERE email = ? AND del <> 1", 0, 1, $user);
+ ." FROM " . $dbh->table_name('users', true) . " u"
+ ." JOIN " . $dbh->table_name('identities', true) . " i ON (i.`user_id` = u.`user_id`)"
+ ." WHERE `email` = ? AND `del` <> 1", 0, 1, $user);
$sql_arr = $dbh->fetch_assoc($sql_result);
}
// user already registered -> overwrite username
if ($sql_arr)
return new rcube_user($sql_arr['user_id'], $sql_arr);
else
return false;
}
/**
* Create a new user record and return a rcube_user instance
*
* @param string $user IMAP user name
* @param string $host IMAP host
* @return rcube_user New user instance
*/
static function create($user, $host)
{
$user_name = '';
$user_email = '';
$rcube = rcube::get_instance();
$dbh = $rcube->get_dbh();
// try to resolve user in virtuser table and file
if ($email_list = self::user2email($user, false, true)) {
$user_email = is_array($email_list[0]) ? $email_list[0]['email'] : $email_list[0];
}
$data = $rcube->plugins->exec_hook('user_create', array(
'host' => $host,
'user' => $user,
'user_name' => $user_name,
'user_email' => $user_email,
'email_list' => $email_list,
'language' => $_SESSION['language'],
));
// plugin aborted this operation
if ($data['abort']) {
return false;
}
$dbh->query(
- "INSERT INTO ".$dbh->table_name('users').
- " (created, last_login, username, mail_host, language)".
+ "INSERT INTO ".$dbh->table_name('users', true).
+ " (`created`, `last_login`, `username`, `mail_host`, `language`)".
" VALUES (".$dbh->now().", ".$dbh->now().", ?, ?, ?)",
$data['user'],
$data['host'],
$data['language']);
if ($user_id = $dbh->insert_id('users')) {
// create rcube_user instance to make plugin hooks work
$user_instance = new rcube_user($user_id, array(
'user_id' => $user_id,
'username' => $data['user'],
'mail_host' => $data['host'],
'language' => $data['language'],
));
$rcube->user = $user_instance;
$mail_domain = $rcube->config->mail_domain($data['host']);
$user_name = $data['user_name'];
$user_email = $data['user_email'];
$email_list = $data['email_list'];
if (empty($email_list)) {
if (empty($user_email)) {
$user_email = strpos($data['user'], '@') ? $user : sprintf('%s@%s', $data['user'], $mail_domain);
}
$email_list[] = $user_email;
}
// identities_level check
else if (count($email_list) > 1 && $rcube->config->get('identities_level', 0) > 1) {
$email_list = array($email_list[0]);
}
if (empty($user_name)) {
$user_name = $data['user'];
}
// create new identities records
$standard = 1;
foreach ($email_list as $row) {
$record = array();
if (is_array($row)) {
if (empty($row['email'])) {
continue;
}
$record = $row;
}
else {
$record['email'] = $row;
}
if (empty($record['name'])) {
$record['name'] = $user_name != $record['email'] ? $user_name : '';
}
$record['user_id'] = $user_id;
$record['standard'] = $standard;
$plugin = $rcube->plugins->exec_hook('identity_create',
array('login' => true, 'record' => $record));
if (!$plugin['abort'] && $plugin['record']['email']) {
$rcube->user->insert_identity($plugin['record']);
}
$standard = 0;
}
}
else {
rcube::raise_error(array(
'code' => 500,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Failed to create new user"), true, false);
}
return $user_id ? $user_instance : false;
}
/**
* Resolve username using a virtuser plugins
*
* @param string $email E-mail address to resolve
* @return string Resolved IMAP username
*/
static function email2user($email)
{
$rcube = rcube::get_instance();
$plugin = $rcube->plugins->exec_hook('email2user',
array('email' => $email, 'user' => NULL));
return $plugin['user'];
}
/**
* Resolve e-mail address from virtuser plugins
*
* @param string $user User name
* @param boolean $first If true returns first found entry
* @param boolean $extended If true returns email as array (email and name for identity)
* @return mixed Resolved e-mail address string or array of strings
*/
static function user2email($user, $first=true, $extended=false)
{
$rcube = rcube::get_instance();
$plugin = $rcube->plugins->exec_hook('user2email',
array('email' => NULL, 'user' => $user,
'first' => $first, 'extended' => $extended));
return empty($plugin['email']) ? NULL : $plugin['email'];
}
/**
* Return a list of saved searches linked with this user
*
* @param int $type Search type
*
* @return array List of saved searches indexed by search ID
*/
function list_searches($type)
{
$plugin = $this->rc->plugins->exec_hook('saved_search_list', array('type' => $type));
if ($plugin['abort']) {
return (array) $plugin['result'];
}
$result = array();
$sql_result = $this->db->query(
- "SELECT search_id AS id, ".$this->db->quote_identifier('name')
- ." FROM ".$this->db->table_name('searches')
- ." WHERE user_id = ?"
- ." AND ".$this->db->quote_identifier('type')." = ?"
- ." ORDER BY ".$this->db->quote_identifier('name'),
+ "SELECT `search_id` AS id, `name`"
+ ." FROM ".$this->db->table_name('searches', true)
+ ." WHERE `user_id` = ? AND `type` = ?"
+ ." ORDER BY `name`",
(int) $this->ID, (int) $type);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$sql_arr['data'] = unserialize($sql_arr['data']);
$result[$sql_arr['id']] = $sql_arr;
}
return $result;
}
/**
* Return saved search data.
*
* @param int $id Row identifier
*
* @return array Data
*/
function get_search($id)
{
$plugin = $this->rc->plugins->exec_hook('saved_search_get', array('id' => $id));
if ($plugin['abort']) {
return $plugin['result'];
}
$sql_result = $this->db->query(
- "SELECT ".$this->db->quote_identifier('name')
- .", ".$this->db->quote_identifier('data')
- .", ".$this->db->quote_identifier('type')
- ." FROM ".$this->db->table_name('searches')
- ." WHERE user_id = ?"
- ." AND search_id = ?",
+ "SELECT `name`, `data`, `type`"
+ . " FROM ".$this->db->table_name('searches', true)
+ . " WHERE `user_id` = ?"
+ ." AND `search_id` = ?",
(int) $this->ID, (int) $id);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
return array(
'id' => $id,
'name' => $sql_arr['name'],
'type' => $sql_arr['type'],
'data' => unserialize($sql_arr['data']),
);
}
return null;
}
/**
* Deletes given saved search record
*
* @param int $sid Search ID
*
* @return boolean True if deleted successfully, false if nothing changed
*/
function delete_search($sid)
{
if (!$this->ID)
return false;
$this->db->query(
- "DELETE FROM ".$this->db->table_name('searches')
- ." WHERE user_id = ?"
- ." AND search_id = ?",
+ "DELETE FROM ".$this->db->table_name('searches', true)
+ ." WHERE `user_id` = ?"
+ ." AND `search_id` = ?",
(int) $this->ID, $sid);
return $this->db->affected_rows();
}
/**
* Create a new saved search record linked with this user
*
* @param array $data Hash array with col->value pairs to save
*
* @return int The inserted search ID or false on error
*/
function insert_search($data)
{
if (!$this->ID)
return false;
$insert_cols[] = 'user_id';
$insert_values[] = (int) $this->ID;
$insert_cols[] = $this->db->quote_identifier('type');
$insert_values[] = (int) $data['type'];
$insert_cols[] = $this->db->quote_identifier('name');
$insert_values[] = $data['name'];
$insert_cols[] = $this->db->quote_identifier('data');
$insert_values[] = serialize($data['data']);
- $sql = "INSERT INTO ".$this->db->table_name('searches')
+ $sql = "INSERT INTO ".$this->db->table_name('searches', true)
." (".join(', ', $insert_cols).")"
." VALUES (".join(', ', array_pad(array(), sizeof($insert_values), '?')).")";
call_user_func_array(array($this->db, 'query'),
array_merge(array($sql), $insert_values));
return $this->db->insert_id('searches');
}
-
}
diff --git a/program/steps/utils/killcache.inc b/program/steps/utils/killcache.inc
index 1cb99b879..1adaebd8c 100644
--- a/program/steps/utils/killcache.inc
+++ b/program/steps/utils/killcache.inc
@@ -1,50 +1,55 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/steps/utils/killcache.inc |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2010, The Roundcube Dev Team |
| |
| 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: |
| Delete rows from cache tables |
| |
+-----------------------------------------------------------------------+
| Author: Dennis P. Nikolaenko <dennis@nikolaenko.ru> |
+-----------------------------------------------------------------------+
*/
// don't allow public access if not in devel_mode
if (!$RCMAIL->config->get('devel_mode')) {
header("HTTP/1.0 401 Access denied");
die("Access denied!");
}
// @TODO: transaction here (if supported by DB) would be a good thing
-$res = $RCMAIL->db->query("DELETE FROM cache");
+$res = $RCMAIL->db->query("DELETE FROM " . $RCMAIL->db->table_name('cache', true));
if ($err = $RCMAIL->db->is_error($res)) {
exit($err);
}
-$res = $RCMAIL->db->query("DELETE FROM cache_messages");
+$res = $RCMAIL->db->query("DELETE FROM " . $RCMAIL->db->table_name('cache_shared', true));
if ($err = $RCMAIL->db->is_error($res)) {
exit($err);
}
-$res = $RCMAIL->db->query("DELETE FROM cache_index");
+$res = $RCMAIL->db->query("DELETE FROM " . $RCMAIL->db->table_name('cache_messages', true));
if ($err = $RCMAIL->db->is_error($res)) {
exit($err);
}
-$res = $RCMAIL->db->query("DELETE FROM cache_thread");
+$res = $RCMAIL->db->query("DELETE FROM " . $RCMAIL->db->table_name('cache_index', true));
+if ($err = $RCMAIL->db->is_error($res)) {
+ exit($err);
+}
+
+$res = $RCMAIL->db->query("DELETE FROM " . $RCMAIL->db->table_name('cache_thread', true));
if ($err = $RCMAIL->db->is_error($res)) {
exit($err);
}
echo "Cache cleared\n";
exit;
diff --git a/tests/Framework/ImapCache.php b/tests/Framework/ImapCache.php
index 83612c549..6c7f98807 100644
--- a/tests/Framework/ImapCache.php
+++ b/tests/Framework/ImapCache.php
@@ -1,20 +1,20 @@
<?php
/**
* Test class to test rcube_imap_cache class
*
* @package Tests
*/
class Framework_ImapCache extends PHPUnit_Framework_TestCase
{
/**
* Class constructor
*/
function test_class()
{
- $object = new rcube_imap_cache(null, null, null, null);
+ $object = new rcube_imap_cache(new rcube_db('test'), null, null, null);
$this->assertInstanceOf('rcube_imap_cache', $object, "Class constructor");
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Mar 1, 2:37 AM (21 h, 4 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
165546
Default Alt Text
(286 KB)

Event Timeline