Stefan Liebl 79f4015906 roundcube
2020-07-09 15:41:32 +02:00

685 lines
23 KiB
PHP

<?php
/*
RCM CardDAV Plugin
Copyright (C) 2011-2016 Benjamin Schieder <rcmcarddav@wegwerf.anderdonau.de>,
Michael Stilkerich <ms@mike2k.de>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
class carddav extends rcube_plugin
{
private static $helper;
// the dummy task is used by the calendar plugin, which requires
// the addressbook to be initialized
public $task = 'addressbook|login|mail|settings|dummy';
public function checkMigrations(){
$dbh = rcmail::get_instance()->db;
$db_backend = "unknown";
switch ($dbh->db_provider){
case "mysql":
$db_backend = "mysql";
break;
case "sqlite":
$db_backend = "sqlite3";
break;
case "pgsql":
case "postgres":
$db_backend = "postgres";
break;
}
if ($db_backend == "unknown"){
rcmail::write_log("carddav", "Unknown database backend: ".$dbh->db_provider);
return;
}
# first initialize the carddav_migrations table if it doesn't exist.
/*
$query = file_get_contents(dirname(__FILE__)."/dbinit/".$db_backend.".sql");
if (strlen($query) > 0){
$query = str_replace("TABLE_PREFIX", $config->get('db_prefix', ""), $query);
$dbh->query($query);
rcmail::write_log("carddav", "Processed initialization of carddav_migrations table");
} else {
rcmail::write_log("carddav", "Can't find migration: /dbinit/".$db_backend.".sql");
}
*/
$config = rcmail::get_instance()->config;
$migrations = array_diff(scandir(dirname(__FILE__)."/dbmigrations/"), array('..', '.'));
$mignew = array();
foreach ($migrations as $k => $v){
$mignew[] = $v;
}
$migrations = $mignew;
$qmarks = "?";
for ($i=1;$i<count($migrations);$i++){
$qmarks .= ",?";
}
$dbh->set_option('ignore_key_errors', true);
$sql_result = $dbh->query('SELECT * FROM '.
$dbh->table_name('carddav_migrations') .
' WHERE filename IN ('.$qmarks.');', $migrations);
if ($sql_result){
while ($processed = $dbh->fetch_assoc($sql_result)) {
if(($key = array_search($processed['filename'], $migrations)) !== false) {
unset($migrations[$key]);
}
}
}
$dbh->set_option('ignore_key_errors', null);
foreach ($migrations as $migration) {
rcmail::write_log('carddav', "In migration: ".$migration);
$queries_raw = file_get_contents(dirname(__FILE__)."/dbmigrations/".$migration."/".$db_backend.".sql");
$match_count = preg_match_all('/(.+?;)/s', $queries_raw, $matches);
rcmail::write_log('carddav', 'Found '.$match_count.' matches');
if($match_count > 0){
foreach ($matches[0] as $query){ // array will have two elements, each holding all queries. Only iterate over one of them
if (strlen($query) > 0){
$query = str_replace("TABLE_PREFIX", $config->get('db_prefix', ""), $query);
$dbh->query($query);
}
}
$dbh->query("INSERT INTO ".$dbh->table_name("carddav_migrations")." (filename) VALUES (?)", $migration);
}else{
rcmail::write_log('carddav', "Did not match any instructions from migration ".$migration);
}
}
}
public function init()
{{{
$this->rc = rcmail::get_instance();
$tasks = explode('|', $this->task);
// Since other plugins may also use the Sabre library
// In order to avoid version conflicts between Sabre libraries
// which might be used by other plugins
// It is better to restrict the loading of Sabre library
// under necessary tasks
if(!in_array($this->rc->task, $tasks))
return;
else {
require_once('carddav_backend.php');
require_once('carddav_discovery.php');
require_once('carddav_common.php');
}
self::$helper = new carddav_common('BACKEND: ');
$this->add_hook('addressbooks_list', array($this, 'address_sources'));
$this->add_hook('addressbook_get', array($this, 'get_address_book'));
$this->add_hook('preferences_list', array($this, 'cd_preferences'));
$this->add_hook('preferences_save', array($this, 'cd_save'));
$this->add_hook('preferences_sections_list',array($this, 'cd_preferences_section'));
$this->add_hook('login_after',array($this, 'checkMigrations'));
$this->add_hook('login_after',array($this, 'init_presets'));
if(!array_key_exists('user_id', $_SESSION))
return;
// use this address book for autocompletion queries
// (maybe this should be configurable by the user?)
$config = rcmail::get_instance()->config;
$sources = (array) $config->get('autocomplete_addressbooks', array('sql'));
$dbh = rcmail::get_instance()->db;
$sql_result = $dbh->query('SELECT id FROM ' .
$dbh->table_name('carddav_addressbooks') .
' WHERE user_id=? AND active=1',
$_SESSION['user_id']);
while ($abookrow = $dbh->fetch_assoc($sql_result)) {
$abookname = "carddav_" . $abookrow['id'];
if (!in_array($abookname, $sources)) {
$sources[] = $abookname;
}
}
$config->set('autocomplete_addressbooks', $sources);
$skin_path = $this->local_skin_path();
$this->include_stylesheet($skin_path . '/carddav.css');
}}}
public function init_presets()
{{{
$dbh = rcmail::get_instance()->db;
$prefs = carddav_common::get_adminsettings();
// migrate old settings
carddav_backend::migrateconfig();
// read existing presets from DB
$sql_result = $dbh->query('SELECT * FROM ' .
$dbh->table_name('carddav_addressbooks') .
' WHERE user_id=? AND presetname is not null',
$_SESSION['user_id']);
$existing_presets = array( );
while ($abookrow = $dbh->fetch_assoc($sql_result)) {
$pn = $abookrow['presetname'];
if(!array_key_exists($pn,$existing_presets)) {
$existing_presets[$pn] = array();
}
$existing_presets[$pn][] = $abookrow;
}
// add not existing preset addressbooks
foreach($prefs as $presetname => $preset) {
if($presetname === '_GLOBAL') continue;
// addressbooks exist for this preset => update settings
if(array_key_exists($presetname, $existing_presets)) {
if(is_array($preset['fixed'])) {
// update all existing addressbooks for this preset
foreach($existing_presets[$presetname] as $abookrow) {
// decrypt password so that the comparison works
$abookrow['password'] = self::$helper->decrypt_password($abookrow['password']);
// update: only admin fix keys, only if it's fixed
// otherwise there may be user changes that should not be destroyed
$pa = array();
foreach($preset['fixed'] as $k) {
if(array_key_exists($k, $abookrow) && array_key_exists($k,$preset)) {
// only update the name if it is used
if($k === 'name') {
if(!$preset['carddav_name_only']) {
$fullname = $abookrow['name'];
$cnpos = strpos($fullname, ' (');
if($cnpos === FALSE && strcmp($preset['name'],$fullname)!==0) {
$pa['name'] = $preset['name'];
} else if($cnpos !== FALSE && strcmp($preset['name'],substr($fullname,0,$cnpos))!==0) {
$pa['name'] = $preset['name'] . substr($fullname, $cnpos);
}
}
} else if ($abookrow[$k] != $preset[$k]) {
$pa[$k] = $preset[$k];
}
}
}
// only update if something changed
if(count($pa)===0) continue;
self::update_abook($abookrow['id'],$pa);
}
}
unset($existing_presets[$presetname]);
} else { // create new
$preset['presetname'] = $presetname;
$preset['password'] = self::$helper->encrypt_password($preset['password']);
$abname = $preset['name'];
$discovery = new carddav_discovery();
$srvs = $discovery->find_addressbooks($preset['url'], $preset['username'], $preset['password']);
if(is_array($srvs)) {
foreach($srvs as $srv){
if($srv['name']) {
if($preset['carddav_name_only'])
$preset['name'] = $srv['name'];
else
$preset['name'] = "$abname (" . $srv['name'] . ')';
} else {
$preset['name'] = $abname;
}
$preset['url'] = $srv['href'];
self::insert_abook($preset);
}}
}
}
// delete existing preset addressbooks that where removed by admin
foreach($existing_presets as $ep) {
foreach($ep as $abookrow) {
self::delete_abook($abookrow['id']);
}
}
}}}
public function address_sources($p)
{{{
$dbh = rcmail::get_instance()->db;
$prefs = carddav_common::get_adminsettings();
$sql_result = $dbh->query('SELECT id,name,presetname FROM ' .
$dbh->table_name('carddav_addressbooks') .
' WHERE user_id=? AND active=1',
$_SESSION['user_id']);
while ($abookrow = $dbh->fetch_assoc($sql_result)) {
$ro = false;
if($abookrow['presetname'] && $prefs[$abookrow['presetname']]['readonly'])
$ro = true;
$p['sources']["carddav_".$abookrow['id']] = array(
'id' => "carddav_".$abookrow['id'],
'name' => $abookrow['name'],
'groups' => true,
'autocomplete' => true,
'readonly' => $ro,
);
}
return $p;
}}}
public function get_address_book($p)
{{{
if (preg_match(";^carddav_(\d+)$;", $p['id'], $match)){
$p['instance'] = new carddav_backend($match[1]);
}
return $p;
}}}
private static function process_cd_time($refresht)
{{{
if(preg_match('/^(\d+)(:([0-5]?\d))?(:([0-5]?\d))?$/', $refresht, $match)) {
$refresht = sprintf("%02d:%02d:%02d", $match[1],
count($match)>3 ? $match[3] : 0,
count($match)>5 ? $match[5] : 0);
} else {
$refresht = '01:00:00';
}
return $refresht;
}}}
private static function no_override($pref, $abook, $prefs)
{{{
$pn = $abook['presetname'];
if(!$pn) return false;
// never enable user change for preset URLs
if($pref === 'url') return true;
if(!is_array($prefs[$pn])) return false;
if(!is_array($prefs[$pn]['fixed'])) return false;
return in_array($pref,$prefs[$pn]['fixed']);
}}}
/**
* Builds a setting block for one address book for the preference page.
*/
private function cd_preferences_buildblock($blockheader,$abook,$prefs)
{{{
$abookid = $abook['id'];
$rcmail = rcmail::get_instance();
if (self::no_override('active', $abook, $prefs)) {
$content_active = $prefs[$abook['presetname']] ? $this->gettext('cd_enabled') : $this->gettext('cd_disabled');
} else {
// check box for activating
$checkbox = new html_checkbox(array('name' => $abookid.'_cd_active', 'value' => 1));
$content_active = $checkbox->show($abook['active']?1:0);
}
if (self::no_override('use_categories', $abook, $prefs) || $abook['id'] !== "new") {
$content_use_categories = $abook['use_categories'] ? $this->gettext('cd_enabled') : $this->gettext('cd_disabled');
} else {
// check box for use categories
$checkbox = new html_checkbox(array('name' => $abookid.'_cd_use_categories', 'value' => 1));
$content_use_categories = $checkbox->show($abook['use_categories']?1:0);
}
if (self::no_override('username', $abook, $prefs)) {
// %V parses username for macosx, replaces periods and @ by _, work around bugs in contacts.app
$content_username = $abook['username'] === '%V' ? str_replace('@','_', str_replace('.','_',$_SESSION['username'])) : $abook['username'] === '%u' ? $_SESSION['username'] : $abook['username'] === '%l' ? $rcmail->user->get_username('local') : $abook['username'];
} else {
// input box for username
$input = new html_inputfield(array('name' => $abookid.'_cd_username', 'type' => 'text', 'autocomplete' => 'off', 'value' => $abook['username']));
$content_username = $input->show();
}
if (self::no_override('password', $abook, $prefs)) {
$content_password = "***";
} else {
// input box for password
$input = new html_inputfield(array('name' => $abookid.'_cd_password', 'type' => 'password', 'autocomplete' => 'off', 'value' => ''));
$content_password = $input->show();
}
if (self::no_override('url', $abook, $prefs)) {
$content_url = str_replace("%u", $abook['username'], $abook['url']);
} else {
// input box for URL
$size = max(strlen($abook['url']),40);
$input = new html_inputfield(array('name' => $abookid.'_cd_url', 'type' => 'text', 'autocomplete' => 'off', 'value' => $abook['url'], 'size' => $size));
$content_url = $input->show();
}
// input box for refresh time
if (self::no_override('refresh_time', $abook, $prefs)) {
$content_refresh_time = $abook['refresh_time'];
} else {
$input = new html_inputfield(array('name' => $abookid.'_cd_refresh_time', 'type' => 'text', 'autocomplete' => 'off', 'value' => $abook['refresh_time'], 'size' => 10));
$content_refresh_time = $input->show();
}
if (self::no_override('name', $abook, $prefs)) {
$content_name = $abook['name'];
} else {
$input = new html_inputfield(array('name' => $abookid.'_cd_name', 'type' => 'text', 'autocomplete' => 'off', 'value' => $abook['name'], 'size' => 40));
$content_name = $input->show();
}
$retval = array(
'options' => array(
array('title'=> self::$helper->Q($this->gettext('cd_name')), 'content' => $content_name),
array('title'=> self::$helper->Q($this->gettext('cd_active')), 'content' => $content_active),
array('title'=> self::$helper->Q($this->gettext('cd_use_categories')), 'content' => $content_use_categories),
array('title'=> self::$helper->Q($this->gettext('cd_username')), 'content' => $content_username),
array('title'=> self::$helper->Q($this->gettext('cd_password')), 'content' => $content_password),
array('title'=> self::$helper->Q($this->gettext('cd_url')), 'content' => $content_url),
array('title'=> self::$helper->Q($this->gettext('cd_refresh_time')), 'content' => $content_refresh_time),
),
'name' => $blockheader
);
if (!$abook['presetname'] && preg_match('/^\d+$/',$abookid)) {
$checkbox = new html_checkbox(array('name' => $abookid.'_cd_delete', 'value' => 1));
$content_delete = $checkbox->show(0);
$retval['options'][] = array('title'=> self::$helper->Q($this->gettext('cd_delete')), 'content' => $content_delete);
}
return $retval;
}}}
// user preferences
function cd_preferences($args)
{{{
if($args['section'] != 'cd_preferences')
return;
$this->include_stylesheet($this->local_skin_path().'/carddav.css');
$this->add_texts('localization/', false);
$prefs = carddav_common::get_adminsettings();
if (!$prefs['_GLOBAL']['suppress_version_warning']){
if (version_compare(PHP_VERSION, '5.6.18', '<')) {
$args['blocks']['cd_preferences'] = array(
'options' => array(
array('title'=> self::$helper->Q($this->gettext('cd_php_too_old')), 'content' => PHP_VERSION)
),
'name' => self::$helper->Q($this->gettext('cd_title'))
);
return $args;
}
}
$abooks = carddav_backend::get_dbrecord($_SESSION['user_id'],'*','addressbooks',false,'user_id');
foreach($abooks as $abook) {
$presetname = $abook['presetname'];
if (empty($presetname) ||
(!isset($prefs[$presetname]['hide']) || (isset($prefs[$presetname]['hide']) && $prefs[$presetname]['hide'] === FALSE))) {
$abookid = $abook['id'];
$blockhdr = $abook['name'];
if($abook['presetname'])
$blockhdr .= str_replace("_PRESETNAME_", $abook['presetname'], self::$helper->Q($this->gettext('cd_frompreset')));
$args['blocks']['cd_preferences'.$abookid] = $this->cd_preferences_buildblock($blockhdr,$abook,$prefs);
}
}
if(!array_key_exists('_GLOBAL', $prefs) || !$prefs['_GLOBAL']['fixed']) {
$args['blocks']['cd_preferences_section_new'] = $this->cd_preferences_buildblock(
self::$helper->Q($this->gettext('cd_newabboxtitle')),
array(
'id' => 'new',
'active' => 1,
'use_categories' => 1,
'username' => '',
'url' => '',
'name' => '',
'refresh_time' => 1,
'presetname' => '',
), $prefs);
}
return($args);
}}}
// add a section to the preferences tab
function cd_preferences_section($args)
{{{
$prefs = carddav_common::get_adminsettings();
if (!isset($prefs['_GLOBAL']['hide_preferences']) || (isset($prefs['_GLOBAL']['hide_preferences']) && $prefs['_GLOBAL']['hide_preferences'] === FALSE)) {
$this->add_texts('localization/', false);
$args['list']['cd_preferences'] = array(
'id' => 'cd_preferences',
'section' => self::$helper->Q($this->gettext('cd_title'))
);
}
return($args);
}}}
// save preferences
function cd_save($args)
{{{
$this->add_texts('localization/', false);
if($args['section'] != 'cd_preferences')
return;
$prefs = carddav_common::get_adminsettings();
if (isset($prefs['_GLOBAL']['hide_preferences']) && $prefs['_GLOBAL']['hide_preferences'] === TRUE) {
return;
}
// update existing in DB
$abooks = carddav_backend::get_dbrecord($_SESSION['user_id'],'id,presetname',
'addressbooks', false, 'user_id');
foreach($abooks as $abook) {
$abookid = $abook['id'];
if( isset($_POST[$abookid."_cd_delete"]) ) {
self::delete_abook($abookid);
} else {
$newset = array (
'name' => rcube_utils::get_input_value($abookid."_cd_name", rcube_utils::INPUT_POST),
'username' => rcube_utils::get_input_value($abookid."_cd_username", rcube_utils::INPUT_POST, true),
'url' => rcube_utils::get_input_value($abookid."_cd_url", rcube_utils::INPUT_POST),
'active' => isset($_POST[$abookid.'_cd_active']) ? 1 : 0,
'use_categories' => isset($_POST[$abookid.'_cd_use_categories']) ? 1 : 0,
'refresh_time' => rcube_utils::get_input_value($abookid."_cd_refresh_time", rcube_utils::INPUT_POST),
);
// only set the password if the user entered a new one
$password = rcube_utils::get_input_value($abookid."_cd_password", rcube_utils::INPUT_POST, true);
if(strlen($password) > 0) {
$newset['password'] = $password;
}
// remove admin only settings
foreach($newset as $pref => $value) {
if(self::no_override($pref, $abook, $prefs)) {
unset($newset[$pref]);
}
}
self::update_abook($abookid, $newset);
}
}
// add a new address book?
$new = rcube_utils::get_input_value('new_cd_name', rcube_utils::INPUT_POST);
if ( (!array_key_exists('_GLOBAL', $prefs) || !$prefs['_GLOBAL']['fixed']) && strlen($new) > 0) {
$srv = rcube_utils::get_input_value('new_cd_url', rcube_utils::INPUT_POST);
$usr = rcube_utils::get_input_value('new_cd_username', rcube_utils::INPUT_POST, true);
$pass = rcube_utils::get_input_value('new_cd_password', rcube_utils::INPUT_POST, true);
$pass = self::$helper->encrypt_password($pass);
$abname = rcube_utils::get_input_value('new_cd_name', rcube_utils::INPUT_POST);
$use_categories = intval(rcube_utils::get_input_value('new_cd_use_categories', rcube_utils::INPUT_POST, true), 0);
$discovery = new carddav_discovery();
$srvs = $discovery->find_addressbooks($srv, $usr, $pass);
if(is_array($srvs) && count($srvs)>0) {
foreach($srvs as $srv){
self::$helper->debug("ADDING ABOOK " . print_r($srv,true));
$this_abname = $abname;
if($srv['name']) {
$this_abname .= ' (' . $srv['name'] . ')';
}
self::insert_abook(array(
'name' => $this_abname,
'username' => $usr,
'password' => $pass,
'use_categories' => $use_categories,
'url' => $srv['href'],
'refresh_time' => rcube_utils::get_input_value('new_cd_refresh_time', rcube_utils::INPUT_POST)
));
}
} else {
$args['abort'] = true;
$args['message'] = $abname . ': ' . $this->gettext('cd_err_noabfound');
}
}
return($args);
}}}
private static function delete_abook($abookid)
{{{
carddav_backend::delete_dbrecord($abookid,'addressbooks');
// we explicitly delete all data belonging to the addressbook, since
// cascaded deleted are not supported by all database backends
// ...contacts
carddav_backend::delete_dbrecord($abookid,'contacts','abook_id');
// ...custom subtypes
carddav_backend::delete_dbrecord($abookid,'xsubtypes','abook_id');
// ...groups and memberships
$delgroups = carddav_backend::get_dbrecord($abookid, 'id as group_id', 'groups', false, 'abook_id');
carddav_backend::delete_dbrecord($abookid,'groups','abook_id');
carddav_backend::delete_dbrecord($delgroups,'group_user','group_id');
}}}
private static function insert_abook($pa)
{{{
$dbh = rcmail::get_instance()->db;
// check parameters
if(array_key_exists('refresh_time', $pa)) {
$pa['refresh_time'] = self::process_cd_time($pa['refresh_time']);
}
/* Ensure field lengths */
if (array_key_exists('name', $pa)) {
if (strlen($pa['name']) > 64){
$pa['name'] = substr($pa['name'], 0, 64);
}
}
if (array_key_exists('username', $pa)) {
if (strlen($pa['username']) > 255){
$pa['username'] = substr($pa['username'], 0, 255);
}
}
if (array_key_exists('presetname', $pa)) {
if (strlen($pa['presetname']) > 255){
$pa['presetname'] = substr($pa['presetname'], 0, 255);
}
}
$pa['user_id'] = $_SESSION['user_id'];
// required fields
$qf=array('name','username','password','url','user_id');
$qv=array();
foreach($qf as $f) {
if(!array_key_exists($f,$pa)) return false;
$qv[] = $pa[$f];
}
// optional fields
$qfo = array('active','presetname','use_categories','refresh_time');
foreach($qfo as $f) {
if(array_key_exists($f,$pa)) {
$qf[] = $f;
$qv[] = $pa[$f];
}
}
$dbh->query('INSERT INTO ' . $dbh->table_name('carddav_addressbooks') .
'('. implode(',',$qf) .') ' .
'VALUES (?'. str_repeat(',?', count($qf)-1) . ')',
$qv
);
}}}
public static function update_abook($abookid, $pa)
{{{
$dbh = rcmail::get_instance()->db;
// check parameters
if(array_key_exists('refresh_time', $pa))
$pa['refresh_time'] = self::process_cd_time($pa['refresh_time']);
// encrypt the password before storing it
if(array_key_exists('password', $pa))
$pa['password'] = self::$helper->encrypt_password($pa['password']);
/* Ensure field lengths */
if (array_key_exists('name', $pa)) {
if (strlen($pa['name']) > 64){
$pa['name'] = substr($pa['name'], 0, 64);
}
}
if (array_key_exists('username', $pa)) {
if (strlen($pa['username']) > 255){
$pa['username'] = substr($pa['username'], 0, 255);
}
}
if (array_key_exists('presetname', $pa)) {
if (strlen($pa['presetname']) > 255){
$pa['presetname'] = substr($pa['presetname'], 0, 255);
}
}
// optional fields
$qfo=array('name','username','password','url','active','refresh_time','sync_token');
$qf=array();
$qv=array();
foreach($qfo as $f) {
if(array_key_exists($f,$pa)) {
$qf[] = $f;
$qv[] = $pa[$f];
}
}
if(count($qf) <= 0) return true;
$qv[] = $abookid;
$dbh->query('UPDATE ' .
$dbh->table_name('carddav_addressbooks') .
' SET ' . implode('=?,', $qf) . '=?' .
' WHERE id=?',
$qv
);
}}}
}
?>